ClassifyPhotoPlugin.swift 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859
  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. DispatchQueue.global(qos: .userInitiated).async {
  667. let assets = PHAsset.fetchAssets(withLocalIdentifiers: assetIds, options: nil)
  668. // 使用原子操作确保线程安全
  669. let totalSize = Atomic<Int64>(0)
  670. let processingGroup = DispatchGroup()
  671. // 创建一个具有相同QoS的调度队列用于信号量操作
  672. let processingQueue = DispatchQueue(label: "com.yourapp.photosize.processing",
  673. qos: .userInitiated,
  674. attributes: .concurrent)
  675. // 控制并发数量
  676. let semaphore = DispatchSemaphore(value: 4)
  677. // 分批处理资源
  678. let batchSize = 20
  679. let totalCount = assets.count
  680. for batchStart in stride(from: 0, to: totalCount, by: batchSize) {
  681. let end = min(batchStart + batchSize, totalCount)
  682. processingGroup.enter()
  683. // 使用具有明确QoS的队列
  684. processingQueue.async {
  685. var batchSize: Int64 = 0
  686. for i in batchStart..<end {
  687. autoreleasepool {
  688. // 使用带超时的等待,避免无限期阻塞
  689. let waitResult = semaphore.wait(timeout: .now() + 5)
  690. defer {
  691. // 确保信号量总是被释放
  692. if waitResult != .timedOut {
  693. semaphore.signal()
  694. }
  695. }
  696. // 如果等待超时,跳过当前资源
  697. if waitResult == .timedOut {
  698. print("警告: 等待资源超时,跳过资源")
  699. return
  700. }
  701. let asset = assets.object(at: i)
  702. // 使用资源管理器获取大小
  703. PHAssetResource.assetResources(for: asset).forEach { resource in
  704. var resourceSize: Int64 = 0
  705. // 尝试获取文件大小
  706. if let fileSize = resource.value(forKey: "fileSize") as? CLong {
  707. resourceSize = Int64(fileSize)
  708. }
  709. batchSize += resourceSize
  710. }
  711. }
  712. }
  713. // 更新总大小
  714. totalSize.mutate { $0 += batchSize }
  715. processingGroup.leave()
  716. }
  717. }
  718. // 使用带超时的等待,避免无限期阻塞
  719. let waitResult = processingGroup.wait(timeout: .now() + 30)
  720. // 返回结果到主线程
  721. DispatchQueue.main.async {
  722. if waitResult == .timedOut {
  723. print("警告: 处理照片大小超时")
  724. completion(FlutterError(code: "TIMEOUT",
  725. message: "计算照片大小超时",
  726. details: nil))
  727. } else {
  728. completion(totalSize.value)
  729. }
  730. }
  731. }
  732. }
  733. }
  734. class SubscriptionHandler: NSObject {
  735. @available(iOS 15.0.0, *)
  736. func checkTrialEligibility() async -> Bool {
  737. do {
  738. // 获取产品信息
  739. let productIds = ["clean.vip.1week"]
  740. let products = try await Product.products(for: Set(productIds))
  741. // 检查第一个产品的试用资格
  742. if let product = products.first {
  743. return await product.subscription?.isEligibleForIntroOffer ?? false
  744. }
  745. return false
  746. } catch {
  747. print("Error checking trial eligibility: \(error)")
  748. return false
  749. }
  750. }
  751. }