feat: 添加图片缓存系统和优化FeedView组件
- 在yana/Utils中新增ImageCacheManager类,提供内存和磁盘缓存功能,支持图片的异步加载和预加载。 - 更新FeedView,使用优化后的动态卡片组件OptimizedDynamicCardView,集成图片缓存,提升用户体验。 - 在yana/yana-Bridging-Header.h中引入CommonCrypto以支持MD5哈希。 - 更新FeedFeature以增加动态请求的页面大小,提升数据加载效率。 - 删除不再使用的data.txt文件,保持项目整洁。
This commit is contained in:
227
yana/Utils/ImageCacheManager.swift
Normal file
227
yana/Utils/ImageCacheManager.swift
Normal file
@@ -0,0 +1,227 @@
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
// MARK: - 图片缓存管理器
|
||||
@MainActor
|
||||
class ImageCacheManager: ObservableObject {
|
||||
static let shared = ImageCacheManager()
|
||||
|
||||
private let memoryCache = NSCache<NSString, UIImage>()
|
||||
private let diskCache = DiskImageCache()
|
||||
private let urlSession: URLSession
|
||||
|
||||
// 正在下载的任务
|
||||
private var downloadTasks: [String: Task<UIImage?, Never>] = [:]
|
||||
|
||||
private init() {
|
||||
// 配置内存缓存
|
||||
memoryCache.countLimit = 100 // 最多缓存100张图片
|
||||
memoryCache.totalCostLimit = 50 * 1024 * 1024 // 50MB内存限制
|
||||
|
||||
// 配置URLSession
|
||||
let config = URLSessionConfiguration.default
|
||||
config.requestCachePolicy = .returnCacheDataElseLoad
|
||||
config.urlCache = URLCache(
|
||||
memoryCapacity: 20 * 1024 * 1024, // 20MB内存缓存
|
||||
diskCapacity: 100 * 1024 * 1024, // 100MB磁盘缓存
|
||||
diskPath: "image_cache"
|
||||
)
|
||||
self.urlSession = URLSession(configuration: config)
|
||||
}
|
||||
|
||||
/// 获取图片(优先从缓存获取)
|
||||
func getImage(from url: String) async -> UIImage? {
|
||||
let cacheKey = NSString(string: url)
|
||||
|
||||
// 1. 检查内存缓存
|
||||
if let cachedImage = memoryCache.object(forKey: cacheKey) {
|
||||
return cachedImage
|
||||
}
|
||||
|
||||
// 2. 检查磁盘缓存
|
||||
if let diskImage = await diskCache.getImage(for: url) {
|
||||
// 存入内存缓存
|
||||
memoryCache.setObject(diskImage, forKey: cacheKey)
|
||||
return diskImage
|
||||
}
|
||||
|
||||
// 3. 检查是否已经在下载中
|
||||
if let existingTask = downloadTasks[url] {
|
||||
return await existingTask.value
|
||||
}
|
||||
|
||||
// 4. 开始下载
|
||||
let downloadTask = Task<UIImage?, Never> {
|
||||
await downloadImage(from: url)
|
||||
}
|
||||
|
||||
downloadTasks[url] = downloadTask
|
||||
let image = await downloadTask.value
|
||||
downloadTasks.removeValue(forKey: url)
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
/// 预加载图片
|
||||
func preloadImages(urls: [String]) {
|
||||
Task {
|
||||
await withTaskGroup(of: Void.self) { group in
|
||||
for url in urls {
|
||||
group.addTask {
|
||||
_ = await self.getImage(from: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 下载图片
|
||||
private func downloadImage(from urlString: String) async -> UIImage? {
|
||||
guard let url = URL(string: urlString) else { return nil }
|
||||
|
||||
do {
|
||||
let (data, _) = try await urlSession.data(from: url)
|
||||
guard let image = UIImage(data: data) else { return nil }
|
||||
|
||||
// 存入缓存
|
||||
let cacheKey = NSString(string: urlString)
|
||||
memoryCache.setObject(image, forKey: cacheKey)
|
||||
|
||||
// 存入磁盘缓存
|
||||
await diskCache.setImage(image, for: urlString)
|
||||
|
||||
return image
|
||||
} catch {
|
||||
print("图片下载失败: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// 清理缓存
|
||||
func clearCache() {
|
||||
memoryCache.removeAllObjects()
|
||||
Task {
|
||||
await diskCache.clearCache()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 磁盘缓存
|
||||
private actor DiskImageCache {
|
||||
private let cacheDirectory: URL
|
||||
|
||||
init() {
|
||||
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
cacheDirectory = documentsPath.appendingPathComponent("ImageCache")
|
||||
|
||||
// 创建缓存目录
|
||||
try? FileManager.default.createDirectory(at: cacheDirectory, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
func getImage(for url: String) async -> UIImage? {
|
||||
let fileName: String
|
||||
if #available(iOS 13.0, *) {
|
||||
fileName = url.sha256()
|
||||
} else {
|
||||
fileName = url.md5()
|
||||
}
|
||||
let fileURL = cacheDirectory.appendingPathComponent(fileName)
|
||||
|
||||
guard FileManager.default.fileExists(atPath: fileURL.path),
|
||||
let data = try? Data(contentsOf: fileURL),
|
||||
let image = UIImage(data: data) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
func setImage(_ image: UIImage, for url: String) async {
|
||||
let fileName: String
|
||||
if #available(iOS 13.0, *) {
|
||||
fileName = url.sha256()
|
||||
} else {
|
||||
fileName = url.md5()
|
||||
}
|
||||
let fileURL = cacheDirectory.appendingPathComponent(fileName)
|
||||
|
||||
guard let data = image.jpegData(compressionQuality: 0.8) else { return }
|
||||
try? data.write(to: fileURL)
|
||||
}
|
||||
|
||||
func clearCache() async {
|
||||
try? FileManager.default.removeItem(at: cacheDirectory)
|
||||
try? FileManager.default.createDirectory(at: cacheDirectory, withIntermediateDirectories: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 缓存图片组件
|
||||
struct CachedAsyncImage<Content: View, Placeholder: View>: View {
|
||||
let url: String
|
||||
let content: (Image) -> Content
|
||||
let placeholder: () -> Placeholder
|
||||
|
||||
@State private var image: UIImage?
|
||||
@State private var isLoading = false
|
||||
|
||||
init(
|
||||
url: String,
|
||||
@ViewBuilder content: @escaping (Image) -> Content,
|
||||
@ViewBuilder placeholder: @escaping () -> Placeholder
|
||||
) {
|
||||
self.url = url
|
||||
self.content = content
|
||||
self.placeholder = placeholder
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let image = image {
|
||||
content(Image(uiImage: image))
|
||||
} else {
|
||||
placeholder()
|
||||
.onAppear {
|
||||
loadImage()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadImage() {
|
||||
guard !isLoading else { return }
|
||||
isLoading = true
|
||||
|
||||
Task {
|
||||
let loadedImage = await ImageCacheManager.shared.getImage(from: url)
|
||||
await MainActor.run {
|
||||
self.image = loadedImage
|
||||
self.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 便利构造器
|
||||
extension CachedAsyncImage where Content == Image, Placeholder == Color {
|
||||
init(url: String) {
|
||||
self.init(
|
||||
url: url,
|
||||
content: { $0 },
|
||||
placeholder: { Color.gray.opacity(0.3) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension CachedAsyncImage where Placeholder == Color {
|
||||
init(
|
||||
url: String,
|
||||
@ViewBuilder content: @escaping (Image) -> Content
|
||||
) {
|
||||
self.init(
|
||||
url: url,
|
||||
content: content,
|
||||
placeholder: { Color.gray.opacity(0.3) }
|
||||
)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user