| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198 |
- 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<T>(type:T.Type) -> T {
- let ptr = UnsafeMutablePointer<T>.allocate(capacity: MemoryLayout<T>.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..<endIndex]
-
- let processingQueue = DispatchQueue(label: "com.yourapp.similarProcessing.batch\(batchIndex)")
- let group = DispatchGroup()
-
- // 处理当前批次的照片组
- for (index, photoGroup) in currentBatchGroups.enumerated() {
- if photoGroup.isEmpty { continue }
-
- group.enter()
- processingQueue.async {
- autoreleasepool {
- self.processPhotoGroup(
- assets: photoGroup,
- groupName: "similar_\(startIndex + index)",
- sizeInfo: sizeInfo
- ) { groupData in
- if !groupData.isEmpty {
- resultData.mutate { $0.append([
- "group": groupData,
- "type": "similar"
- ]) }
- }
- group.leave()
- }
- }
- }
- }
-
- // 当前批次处理完成后
- group.notify(queue: .global()) {
- // 清理内存
- self.cleanupMemory()
-
- // 延迟一小段时间再处理下一批,给系统一些恢复时间
- DispatchQueue.global().asyncAfter(deadline: .now() + 0.3) {
- // 递归处理下一批
- self.processSimilarPhotoGroupsInBatches(
- groups: groups,
- batchIndex: batchIndex + 1,
- totalBatches: totalBatches,
- batchSize: batchSize,
- sizeInfo: sizeInfo,
- resultData: resultData,
- completion: completion
- )
- }
- }
- }
-
- private func getPhoto(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.classifyPhotos(
- assets: allPhotos,
- progressHandler: { (stage, progress) in
- print("Progress: \(stage) - \(progress)")
- },
- completion: { result in
- // 使用串行队列处理结果,避免同时处理多个组
- let processingQueue = DispatchQueue(label: "com.yourapp.photoProcessing")
- let resultData = Atomic<[[String: Any]]>([])
-
- // 创建一个函数来依次处理每个组
- 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<Int64>(0)
- // let processedCount = Atomic<Int>(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..<batchEnd {
- // autoreleasepool {
- // let asset = assets.object(at: i)
- //
- // // 使用资源管理器获取大小
- // PHAssetResource.assetResources(for: asset).forEach { resource in
- // if let fileSize = resource.value(forKey: "fileSize") as? CLong {
- // batchSize += Int64(fileSize)
- // }
- // }
- // }
- // }
- //
- // // 更新总大小
- // totalSize.mutate { $0 += batchSize }
- //
- // // 处理下一个批次
- // semaphoreQueue.async {
- // activeWorkers -= 1
- // scheduleMoreWorkIfNeeded()
- // }
- //
- // processingGroup.leave()
- // }
- // }
- //
- // // 调度更多工作如果需要
- // func scheduleMoreWorkIfNeeded() {
- // while activeWorkers < maxConcurrent && processedCount.value < totalCount {
- // activeWorkers += 1
- // processNextBatch()
- // }
- // }
- //
- // // 开始处理
- // semaphoreQueue.async {
- // scheduleMoreWorkIfNeeded()
- // }
- //
- // // 使用带超时的等待,避免无限期阻塞
- // let waitResult = processingGroup.wait(timeout: .now() + 30)
- //
- // // 返回结果到主线程
- // DispatchQueue.main.async {
- // if waitResult == .timedOut {
- // print("警告: 处理照片大小超时")
- // completion(FlutterError(code: "TIMEOUT",
- // message: "计算照片大小超时",
- // details: nil))
- // } else {
- // completion(totalSize.value)
- // }
- // }
- // }
- // }
-
- private func calculatePhotosSize(assetIds: [String], completion: @escaping FlutterResult) {
-
- // 使用与调用者相同的QoS级别
- DispatchQueue.global(qos: .userInitiated).async {
- let assets = PHAsset.fetchAssets(withLocalIdentifiers: assetIds, options: nil)
-
- // 将 PHFetchResult 转换为数组
- var assetArray: [PHAsset] = []
- assets.enumerateObjects { (asset, _, _) in
- assetArray.append(asset)
- }
-
- // 使用 ClassifyPhoto 中的方法计算大小
- self.photoClassifier.calculateAssetsSize(assetArray) { sizeInfo in
- DispatchQueue.main.async {
- // 返回总大小
- completion(sizeInfo.totalSize)
- }
- }
- }
-
- // // 使用与调用者相同的QoS级别
- // DispatchQueue.global(qos: .userInitiated).async {
- // let assets = PHAsset.fetchAssets(withLocalIdentifiers: assetIds, options: nil)
- //
- // // 如果没有资产,直接返回
- // if assets.count == 0 {
- // DispatchQueue.main.async {
- // completion(0)
- // }
- // return
- // }
- //
- // // 使用原子操作确保线程安全
- // let totalSize = Atomic<Int64>(0)
- // let processedCount = Atomic<Int>(0)
- //
- // // 创建一个与调用者相同QoS的组
- // let processingGroup = DispatchGroup()
- //
- // // 创建一个与调用者相同QoS的队列
- // let processingQueue = DispatchQueue(label: "com.yourapp.photosize.processing",
- // qos: .userInitiated,
- // attributes: .concurrent)
- //
- // // 遍历所有资产
- // for i in 0..<assets.count {
- // processingGroup.enter()
- //
- // // 使用DispatchWorkItem明确指定QoS
- // let workItem = DispatchWorkItem(qos: .userInitiated) {
- // let asset = assets.object(at: i)
- //
- // // 获取资源
- // let resources = PHAssetResource.assetResources(for: asset)
- // var assetSize: Int64 = 0
- //
- // // 计算资源大小
- // for resource in resources {
- // if let fileSize = resource.value(forKey: "fileSize") as? CLong {
- // assetSize += Int64(fileSize)
- // }
- // }
- //
- // // 更新总大小
- // totalSize.mutate { $0 += assetSize }
- //
- // // 更新处理计数
- // processedCount.mutate { $0 += 1 }
- //
- // processingGroup.leave()
- // }
- //
- // let workItem = DispatchWorkItem(qos: .userInitiated) {
- // let asset = assets.object(at: i)
- // var assetSize: Int64 = 0
- //
- // // 获取所有相关资源
- // let resources = PHAssetResource.assetResources(for: asset)
- //
- // // 对每个资源使用PHAssetResourceManager获取准确大小
- // let resourceManager = PHAssetResourceManager.default()
- // let resourcesGroup = DispatchGroup()
- //
- // for resource in resources {
- // resourcesGroup.enter()
- //
- // // 尝试获取准确的文件大小
- // let options = PHAssetResourceRequestOptions()
- // options.isNetworkAccessAllowed = true
- //
- // resourceManager.requestDataForAssetResource(resource, options: options) { (data, _, _) in
- // if let data = data {
- // assetSize += Int64(data.count)
- // } else if let fileSize = resource.value(forKey: "fileSize") as? CLong {
- // // 回退到元数据中的大小
- // assetSize += Int64(fileSize)
- // }
- // resourcesGroup.leave()
- // }
- // }
- //
- // // 等待所有资源处理完成
- // resourcesGroup.wait()
- //
- // // 更新总大小
- // totalSize.mutate { $0 += assetSize }
- // processedCount.mutate { $0 += 1 }
- //
- // processingGroup.leave()
- // }
- //
- // // 在指定QoS的队列上执行任务
- // processingQueue.async(execute: workItem)
- // }
- //
- // // 使用带超时的等待,避免无限期阻塞
- // let waitResult = processingGroup.wait(timeout: .now() + 30)
- //
- // // 返回结果到主线程
- // DispatchQueue.main.async {
- // if waitResult == .timedOut {
- // print("警告: 处理照片大小超时,已处理 \(processedCount.value)/\(assets.count) 个资源")
- // completion(FlutterError(code: "TIMEOUT",
- // message: "计算照片大小超时",
- // details: nil))
- // } else {
- // completion(totalSize.value)
- // }
- // }
- // }
- }
-
- // private func calculatePhotosSize(assetIds: [String], completion: @escaping FlutterResult) {
- // // 使用与调用者相同的QoS级别
- // DispatchQueue.global(qos: .userInitiated).async {
- // let assets = PHAsset.fetchAssets(withLocalIdentifiers: assetIds, options: nil)
- //
- // // 使用原子操作确保线程安全
- // let totalSize = Atomic<Int64>(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..<end {
- // // 使用带超时的等待,避免无限期阻塞
- // // 重要:在同一个队列中等待和信号,避免优先级反转
- // let waitResult = semaphore.wait(timeout: .now() + 5)
- //
- // defer {
- // // 确保信号量总是被释放
- // if waitResult != .timedOut {
- // semaphore.signal()
- // }
- // }
- //
- // // 如果等待超时,跳过当前资源
- // if waitResult == .timedOut {
- // print("警告: 等待资源超时,跳过资源")
- // continue
- // }
- //
- // let asset = assets.object(at: i)
- //
- // // 使用资源管理器获取大小
- // PHAssetResource.assetResources(for: asset).forEach { resource in
- // if let fileSize = resource.value(forKey: "fileSize") as? CLong {
- // batchSize += Int64(fileSize)
- // }
- // }
- // }
- //
- // // 更新总大小
- // totalSize.mutate { $0 += batchSize }
- // }
- //
- // processingGroup.leave()
- // }
- // }
- //
- // // 使用带超时的等待,避免无限期阻塞
- // let waitResult = processingGroup.wait(timeout: .now() + 30)
- //
- // // 返回结果到主线程
- // DispatchQueue.main.async {
- // if waitResult == .timedOut {
- // print("警告: 处理照片大小超时")
- // completion(FlutterError(code: "TIMEOUT",
- // message: "计算照片大小超时",
- // details: nil))
- // } else {
- // completion(totalSize.value)
- // }
- // }
- // }
- // }
-
- // private func calculatePhotosSize(assetIds: [String], completion: @escaping FlutterResult) {
- // // 使用与调用者相同的QoS级别,避免优先级反转
- // DispatchQueue.global(qos: .userInitiated).async {
- // let assets = PHAsset.fetchAssets(withLocalIdentifiers: assetIds, options: nil)
- //
- // // 使用原子操作确保线程安全
- // let totalSize = Atomic<Int64>(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..<end {
- // autoreleasepool {
- // // 使用带超时的等待,避免无限期阻塞
- // let waitResult = semaphore.wait(timeout: .now() + 5)
- //
- // defer {
- // // 确保信号量总是被释放
- // if waitResult != .timedOut {
- // semaphore.signal()
- // }
- // }
- //
- // // 如果等待超时,跳过当前资源
- // if waitResult == .timedOut {
- // print("警告: 等待资源超时,跳过资源")
- // return
- // }
- //
- // let asset = assets.object(at: i)
- //
- // // 使用资源管理器获取大小
- // PHAssetResource.assetResources(for: asset).forEach { resource in
- // var resourceSize: Int64 = 0
- //
- // // 尝试获取文件大小
- // if let fileSize = resource.value(forKey: "fileSize") as? CLong {
- // resourceSize = Int64(fileSize)
- // }
- //
- // batchSize += resourceSize
- // }
- // }
- // }
- //
- // // 更新总大小
- // totalSize.mutate { $0 += batchSize }
- // processingGroup.leave()
- // }
- // }
- //
- // // 使用带超时的等待,避免无限期阻塞
- // let waitResult = processingGroup.wait(timeout: .now() + 30)
- //
- // // 返回结果到主线程
- // DispatchQueue.main.async {
- // if waitResult == .timedOut {
- // print("警告: 处理照片大小超时")
- // completion(FlutterError(code: "TIMEOUT",
- // message: "计算照片大小超时",
- // details: nil))
- // } else {
- // completion(totalSize.value)
- // }
- // }
- // }
- // }
- }
- class SubscriptionHandler: NSObject {
-
- @available(iOS 15.0.0, *)
- func checkTrialEligibility() async -> 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
- }
- }
- }
|