|
|
@@ -0,0 +1,375 @@
|
|
|
+import Foundation
|
|
|
+import UIKit
|
|
|
+
|
|
|
+// 图片加载进度回调
|
|
|
+typealias ImageLoadingProgress = (Double) -> Void
|
|
|
+// 图片加载完成回调
|
|
|
+typealias ImageCompletion = (UIImage?, Error?) -> Void
|
|
|
+
|
|
|
+/// 头像样式
|
|
|
+enum AvatarStyle {
|
|
|
+ case circle // 圆形
|
|
|
+ case rounded(radius: CGFloat) // 圆角矩形
|
|
|
+}
|
|
|
+
|
|
|
+/// 图片加载工具类(含缓存功能,支持头像专用圆角处理)
|
|
|
+class ImageLoader {
|
|
|
+ // 单例模式
|
|
|
+ static let shared = ImageLoader()
|
|
|
+
|
|
|
+ // 内存缓存(LRU策略)
|
|
|
+ private let memoryCache = NSCache<NSString, UIImage>()
|
|
|
+ // 磁盘缓存路径
|
|
|
+ private let diskCachePath: String
|
|
|
+ // 最大内存缓存大小(默认100MB)
|
|
|
+ private let maxMemoryCacheSize: Int64 = 100 * 1024 * 1024
|
|
|
+ // 最大磁盘缓存大小(默认200MB)
|
|
|
+ private let maxDiskCacheSize: Int64 = 200 * 1024 * 1024
|
|
|
+
|
|
|
+ // 初始化
|
|
|
+ private init() {
|
|
|
+ // 创建磁盘缓存目录
|
|
|
+ let documentsPath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0]
|
|
|
+ diskCachePath = "\(documentsPath)/ImageCache"
|
|
|
+
|
|
|
+ // 配置内存缓存
|
|
|
+ memoryCache.countLimit = 100 // 最多缓存100张图片
|
|
|
+ memoryCache.totalCostLimit = Int(maxMemoryCacheSize)
|
|
|
+
|
|
|
+ // 清理过期的磁盘缓存
|
|
|
+ cleanExpiredDiskCache()
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 加载头像图片(带缓存)
|
|
|
+ func loadAvatar(
|
|
|
+ from url: URL,
|
|
|
+ placeholder: UIImage? = nil,
|
|
|
+ style: AvatarStyle = .circle,
|
|
|
+ progress: ImageLoadingProgress? = nil,
|
|
|
+ completion: @escaping ImageCompletion
|
|
|
+ ) -> UIImage? {
|
|
|
+ // 生成缓存键(包含样式信息)
|
|
|
+ let cacheKey = generateAvatarCacheKey(url: url, style: style)
|
|
|
+
|
|
|
+ // 先检查内存缓存
|
|
|
+ if let cachedImage = memoryCache.object(forKey: cacheKey) {
|
|
|
+ return cachedImage
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查磁盘缓存
|
|
|
+ if let cachedImage = loadAvatarFromDiskCache(url: url, style: style) {
|
|
|
+ memoryCache.setObject(cachedImage, forKey: cacheKey)
|
|
|
+ return cachedImage
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示占位图
|
|
|
+ completion(placeholder, nil)
|
|
|
+ return placeholder
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 异步加载头像图片(带进度监听)
|
|
|
+ func loadAvatarAsync(
|
|
|
+ from url: URL,
|
|
|
+ placeholder: UIImage? = nil,
|
|
|
+ style: AvatarStyle = .circle,
|
|
|
+ progress: ImageLoadingProgress? = nil,
|
|
|
+ completion: @escaping ImageCompletion
|
|
|
+ ) {
|
|
|
+ // 生成缓存键(包含样式信息)
|
|
|
+ let cacheKey = generateAvatarCacheKey(url: url, style: style)
|
|
|
+
|
|
|
+ // 先检查内存缓存
|
|
|
+ if let cachedImage = memoryCache.object(forKey: cacheKey) {
|
|
|
+ DispatchQueue.main.async {
|
|
|
+ completion(cachedImage, nil)
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查磁盘缓存
|
|
|
+ if let cachedImage = loadAvatarFromDiskCache(url: url, style: style) {
|
|
|
+ memoryCache.setObject(cachedImage, forKey: cacheKey)
|
|
|
+ DispatchQueue.main.async {
|
|
|
+ completion(cachedImage, nil)
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示占位图
|
|
|
+ DispatchQueue.main.async {
|
|
|
+ completion(placeholder, nil)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从网络加载图片
|
|
|
+ let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
|
|
|
+ guard let self = self,
|
|
|
+ let data = data,
|
|
|
+ error == nil,
|
|
|
+ let response = response as? HTTPURLResponse,
|
|
|
+ response.statusCode == 200,
|
|
|
+ let image = UIImage(data: data) else {
|
|
|
+ DispatchQueue.main.async {
|
|
|
+ completion(nil, error)
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 应用头像样式处理
|
|
|
+ let processedImage = self.processAvatar(image: image, style: style)
|
|
|
+
|
|
|
+ // 保存到缓存
|
|
|
+ self.saveAvatarToCache(image: processedImage, url: url, style: style)
|
|
|
+
|
|
|
+ DispatchQueue.main.async {
|
|
|
+ completion(processedImage, nil)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ task.resume()
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 生成头像缓存键
|
|
|
+ private func generateAvatarCacheKey(url: URL, style: AvatarStyle) -> NSString {
|
|
|
+ let styleKey: String
|
|
|
+ switch style {
|
|
|
+ case .circle:
|
|
|
+ styleKey = "circle"
|
|
|
+ case .rounded(let radius):
|
|
|
+ styleKey = "rounded_\(radius)"
|
|
|
+ }
|
|
|
+ return "\(url.absoluteString)_\(styleKey)" as NSString
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 从磁盘缓存加载头像
|
|
|
+ private func loadAvatarFromDiskCache(url: URL, style: AvatarStyle) -> UIImage? {
|
|
|
+ let fileName = generateAvatarFileName(url: url, style: style)
|
|
|
+ let filePath = "\(diskCachePath)/\(fileName)"
|
|
|
+
|
|
|
+ if let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) {
|
|
|
+ return UIImage(data: data)
|
|
|
+ }
|
|
|
+
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 生成头像文件名
|
|
|
+ private func generateAvatarFileName(url: URL, style: AvatarStyle) -> String {
|
|
|
+ let baseFileName = url.lastPathComponent
|
|
|
+ let styleKey: String
|
|
|
+
|
|
|
+ switch style {
|
|
|
+ case .circle:
|
|
|
+ styleKey = "avatar_circle"
|
|
|
+ case .rounded(let radius):
|
|
|
+ styleKey = "avatar_rounded_\(radius)"
|
|
|
+ }
|
|
|
+
|
|
|
+ return "\(styleKey)_\(baseFileName)"
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 保存头像到缓存
|
|
|
+ private func saveAvatarToCache(image: UIImage, url: URL, style: AvatarStyle) {
|
|
|
+ // 保存到内存缓存
|
|
|
+ let cacheKey = generateAvatarCacheKey(url: url, style: style)
|
|
|
+ memoryCache.setObject(image, forKey: cacheKey)
|
|
|
+
|
|
|
+ // 保存到磁盘缓存
|
|
|
+ DispatchQueue.global(qos: .background).async { [weak self] in
|
|
|
+ guard let self = self else { return }
|
|
|
+
|
|
|
+ // 创建缓存目录(如果不存在)
|
|
|
+ if !FileManager.default.fileExists(atPath: self.diskCachePath) {
|
|
|
+ do {
|
|
|
+ try FileManager.default.createDirectory(atPath: self.diskCachePath, withIntermediateDirectories: true)
|
|
|
+ } catch {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let fileName = self.generateAvatarFileName(url: url, style: style)
|
|
|
+ let filePath = "\(self.diskCachePath)/\(fileName)"
|
|
|
+
|
|
|
+ // 压缩图片(JPEG格式,质量0.8)
|
|
|
+ if let data = image.jpegData(compressionQuality: 0.8) {
|
|
|
+ do {
|
|
|
+ try data.write(to: URL(fileURLWithPath: filePath), options: .atomic)
|
|
|
+ self.updateDiskCacheSize()
|
|
|
+ } catch {
|
|
|
+ //print("保存头像到磁盘失败: \(error)")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 处理头像样式
|
|
|
+ private func processAvatar(image: UIImage, style: AvatarStyle) -> UIImage {
|
|
|
+ // 优化:如果是圆形且图片已经是正方形,直接使用CoreImage滤镜
|
|
|
+ if case .circle = style, image.size.width == image.size.height {
|
|
|
+ return applyCircularFilter(image: image)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 否则使用传统绘图方式
|
|
|
+ return applyAvatarStyle(image: image, style: style)
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 应用圆形滤镜(性能优化)
|
|
|
+ private func applyCircularFilter(image: UIImage) -> UIImage {
|
|
|
+ guard let cgImage = image.cgImage,
|
|
|
+ let filter = CIFilter(name: "CICropToCircle") else {
|
|
|
+ return image
|
|
|
+ }
|
|
|
+
|
|
|
+ let ciImage = CIImage(cgImage: cgImage)
|
|
|
+ filter.setValue(ciImage, forKey: kCIInputImageKey)
|
|
|
+
|
|
|
+ if let outputImage = filter.outputImage,
|
|
|
+ let outputCGImage = CIContext().createCGImage(outputImage, from: outputImage.extent) {
|
|
|
+ return UIImage(cgImage: outputCGImage, scale: image.scale, orientation: image.imageOrientation)
|
|
|
+ }
|
|
|
+
|
|
|
+ return image
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 应用头像样式(传统绘图方式)
|
|
|
+ private func applyAvatarStyle(image: UIImage, style: AvatarStyle) -> UIImage {
|
|
|
+ let size = image.size
|
|
|
+ let rect = CGRect(origin: .zero, size: size)
|
|
|
+ let cornerRadius: CGFloat
|
|
|
+
|
|
|
+ switch style {
|
|
|
+ case .circle:
|
|
|
+ cornerRadius = min(size.width, size.height) / 2
|
|
|
+ case .rounded(let radius):
|
|
|
+ cornerRadius = radius
|
|
|
+ }
|
|
|
+
|
|
|
+ UIGraphicsBeginImageContextWithOptions(size, false, image.scale)
|
|
|
+ defer { UIGraphicsEndImageContext() }
|
|
|
+
|
|
|
+ let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius)
|
|
|
+ path.addClip()
|
|
|
+
|
|
|
+ image.draw(in: rect)
|
|
|
+
|
|
|
+ if let outputImage = UIGraphicsGetImageFromCurrentImageContext() {
|
|
|
+ return outputImage
|
|
|
+ }
|
|
|
+
|
|
|
+ return image
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 更新磁盘缓存大小
|
|
|
+ private func updateDiskCacheSize() {
|
|
|
+ // 保持原有逻辑不变...
|
|
|
+ do {
|
|
|
+ let fileManager = FileManager.default
|
|
|
+ let cacheFiles = try fileManager.contentsOfDirectory(atPath: diskCachePath)
|
|
|
+
|
|
|
+ var totalSize: Int64 = 0
|
|
|
+ for file in cacheFiles {
|
|
|
+ let filePath = "\(diskCachePath)/\(file)"
|
|
|
+ if let attributes = try? fileManager.attributesOfItem(atPath: filePath),
|
|
|
+ let size = attributes[.size] as? Int64 {
|
|
|
+ totalSize += size
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果超过最大缓存大小,清理旧文件
|
|
|
+ if totalSize > maxDiskCacheSize {
|
|
|
+ cleanOldestDiskCacheFiles(untilSize: totalSize - maxDiskCacheSize)
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ //print("更新磁盘缓存大小失败: \(error)")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 清理最旧的磁盘缓存文件
|
|
|
+ private func cleanOldestDiskCacheFiles(untilSize: Int64) {
|
|
|
+ // 保持原有逻辑不变...
|
|
|
+ guard untilSize > 0 else { return }
|
|
|
+
|
|
|
+ do {
|
|
|
+ let fileManager = FileManager.default
|
|
|
+ var files: [(path: String, date: Date)] = []
|
|
|
+
|
|
|
+ // 获取所有缓存文件及其修改日期
|
|
|
+ for file in try fileManager.contentsOfDirectory(atPath: diskCachePath) {
|
|
|
+ let filePath = "\(diskCachePath)/\(file)"
|
|
|
+ if let attributes = try? fileManager.attributesOfItem(atPath: filePath),
|
|
|
+ let date = attributes[.modificationDate] as? Date,
|
|
|
+ let size = attributes[.size] as? Int64,
|
|
|
+ size > 0 {
|
|
|
+ files.append((path: filePath, date: date))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按修改日期排序(最旧的在前)
|
|
|
+ files.sort { $0.date < $1.date }
|
|
|
+
|
|
|
+ // 删除最旧的文件,直到达到目标大小
|
|
|
+ var removedSize: Int64 = 0
|
|
|
+ for file in files {
|
|
|
+ if removedSize >= untilSize { break }
|
|
|
+
|
|
|
+ do {
|
|
|
+ let attributes = try fileManager.attributesOfItem(atPath: file.path)
|
|
|
+ if let size = attributes[.size] as? Int64 {
|
|
|
+ try fileManager.removeItem(atPath: file.path)
|
|
|
+ removedSize += size
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ //print("删除缓存文件失败: \(error)")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ //print("清理磁盘缓存失败: \(error)")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 清理过期的磁盘缓存
|
|
|
+ private func cleanExpiredDiskCache() {
|
|
|
+ // 保持原有逻辑不变...
|
|
|
+ // 这里可以添加清理逻辑,例如删除超过30天的缓存
|
|
|
+ let oneMonthAgo = Date().addingTimeInterval(-30 * 24 * 60 * 60)
|
|
|
+
|
|
|
+ do {
|
|
|
+ let fileManager = FileManager.default
|
|
|
+ for file in try fileManager.contentsOfDirectory(atPath: diskCachePath) {
|
|
|
+ let filePath = "\(diskCachePath)/\(file)"
|
|
|
+ if let attributes = try? fileManager.attributesOfItem(atPath: filePath),
|
|
|
+ let date = attributes[.modificationDate] as? Date,
|
|
|
+ date < oneMonthAgo {
|
|
|
+ try fileManager.removeItem(atPath: filePath)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ //print("清理过期缓存失败: \(error)")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 清除所有缓存
|
|
|
+ func clearAllCache(completion: (() -> Void)? = nil) {
|
|
|
+ // 保持原有逻辑不变...
|
|
|
+ // 清除内存缓存
|
|
|
+ memoryCache.removeAllObjects()
|
|
|
+
|
|
|
+ // 清除磁盘缓存
|
|
|
+ DispatchQueue.global(qos: .background).async { [weak self] in
|
|
|
+ guard let self = self else { return }
|
|
|
+
|
|
|
+ do {
|
|
|
+ if FileManager.default.fileExists(atPath: self.diskCachePath) {
|
|
|
+ try FileManager.default.removeItem(atPath: self.diskCachePath)
|
|
|
+ // 重新创建空目录
|
|
|
+ try FileManager.default.createDirectory(atPath: self.diskCachePath, withIntermediateDirectories: true)
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ //print("清除磁盘缓存失败: \(error)")
|
|
|
+ }
|
|
|
+
|
|
|
+ DispatchQueue.main.async {
|
|
|
+ completion?()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|