feat: 添加图片缓存系统和优化FeedView组件

- 在yana/Utils中新增ImageCacheManager类,提供内存和磁盘缓存功能,支持图片的异步加载和预加载。
- 更新FeedView,使用优化后的动态卡片组件OptimizedDynamicCardView,集成图片缓存,提升用户体验。
- 在yana/yana-Bridging-Header.h中引入CommonCrypto以支持MD5哈希。
- 更新FeedFeature以增加动态请求的页面大小,提升数据加载效率。
- 删除不再使用的data.txt文件,保持项目整洁。
This commit is contained in:
edwinQQQ
2025-07-11 20:18:36 +08:00
parent 12bb4a5f8c
commit f686480cdc
6 changed files with 566 additions and 135 deletions

View 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) }
)
}
}