import Flutter import StoreKit import Photos import UIKit public class ClassifyPhotoPlugin : NSObject, FlutterPlugin { var photoClassifier = ClassifyPhoto() public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "classify_photo", binaryMessenger: registrar.messenger()) let instance = ClassifyPhotoPlugin() registrar.addMethodCallDelegate(instance, channel: channel) } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { print("iOS: Received method call: \(call.method)") switch call.method { case "getScreenshots": self.getScreenshots(flutterResult: result) case "getBlurryPhotos": self.getBlurryPhotos(flutterResult: result) case "getPeoplePhotos" : self.getPeoplePhotos(flutterResult: result) case "getSimilarPhotos": self.getSimilarPhotos(flutterResult: result) case "getPhoto": self.getPhoto(flutterResult: result) case "getStorageInfo": getStorageInfo(result: result) case "getExifInfo": guard let args = call.arguments as? [String: Any], let filePath = args["filePath"] as? String else { result(FlutterError( code: "INVALID_ARGUMENTS", message: "Missing filePath parameter", details: nil )) return } getExifInfo(filePath: filePath, completion: result) case "getPhotosSize": guard let args = call.arguments as? [String: Any], let assetIds = args["assetIds"] as? [String] else { result(FlutterError( code: "INVALID_ARGUMENTS", message: "Missing filePath parameter", details: nil )) return } calculatePhotosSize(assetIds: assetIds, completion: result) case "getPlatformVersion": result("iOS " + UIDevice.current.systemVersion) default: result(FlutterMethodNotImplemented) } } private class func blankof(type:T.Type) -> T { let ptr = UnsafeMutablePointer.allocate(capacity: MemoryLayout.size) let val = ptr.pointee return val } /// 磁盘总大小 private class func getTotalDiskSize() -> Int64 { var fs = blankof(type: statfs.self) if statfs("/var",&fs) >= 0{ return Int64(UInt64(fs.f_bsize) * fs.f_blocks) } return -1 } private func getStorageInfo(result: @escaping FlutterResult) { DispatchQueue.global(qos: .userInitiated).async { var storageInfo: [String: Int64] = [:] // 获取总容量和可用容量 if let space = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory()) { let totalSpace = ClassifyPhotoPlugin.getTotalDiskSize() let freeSpace = space[.systemFreeSize] as? Int64 ?? 0 storageInfo["totalSpace"] = totalSpace storageInfo["freeSpace"] = freeSpace storageInfo["usedSpace"] = totalSpace - freeSpace } // 获取照片占用的空间 let options = PHFetchOptions() let allPhotos = PHAsset.fetchAssets(with: .image, options: options) var photoSize: Int64 = 0 let group = DispatchGroup() let queue = DispatchQueue(label: "com.app.photosize", attributes: .concurrent) let semaphore = DispatchSemaphore(value: 5) // 限制并发 allPhotos.enumerateObjects { (asset, index, stop) in group.enter() semaphore.wait() queue.async { let resources = PHAssetResource.assetResources(for: asset) if let resource = resources.first { if let unsignedInt64 = resource.value(forKey: "fileSize") as? CLong { photoSize += Int64(unsignedInt64) } } semaphore.signal() group.leave() } } group.notify(queue: .main) { storageInfo["photoSpace"] = photoSize result(storageInfo) } } } // 获取截图 private func getScreenshots(flutterResult: @escaping FlutterResult) { DispatchQueue.global(qos: .userInitiated).async { [weak self] in guard let self = self else { return } let fetchOptions = PHFetchOptions() let allPhotos = PHAsset.fetchAssets(with: .image, options: fetchOptions) // 只处理截图 self.photoClassifier.fetchScreenshots(from: allPhotos) { screenshots in // 清理内存 self.cleanupMemory() self.photoClassifier.calculateAssetsSize(screenshots) { sizeInfo in self.processPhotoGroup( assets: screenshots, groupName: "screenshots", sizeInfo: sizeInfo ) { groupData in // 再次清理内存 self.cleanupMemory() DispatchQueue.main.async { if !groupData.isEmpty { flutterResult([["group": groupData, "type": "screenshots"]]) } else { flutterResult([]) } } } } } } } // 获取模糊照片 private func getBlurryPhotos(flutterResult: @escaping FlutterResult) { DispatchQueue.global(qos: .userInitiated).async { [weak self] in guard let self = self else { return } let fetchOptions = PHFetchOptions() let allPhotos = PHAsset.fetchAssets(with: .image, options: fetchOptions) // 只处理模糊照片 self.photoClassifier.detectBlurryPhotos(from: allPhotos) { blurryPhotos in // 清理内存 self.cleanupMemory() self.photoClassifier.calculateAssetsSize(blurryPhotos) { sizeInfo in self.processPhotoGroup( assets: blurryPhotos, groupName: "blurry", sizeInfo: sizeInfo ) { groupData in // 再次清理内存 self.cleanupMemory() DispatchQueue.main.async { if !groupData.isEmpty { flutterResult([["group": groupData, "type": "blurry"]]) } else { flutterResult([]) } } } } } } } private func getPeoplePhotos(flutterResult: @escaping FlutterResult) { DispatchQueue.global(qos: .userInitiated).async { [weak self] in guard let self = self else { return } let fetchOptions = PHFetchOptions() // 限制处理的照片数量,提高性能 fetchOptions.fetchLimit = 1000 let allPhotos = PHAsset.fetchAssets(with: .image, options: fetchOptions) // 显示进度 DispatchQueue.main.async { print("开始处理人物照片,总数: \(allPhotos.count)") } // 只处理人物照片 self.photoClassifier.classifyByPeople(assets: allPhotos) { peopleGroups in // 清理内存 self.cleanupMemory() let peopleAssets = Array(peopleGroups.values.flatMap { $0 }) // 显示找到的人脸照片数量 DispatchQueue.main.async { print("找到包含人脸的照片: \(peopleAssets.count)") } self.photoClassifier.calculateAssetsSize(peopleAssets) { sizeInfo in let resultData = Atomic<[[String: Any]]>([]) // 分批处理人物组,避免一次性处理太多数据 let processingQueue = DispatchQueue(label: "com.yourapp.peopleProcessing") let group = DispatchGroup() // 处理每个人物组 for (personName, personPhotos) in peopleGroups { if personPhotos.isEmpty { continue } // 限制每组处理的照片数量 let limitedPhotos = Array(personPhotos.prefix(500)) group.enter() processingQueue.async { autoreleasepool { self.processPhotoGroup( assets: limitedPhotos, groupName: personName, sizeInfo: sizeInfo ) { groupData in if !groupData.isEmpty { resultData.mutate { $0.append([ "group": groupData, "type": "people", "name": personName ]) } } group.leave() } } } } group.notify(queue: .main) { // 最终清理内存 self.cleanupMemory() flutterResult(resultData.value) } } } } } // 获取人物照片 // private func getPeoplePhotos(flutterResult: @escaping FlutterResult) { // DispatchQueue.global(qos: .userInitiated).async { [weak self] in // guard let self = self else { return } // // let fetchOptions = PHFetchOptions() // let allPhotos = PHAsset.fetchAssets(with: .image, options: fetchOptions) // // // 只处理人物照片 // self.photoClassifier.classifyByPeople(assets: allPhotos) { peopleGroups in // // 清理内存 // self.cleanupMemory() // // let peopleAssets = Array(peopleGroups.values.flatMap { $0 }) // self.photoClassifier.calculateAssetsSize(peopleAssets) { sizeInfo in // // let resultData = Atomic<[[String: Any]]>([]) // let processingQueue = DispatchQueue(label: "com.yourapp.peopleProcessing") // let group = DispatchGroup() // // // 处理每个人物组 // for (personName, personPhotos) in peopleGroups { // if personPhotos.isEmpty { continue } // // group.enter() // processingQueue.async { // autoreleasepool { // self.processPhotoGroup( // assets: personPhotos, // groupName: personName, // sizeInfo: sizeInfo // ) { groupData in // if !groupData.isEmpty { // resultData.mutate { $0.append([ // "group": groupData, // "type": "people", // "name": personName // ]) } // } // group.leave() // } // } // } // } // // group.notify(queue: .main) { // // 最终清理内存 // self.cleanupMemory() // flutterResult(resultData.value) // } // } // } // } // } // 添加内存清理方法 private func cleanupMemory() { // 强制清理内存 autoreleasepool { // 触发内存警告,促使系统回收内存 UIApplication.shared.performSelector(onMainThread: #selector(UIApplication.beginIgnoringInteractionEvents), with: nil, waitUntilDone: true) UIApplication.shared.performSelector(onMainThread: #selector(UIApplication.endIgnoringInteractionEvents), with: nil, waitUntilDone: true) } } // 获取相似照片 private func getSimilarPhotos(flutterResult: @escaping FlutterResult) { DispatchQueue.global(qos: .userInitiated).async { [weak self] in guard let self = self else { return } let fetchOptions = PHFetchOptions() let allPhotos = PHAsset.fetchAssets(with: .image, options: fetchOptions) // 只处理相似照片 self.photoClassifier.detectSimilarPhotos( assets: allPhotos, progressHandler: { (stage, progress) in print("Similar Photos Progress: \(stage) - \(progress)") }, completion: { similarGroups in // 清理内存 self.cleanupMemory() // 限制处理的组数,避免内存过载 let maxGroupsToProcess = min(50, similarGroups.count) let limitedGroups = Array(similarGroups.prefix(maxGroupsToProcess)) let similarAssets = Array(limitedGroups.flatMap { $0 }) self.photoClassifier.calculateAssetsSize(similarAssets) { sizeInfo in let resultData = Atomic<[[String: Any]]>([]) // 分批处理照片组,每批处理少量组 let batchSize = 5 let totalBatches = Int(ceil(Double(limitedGroups.count) / Double(batchSize))) self.processSimilarPhotoGroupsInBatches( groups: limitedGroups, batchIndex: 0, totalBatches: totalBatches, batchSize: batchSize, sizeInfo: sizeInfo, resultData: resultData ) { // 所有批次处理完成后的回调 // 最终清理内存 self.cleanupMemory() flutterResult(resultData.value) } } } ) } } // 添加分批处理照片组的方法 private func processSimilarPhotoGroupsInBatches( groups: [[PHAsset]], batchIndex: Int, totalBatches: Int, batchSize: Int, sizeInfo: ClassifyPhoto.PhotoSizeInfo, resultData: Atomic<[[String: Any]]>, completion: @escaping () -> Void ) { // 检查是否处理完所有批次 if batchIndex >= totalBatches { completion() return } // 计算当前批次的范围 let startIndex = batchIndex * batchSize let endIndex = min(startIndex + batchSize, groups.count) let currentBatchGroups = groups[startIndex..([]) // 创建一个函数来依次处理每个组 func processGroups(index: Int, groups: [(assets: [PHAsset], name: String, type: String, sizeInfo: ClassifyPhoto.PhotoSizeInfo)]) { // 检查是否处理完所有组 if index >= groups.count { // 所有组处理完毕,返回结果 DispatchQueue.main.async { print("Final result count: \(resultData.value.count)") flutterResult(resultData.value) } return } let currentGroup = groups[index] // 处理当前组 self.processPhotoGroup( assets: currentGroup.assets, groupName: currentGroup.name, sizeInfo: currentGroup.sizeInfo ) { groupData in // 使用自动释放池减少内存占用 autoreleasepool { if !groupData.isEmpty { resultData.mutate { $0.append([ "group": groupData, "type": currentGroup.type, currentGroup.type == "location" ? "name" : "" : currentGroup.name ].filter { !($0.value is String && $0.value as! String == "") }) } } // 手动触发内存清理 self.cleanupMemory() // 延迟一小段时间,让系统有机会回收内存 DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { // 处理下一个组 processGroups(index: index + 1, groups: groups) } } } } // 准备所有需要处理的组 var allGroups: [(assets: [PHAsset], name: String, type: String, sizeInfo: ClassifyPhoto.PhotoSizeInfo)] = [] // 添加截图组 if !result.screenshots.isEmpty { allGroups.append((result.screenshots, "screenshots", "screenshots", result.screenshotsSize)) } // 添加相似照片组 for photoGroup in result.similarPhotos { if !photoGroup.isEmpty { allGroups.append((photoGroup, "similar", "similar", result.similarPhotosSize)) } } // 添加模糊照片组 if !result.blurryPhotos.isEmpty { allGroups.append((result.blurryPhotos, "blurry", "blurry", result.blurryPhotosSize)) } // 添加人物照片组 for (personName, personPhotos) in result.people { if !personPhotos.isEmpty { allGroups.append((personPhotos, personName, "people", result.peopleSize)) } } // 开始处理第一个组 if allGroups.isEmpty { DispatchQueue.main.async { print("No groups to process") flutterResult([]) } } else { processGroups(index: 0, groups: allGroups) } } ) // self.photoClassifier.classifyPhotos( // assets: allPhotos, // progressHandler: { (stage, progress) in // print("Progress: \(stage) - \(progress)") // }, // completion: { result in // var resultData: [[String: Any]] = [] // let mainGroup = DispatchGroup() // // // 处理截图 // mainGroup.enter() // self.processPhotoGroup(assets: result.screenshots, groupName: "screenshots", sizeInfo: result.screenshotsSize) { groupData in // if !groupData.isEmpty { // resultData.append(["group": groupData, "type": "screenshots"]) // } // mainGroup.leave() // } // // // 处理相似照片组 // for photoGroup in result.similarPhotos { // mainGroup.enter() // self.processPhotoGroup(assets: photoGroup, groupName: "similar", sizeInfo: result.similarPhotosSize) { groupData in // if !groupData.isEmpty { // resultData.append(["group": groupData, "type": "similar"]) // } // mainGroup.leave() // } // } // // // 处理地点分组 //// for (location, assets) in result.locations { //// mainGroup.enter() //// self.processPhotoGroup(assets: assets, groupName: location, sizeInfo: result.locationsSize) { groupData in //// if !groupData.isEmpty { //// resultData.append(["group": groupData, "type": "location", "name": location]) //// } //// mainGroup.leave() //// } //// } // // // 处理人物分组 //// for (person, assets) in result.people { //// mainGroup.enter() //// self.processPhotoGroup(assets: assets, groupName: person, sizeInfo: result.peopleSize) { groupData in //// if !groupData.isEmpty { //// resultData.append(["group": groupData, "type": "people"]) //// } //// mainGroup.leave() //// } //// } // // // 处理模糊照片 // mainGroup.enter() // self.processPhotoGroup(assets: result.blurryPhotos, groupName: "blurry", sizeInfo: result.blurryPhotosSize) { groupData in // if !groupData.isEmpty { // resultData.append(["group": groupData, "type": "blurry"]) // } // mainGroup.leave() // } // // mainGroup.notify(queue: .main) { // print("Final result count: \(resultData.count)") // flutterResult(resultData) // } // } // ) } } // // 添加内存清理辅助方法 // private func cleanupMemory() { // // 清理图像缓存 // URLCache.shared.removeAllCachedResponses() // // // 强制进行一次垃圾回收 // autoreleasepool { // let _ = [String](repeating: "temp", count: 1) // } // // #if os(iOS) // // 发送低内存警告 // UIApplication.shared.perform(Selector(("_performMemoryWarning"))) // #endif // } // 添加内存清理方法 // private func cleanupMemory() { // // 强制清理内存 // autoreleasepool { // // 触发内存警告,促使系统回收内存 // UIApplication.shared.performSelector(onMainThread: #selector(UIApplication.beginIgnoringInteractionEvents), with: nil, waitUntilDone: true) // UIApplication.shared.performSelector(onMainThread: #selector(UIApplication.endIgnoringInteractionEvents), with: nil, waitUntilDone: true) // } // } // 处理照片组的辅助方法 private func processPhotoGroup( assets: [PHAsset], groupName: String, sizeInfo: ClassifyPhoto.PhotoSizeInfo, completion: @escaping ([String: Any]) -> Void ) { let photoProcessGroup = DispatchGroup() var photosData: [[String: Any]] = [] for asset in assets { photoProcessGroup.enter() let options = PHContentEditingInputRequestOptions() options.isNetworkAccessAllowed = false DispatchQueue.global(qos: .background).async { asset.requestContentEditingInput(with: options) { (input, info) in defer { photoProcessGroup.leave() } if let input = input, let url = input.fullSizeImageURL { let photoInfo: [String: Any] = [ // "path": url.path, "id": asset.localIdentifier, // "width": asset.pixelWidth, // "height": asset.pixelHeight, // "creationDate": asset.creationDate?.timeIntervalSince1970 ?? 0 ] photosData.append(photoInfo) } } } } photoProcessGroup.notify(queue: .main) { completion([ "photos": photosData, "totalSize": sizeInfo.totalSize, "count": sizeInfo.count ]) } } private func getExifInfo(filePath: String, completion: @escaping FlutterResult) { // 创建文件 URL let fileURL: URL if filePath.starts(with: "file://") { guard let url = URL(string: filePath) else { print("Invalid URL string: \(filePath)") completion([:]) return } fileURL = url } else { fileURL = URL(fileURLWithPath: filePath) } // 检查文件是否存在 guard FileManager.default.fileExists(atPath: fileURL.path) else { print("File does not exist at path: \(fileURL.path)") completion([:]) return } // 创建图片源 guard let imageSource = CGImageSourceCreateWithURL(fileURL as CFURL, nil) else { print("Failed to create image source for path: \(fileURL.path)") completion([:]) return } var exifInfo: [String: Any] = [:] // 获取所有元数据 if let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [String: Any] { // EXIF 数据 if let exif = imageProperties[kCGImagePropertyExifDictionary as String] as? [String: Any] { exifInfo["aperture"] = exif[kCGImagePropertyExifFNumber as String] exifInfo["exposureTime"] = exif[kCGImagePropertyExifExposureTime as String] exifInfo["iso"] = exif[kCGImagePropertyExifISOSpeedRatings as String] exifInfo["focalLength"] = exif[kCGImagePropertyExifFocalLength as String] exifInfo["dateTimeOriginal"] = exif[kCGImagePropertyExifDateTimeOriginal as String] } // TIFF 数据 if let tiff = imageProperties[kCGImagePropertyTIFFDictionary as String] as? [String: Any] { exifInfo["make"] = tiff[kCGImagePropertyTIFFMake as String] exifInfo["model"] = tiff[kCGImagePropertyTIFFModel as String] } // GPS 数据 if let gps = imageProperties[kCGImagePropertyGPSDictionary as String] as? [String: Any] { exifInfo["latitude"] = gps[kCGImagePropertyGPSLatitude as String] exifInfo["longitude"] = gps[kCGImagePropertyGPSLongitude as String] exifInfo["altitude"] = gps[kCGImagePropertyGPSAltitude as String] } // 图片基本信息 exifInfo["pixelWidth"] = imageProperties[kCGImagePropertyPixelWidth as String] exifInfo["pixelHeight"] = imageProperties[kCGImagePropertyPixelHeight as String] exifInfo["dpi"] = imageProperties[kCGImagePropertyDPIHeight as String] exifInfo["colorModel"] = imageProperties[kCGImagePropertyColorModel as String] exifInfo["profileName"] = imageProperties[kCGImagePropertyProfileName as String] } completion(exifInfo) } } // 计算选择的图片大小 extension ClassifyPhotoPlugin { // private func calculatePhotosSize(assetIds: [String], completion: @escaping FlutterResult) { // // 使用与调用者相同的QoS级别 // let callerQoS: DispatchQoS = .userInitiated // // DispatchQueue.global(qos: callerQoS.qosClass).async { // let assets = PHAsset.fetchAssets(withLocalIdentifiers: assetIds, options: nil) // // // 使用原子操作确保线程安全 // let totalSize = Atomic(0) // let processedCount = Atomic(0) // // // 创建一个与调用者相同QoS的组 // let processingGroup = DispatchGroup() // // // 创建一个与调用者相同QoS的队列 // let processingQueue = DispatchQueue(label: "com.yourapp.photosize.processing", // qos: callerQoS, // attributes: .concurrent) // // // 创建一个与调用者相同QoS的信号量队列 // let semaphoreQueue = DispatchQueue(label: "com.yourapp.photosize.semaphore", // qos: callerQoS) // // // 控制并发数量 // let maxConcurrent = 4 // var activeWorkers = 0 // // // 分批处理资源 // let batchSize = 20 // let totalCount = assets.count // // // 如果没有资产,直接返回 // if totalCount == 0 { // DispatchQueue.main.async { // completion(0) // } // return // } // // // 创建一个函数来处理下一个批次 // @Sendable func processNextBatch() { // // 计算下一个批次的范围 // let currentProcessed = processedCount.value // if currentProcessed >= totalCount { // // 所有批次已处理完毕 // return // } // // let batchStart = currentProcessed // let batchEnd = min(batchStart + batchSize, totalCount) // // // 更新已处理计数 // processedCount.mutate { $0 = batchEnd } // // processingGroup.enter() // processingQueue.async { // var batchSize: Int64 = 0 // // for i in batchStart..(0) // let processedCount = Atomic(0) // // // 创建一个与调用者相同QoS的组 // let processingGroup = DispatchGroup() // // // 创建一个与调用者相同QoS的队列 // let processingQueue = DispatchQueue(label: "com.yourapp.photosize.processing", // qos: .userInitiated, // attributes: .concurrent) // // // 遍历所有资产 // for i in 0..(0) // // // 创建一个与调用者相同QoS的组 // let processingGroup = DispatchGroup() // // // 创建一个与调用者相同QoS的队列,确保所有操作都有明确的QoS // let processingQueue = DispatchQueue(label: "com.yourapp.photosize.processing", // qos: .userInitiated, // attributes: .concurrent) // // // 控制并发数量 // let semaphore = DispatchSemaphore(value: 4) // // // 分批处理资源 // let batchSize = 20 // let totalCount = assets.count // // // 如果没有资产,直接返回 // if totalCount == 0 { // DispatchQueue.main.async { // completion(0) // } // return // } // // for batchStart in stride(from: 0, to: totalCount, by: batchSize) { // let end = min(batchStart + batchSize, totalCount) // // processingGroup.enter() // // 确保使用明确QoS的队列 // processingQueue.async { // autoreleasepool { // var batchSize: Int64 = 0 // // for i in batchStart..(0) // let processingGroup = DispatchGroup() // // // 创建一个具有相同QoS的调度队列用于信号量操作 // let processingQueue = DispatchQueue(label: "com.yourapp.photosize.processing", // qos: .userInitiated, // attributes: .concurrent) // // // 控制并发数量 // let semaphore = DispatchSemaphore(value: 4) // // // 分批处理资源 // let batchSize = 20 // let totalCount = assets.count // // for batchStart in stride(from: 0, to: totalCount, by: batchSize) { // let end = min(batchStart + batchSize, totalCount) // // processingGroup.enter() // // 使用具有明确QoS的队列 // processingQueue.async { // var batchSize: Int64 = 0 // // for i in batchStart.. Bool { do { // 获取产品信息 let productIds = ["clean.vip.1week"] let products = try await Product.products(for: Set(productIds)) // 检查第一个产品的试用资格 if let product = products.first { return await product.subscription?.isEligibleForIntroOffer ?? false } return false } catch { print("Error checking trial eligibility: \(error)") return false } } }