MapAmapMarkPictureLoadMananger.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. //
  2. // MapAmapMarkPictureLoadMananger.swift
  3. // map_amap_ios
  4. //
  5. // Created by 诺诺诺的言 on 2025/7/22.
  6. //
  7. import Foundation
  8. import UIKit
  9. // 图片加载进度回调
  10. typealias ImageLoadingProgress = (Double) -> Void
  11. // 图片加载完成回调
  12. typealias ImageCompletion = (UIImage?, Error?) -> Void
  13. /// 头像样式
  14. enum AvatarStyle {
  15. case circle // 圆形
  16. case rounded(radius: CGFloat) // 圆角矩形
  17. }
  18. /// 图片加载工具类(含缓存功能,支持头像专用圆角处理)
  19. class MapAmapMarkPictureLoadMananger {
  20. // 单例模式
  21. static let shared = MapAmapMarkPictureLoadMananger()
  22. // 内存缓存(LRU策略)
  23. private let memoryCache = NSCache<NSString, UIImage>()
  24. // 磁盘缓存路径
  25. private let diskCachePath: String
  26. // 最大内存缓存大小(默认100MB)
  27. private let maxMemoryCacheSize: Int64 = 100 * 1024 * 1024
  28. // 最大磁盘缓存大小(默认200MB)
  29. private let maxDiskCacheSize: Int64 = 200 * 1024 * 1024
  30. // 初始化
  31. private init() {
  32. // 创建磁盘缓存目录
  33. let documentsPath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0]
  34. diskCachePath = "\(documentsPath)/ImageCache"
  35. // 配置内存缓存
  36. memoryCache.countLimit = 100 // 最多缓存100张图片
  37. memoryCache.totalCostLimit = Int(maxMemoryCacheSize)
  38. // 清理过期的磁盘缓存
  39. cleanExpiredDiskCache()
  40. }
  41. /// 加载头像图片(带缓存)
  42. func loadAvatar(
  43. from url: URL,
  44. placeholder: UIImage? = nil,
  45. style: AvatarStyle = .circle,
  46. progress: ImageLoadingProgress? = nil,
  47. completion: @escaping ImageCompletion
  48. ) -> UIImage? {
  49. // 生成缓存键(包含样式信息)
  50. let cacheKey = generateAvatarCacheKey(url: url, style: style)
  51. // 先检查内存缓存
  52. if let cachedImage = memoryCache.object(forKey: cacheKey) {
  53. return cachedImage
  54. }
  55. // 检查磁盘缓存
  56. if let cachedImage = loadAvatarFromDiskCache(url: url, style: style) {
  57. memoryCache.setObject(cachedImage, forKey: cacheKey)
  58. return cachedImage
  59. }
  60. // 显示占位图
  61. completion(placeholder, nil)
  62. return placeholder
  63. }
  64. /// 异步加载头像图片(带进度监听)
  65. func loadAvatarAsync(
  66. from url: URL,
  67. placeholder: UIImage? = nil,
  68. style: AvatarStyle = .circle,
  69. progress: ImageLoadingProgress? = nil,
  70. completion: @escaping ImageCompletion
  71. ) {
  72. // 生成缓存键(包含样式信息)
  73. let cacheKey = generateAvatarCacheKey(url: url, style: style)
  74. // 先检查内存缓存
  75. if let cachedImage = memoryCache.object(forKey: cacheKey) {
  76. DispatchQueue.main.async {
  77. completion(cachedImage, nil)
  78. }
  79. return
  80. }
  81. // 检查磁盘缓存
  82. if let cachedImage = loadAvatarFromDiskCache(url: url, style: style) {
  83. memoryCache.setObject(cachedImage, forKey: cacheKey)
  84. DispatchQueue.main.async {
  85. completion(cachedImage, nil)
  86. }
  87. return
  88. }
  89. // 显示占位图
  90. DispatchQueue.main.async {
  91. completion(placeholder, nil)
  92. }
  93. // 从网络加载图片
  94. let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
  95. guard let self = self,
  96. let data = data,
  97. error == nil,
  98. let response = response as? HTTPURLResponse,
  99. response.statusCode == 200,
  100. let image = UIImage(data: data) else {
  101. DispatchQueue.main.async {
  102. completion(nil, error)
  103. }
  104. return
  105. }
  106. // 应用头像样式处理
  107. let processedImage = self.processAvatar(image: image, style: style)
  108. // 保存到缓存
  109. self.saveAvatarToCache(image: processedImage, url: url, style: style)
  110. DispatchQueue.main.async {
  111. completion(processedImage, nil)
  112. }
  113. }
  114. task.resume()
  115. }
  116. /// 生成头像缓存键
  117. private func generateAvatarCacheKey(url: URL, style: AvatarStyle) -> NSString {
  118. let styleKey: String
  119. switch style {
  120. case .circle:
  121. styleKey = "circle"
  122. case .rounded(let radius):
  123. styleKey = "rounded_\(radius)"
  124. }
  125. return "\(url.absoluteString)_\(styleKey)" as NSString
  126. }
  127. /// 从磁盘缓存加载头像
  128. private func loadAvatarFromDiskCache(url: URL, style: AvatarStyle) -> UIImage? {
  129. let fileName = generateAvatarFileName(url: url, style: style)
  130. let filePath = "\(diskCachePath)/\(fileName)"
  131. if let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) {
  132. return UIImage(data: data)
  133. }
  134. return nil
  135. }
  136. /// 生成头像文件名
  137. private func generateAvatarFileName(url: URL, style: AvatarStyle) -> String {
  138. let baseFileName = url.lastPathComponent
  139. let styleKey: String
  140. switch style {
  141. case .circle:
  142. styleKey = "avatar_circle"
  143. case .rounded(let radius):
  144. styleKey = "avatar_rounded_\(radius)"
  145. }
  146. return "\(styleKey)_\(baseFileName)"
  147. }
  148. /// 保存头像到缓存
  149. private func saveAvatarToCache(image: UIImage, url: URL, style: AvatarStyle) {
  150. // 保存到内存缓存
  151. let cacheKey = generateAvatarCacheKey(url: url, style: style)
  152. memoryCache.setObject(image, forKey: cacheKey)
  153. // 保存到磁盘缓存
  154. DispatchQueue.global(qos: .background).async { [weak self] in
  155. guard let self = self else { return }
  156. // 创建缓存目录(如果不存在)
  157. if !FileManager.default.fileExists(atPath: self.diskCachePath) {
  158. do {
  159. try FileManager.default.createDirectory(atPath: self.diskCachePath, withIntermediateDirectories: true)
  160. } catch {
  161. return
  162. }
  163. }
  164. let fileName = self.generateAvatarFileName(url: url, style: style)
  165. let filePath = "\(self.diskCachePath)/\(fileName)"
  166. // 压缩图片(JPEG格式,质量0.8)
  167. if let data = image.jpegData(compressionQuality: 0.8) {
  168. do {
  169. try data.write(to: URL(fileURLWithPath: filePath), options: .atomic)
  170. self.updateDiskCacheSize()
  171. } catch {
  172. //print("保存头像到磁盘失败: \(error)")
  173. }
  174. }
  175. }
  176. }
  177. /// 处理头像样式
  178. private func processAvatar(image: UIImage, style: AvatarStyle) -> UIImage {
  179. // 优化:如果是圆形且图片已经是正方形,直接使用CoreImage滤镜
  180. if case .circle = style, image.size.width == image.size.height {
  181. return applyCircularFilter(image: image)
  182. }
  183. // 否则使用传统绘图方式
  184. return applyAvatarStyle(image: image, style: style)
  185. }
  186. /// 应用圆形滤镜(性能优化)
  187. private func applyCircularFilter(image: UIImage) -> UIImage {
  188. guard let cgImage = image.cgImage,
  189. let filter = CIFilter(name: "CICropToCircle") else {
  190. return image
  191. }
  192. let ciImage = CIImage(cgImage: cgImage)
  193. filter.setValue(ciImage, forKey: kCIInputImageKey)
  194. if let outputImage = filter.outputImage,
  195. let outputCGImage = CIContext().createCGImage(outputImage, from: outputImage.extent) {
  196. return UIImage(cgImage: outputCGImage, scale: image.scale, orientation: image.imageOrientation)
  197. }
  198. return image
  199. }
  200. /// 应用头像样式(传统绘图方式)
  201. private func applyAvatarStyle(image: UIImage, style: AvatarStyle) -> UIImage {
  202. let size = image.size
  203. let rect = CGRect(origin: .zero, size: size)
  204. let cornerRadius: CGFloat
  205. switch style {
  206. case .circle:
  207. cornerRadius = min(size.width, size.height) / 2
  208. case .rounded(let radius):
  209. cornerRadius = radius
  210. }
  211. UIGraphicsBeginImageContextWithOptions(size, false, image.scale)
  212. defer { UIGraphicsEndImageContext() }
  213. let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius)
  214. path.addClip()
  215. image.draw(in: rect)
  216. if let outputImage = UIGraphicsGetImageFromCurrentImageContext() {
  217. return outputImage
  218. }
  219. return image
  220. }
  221. /// 更新磁盘缓存大小
  222. private func updateDiskCacheSize() {
  223. // 保持原有逻辑不变...
  224. do {
  225. let fileManager = FileManager.default
  226. let cacheFiles = try fileManager.contentsOfDirectory(atPath: diskCachePath)
  227. var totalSize: Int64 = 0
  228. for file in cacheFiles {
  229. let filePath = "\(diskCachePath)/\(file)"
  230. if let attributes = try? fileManager.attributesOfItem(atPath: filePath),
  231. let size = attributes[.size] as? Int64 {
  232. totalSize += size
  233. }
  234. }
  235. // 如果超过最大缓存大小,清理旧文件
  236. if totalSize > maxDiskCacheSize {
  237. cleanOldestDiskCacheFiles(untilSize: totalSize - maxDiskCacheSize)
  238. }
  239. } catch {
  240. //print("更新磁盘缓存大小失败: \(error)")
  241. }
  242. }
  243. /// 清理最旧的磁盘缓存文件
  244. private func cleanOldestDiskCacheFiles(untilSize: Int64) {
  245. // 保持原有逻辑不变...
  246. guard untilSize > 0 else { return }
  247. do {
  248. let fileManager = FileManager.default
  249. var files: [(path: String, date: Date)] = []
  250. // 获取所有缓存文件及其修改日期
  251. for file in try fileManager.contentsOfDirectory(atPath: diskCachePath) {
  252. let filePath = "\(diskCachePath)/\(file)"
  253. if let attributes = try? fileManager.attributesOfItem(atPath: filePath),
  254. let date = attributes[.modificationDate] as? Date,
  255. let size = attributes[.size] as? Int64,
  256. size > 0 {
  257. files.append((path: filePath, date: date))
  258. }
  259. }
  260. // 按修改日期排序(最旧的在前)
  261. files.sort { $0.date < $1.date }
  262. // 删除最旧的文件,直到达到目标大小
  263. var removedSize: Int64 = 0
  264. for file in files {
  265. if removedSize >= untilSize { break }
  266. do {
  267. let attributes = try fileManager.attributesOfItem(atPath: file.path)
  268. if let size = attributes[.size] as? Int64 {
  269. try fileManager.removeItem(atPath: file.path)
  270. removedSize += size
  271. }
  272. } catch {
  273. //print("删除缓存文件失败: \(error)")
  274. }
  275. }
  276. } catch {
  277. //print("清理磁盘缓存失败: \(error)")
  278. }
  279. }
  280. /// 清理过期的磁盘缓存
  281. private func cleanExpiredDiskCache() {
  282. // 保持原有逻辑不变...
  283. // 这里可以添加清理逻辑,例如删除超过30天的缓存
  284. let oneMonthAgo = Date().addingTimeInterval(-30 * 24 * 60 * 60)
  285. do {
  286. let fileManager = FileManager.default
  287. for file in try fileManager.contentsOfDirectory(atPath: diskCachePath) {
  288. let filePath = "\(diskCachePath)/\(file)"
  289. if let attributes = try? fileManager.attributesOfItem(atPath: filePath),
  290. let date = attributes[.modificationDate] as? Date,
  291. date < oneMonthAgo {
  292. try fileManager.removeItem(atPath: filePath)
  293. }
  294. }
  295. } catch {
  296. //print("清理过期缓存失败: \(error)")
  297. }
  298. }
  299. /// 清除所有缓存
  300. func clearAllCache(completion: (() -> Void)? = nil) {
  301. // 保持原有逻辑不变...
  302. // 清除内存缓存
  303. memoryCache.removeAllObjects()
  304. // 清除磁盘缓存
  305. DispatchQueue.global(qos: .background).async { [weak self] in
  306. guard let self = self else { return }
  307. do {
  308. if FileManager.default.fileExists(atPath: self.diskCachePath) {
  309. try FileManager.default.removeItem(atPath: self.diskCachePath)
  310. // 重新创建空目录
  311. try FileManager.default.createDirectory(atPath: self.diskCachePath, withIntermediateDirectories: true)
  312. }
  313. } catch {
  314. //print("清除磁盘缓存失败: \(error)")
  315. }
  316. DispatchQueue.main.async {
  317. completion?()
  318. }
  319. }
  320. }
  321. }