|
|
import UIKit
|
|
|
import Foundation
|
|
|
import Combine
|
|
|
|
|
|
// MARK: - 图片缓存管理器
|
|
|
class ImageCacheManager: ObservableObject {
|
|
|
static let shared = ImageCacheManager()
|
|
|
|
|
|
private let cache = NSCache<NSString, UIImage>()
|
|
|
private let fileManager = FileManager.default
|
|
|
private let documentsPath: String
|
|
|
|
|
|
init() {
|
|
|
// 设置缓存限制
|
|
|
cache.countLimit = 50 // 限制缓存数量
|
|
|
cache.totalCostLimit = 50 * 1024 * 1024 // 限制缓存大小(50MB)
|
|
|
|
|
|
// 获取文档目录路径
|
|
|
documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
|
|
|
|
|
|
// 监听内存警告
|
|
|
NotificationCenter.default.addObserver(
|
|
|
self,
|
|
|
selector: #selector(handleMemoryWarning),
|
|
|
name: UIApplication.didReceiveMemoryWarningNotification,
|
|
|
object: nil
|
|
|
)
|
|
|
}
|
|
|
|
|
|
deinit {
|
|
|
NotificationCenter.default.removeObserver(self)
|
|
|
}
|
|
|
|
|
|
// MARK: - 内存缓存管理
|
|
|
|
|
|
/// 设置图片到内存缓存
|
|
|
func setImage(_ image: UIImage, forKey key: String) {
|
|
|
cache.setObject(image, forKey: key as NSString)
|
|
|
}
|
|
|
|
|
|
/// 从内存缓存获取图片
|
|
|
func getImage(forKey key: String) -> UIImage? {
|
|
|
return cache.object(forKey: key as NSString)
|
|
|
}
|
|
|
|
|
|
/// 从内存缓存移除图片
|
|
|
func removeImage(forKey key: String) {
|
|
|
cache.removeObject(forKey: key as NSString)
|
|
|
}
|
|
|
|
|
|
/// 清空内存缓存
|
|
|
func clearCache() {
|
|
|
cache.removeAllObjects()
|
|
|
print("🧹 图片内存缓存已清空")
|
|
|
}
|
|
|
|
|
|
// MARK: - 文件缓存管理
|
|
|
|
|
|
/// 保存图片到文件系统
|
|
|
func saveImageToFile(_ image: UIImage, withName name: String) -> String? {
|
|
|
let imagePath = (documentsPath as NSString).appendingPathComponent("\(name).jpg")
|
|
|
|
|
|
if let imageData = image.jpegData(compressionQuality: 0.8) {
|
|
|
do {
|
|
|
try imageData.write(to: URL(fileURLWithPath: imagePath))
|
|
|
print("💾 图片已保存到文件: \(imagePath)")
|
|
|
return imagePath
|
|
|
} catch {
|
|
|
print("❌ 保存图片失败: \(error)")
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
/// 从文件系统加载图片
|
|
|
func loadImageFromFile(path: String) -> UIImage? {
|
|
|
return UIImage(contentsOfFile: path)
|
|
|
}
|
|
|
|
|
|
/// 删除文件系统中的图片
|
|
|
func deleteImageFromFile(path: String) {
|
|
|
do {
|
|
|
try fileManager.removeItem(atPath: path)
|
|
|
print("🗑️ 图片文件已删除: \(path)")
|
|
|
} catch {
|
|
|
print("❌ 删除图片文件失败: \(error)")
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/// 清理过期的文件缓存
|
|
|
func cleanupExpiredFiles() {
|
|
|
let imageCachePath = (documentsPath as NSString).appendingPathComponent("ImageCache")
|
|
|
|
|
|
do {
|
|
|
let files = try fileManager.contentsOfDirectory(atPath: imageCachePath)
|
|
|
let currentDate = Date()
|
|
|
|
|
|
for file in files {
|
|
|
let filePath = (imageCachePath as NSString).appendingPathComponent(file)
|
|
|
let attributes = try fileManager.attributesOfItem(atPath: filePath)
|
|
|
|
|
|
if let creationDate = attributes[.creationDate] as? Date {
|
|
|
// 删除7天前的文件
|
|
|
if currentDate.timeIntervalSince(creationDate) > 7 * 24 * 60 * 60 {
|
|
|
try fileManager.removeItem(atPath: filePath)
|
|
|
print("🗑️ 删除过期文件: \(file)")
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
} catch {
|
|
|
print("❌ 清理过期文件失败: \(error)")
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// MARK: - 智能缓存管理
|
|
|
|
|
|
/// 智能获取图片(先查内存缓存,再查文件缓存)
|
|
|
func getImageIntelligently(forKey key: String) -> UIImage? {
|
|
|
// 1. 先查内存缓存
|
|
|
if let cachedImage = getImage(forKey: key) {
|
|
|
return cachedImage
|
|
|
}
|
|
|
|
|
|
// 2. 再查文件缓存
|
|
|
let imagePath = (documentsPath as NSString).appendingPathComponent("\(key).jpg")
|
|
|
if let fileImage = loadImageFromFile(path: imagePath) {
|
|
|
// 加载到内存缓存
|
|
|
setImage(fileImage, forKey: key)
|
|
|
return fileImage
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
/// 智能保存图片(同时保存到内存和文件)
|
|
|
func saveImageIntelligently(_ image: UIImage, forKey key: String) {
|
|
|
// 1. 保存到内存缓存
|
|
|
setImage(image, forKey: key)
|
|
|
|
|
|
// 2. 保存到文件缓存
|
|
|
_ = saveImageToFile(image, withName: key)
|
|
|
}
|
|
|
|
|
|
// MARK: - 内存警告处理
|
|
|
|
|
|
@objc private func handleMemoryWarning() {
|
|
|
print("🚨 收到内存警告,清理图片缓存")
|
|
|
clearCache()
|
|
|
}
|
|
|
|
|
|
// MARK: - 缓存统计
|
|
|
|
|
|
/// 获取缓存统计信息
|
|
|
func getCacheStatistics() -> (memoryCount: Int, memorySize: String, fileCount: Int) {
|
|
|
let memoryCount = cache.totalCostLimit > 0 ? cache.totalCostLimit : 0
|
|
|
let memorySize = ByteCountFormatter.string(fromByteCount: Int64(memoryCount), countStyle: .memory)
|
|
|
|
|
|
let imageCachePath = (documentsPath as NSString).appendingPathComponent("ImageCache")
|
|
|
var fileCount = 0
|
|
|
|
|
|
do {
|
|
|
let files = try fileManager.contentsOfDirectory(atPath: imageCachePath)
|
|
|
fileCount = files.count
|
|
|
} catch {
|
|
|
fileCount = 0
|
|
|
}
|
|
|
|
|
|
return (memoryCount, memorySize, fileCount)
|
|
|
}
|
|
|
|
|
|
/// 打印缓存统计信息
|
|
|
func printCacheStatistics() {
|
|
|
let stats = getCacheStatistics()
|
|
|
print("📊 图片缓存统计:")
|
|
|
print(" - 内存缓存数量: \(stats.memoryCount)")
|
|
|
print(" - 内存缓存大小: \(stats.memorySize)")
|
|
|
print(" - 文件缓存数量: \(stats.fileCount)")
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// MARK: - 图片压缩工具
|
|
|
extension ImageCacheManager {
|
|
|
|
|
|
/// 压缩图片到指定大小
|
|
|
func compressImage(_ image: UIImage, maxSize: CGSize) -> UIImage {
|
|
|
let originalSize = image.size
|
|
|
|
|
|
// 如果图片已经小于最大尺寸,直接返回
|
|
|
if originalSize.width <= maxSize.width && originalSize.height <= maxSize.height {
|
|
|
return image
|
|
|
}
|
|
|
|
|
|
// 计算缩放比例
|
|
|
let scaleX = maxSize.width / originalSize.width
|
|
|
let scaleY = maxSize.height / originalSize.height
|
|
|
let scale = min(scaleX, scaleY)
|
|
|
|
|
|
let newSize = CGSize(width: originalSize.width * scale, height: originalSize.height * scale)
|
|
|
|
|
|
// 重新渲染图片
|
|
|
let renderer = UIGraphicsImageRenderer(size: newSize)
|
|
|
return renderer.image { context in
|
|
|
image.draw(in: CGRect(origin: .zero, size: newSize))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/// 压缩图片到指定文件大小
|
|
|
func compressImageToFileSize(_ image: UIImage, targetSizeInKB: Double) -> UIImage {
|
|
|
let targetSizeInBytes = Int64(targetSizeInKB * 1024)
|
|
|
|
|
|
// 尝试不同的压缩质量
|
|
|
let compressionQualities: [CGFloat] = [0.8, 0.6, 0.4, 0.2, 0.1, 0.05]
|
|
|
|
|
|
for quality in compressionQualities {
|
|
|
if let imageData = image.jpegData(compressionQuality: quality) {
|
|
|
let dataSize = Int64(imageData.count)
|
|
|
|
|
|
if dataSize <= targetSizeInBytes {
|
|
|
if let compressedImage = UIImage(data: imageData) {
|
|
|
print("✅ 图片压缩成功: \(dataSize) bytes (质量: \(quality))")
|
|
|
return compressedImage
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 如果JPEG压缩仍然太大,尝试缩小尺寸
|
|
|
print("⚠️ JPEG压缩后仍超过目标大小,尝试缩小尺寸")
|
|
|
return compressImageByReducingSize(image, targetSizeInBytes: targetSizeInBytes)
|
|
|
}
|
|
|
|
|
|
/// 通过缩小尺寸来压缩图片
|
|
|
private func compressImageByReducingSize(_ image: UIImage, targetSizeInBytes: Int64) -> UIImage {
|
|
|
let originalSize = image.size
|
|
|
let originalWidth = originalSize.width
|
|
|
let originalHeight = originalSize.height
|
|
|
|
|
|
// 计算当前图片的内存大小
|
|
|
let currentMemorySize = Int64(originalWidth * originalHeight * 4) // 假设RGBA格式
|
|
|
|
|
|
// 计算需要的缩放比例
|
|
|
let scaleFactor = sqrt(Double(targetSizeInBytes) / Double(currentMemorySize))
|
|
|
let newWidth = max(originalWidth * CGFloat(scaleFactor), 40) // 最小40像素
|
|
|
let newHeight = max(originalHeight * CGFloat(scaleFactor), 40)
|
|
|
|
|
|
let newSize = CGSize(width: newWidth, height: newHeight)
|
|
|
|
|
|
// 重新渲染到新尺寸
|
|
|
let renderer = UIGraphicsImageRenderer(size: newSize)
|
|
|
let resizedImage = renderer.image { context in
|
|
|
image.draw(in: CGRect(origin: .zero, size: newSize))
|
|
|
}
|
|
|
|
|
|
// 再次尝试JPEG压缩
|
|
|
if let imageData = resizedImage.jpegData(compressionQuality: 0.3) {
|
|
|
let finalSize = Int64(imageData.count)
|
|
|
print("✅ 通过缩小尺寸压缩成功: \(finalSize) bytes (新尺寸: \(newWidth) x \(newHeight))")
|
|
|
|
|
|
if let finalImage = UIImage(data: imageData) {
|
|
|
return finalImage
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 如果还是太大,返回最小尺寸的图片
|
|
|
print("⚠️ 无法压缩到目标大小,返回最小尺寸图片")
|
|
|
let minSize = CGSize(width: 40, height: 40)
|
|
|
let rendererMin = UIGraphicsImageRenderer(size: minSize)
|
|
|
return rendererMin.image { context in
|
|
|
image.draw(in: CGRect(origin: .zero, size: minSize))
|
|
|
}
|
|
|
}
|
|
|
}
|