ClassifyPhotoPlugin.swift 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066
  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. // 使用原子操作确保线程安全
  782. let totalSize = Atomic<Int64>(0)
  783. // 创建一个与调用者相同QoS的组
  784. let processingGroup = DispatchGroup()
  785. // 创建一个与调用者相同QoS的队列,确保所有操作都有明确的QoS
  786. let processingQueue = DispatchQueue(label: "com.yourapp.photosize.processing",
  787. qos: .userInitiated,
  788. attributes: .concurrent)
  789. // 控制并发数量
  790. let semaphore = DispatchSemaphore(value: 4)
  791. // 分批处理资源
  792. let batchSize = 20
  793. let totalCount = assets.count
  794. // 如果没有资产,直接返回
  795. if totalCount == 0 {
  796. DispatchQueue.main.async {
  797. completion(0)
  798. }
  799. return
  800. }
  801. for batchStart in stride(from: 0, to: totalCount, by: batchSize) {
  802. let end = min(batchStart + batchSize, totalCount)
  803. processingGroup.enter()
  804. // 确保使用明确QoS的队列
  805. processingQueue.async {
  806. autoreleasepool {
  807. var batchSize: Int64 = 0
  808. for i in batchStart..<end {
  809. // 使用带超时的等待,避免无限期阻塞
  810. // 重要:在同一个队列中等待和信号,避免优先级反转
  811. let waitResult = semaphore.wait(timeout: .now() + 5)
  812. defer {
  813. // 确保信号量总是被释放
  814. if waitResult != .timedOut {
  815. semaphore.signal()
  816. }
  817. }
  818. // 如果等待超时,跳过当前资源
  819. if waitResult == .timedOut {
  820. print("警告: 等待资源超时,跳过资源")
  821. continue
  822. }
  823. let asset = assets.object(at: i)
  824. // 使用资源管理器获取大小
  825. PHAssetResource.assetResources(for: asset).forEach { resource in
  826. if let fileSize = resource.value(forKey: "fileSize") as? CLong {
  827. batchSize += Int64(fileSize)
  828. }
  829. }
  830. }
  831. // 更新总大小
  832. totalSize.mutate { $0 += batchSize }
  833. }
  834. processingGroup.leave()
  835. }
  836. }
  837. // 使用带超时的等待,避免无限期阻塞
  838. let waitResult = processingGroup.wait(timeout: .now() + 30)
  839. // 返回结果到主线程
  840. DispatchQueue.main.async {
  841. if waitResult == .timedOut {
  842. print("警告: 处理照片大小超时")
  843. completion(FlutterError(code: "TIMEOUT",
  844. message: "计算照片大小超时",
  845. details: nil))
  846. } else {
  847. completion(totalSize.value)
  848. }
  849. }
  850. }
  851. }
  852. // private func calculatePhotosSize(assetIds: [String], completion: @escaping FlutterResult) {
  853. // // 使用与调用者相同的QoS级别,避免优先级反转
  854. // DispatchQueue.global(qos: .userInitiated).async {
  855. // let assets = PHAsset.fetchAssets(withLocalIdentifiers: assetIds, options: nil)
  856. //
  857. // // 使用原子操作确保线程安全
  858. // let totalSize = Atomic<Int64>(0)
  859. // let processingGroup = DispatchGroup()
  860. //
  861. // // 创建一个具有相同QoS的调度队列用于信号量操作
  862. // let processingQueue = DispatchQueue(label: "com.yourapp.photosize.processing",
  863. // qos: .userInitiated,
  864. // attributes: .concurrent)
  865. //
  866. // // 控制并发数量
  867. // let semaphore = DispatchSemaphore(value: 4)
  868. //
  869. // // 分批处理资源
  870. // let batchSize = 20
  871. // let totalCount = assets.count
  872. //
  873. // for batchStart in stride(from: 0, to: totalCount, by: batchSize) {
  874. // let end = min(batchStart + batchSize, totalCount)
  875. //
  876. // processingGroup.enter()
  877. // // 使用具有明确QoS的队列
  878. // processingQueue.async {
  879. // var batchSize: Int64 = 0
  880. //
  881. // for i in batchStart..<end {
  882. // autoreleasepool {
  883. // // 使用带超时的等待,避免无限期阻塞
  884. // let waitResult = semaphore.wait(timeout: .now() + 5)
  885. //
  886. // defer {
  887. // // 确保信号量总是被释放
  888. // if waitResult != .timedOut {
  889. // semaphore.signal()
  890. // }
  891. // }
  892. //
  893. // // 如果等待超时,跳过当前资源
  894. // if waitResult == .timedOut {
  895. // print("警告: 等待资源超时,跳过资源")
  896. // return
  897. // }
  898. //
  899. // let asset = assets.object(at: i)
  900. //
  901. // // 使用资源管理器获取大小
  902. // PHAssetResource.assetResources(for: asset).forEach { resource in
  903. // var resourceSize: Int64 = 0
  904. //
  905. // // 尝试获取文件大小
  906. // if let fileSize = resource.value(forKey: "fileSize") as? CLong {
  907. // resourceSize = Int64(fileSize)
  908. // }
  909. //
  910. // batchSize += resourceSize
  911. // }
  912. // }
  913. // }
  914. //
  915. // // 更新总大小
  916. // totalSize.mutate { $0 += batchSize }
  917. // processingGroup.leave()
  918. // }
  919. // }
  920. //
  921. // // 使用带超时的等待,避免无限期阻塞
  922. // let waitResult = processingGroup.wait(timeout: .now() + 30)
  923. //
  924. // // 返回结果到主线程
  925. // DispatchQueue.main.async {
  926. // if waitResult == .timedOut {
  927. // print("警告: 处理照片大小超时")
  928. // completion(FlutterError(code: "TIMEOUT",
  929. // message: "计算照片大小超时",
  930. // details: nil))
  931. // } else {
  932. // completion(totalSize.value)
  933. // }
  934. // }
  935. // }
  936. // }
  937. }
  938. class SubscriptionHandler: NSObject {
  939. @available(iOS 15.0.0, *)
  940. func checkTrialEligibility() async -> Bool {
  941. do {
  942. // 获取产品信息
  943. let productIds = ["clean.vip.1week"]
  944. let products = try await Product.products(for: Set(productIds))
  945. // 检查第一个产品的试用资格
  946. if let product = products.first {
  947. return await product.subscription?.isEligibleForIntroOffer ?? false
  948. }
  949. return false
  950. } catch {
  951. print("Error checking trial eligibility: \(error)")
  952. return false
  953. }
  954. }
  955. }