diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 48826a20f1..c5e20dd48f 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -29,9 +29,11 @@ FAC6F89B2D287C890078CB2F /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = FAC6F8902D287C890078CB2F /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; FAC6F8B72D287F120078CB2F /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6F8B52D287F120078CB2F /* ShareViewController.swift */; }; FAC6F8B92D287F120078CB2F /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FAC6F8B32D287F120078CB2F /* MainInterface.storyboard */; }; - FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEAFA8722E4D42F4001E47FE /* Thumbhash.swift */; }; - FED3B1962E253E9B0030FD97 /* ThumbnailsImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED3B1942E253E9B0030FD97 /* ThumbnailsImpl.swift */; }; - FED3B1972E253E9B0030FD97 /* Thumbnails.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED3B1932E253E9B0030FD97 /* Thumbnails.g.swift */; }; + FEC340D12E7326630050078A /* AssetResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC340C92E7326630050078A /* AssetResolver.swift */; }; + FEC340D22E7326630050078A /* Thumbhash.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC340CB2E7326630050078A /* Thumbhash.swift */; }; + FEC340D32E7326630050078A /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC340CF2E7326630050078A /* Request.swift */; }; + FEC340D42E7326630050078A /* ThumbnailResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC340CC2E7326630050078A /* ThumbnailResolver.swift */; }; + FEC340D52E7326630050078A /* Thumbnails.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC340CD2E7326630050078A /* Thumbnails.g.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -115,9 +117,11 @@ FAC6F8B42D287F120078CB2F /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = ""; }; FAC6F8B52D287F120078CB2F /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = ""; }; - FEAFA8722E4D42F4001E47FE /* Thumbhash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbhash.swift; sourceTree = ""; }; - FED3B1932E253E9B0030FD97 /* Thumbnails.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbnails.g.swift; sourceTree = ""; }; - FED3B1942E253E9B0030FD97 /* ThumbnailsImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailsImpl.swift; sourceTree = ""; }; + FEC340C92E7326630050078A /* AssetResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetResolver.swift; sourceTree = ""; }; + FEC340CB2E7326630050078A /* Thumbhash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbhash.swift; sourceTree = ""; }; + FEC340CC2E7326630050078A /* ThumbnailResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailResolver.swift; sourceTree = ""; }; + FEC340CD2E7326630050078A /* Thumbnails.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbnails.g.swift; sourceTree = ""; }; + FEC340CF2E7326630050078A /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -247,6 +251,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + FEC340D02E7326630050078A /* Resolvers */, B25D37792E72CA15008B6CA7 /* Connectivity */, B21E34A62E5AF9760031FDB9 /* Background */, B2CF7F8C2DDE4EBB00744BF6 /* Sync */, @@ -261,7 +266,6 @@ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, - FED3B1952E253E9B0030FD97 /* Images */, ); path = Runner; sourceTree = ""; @@ -296,16 +300,34 @@ path = ShareExtension; sourceTree = ""; }; - FED3B1952E253E9B0030FD97 /* Images */ = { + FEC340CA2E7326630050078A /* Assets */ = { isa = PBXGroup; children = ( - FEAFA8722E4D42F4001E47FE /* Thumbhash.swift */, - FED3B1932E253E9B0030FD97 /* Thumbnails.g.swift */, - FED3B1942E253E9B0030FD97 /* ThumbnailsImpl.swift */, + FEC340C92E7326630050078A /* AssetResolver.swift */, + ); + path = Assets; + sourceTree = ""; + }; + FEC340CE2E7326630050078A /* Images */ = { + isa = PBXGroup; + children = ( + FEC340CB2E7326630050078A /* Thumbhash.swift */, + FEC340CC2E7326630050078A /* ThumbnailResolver.swift */, + FEC340CD2E7326630050078A /* Thumbnails.g.swift */, ); path = Images; sourceTree = ""; }; + FEC340D02E7326630050078A /* Resolvers */ = { + isa = PBXGroup; + children = ( + FEC340CA2E7326630050078A /* Assets */, + FEC340CE2E7326630050078A /* Images */, + FEC340CF2E7326630050078A /* Request.swift */, + ); + path = Resolvers; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -573,14 +595,16 @@ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */, B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */, - FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */, B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */, - FED3B1962E253E9B0030FD97 /* ThumbnailsImpl.swift in Sources */, B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */, - FED3B1972E253E9B0030FD97 /* Thumbnails.g.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */, 65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */, + FEC340D12E7326630050078A /* AssetResolver.swift in Sources */, + FEC340D22E7326630050078A /* Thumbhash.swift in Sources */, + FEC340D32E7326630050078A /* Request.swift in Sources */, + FEC340D42E7326630050078A /* ThumbnailResolver.swift in Sources */, + FEC340D52E7326630050078A /* Thumbnails.g.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift index 3476030923..4f0fc67595 100644 --- a/mobile/ios/Runner/AppDelegate.swift +++ b/mobile/ios/Runner/AppDelegate.swift @@ -15,7 +15,7 @@ import UIKit ) -> Bool { // Required for flutter_local_notification if #available(iOS 10.0, *) { - UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate + UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate } GeneratedPluginRegistrant.register(with: self) @@ -53,7 +53,7 @@ import UIKit public static func registerPlugins(binaryMessenger: FlutterBinaryMessenger) { NativeSyncApiSetup.setUp(binaryMessenger: binaryMessenger, api: NativeSyncApiImpl()) - ThumbnailApiSetup.setUp(binaryMessenger: binaryMessenger, api: ThumbnailApiImpl()) + ThumbnailApiSetup.setUp(binaryMessenger: binaryMessenger, api: ThumbnailResolver()) BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: BackgroundWorkerApiImpl()) } } diff --git a/mobile/ios/Runner/Resolvers/Assets/AssetResolver.swift b/mobile/ios/Runner/Resolvers/Assets/AssetResolver.swift new file mode 100644 index 0000000000..480f6088e3 --- /dev/null +++ b/mobile/ios/Runner/Resolvers/Assets/AssetResolver.swift @@ -0,0 +1,103 @@ +import Photos + +class AssetRequest: Request { + let assetId: String + var completion: (PHAsset?) -> Void + + init(cancellationToken: CancellationToken, assetId: String, completion: @escaping (PHAsset?) -> Void) { + self.assetId = assetId + self.completion = completion + super.init(cancellationToken: cancellationToken) + } +} + +class AssetResolver { + private static let requestQueue = DispatchQueue(label: "assets.requests", qos: .userInitiated) + private static let processingQueue = DispatchQueue(label: "assets.processing", qos: .userInitiated) + + private static var batchTimer: DispatchWorkItem? + private static let batchLock = NSLock() + private static let batchTimeout: TimeInterval = 0.001 // 1ms + + private static let fetchOptions = { + let fetchOptions = PHFetchOptions() + fetchOptions.wantsIncrementalChangeDetails = false + return fetchOptions + }() + private static var assetRequests = [AssetRequest]() + private static let assetCache = { + let assetCache = NSCache() + assetCache.countLimit = 10000 + return assetCache + }() + + static func requestAsset(request: AssetRequest) { + requestQueue.async { + if (request.isCancelled) { + request.completion(nil) + } + + if let cachedAsset = assetCache.object(forKey: request.assetId as NSString) { + request.completion(cachedAsset) + return + } + + batchLock.lock() + if (request.isCancelled) { + batchLock.unlock() + request.completion(nil) + return + } + + assetRequests.append(request) + + batchTimer?.cancel() + let timer = DispatchWorkItem(block: processBatch) + batchTimer = timer + batchLock.unlock() + + processingQueue.asyncAfter(deadline: .now() + batchTimeout, execute: timer) + } + } + + private static func processBatch() { + batchLock.lock() + var completionMap = [String: [(PHAsset?) -> Void]]() + var activeAssetIds = [String]() + completionMap.reserveCapacity(assetRequests.count) + activeAssetIds.reserveCapacity(assetRequests.count) + for request in assetRequests { + if (request.isCancelled) { + request.completion(nil) + continue + } + + if var completions = completionMap[request.assetId] { + completions.append(request.completion) + } else { + activeAssetIds.append(request.assetId) + completionMap[request.assetId] = [request.completion] + } + } + assetRequests.removeAll(keepingCapacity: true) + batchTimer = nil + batchLock.unlock() + + guard !activeAssetIds.isEmpty else { return } + + let assets = PHAsset.fetchAssets(withLocalIdentifiers: activeAssetIds, options: Self.fetchOptions) + assets.enumerateObjects { asset, _, _ in + let assetId = asset.localIdentifier + for completion in completionMap.removeValue(forKey: assetId)! { + completion(asset) + } + requestQueue.async { assetCache.setObject(asset, forKey: assetId as NSString) } + } + + for completions in completionMap.values { + for completion in completions { + completion(nil) + } + } + } +} diff --git a/mobile/ios/Runner/Images/Thumbhash.swift b/mobile/ios/Runner/Resolvers/Images/Thumbhash.swift similarity index 100% rename from mobile/ios/Runner/Images/Thumbhash.swift rename to mobile/ios/Runner/Resolvers/Images/Thumbhash.swift diff --git a/mobile/ios/Runner/Images/ThumbnailsImpl.swift b/mobile/ios/Runner/Resolvers/Images/ThumbnailResolver.swift similarity index 67% rename from mobile/ios/Runner/Images/ThumbnailsImpl.swift rename to mobile/ios/Runner/Resolvers/Images/ThumbnailResolver.swift index f611f3f5a6..048afd530f 100644 --- a/mobile/ios/Runner/Images/ThumbnailsImpl.swift +++ b/mobile/ios/Runner/Resolvers/Images/ThumbnailResolver.swift @@ -3,38 +3,6 @@ import Flutter import MobileCoreServices import Photos -class CancellationToken { - var isCancelled = false -} - -class Request { - let cancellationToken: CancellationToken - - init(cancellationToken: CancellationToken) { - self.cancellationToken = cancellationToken - } - - var isCancelled: Bool { - get { - return cancellationToken.isCancelled - } - set(newValue) { - cancellationToken.isCancelled = newValue - } - } -} - -class AssetRequest: Request { - let assetId: String - var completion: (PHAsset?) -> Void - - init(cancellationToken: CancellationToken, assetId: String, completion: @escaping (PHAsset?) -> Void) { - self.assetId = assetId - self.completion = completion - super.init(cancellationToken: cancellationToken) - } -} - class ThumbnailRequest: Request { weak var workItem: DispatchWorkItem? let completion: (Result<[String: Int64], any Error>) -> Void @@ -45,13 +13,8 @@ class ThumbnailRequest: Request { } } -class ThumbnailApiImpl: ThumbnailApi { +class ThumbnailResolver: ThumbnailApi { private static let imageManager = PHImageManager.default() - private static let fetchOptions = { - let fetchOptions = PHFetchOptions() - fetchOptions.wantsIncrementalChangeDetails = false - return fetchOptions - }() private static let requestOptions = { let requestOptions = PHImageRequestOptions() requestOptions.isNetworkAccessAllowed = true @@ -62,29 +25,17 @@ class ThumbnailApiImpl: ThumbnailApi { return requestOptions }() - private static let assetQueue = DispatchQueue(label: "thumbnail.assets", qos: .userInitiated) private static let requestQueue = DispatchQueue(label: "thumbnail.requests", qos: .userInitiated) private static let cancelQueue = DispatchQueue(label: "thumbnail.cancellation", qos: .default) private static let processingQueue = DispatchQueue(label: "thumbnail.processing", qos: .userInteractive, attributes: .concurrent) - private static let batchQueue = DispatchQueue(label: "thumbnail.batching", qos: .userInitiated) private static let rgbColorSpace = CGColorSpaceCreateDeviceRGB() private static let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).rawValue private static var requests = [Int64: ThumbnailRequest]() private static let cancelledResult = Result<[String: Int64], any Error>.success([:]) private static let thumbnailConcurrencySemaphore = DispatchSemaphore(value: ProcessInfo.processInfo.activeProcessorCount / 2 + 1) - private static let assetCache = { - let assetCache = NSCache() - assetCache.countLimit = 10000 - return assetCache - }() private static let activitySemaphore = DispatchSemaphore(value: 1) - private static var assetRequests = [AssetRequest]() - private static var batchTimer: DispatchWorkItem? - private static let batchLock = NSLock() - private static let batchTimeout: TimeInterval = 0.001 // 1ms - private static let willResignActiveObserver = NotificationCenter.default.addObserver( forName: UIApplication.willResignActiveNotification, object: nil, @@ -116,7 +67,7 @@ class ThumbnailApiImpl: ThumbnailApi { func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, completion: @escaping (Result<[String: Int64], any Error>) -> Void) { let cancellationToken = CancellationToken() let thumbnailRequest = ThumbnailRequest(cancellationToken: cancellationToken, completion: completion) - Self.requestAsset(request: AssetRequest(cancellationToken: cancellationToken, assetId: assetId) { asset in + AssetResolver.requestAsset(request: AssetRequest(cancellationToken: cancellationToken, assetId: assetId) { asset in let item = DispatchWorkItem { if cancellationToken.isCancelled { return completion(Self.cancelledResult) @@ -231,70 +182,6 @@ class ThumbnailApiImpl: ThumbnailApi { } } - private static func requestAsset(request: AssetRequest) { - assetQueue.async { - if (request.isCancelled) { - request.completion(nil) - } - - if let cachedAsset = assetCache.object(forKey: request.assetId as NSString) { - request.completion(cachedAsset) - return - } - - batchLock.lock() - if (request.isCancelled) { - batchLock.unlock() - request.completion(nil) - return - } - - assetRequests.append(request) - - batchTimer?.cancel() - let timer = DispatchWorkItem(block: processBatch) - batchTimer = timer - batchLock.unlock() - - batchQueue.asyncAfter(deadline: .now() + batchTimeout, execute: timer) - } - } - - private static func processBatch() { - batchLock.lock() - var completionMap = [String: [(PHAsset?) -> Void]]() - var activeAssetIds = [String]() - completionMap.reserveCapacity(assetRequests.count) - activeAssetIds.reserveCapacity(assetRequests.count) - for request in assetRequests { - if (request.isCancelled) { - request.completion(nil) - continue - } - - if var completions = completionMap[request.assetId] { - completions.append(request.completion) - } else { - activeAssetIds.append(request.assetId) - completionMap[request.assetId] = [request.completion] - } - } - assetRequests.removeAll(keepingCapacity: true) - batchTimer = nil - batchLock.unlock() - - guard !requests.isEmpty else { return } - - let assets = PHAsset.fetchAssets(withLocalIdentifiers: activeAssetIds, options: Self.fetchOptions) - assets.enumerateObjects { asset, _, _ in - let assetId = asset.localIdentifier - for completion in completionMap[assetId]! { - completion(asset) - } - assetQueue.async { assetCache.setObject(asset, forKey: assetId as NSString) } - } - } - func waitForActiveState() { Self.activitySemaphore.wait() Self.activitySemaphore.signal() diff --git a/mobile/ios/Runner/Images/Thumbnails.g.swift b/mobile/ios/Runner/Resolvers/Images/Thumbnails.g.swift similarity index 100% rename from mobile/ios/Runner/Images/Thumbnails.g.swift rename to mobile/ios/Runner/Resolvers/Images/Thumbnails.g.swift diff --git a/mobile/ios/Runner/Resolvers/Request.swift b/mobile/ios/Runner/Resolvers/Request.swift new file mode 100644 index 0000000000..52370630fc --- /dev/null +++ b/mobile/ios/Runner/Resolvers/Request.swift @@ -0,0 +1,20 @@ +class CancellationToken { + var isCancelled = false +} + +class Request { + let cancellationToken: CancellationToken + + init(cancellationToken: CancellationToken) { + self.cancellationToken = cancellationToken + } + + var isCancelled: Bool { + get { + return cancellationToken.isCancelled + } + set(newValue) { + cancellationToken.isCancelled = newValue + } + } +}