ClassifyPhotoPlugin.swift 51 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198
  1. import Flutter
  2. import StoreKit
  3. import Photos
  4. import UIKit
  5. public class ClassifyPhotoPlugin : NSObject, FlutterPlugin {
  6. var photoClassifier = ClassifyPhoto()
  7. public static func register(with registrar: FlutterPluginRegistrar) {
  8. let channel = FlutterMethodChannel(name: "classify_photo", binaryMessenger: registrar.messenger())
  9. let instance = ClassifyPhotoPlugin()
  10. registrar.addMethodCallDelegate(instance, channel: channel)
  11. }
  12. public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
  13. print("iOS: Received method call: \(call.method)")
  14. switch call.method {
  15. case "getScreenshots":
  16. self.getScreenshots(flutterResult: result)
  17. case "getBlurryPhotos":
  18. self.getBlurryPhotos(flutterResult: result)
  19. case "getPeoplePhotos" :
  20. self.getPeoplePhotos(flutterResult: result)
  21. case "getSimilarPhotos":
  22. self.getSimilarPhotos(flutterResult: result)
  23. case "getPhoto":
  24. self.getPhoto(flutterResult: result)
  25. case "getStorageInfo":
  26. getStorageInfo(result: result)
  27. case "getExifInfo":
  28. guard let args = call.arguments as? [String: Any],
  29. let filePath = args["filePath"] as? String else {
  30. result(FlutterError(
  31. code: "INVALID_ARGUMENTS",
  32. message: "Missing filePath parameter",
  33. details: nil
  34. ))
  35. return
  36. }
  37. getExifInfo(filePath: filePath, completion: result)
  38. case "getPhotosSize":
  39. guard let args = call.arguments as? [String: Any],
  40. let assetIds = args["assetIds"] as? [String] else {
  41. result(FlutterError(
  42. code: "INVALID_ARGUMENTS",
  43. message: "Missing filePath parameter",
  44. details: nil
  45. ))
  46. return
  47. }
  48. calculatePhotosSize(assetIds: assetIds, completion: result)
  49. case "getPlatformVersion":
  50. result("iOS " + UIDevice.current.systemVersion)
  51. default:
  52. result(FlutterMethodNotImplemented)
  53. }
  54. }
  55. private class func blankof<T>(type:T.Type) -> T {
  56. let ptr = UnsafeMutablePointer<T>.allocate(capacity: MemoryLayout<T>.size)
  57. let val = ptr.pointee
  58. return val
  59. }
  60. /// 磁盘总大小
  61. private class func getTotalDiskSize() -> Int64 {
  62. var fs = blankof(type: statfs.self)
  63. if statfs("/var",&fs) >= 0{
  64. return Int64(UInt64(fs.f_bsize) * fs.f_blocks)
  65. }
  66. return -1
  67. }
  68. private func getStorageInfo(result: @escaping FlutterResult) {
  69. DispatchQueue.global(qos: .userInitiated).async {
  70. var storageInfo: [String: Int64] = [:]
  71. // 获取总容量和可用容量
  72. if let space = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory()) {
  73. let totalSpace = ClassifyPhotoPlugin.getTotalDiskSize()
  74. let freeSpace = space[.systemFreeSize] as? Int64 ?? 0
  75. storageInfo["totalSpace"] = totalSpace
  76. storageInfo["freeSpace"] = freeSpace
  77. storageInfo["usedSpace"] = totalSpace - freeSpace
  78. }
  79. // 获取照片占用的空间
  80. let options = PHFetchOptions()
  81. let allPhotos = PHAsset.fetchAssets(with: .image, options: options)
  82. var photoSize: Int64 = 0
  83. let group = DispatchGroup()
  84. let queue = DispatchQueue(label: "com.app.photosize", attributes: .concurrent)
  85. let semaphore = DispatchSemaphore(value: 5) // 限制并发
  86. allPhotos.enumerateObjects { (asset, index, stop) in
  87. group.enter()
  88. semaphore.wait()
  89. queue.async {
  90. let resources = PHAssetResource.assetResources(for: asset)
  91. if let resource = resources.first {
  92. if let unsignedInt64 = resource.value(forKey: "fileSize") as? CLong {
  93. photoSize += Int64(unsignedInt64)
  94. }
  95. }
  96. semaphore.signal()
  97. group.leave()
  98. }
  99. }
  100. group.notify(queue: .main) {
  101. storageInfo["photoSpace"] = photoSize
  102. result(storageInfo)
  103. }
  104. }
  105. }
  106. // 获取截图
  107. private func getScreenshots(flutterResult: @escaping FlutterResult) {
  108. DispatchQueue.global(qos: .userInitiated).async { [weak self] in
  109. guard let self = self else { return }
  110. let fetchOptions = PHFetchOptions()
  111. let allPhotos = PHAsset.fetchAssets(with: .image, options: fetchOptions)
  112. // 只处理截图
  113. self.photoClassifier.fetchScreenshots(from: allPhotos) { screenshots in
  114. // 清理内存
  115. self.cleanupMemory()
  116. self.photoClassifier.calculateAssetsSize(screenshots) { sizeInfo in
  117. self.processPhotoGroup(
  118. assets: screenshots,
  119. groupName: "screenshots",
  120. sizeInfo: sizeInfo
  121. ) { groupData in
  122. // 再次清理内存
  123. self.cleanupMemory()
  124. DispatchQueue.main.async {
  125. if !groupData.isEmpty {
  126. flutterResult([["group": groupData, "type": "screenshots"]])
  127. } else {
  128. flutterResult([])
  129. }
  130. }
  131. }
  132. }
  133. }
  134. }
  135. }
  136. // 获取模糊照片
  137. private func getBlurryPhotos(flutterResult: @escaping FlutterResult) {
  138. DispatchQueue.global(qos: .userInitiated).async { [weak self] in
  139. guard let self = self else { return }
  140. let fetchOptions = PHFetchOptions()
  141. let allPhotos = PHAsset.fetchAssets(with: .image, options: fetchOptions)
  142. // 只处理模糊照片
  143. self.photoClassifier.detectBlurryPhotos(from: allPhotos) { blurryPhotos in
  144. // 清理内存
  145. self.cleanupMemory()
  146. self.photoClassifier.calculateAssetsSize(blurryPhotos) { sizeInfo in
  147. self.processPhotoGroup(
  148. assets: blurryPhotos,
  149. groupName: "blurry",
  150. sizeInfo: sizeInfo
  151. ) { groupData in
  152. // 再次清理内存
  153. self.cleanupMemory()
  154. DispatchQueue.main.async {
  155. if !groupData.isEmpty {
  156. flutterResult([["group": groupData, "type": "blurry"]])
  157. } else {
  158. flutterResult([])
  159. }
  160. }
  161. }
  162. }
  163. }
  164. }
  165. }
  166. private func getPeoplePhotos(flutterResult: @escaping FlutterResult) {
  167. DispatchQueue.global(qos: .userInitiated).async { [weak self] in
  168. guard let self = self else { return }
  169. let fetchOptions = PHFetchOptions()
  170. // 限制处理的照片数量,提高性能
  171. fetchOptions.fetchLimit = 1000
  172. let allPhotos = PHAsset.fetchAssets(with: .image, options: fetchOptions)
  173. // 显示进度
  174. DispatchQueue.main.async {
  175. print("开始处理人物照片,总数: \(allPhotos.count)")
  176. }
  177. // 只处理人物照片
  178. self.photoClassifier.classifyByPeople(assets: allPhotos) { peopleGroups in
  179. // 清理内存
  180. self.cleanupMemory()
  181. let peopleAssets = Array(peopleGroups.values.flatMap { $0 })
  182. // 显示找到的人脸照片数量
  183. DispatchQueue.main.async {
  184. print("找到包含人脸的照片: \(peopleAssets.count)")
  185. }
  186. self.photoClassifier.calculateAssetsSize(peopleAssets) { sizeInfo in
  187. let resultData = Atomic<[[String: Any]]>([])
  188. // 分批处理人物组,避免一次性处理太多数据
  189. let processingQueue = DispatchQueue(label: "com.yourapp.peopleProcessing")
  190. let group = DispatchGroup()
  191. // 处理每个人物组
  192. for (personName, personPhotos) in peopleGroups {
  193. if personPhotos.isEmpty { continue }
  194. // 限制每组处理的照片数量
  195. let limitedPhotos = Array(personPhotos.prefix(500))
  196. group.enter()
  197. processingQueue.async {
  198. autoreleasepool {
  199. self.processPhotoGroup(
  200. assets: limitedPhotos,
  201. groupName: personName,
  202. sizeInfo: sizeInfo
  203. ) { groupData in
  204. if !groupData.isEmpty {
  205. resultData.mutate { $0.append([
  206. "group": groupData,
  207. "type": "people",
  208. "name": personName
  209. ]) }
  210. }
  211. group.leave()
  212. }
  213. }
  214. }
  215. }
  216. group.notify(queue: .main) {
  217. // 最终清理内存
  218. self.cleanupMemory()
  219. flutterResult(resultData.value)
  220. }
  221. }
  222. }
  223. }
  224. }
  225. // 获取人物照片
  226. // private func getPeoplePhotos(flutterResult: @escaping FlutterResult) {
  227. // DispatchQueue.global(qos: .userInitiated).async { [weak self] in
  228. // guard let self = self else { return }
  229. //
  230. // let fetchOptions = PHFetchOptions()
  231. // let allPhotos = PHAsset.fetchAssets(with: .image, options: fetchOptions)
  232. //
  233. // // 只处理人物照片
  234. // self.photoClassifier.classifyByPeople(assets: allPhotos) { peopleGroups in
  235. // // 清理内存
  236. // self.cleanupMemory()
  237. //
  238. // let peopleAssets = Array(peopleGroups.values.flatMap { $0 })
  239. // self.photoClassifier.calculateAssetsSize(peopleAssets) { sizeInfo in
  240. //
  241. // let resultData = Atomic<[[String: Any]]>([])
  242. // let processingQueue = DispatchQueue(label: "com.yourapp.peopleProcessing")
  243. // let group = DispatchGroup()
  244. //
  245. // // 处理每个人物组
  246. // for (personName, personPhotos) in peopleGroups {
  247. // if personPhotos.isEmpty { continue }
  248. //
  249. // group.enter()
  250. // processingQueue.async {
  251. // autoreleasepool {
  252. // self.processPhotoGroup(
  253. // assets: personPhotos,
  254. // groupName: personName,
  255. // sizeInfo: sizeInfo
  256. // ) { groupData in
  257. // if !groupData.isEmpty {
  258. // resultData.mutate { $0.append([
  259. // "group": groupData,
  260. // "type": "people",
  261. // "name": personName
  262. // ]) }
  263. // }
  264. // group.leave()
  265. // }
  266. // }
  267. // }
  268. // }
  269. //
  270. // group.notify(queue: .main) {
  271. // // 最终清理内存
  272. // self.cleanupMemory()
  273. // flutterResult(resultData.value)
  274. // }
  275. // }
  276. // }
  277. // }
  278. // }
  279. // 添加内存清理方法
  280. private func cleanupMemory() {
  281. // 强制清理内存
  282. autoreleasepool {
  283. // 触发内存警告,促使系统回收内存
  284. UIApplication.shared.performSelector(onMainThread: #selector(UIApplication.beginIgnoringInteractionEvents), with: nil, waitUntilDone: true)
  285. UIApplication.shared.performSelector(onMainThread: #selector(UIApplication.endIgnoringInteractionEvents), with: nil, waitUntilDone: true)
  286. }
  287. }
  288. // 获取相似照片
  289. private func getSimilarPhotos(flutterResult: @escaping FlutterResult) {
  290. DispatchQueue.global(qos: .userInitiated).async { [weak self] in
  291. guard let self = self else { return }
  292. let fetchOptions = PHFetchOptions()
  293. let allPhotos = PHAsset.fetchAssets(with: .image, options: fetchOptions)
  294. // 只处理相似照片
  295. self.photoClassifier.detectSimilarPhotos(
  296. assets: allPhotos,
  297. progressHandler: { (stage, progress) in
  298. print("Similar Photos Progress: \(stage) - \(progress)")
  299. },
  300. completion: { similarGroups in
  301. // 清理内存
  302. self.cleanupMemory()
  303. // 限制处理的组数,避免内存过载
  304. let maxGroupsToProcess = min(50, similarGroups.count)
  305. let limitedGroups = Array(similarGroups.prefix(maxGroupsToProcess))
  306. let similarAssets = Array(limitedGroups.flatMap { $0 })
  307. self.photoClassifier.calculateAssetsSize(similarAssets) { sizeInfo in
  308. let resultData = Atomic<[[String: Any]]>([])
  309. // 分批处理照片组,每批处理少量组
  310. let batchSize = 5
  311. let totalBatches = Int(ceil(Double(limitedGroups.count) / Double(batchSize)))
  312. self.processSimilarPhotoGroupsInBatches(
  313. groups: limitedGroups,
  314. batchIndex: 0,
  315. totalBatches: totalBatches,
  316. batchSize: batchSize,
  317. sizeInfo: sizeInfo,
  318. resultData: resultData
  319. ) {
  320. // 所有批次处理完成后的回调
  321. // 最终清理内存
  322. self.cleanupMemory()
  323. flutterResult(resultData.value)
  324. }
  325. }
  326. }
  327. )
  328. }
  329. }
  330. // 添加分批处理照片组的方法
  331. private func processSimilarPhotoGroupsInBatches(
  332. groups: [[PHAsset]],
  333. batchIndex: Int,
  334. totalBatches: Int,
  335. batchSize: Int,
  336. sizeInfo: ClassifyPhoto.PhotoSizeInfo,
  337. resultData: Atomic<[[String: Any]]>,
  338. completion: @escaping () -> Void
  339. ) {
  340. // 检查是否处理完所有批次
  341. if batchIndex >= totalBatches {
  342. completion()
  343. return
  344. }
  345. // 计算当前批次的范围
  346. let startIndex = batchIndex * batchSize
  347. let endIndex = min(startIndex + batchSize, groups.count)
  348. let currentBatchGroups = groups[startIndex..<endIndex]
  349. let processingQueue = DispatchQueue(label: "com.yourapp.similarProcessing.batch\(batchIndex)")
  350. let group = DispatchGroup()
  351. // 处理当前批次的照片组
  352. for (index, photoGroup) in currentBatchGroups.enumerated() {
  353. if photoGroup.isEmpty { continue }
  354. group.enter()
  355. processingQueue.async {
  356. autoreleasepool {
  357. self.processPhotoGroup(
  358. assets: photoGroup,
  359. groupName: "similar_\(startIndex + index)",
  360. sizeInfo: sizeInfo
  361. ) { groupData in
  362. if !groupData.isEmpty {
  363. resultData.mutate { $0.append([
  364. "group": groupData,
  365. "type": "similar"
  366. ]) }
  367. }
  368. group.leave()
  369. }
  370. }
  371. }
  372. }
  373. // 当前批次处理完成后
  374. group.notify(queue: .global()) {
  375. // 清理内存
  376. self.cleanupMemory()
  377. // 延迟一小段时间再处理下一批,给系统一些恢复时间
  378. DispatchQueue.global().asyncAfter(deadline: .now() + 0.3) {
  379. // 递归处理下一批
  380. self.processSimilarPhotoGroupsInBatches(
  381. groups: groups,
  382. batchIndex: batchIndex + 1,
  383. totalBatches: totalBatches,
  384. batchSize: batchSize,
  385. sizeInfo: sizeInfo,
  386. resultData: resultData,
  387. completion: completion
  388. )
  389. }
  390. }
  391. }
  392. private func getPhoto(flutterResult: @escaping FlutterResult) {
  393. DispatchQueue.global(qos: .userInitiated).async { [weak self] in
  394. guard let self = self else { return }
  395. let fetchOptions = PHFetchOptions()
  396. let allPhotos = PHAsset.fetchAssets(with: .image, options: fetchOptions)
  397. self.photoClassifier.classifyPhotos(
  398. assets: allPhotos,
  399. progressHandler: { (stage, progress) in
  400. print("Progress: \(stage) - \(progress)")
  401. },
  402. completion: { result in
  403. // 使用串行队列处理结果,避免同时处理多个组
  404. let processingQueue = DispatchQueue(label: "com.yourapp.photoProcessing")
  405. let resultData = Atomic<[[String: Any]]>([])
  406. // 创建一个函数来依次处理每个组
  407. func processGroups(index: Int, groups: [(assets: [PHAsset], name: String, type: String, sizeInfo: ClassifyPhoto.PhotoSizeInfo)]) {
  408. // 检查是否处理完所有组
  409. if index >= groups.count {
  410. // 所有组处理完毕,返回结果
  411. DispatchQueue.main.async {
  412. print("Final result count: \(resultData.value.count)")
  413. flutterResult(resultData.value)
  414. }
  415. return
  416. }
  417. let currentGroup = groups[index]
  418. // 处理当前组
  419. self.processPhotoGroup(
  420. assets: currentGroup.assets,
  421. groupName: currentGroup.name,
  422. sizeInfo: currentGroup.sizeInfo
  423. ) { groupData in
  424. // 使用自动释放池减少内存占用
  425. autoreleasepool {
  426. if !groupData.isEmpty {
  427. resultData.mutate { $0.append([
  428. "group": groupData,
  429. "type": currentGroup.type,
  430. currentGroup.type == "location" ? "name" : "" : currentGroup.name
  431. ].filter { !($0.value is String && $0.value as! String == "") }) }
  432. }
  433. // 手动触发内存清理
  434. self.cleanupMemory()
  435. // 延迟一小段时间,让系统有机会回收内存
  436. DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) {
  437. // 处理下一个组
  438. processGroups(index: index + 1, groups: groups)
  439. }
  440. }
  441. }
  442. }
  443. // 准备所有需要处理的组
  444. var allGroups: [(assets: [PHAsset], name: String, type: String, sizeInfo: ClassifyPhoto.PhotoSizeInfo)] = []
  445. // 添加截图组
  446. if !result.screenshots.isEmpty {
  447. allGroups.append((result.screenshots, "screenshots", "screenshots", result.screenshotsSize))
  448. }
  449. // 添加相似照片组
  450. for photoGroup in result.similarPhotos {
  451. if !photoGroup.isEmpty {
  452. allGroups.append((photoGroup, "similar", "similar", result.similarPhotosSize))
  453. }
  454. }
  455. // 添加模糊照片组
  456. if !result.blurryPhotos.isEmpty {
  457. allGroups.append((result.blurryPhotos, "blurry", "blurry", result.blurryPhotosSize))
  458. }
  459. // 添加人物照片组
  460. for (personName, personPhotos) in result.people {
  461. if !personPhotos.isEmpty {
  462. allGroups.append((personPhotos, personName, "people", result.peopleSize))
  463. }
  464. }
  465. // 开始处理第一个组
  466. if allGroups.isEmpty {
  467. DispatchQueue.main.async {
  468. print("No groups to process")
  469. flutterResult([])
  470. }
  471. } else {
  472. processGroups(index: 0, groups: allGroups)
  473. }
  474. }
  475. )
  476. // self.photoClassifier.classifyPhotos(
  477. // assets: allPhotos,
  478. // progressHandler: { (stage, progress) in
  479. // print("Progress: \(stage) - \(progress)")
  480. // },
  481. // completion: { result in
  482. // var resultData: [[String: Any]] = []
  483. // let mainGroup = DispatchGroup()
  484. //
  485. // // 处理截图
  486. // mainGroup.enter()
  487. // self.processPhotoGroup(assets: result.screenshots, groupName: "screenshots", sizeInfo: result.screenshotsSize) { groupData in
  488. // if !groupData.isEmpty {
  489. // resultData.append(["group": groupData, "type": "screenshots"])
  490. // }
  491. // mainGroup.leave()
  492. // }
  493. //
  494. // // 处理相似照片组
  495. // for photoGroup in result.similarPhotos {
  496. // mainGroup.enter()
  497. // self.processPhotoGroup(assets: photoGroup, groupName: "similar", sizeInfo: result.similarPhotosSize) { groupData in
  498. // if !groupData.isEmpty {
  499. // resultData.append(["group": groupData, "type": "similar"])
  500. // }
  501. // mainGroup.leave()
  502. // }
  503. // }
  504. //
  505. // // 处理地点分组
  506. //// for (location, assets) in result.locations {
  507. //// mainGroup.enter()
  508. //// self.processPhotoGroup(assets: assets, groupName: location, sizeInfo: result.locationsSize) { groupData in
  509. //// if !groupData.isEmpty {
  510. //// resultData.append(["group": groupData, "type": "location", "name": location])
  511. //// }
  512. //// mainGroup.leave()
  513. //// }
  514. //// }
  515. //
  516. // // 处理人物分组
  517. //// for (person, assets) in result.people {
  518. //// mainGroup.enter()
  519. //// self.processPhotoGroup(assets: assets, groupName: person, sizeInfo: result.peopleSize) { groupData in
  520. //// if !groupData.isEmpty {
  521. //// resultData.append(["group": groupData, "type": "people"])
  522. //// }
  523. //// mainGroup.leave()
  524. //// }
  525. //// }
  526. //
  527. // // 处理模糊照片
  528. // mainGroup.enter()
  529. // self.processPhotoGroup(assets: result.blurryPhotos, groupName: "blurry", sizeInfo: result.blurryPhotosSize) { groupData in
  530. // if !groupData.isEmpty {
  531. // resultData.append(["group": groupData, "type": "blurry"])
  532. // }
  533. // mainGroup.leave()
  534. // }
  535. //
  536. // mainGroup.notify(queue: .main) {
  537. // print("Final result count: \(resultData.count)")
  538. // flutterResult(resultData)
  539. // }
  540. // }
  541. // )
  542. }
  543. }
  544. // // 添加内存清理辅助方法
  545. // private func cleanupMemory() {
  546. // // 清理图像缓存
  547. // URLCache.shared.removeAllCachedResponses()
  548. //
  549. // // 强制进行一次垃圾回收
  550. // autoreleasepool {
  551. // let _ = [String](repeating: "temp", count: 1)
  552. // }
  553. //
  554. // #if os(iOS)
  555. // // 发送低内存警告
  556. // UIApplication.shared.perform(Selector(("_performMemoryWarning")))
  557. // #endif
  558. // }
  559. // 添加内存清理方法
  560. // private func cleanupMemory() {
  561. // // 强制清理内存
  562. // autoreleasepool {
  563. // // 触发内存警告,促使系统回收内存
  564. // UIApplication.shared.performSelector(onMainThread: #selector(UIApplication.beginIgnoringInteractionEvents), with: nil, waitUntilDone: true)
  565. // UIApplication.shared.performSelector(onMainThread: #selector(UIApplication.endIgnoringInteractionEvents), with: nil, waitUntilDone: true)
  566. // }
  567. // }
  568. // 处理照片组的辅助方法
  569. private func processPhotoGroup(
  570. assets: [PHAsset],
  571. groupName: String,
  572. sizeInfo: ClassifyPhoto.PhotoSizeInfo,
  573. completion: @escaping ([String: Any]) -> Void
  574. ) {
  575. let photoProcessGroup = DispatchGroup()
  576. var photosData: [[String: Any]] = []
  577. for asset in assets {
  578. photoProcessGroup.enter()
  579. let options = PHContentEditingInputRequestOptions()
  580. options.isNetworkAccessAllowed = false
  581. DispatchQueue.global(qos: .background).async {
  582. asset.requestContentEditingInput(with: options) { (input, info) in
  583. defer { photoProcessGroup.leave() }
  584. if let input = input, let url = input.fullSizeImageURL {
  585. let photoInfo: [String: Any] = [
  586. // "path": url.path,
  587. "id": asset.localIdentifier,
  588. // "width": asset.pixelWidth,
  589. // "height": asset.pixelHeight,
  590. // "creationDate": asset.creationDate?.timeIntervalSince1970 ?? 0
  591. ]
  592. photosData.append(photoInfo)
  593. }
  594. }
  595. }
  596. }
  597. photoProcessGroup.notify(queue: .main) {
  598. completion([
  599. "photos": photosData,
  600. "totalSize": sizeInfo.totalSize,
  601. "count": sizeInfo.count
  602. ])
  603. }
  604. }
  605. private func getExifInfo(filePath: String, completion: @escaping FlutterResult) {
  606. // 创建文件 URL
  607. let fileURL: URL
  608. if filePath.starts(with: "file://") {
  609. guard let url = URL(string: filePath) else {
  610. print("Invalid URL string: \(filePath)")
  611. completion([:])
  612. return
  613. }
  614. fileURL = url
  615. } else {
  616. fileURL = URL(fileURLWithPath: filePath)
  617. }
  618. // 检查文件是否存在
  619. guard FileManager.default.fileExists(atPath: fileURL.path) else {
  620. print("File does not exist at path: \(fileURL.path)")
  621. completion([:])
  622. return
  623. }
  624. // 创建图片源
  625. guard let imageSource = CGImageSourceCreateWithURL(fileURL as CFURL, nil) else {
  626. print("Failed to create image source for path: \(fileURL.path)")
  627. completion([:])
  628. return
  629. }
  630. var exifInfo: [String: Any] = [:]
  631. // 获取所有元数据
  632. if let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [String: Any] {
  633. // EXIF 数据
  634. if let exif = imageProperties[kCGImagePropertyExifDictionary as String] as? [String: Any] {
  635. exifInfo["aperture"] = exif[kCGImagePropertyExifFNumber as String]
  636. exifInfo["exposureTime"] = exif[kCGImagePropertyExifExposureTime as String]
  637. exifInfo["iso"] = exif[kCGImagePropertyExifISOSpeedRatings as String]
  638. exifInfo["focalLength"] = exif[kCGImagePropertyExifFocalLength as String]
  639. exifInfo["dateTimeOriginal"] = exif[kCGImagePropertyExifDateTimeOriginal as String]
  640. }
  641. // TIFF 数据
  642. if let tiff = imageProperties[kCGImagePropertyTIFFDictionary as String] as? [String: Any] {
  643. exifInfo["make"] = tiff[kCGImagePropertyTIFFMake as String]
  644. exifInfo["model"] = tiff[kCGImagePropertyTIFFModel as String]
  645. }
  646. // GPS 数据
  647. if let gps = imageProperties[kCGImagePropertyGPSDictionary as String] as? [String: Any] {
  648. exifInfo["latitude"] = gps[kCGImagePropertyGPSLatitude as String]
  649. exifInfo["longitude"] = gps[kCGImagePropertyGPSLongitude as String]
  650. exifInfo["altitude"] = gps[kCGImagePropertyGPSAltitude as String]
  651. }
  652. // 图片基本信息
  653. exifInfo["pixelWidth"] = imageProperties[kCGImagePropertyPixelWidth as String]
  654. exifInfo["pixelHeight"] = imageProperties[kCGImagePropertyPixelHeight as String]
  655. exifInfo["dpi"] = imageProperties[kCGImagePropertyDPIHeight as String]
  656. exifInfo["colorModel"] = imageProperties[kCGImagePropertyColorModel as String]
  657. exifInfo["profileName"] = imageProperties[kCGImagePropertyProfileName as String]
  658. }
  659. completion(exifInfo)
  660. }
  661. }
  662. // 计算选择的图片大小
  663. extension ClassifyPhotoPlugin {
  664. // private func calculatePhotosSize(assetIds: [String], completion: @escaping FlutterResult) {
  665. // // 使用与调用者相同的QoS级别
  666. // let callerQoS: DispatchQoS = .userInitiated
  667. //
  668. // DispatchQueue.global(qos: callerQoS.qosClass).async {
  669. // let assets = PHAsset.fetchAssets(withLocalIdentifiers: assetIds, options: nil)
  670. //
  671. // // 使用原子操作确保线程安全
  672. // let totalSize = Atomic<Int64>(0)
  673. // let processedCount = Atomic<Int>(0)
  674. //
  675. // // 创建一个与调用者相同QoS的组
  676. // let processingGroup = DispatchGroup()
  677. //
  678. // // 创建一个与调用者相同QoS的队列
  679. // let processingQueue = DispatchQueue(label: "com.yourapp.photosize.processing",
  680. // qos: callerQoS,
  681. // attributes: .concurrent)
  682. //
  683. // // 创建一个与调用者相同QoS的信号量队列
  684. // let semaphoreQueue = DispatchQueue(label: "com.yourapp.photosize.semaphore",
  685. // qos: callerQoS)
  686. //
  687. // // 控制并发数量
  688. // let maxConcurrent = 4
  689. // var activeWorkers = 0
  690. //
  691. // // 分批处理资源
  692. // let batchSize = 20
  693. // let totalCount = assets.count
  694. //
  695. // // 如果没有资产,直接返回
  696. // if totalCount == 0 {
  697. // DispatchQueue.main.async {
  698. // completion(0)
  699. // }
  700. // return
  701. // }
  702. //
  703. // // 创建一个函数来处理下一个批次
  704. // @Sendable func processNextBatch() {
  705. // // 计算下一个批次的范围
  706. // let currentProcessed = processedCount.value
  707. // if currentProcessed >= totalCount {
  708. // // 所有批次已处理完毕
  709. // return
  710. // }
  711. //
  712. // let batchStart = currentProcessed
  713. // let batchEnd = min(batchStart + batchSize, totalCount)
  714. //
  715. // // 更新已处理计数
  716. // processedCount.mutate { $0 = batchEnd }
  717. //
  718. // processingGroup.enter()
  719. // processingQueue.async {
  720. // var batchSize: Int64 = 0
  721. //
  722. // for i in batchStart..<batchEnd {
  723. // autoreleasepool {
  724. // let asset = assets.object(at: i)
  725. //
  726. // // 使用资源管理器获取大小
  727. // PHAssetResource.assetResources(for: asset).forEach { resource in
  728. // if let fileSize = resource.value(forKey: "fileSize") as? CLong {
  729. // batchSize += Int64(fileSize)
  730. // }
  731. // }
  732. // }
  733. // }
  734. //
  735. // // 更新总大小
  736. // totalSize.mutate { $0 += batchSize }
  737. //
  738. // // 处理下一个批次
  739. // semaphoreQueue.async {
  740. // activeWorkers -= 1
  741. // scheduleMoreWorkIfNeeded()
  742. // }
  743. //
  744. // processingGroup.leave()
  745. // }
  746. // }
  747. //
  748. // // 调度更多工作如果需要
  749. // func scheduleMoreWorkIfNeeded() {
  750. // while activeWorkers < maxConcurrent && processedCount.value < totalCount {
  751. // activeWorkers += 1
  752. // processNextBatch()
  753. // }
  754. // }
  755. //
  756. // // 开始处理
  757. // semaphoreQueue.async {
  758. // scheduleMoreWorkIfNeeded()
  759. // }
  760. //
  761. // // 使用带超时的等待,避免无限期阻塞
  762. // let waitResult = processingGroup.wait(timeout: .now() + 30)
  763. //
  764. // // 返回结果到主线程
  765. // DispatchQueue.main.async {
  766. // if waitResult == .timedOut {
  767. // print("警告: 处理照片大小超时")
  768. // completion(FlutterError(code: "TIMEOUT",
  769. // message: "计算照片大小超时",
  770. // details: nil))
  771. // } else {
  772. // completion(totalSize.value)
  773. // }
  774. // }
  775. // }
  776. // }
  777. private func calculatePhotosSize(assetIds: [String], completion: @escaping FlutterResult) {
  778. // 使用与调用者相同的QoS级别
  779. DispatchQueue.global(qos: .userInitiated).async {
  780. let assets = PHAsset.fetchAssets(withLocalIdentifiers: assetIds, options: nil)
  781. // 将 PHFetchResult 转换为数组
  782. var assetArray: [PHAsset] = []
  783. assets.enumerateObjects { (asset, _, _) in
  784. assetArray.append(asset)
  785. }
  786. // 使用 ClassifyPhoto 中的方法计算大小
  787. self.photoClassifier.calculateAssetsSize(assetArray) { sizeInfo in
  788. DispatchQueue.main.async {
  789. // 返回总大小
  790. completion(sizeInfo.totalSize)
  791. }
  792. }
  793. }
  794. // // 使用与调用者相同的QoS级别
  795. // DispatchQueue.global(qos: .userInitiated).async {
  796. // let assets = PHAsset.fetchAssets(withLocalIdentifiers: assetIds, options: nil)
  797. //
  798. // // 如果没有资产,直接返回
  799. // if assets.count == 0 {
  800. // DispatchQueue.main.async {
  801. // completion(0)
  802. // }
  803. // return
  804. // }
  805. //
  806. // // 使用原子操作确保线程安全
  807. // let totalSize = Atomic<Int64>(0)
  808. // let processedCount = Atomic<Int>(0)
  809. //
  810. // // 创建一个与调用者相同QoS的组
  811. // let processingGroup = DispatchGroup()
  812. //
  813. // // 创建一个与调用者相同QoS的队列
  814. // let processingQueue = DispatchQueue(label: "com.yourapp.photosize.processing",
  815. // qos: .userInitiated,
  816. // attributes: .concurrent)
  817. //
  818. // // 遍历所有资产
  819. // for i in 0..<assets.count {
  820. // processingGroup.enter()
  821. //
  822. // // 使用DispatchWorkItem明确指定QoS
  823. // let workItem = DispatchWorkItem(qos: .userInitiated) {
  824. // let asset = assets.object(at: i)
  825. //
  826. // // 获取资源
  827. // let resources = PHAssetResource.assetResources(for: asset)
  828. // var assetSize: Int64 = 0
  829. //
  830. // // 计算资源大小
  831. // for resource in resources {
  832. // if let fileSize = resource.value(forKey: "fileSize") as? CLong {
  833. // assetSize += Int64(fileSize)
  834. // }
  835. // }
  836. //
  837. // // 更新总大小
  838. // totalSize.mutate { $0 += assetSize }
  839. //
  840. // // 更新处理计数
  841. // processedCount.mutate { $0 += 1 }
  842. //
  843. // processingGroup.leave()
  844. // }
  845. //
  846. // let workItem = DispatchWorkItem(qos: .userInitiated) {
  847. // let asset = assets.object(at: i)
  848. // var assetSize: Int64 = 0
  849. //
  850. // // 获取所有相关资源
  851. // let resources = PHAssetResource.assetResources(for: asset)
  852. //
  853. // // 对每个资源使用PHAssetResourceManager获取准确大小
  854. // let resourceManager = PHAssetResourceManager.default()
  855. // let resourcesGroup = DispatchGroup()
  856. //
  857. // for resource in resources {
  858. // resourcesGroup.enter()
  859. //
  860. // // 尝试获取准确的文件大小
  861. // let options = PHAssetResourceRequestOptions()
  862. // options.isNetworkAccessAllowed = true
  863. //
  864. // resourceManager.requestDataForAssetResource(resource, options: options) { (data, _, _) in
  865. // if let data = data {
  866. // assetSize += Int64(data.count)
  867. // } else if let fileSize = resource.value(forKey: "fileSize") as? CLong {
  868. // // 回退到元数据中的大小
  869. // assetSize += Int64(fileSize)
  870. // }
  871. // resourcesGroup.leave()
  872. // }
  873. // }
  874. //
  875. // // 等待所有资源处理完成
  876. // resourcesGroup.wait()
  877. //
  878. // // 更新总大小
  879. // totalSize.mutate { $0 += assetSize }
  880. // processedCount.mutate { $0 += 1 }
  881. //
  882. // processingGroup.leave()
  883. // }
  884. //
  885. // // 在指定QoS的队列上执行任务
  886. // processingQueue.async(execute: workItem)
  887. // }
  888. //
  889. // // 使用带超时的等待,避免无限期阻塞
  890. // let waitResult = processingGroup.wait(timeout: .now() + 30)
  891. //
  892. // // 返回结果到主线程
  893. // DispatchQueue.main.async {
  894. // if waitResult == .timedOut {
  895. // print("警告: 处理照片大小超时,已处理 \(processedCount.value)/\(assets.count) 个资源")
  896. // completion(FlutterError(code: "TIMEOUT",
  897. // message: "计算照片大小超时",
  898. // details: nil))
  899. // } else {
  900. // completion(totalSize.value)
  901. // }
  902. // }
  903. // }
  904. }
  905. // private func calculatePhotosSize(assetIds: [String], completion: @escaping FlutterResult) {
  906. // // 使用与调用者相同的QoS级别
  907. // DispatchQueue.global(qos: .userInitiated).async {
  908. // let assets = PHAsset.fetchAssets(withLocalIdentifiers: assetIds, options: nil)
  909. //
  910. // // 使用原子操作确保线程安全
  911. // let totalSize = Atomic<Int64>(0)
  912. //
  913. // // 创建一个与调用者相同QoS的组
  914. // let processingGroup = DispatchGroup()
  915. //
  916. // // 创建一个与调用者相同QoS的队列,确保所有操作都有明确的QoS
  917. // let processingQueue = DispatchQueue(label: "com.yourapp.photosize.processing",
  918. // qos: .userInitiated,
  919. // attributes: .concurrent)
  920. //
  921. // // 控制并发数量
  922. // let semaphore = DispatchSemaphore(value: 4)
  923. //
  924. // // 分批处理资源
  925. // let batchSize = 20
  926. // let totalCount = assets.count
  927. //
  928. // // 如果没有资产,直接返回
  929. // if totalCount == 0 {
  930. // DispatchQueue.main.async {
  931. // completion(0)
  932. // }
  933. // return
  934. // }
  935. //
  936. // for batchStart in stride(from: 0, to: totalCount, by: batchSize) {
  937. // let end = min(batchStart + batchSize, totalCount)
  938. //
  939. // processingGroup.enter()
  940. // // 确保使用明确QoS的队列
  941. // processingQueue.async {
  942. // autoreleasepool {
  943. // var batchSize: Int64 = 0
  944. //
  945. // for i in batchStart..<end {
  946. // // 使用带超时的等待,避免无限期阻塞
  947. // // 重要:在同一个队列中等待和信号,避免优先级反转
  948. // let waitResult = semaphore.wait(timeout: .now() + 5)
  949. //
  950. // defer {
  951. // // 确保信号量总是被释放
  952. // if waitResult != .timedOut {
  953. // semaphore.signal()
  954. // }
  955. // }
  956. //
  957. // // 如果等待超时,跳过当前资源
  958. // if waitResult == .timedOut {
  959. // print("警告: 等待资源超时,跳过资源")
  960. // continue
  961. // }
  962. //
  963. // let asset = assets.object(at: i)
  964. //
  965. // // 使用资源管理器获取大小
  966. // PHAssetResource.assetResources(for: asset).forEach { resource in
  967. // if let fileSize = resource.value(forKey: "fileSize") as? CLong {
  968. // batchSize += Int64(fileSize)
  969. // }
  970. // }
  971. // }
  972. //
  973. // // 更新总大小
  974. // totalSize.mutate { $0 += batchSize }
  975. // }
  976. //
  977. // processingGroup.leave()
  978. // }
  979. // }
  980. //
  981. // // 使用带超时的等待,避免无限期阻塞
  982. // let waitResult = processingGroup.wait(timeout: .now() + 30)
  983. //
  984. // // 返回结果到主线程
  985. // DispatchQueue.main.async {
  986. // if waitResult == .timedOut {
  987. // print("警告: 处理照片大小超时")
  988. // completion(FlutterError(code: "TIMEOUT",
  989. // message: "计算照片大小超时",
  990. // details: nil))
  991. // } else {
  992. // completion(totalSize.value)
  993. // }
  994. // }
  995. // }
  996. // }
  997. // private func calculatePhotosSize(assetIds: [String], completion: @escaping FlutterResult) {
  998. // // 使用与调用者相同的QoS级别,避免优先级反转
  999. // DispatchQueue.global(qos: .userInitiated).async {
  1000. // let assets = PHAsset.fetchAssets(withLocalIdentifiers: assetIds, options: nil)
  1001. //
  1002. // // 使用原子操作确保线程安全
  1003. // let totalSize = Atomic<Int64>(0)
  1004. // let processingGroup = DispatchGroup()
  1005. //
  1006. // // 创建一个具有相同QoS的调度队列用于信号量操作
  1007. // let processingQueue = DispatchQueue(label: "com.yourapp.photosize.processing",
  1008. // qos: .userInitiated,
  1009. // attributes: .concurrent)
  1010. //
  1011. // // 控制并发数量
  1012. // let semaphore = DispatchSemaphore(value: 4)
  1013. //
  1014. // // 分批处理资源
  1015. // let batchSize = 20
  1016. // let totalCount = assets.count
  1017. //
  1018. // for batchStart in stride(from: 0, to: totalCount, by: batchSize) {
  1019. // let end = min(batchStart + batchSize, totalCount)
  1020. //
  1021. // processingGroup.enter()
  1022. // // 使用具有明确QoS的队列
  1023. // processingQueue.async {
  1024. // var batchSize: Int64 = 0
  1025. //
  1026. // for i in batchStart..<end {
  1027. // autoreleasepool {
  1028. // // 使用带超时的等待,避免无限期阻塞
  1029. // let waitResult = semaphore.wait(timeout: .now() + 5)
  1030. //
  1031. // defer {
  1032. // // 确保信号量总是被释放
  1033. // if waitResult != .timedOut {
  1034. // semaphore.signal()
  1035. // }
  1036. // }
  1037. //
  1038. // // 如果等待超时,跳过当前资源
  1039. // if waitResult == .timedOut {
  1040. // print("警告: 等待资源超时,跳过资源")
  1041. // return
  1042. // }
  1043. //
  1044. // let asset = assets.object(at: i)
  1045. //
  1046. // // 使用资源管理器获取大小
  1047. // PHAssetResource.assetResources(for: asset).forEach { resource in
  1048. // var resourceSize: Int64 = 0
  1049. //
  1050. // // 尝试获取文件大小
  1051. // if let fileSize = resource.value(forKey: "fileSize") as? CLong {
  1052. // resourceSize = Int64(fileSize)
  1053. // }
  1054. //
  1055. // batchSize += resourceSize
  1056. // }
  1057. // }
  1058. // }
  1059. //
  1060. // // 更新总大小
  1061. // totalSize.mutate { $0 += batchSize }
  1062. // processingGroup.leave()
  1063. // }
  1064. // }
  1065. //
  1066. // // 使用带超时的等待,避免无限期阻塞
  1067. // let waitResult = processingGroup.wait(timeout: .now() + 30)
  1068. //
  1069. // // 返回结果到主线程
  1070. // DispatchQueue.main.async {
  1071. // if waitResult == .timedOut {
  1072. // print("警告: 处理照片大小超时")
  1073. // completion(FlutterError(code: "TIMEOUT",
  1074. // message: "计算照片大小超时",
  1075. // details: nil))
  1076. // } else {
  1077. // completion(totalSize.value)
  1078. // }
  1079. // }
  1080. // }
  1081. // }
  1082. }
  1083. class SubscriptionHandler: NSObject {
  1084. @available(iOS 15.0.0, *)
  1085. func checkTrialEligibility() async -> Bool {
  1086. do {
  1087. // 获取产品信息
  1088. let productIds = ["clean.vip.1week"]
  1089. let products = try await Product.products(for: Set(productIds))
  1090. // 检查第一个产品的试用资格
  1091. if let product = products.first {
  1092. return await product.subscription?.isEligibleForIntroOffer ?? false
  1093. }
  1094. return false
  1095. } catch {
  1096. print("Error checking trial eligibility: \(error)")
  1097. return false
  1098. }
  1099. }
  1100. }