diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 22a7abcbac8e9..178454f381c5f 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 77; objects = { /* Begin PBXBuildFile section */ @@ -16,6 +16,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + A01DD69B2F7F43B40049AB63 /* ImageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A01DD6982F7F43B40049AB63 /* ImageRequest.swift */; }; B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */; }; B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */; }; B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */; }; @@ -102,6 +103,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A01DD6982F7F43B40049AB63 /* ImageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRequest.swift; sourceTree = ""; }; B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = ""; }; B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorkerApiImpl.swift; sourceTree = ""; }; B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = ""; }; @@ -137,20 +139,23 @@ ); target = F0B57D372DF764BD00DC5BCC /* WidgetExtension */; }; + FE1BB4572F83196E0087DBF9 /* Exceptions for "Utility" folder in "Runner" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Mutex.swift, + ); + target = 97C146ED1CF9000F007C117D /* Runner */; + }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ B231F52D2E93A44A00BC45D1 /* Core */ = { isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); path = Core; sourceTree = ""; }; B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = { isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); path = Sync; sourceTree = ""; }; @@ -162,10 +167,16 @@ path = WidgetExtension; sourceTree = ""; }; - FEE084F22EC172080045228E /* Schemas */ = { + FE1BB4562F8319560087DBF9 /* Utility */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( + FE1BB4572F83196E0087DBF9 /* Exceptions for "Utility" folder in "Runner" target */, ); + path = Utility; + sourceTree = ""; + }; + FEE084F22EC172080045228E /* Schemas */ = { + isa = PBXFileSystemSynchronizedRootGroup; path = Schemas; sourceTree = ""; }; @@ -273,6 +284,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + FE1BB4562F8319560087DBF9 /* Utility */, FEE084F22EC172080045228E /* Schemas */, B231F52D2E93A44A00BC45D1 /* Core */, B25D37792E72CA15008B6CA7 /* Connectivity */, @@ -327,6 +339,7 @@ FED3B1952E253E9B0030FD97 /* Images */ = { isa = PBXGroup; children = ( + A01DD6982F7F43B40049AB63 /* ImageRequest.swift */, FE5FE4AD2F30FBC000A71243 /* ImageProcessing.swift */, FE5499F72F1198DE006016CB /* RemoteImagesImpl.swift */, FE5499F52F11980E006016CB /* LocalImagesImpl.swift */, @@ -558,10 +571,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -590,10 +607,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -608,6 +629,7 @@ files = ( 65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + A01DD69B2F7F43B40049AB63 /* ImageRequest.swift in Sources */, B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */, FE5499F32F1197D8006016CB /* LocalImages.g.swift in Sources */, FE5499F62F11980E006016CB /* LocalImagesImpl.swift in Sources */, diff --git a/mobile/ios/Runner/Images/ImageRequest.swift b/mobile/ios/Runner/Images/ImageRequest.swift new file mode 100644 index 0000000000000..5c05bafb8379d --- /dev/null +++ b/mobile/ios/Runner/Images/ImageRequest.swift @@ -0,0 +1,14 @@ +import Foundation + +struct RequestRegistry: ~Copyable, Sendable { + private let requests = Mutex<[Int64: T]>([:]) + + func add(requestId: Int64, request: T) { + requests.withLock { $0[requestId] = request } + } + + @discardableResult + func remove(requestId: Int64) -> T? { + requests.withLock { $0.removeValue(forKey: requestId) } + } +} diff --git a/mobile/ios/Runner/Images/LocalImagesImpl.swift b/mobile/ios/Runner/Images/LocalImagesImpl.swift index f0710bf75f14a..713a24a2dd594 100644 --- a/mobile/ios/Runner/Images/LocalImagesImpl.swift +++ b/mobile/ios/Runner/Images/LocalImagesImpl.swift @@ -11,6 +11,11 @@ class LocalImageRequest { init(callback: @escaping (Result<[String: Int64]?, any Error>) -> Void) { self.callback = callback } + + func cancel() { + isCancelled = true + operation?.cancel() + } } class LocalImageApiImpl: LocalImageApi { @@ -31,9 +36,7 @@ class LocalImageApiImpl: LocalImageApi { 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 registry = RequestRegistry() private static var rgbaFormat = vImage_CGImageFormat( bitsPerComponent: 8, @@ -42,7 +45,6 @@ class LocalImageApiImpl: LocalImageApi { bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue), renderingIntent: .defaultIntent )! - private static var requests = [Int64: LocalImageRequest]() private static let assetCache = { let assetCache = NSCache() assetCache.countLimit = 10000 @@ -73,7 +75,7 @@ class LocalImageApiImpl: LocalImageApi { guard let asset = Self.requestAsset(assetId: assetId) else { - Self.remove(requestId: requestId) + Self.registry.remove(requestId: requestId) completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil))) return } @@ -98,12 +100,11 @@ class LocalImageApiImpl: LocalImageApi { ) if request.isCancelled { - Self.remove(requestId: requestId) return completion(ImageProcessing.cancelledResult) } guard let data = imageData else { - Self.remove(requestId: requestId) + Self.registry.remove(requestId: requestId) return completion(.failure(PigeonError(code: "", message: "Could not get image data for \(assetId)", details: nil))) } @@ -113,7 +114,6 @@ class LocalImageApiImpl: LocalImageApi { if request.isCancelled { free(pointer) - Self.remove(requestId: requestId) return completion(ImageProcessing.cancelledResult) } @@ -121,7 +121,7 @@ class LocalImageApiImpl: LocalImageApi { "pointer": Int64(Int(bitPattern: pointer)), "length": Int64(length), ])) - Self.remove(requestId: requestId) + Self.registry.remove(requestId: requestId) return } @@ -142,7 +142,7 @@ class LocalImageApiImpl: LocalImageApi { guard let image = image, let cgImage = image.cgImage else { - Self.remove(requestId: requestId) + Self.registry.remove(requestId: requestId) return completion(.failure(PigeonError(code: "", message: "Could not get pixel data for \(assetId)", details: nil))) } @@ -162,51 +162,32 @@ class LocalImageApiImpl: LocalImageApi { "pointer": Int64(Int(bitPattern: buffer.data)), "width": Int64(buffer.width), "height": Int64(buffer.height), - "rowBytes": Int64(buffer.rowBytes) + "rowBytes": Int64(buffer.rowBytes), ])) - Self.remove(requestId: requestId) + Self.registry.remove(requestId: requestId) } catch { - Self.remove(requestId: requestId) + Self.registry.remove(requestId: requestId) return completion(.failure(PigeonError(code: "", message: "Failed to convert image for \(assetId): \(error)", details: nil))) } } request.operation = operation - Self.add(requestId: requestId, request: request) + Self.registry.add(requestId: requestId, request: request) ImageProcessing.queue.addOperation(operation) } func cancelRequest(requestId: Int64) { - Self.cancel(requestId: requestId) - } - - private static func add(requestId: Int64, request: LocalImageRequest) -> Void { - requestQueue.sync { requests[requestId] = request } - } - - private static func remove(requestId: Int64) -> Void { - requestQueue.sync { requests[requestId] = nil } - } - - private static func cancel(requestId: Int64) -> Void { - requestQueue.async { - guard let request = requests.removeValue(forKey: requestId) else { return } - request.isCancelled = true - guard let operation = request.operation else { return } - if operation.isCancelled { - cancelQueue.async { request.callback(ImageProcessing.cancelledResult) } - } - } + Self.registry.remove(requestId: requestId)?.cancel() } private static func requestAsset(assetId: String) -> PHAsset? { - var asset: PHAsset? - assetQueue.sync { asset = assetCache.object(forKey: assetId as NSString) } - if asset != nil { return asset } + if let cached = assetCache.object(forKey: assetId as NSString) { + return cached + } guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: Self.fetchOptions).firstObject else { return nil } - assetQueue.async { assetCache.setObject(asset, forKey: assetId as NSString) } + assetCache.setObject(asset, forKey: assetId as NSString) return asset } } diff --git a/mobile/ios/Runner/Images/RemoteImagesImpl.swift b/mobile/ios/Runner/Images/RemoteImagesImpl.swift index 7c69acba7673f..37d37f597be60 100644 --- a/mobile/ios/Runner/Images/RemoteImagesImpl.swift +++ b/mobile/ios/Runner/Images/RemoteImagesImpl.swift @@ -14,11 +14,15 @@ class RemoteImageRequest { self.task = task self.completion = completion } + + func cancel() { + isCancelled = true + task?.cancel() + } } class RemoteImageApiImpl: NSObject, RemoteImageApi { - private static var lock = os_unfair_lock() - private static var requests = [Int64: RemoteImageRequest]() + private static let registry = RequestRegistry() private static var rgbaFormat = vImage_CGImageFormat( bitsPerComponent: 8, bitsPerPixel: 32, @@ -43,20 +47,15 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi { let request = RemoteImageRequest(id: requestId, task: task, completion: completion) - os_unfair_lock_lock(&Self.lock) - Self.requests[requestId] = request - os_unfair_lock_unlock(&Self.lock) + Self.registry.add(requestId: requestId, request: request) task.resume() } private static func handleCompletion(requestId: Int64, encoded: Bool, data: Data?, response: URLResponse?, error: Error?) { - os_unfair_lock_lock(&Self.lock) - guard let request = requests[requestId] else { - return os_unfair_lock_unlock(&Self.lock) + guard let request = registry.remove(requestId: requestId) else { + return } - requests[requestId] = nil - os_unfair_lock_unlock(&Self.lock) if let error = error { if request.isCancelled || (error as NSError).code == NSURLErrorCancelled { @@ -127,13 +126,7 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi { } func cancelRequest(requestId: Int64) { - os_unfair_lock_lock(&Self.lock) - let request = Self.requests[requestId] - os_unfair_lock_unlock(&Self.lock) - - guard let request = request else { return } - request.isCancelled = true - request.task?.cancel() + Self.registry.remove(requestId: requestId)?.cancel() } func clearCache(completion: @escaping (Result) -> Void) { diff --git a/mobile/ios/Runner/Utility/Mutex.swift b/mobile/ios/Runner/Utility/Mutex.swift new file mode 100644 index 0000000000000..fbfe168ff4577 --- /dev/null +++ b/mobile/ios/Runner/Utility/Mutex.swift @@ -0,0 +1,54 @@ +import Darwin + +// Can be replaced with std Mutex when the deployment target is iOS 18+ +struct Mutex: ~Copyable, @unchecked Sendable { + struct _Buffer: ~Copyable { + var lock: os_unfair_lock = .init() + var value: Value + + init(value: consuming Value) { + self.value = value + } + + deinit {} + } + + let _buffer: UnsafeMutablePointer<_Buffer> + + init(_ initialValue: consuming sending Value) { + _buffer = .allocate(capacity: 1) + _buffer.initialize(to: _Buffer(value: initialValue)) + } + + deinit { + _buffer.deinitialize(count: 1) + _buffer.deallocate() + } + + @discardableResult + borrowing func withLock( + _ body: (inout sending Value) throws(E) -> sending Result + ) throws(E) -> sending Result { + os_unfair_lock_lock(&_buffer.pointee.lock) + defer { os_unfair_lock_unlock(&_buffer.pointee.lock) } + return try body(&_buffer.pointee.value) + } +} + +// Can be replaced with OSAllocatedUnfairLock when the deployment target is iOS 16+ +typealias UnfairLock = Mutex + +extension Mutex where Value == Void { + init() { + self.init(()) + } + + @discardableResult + borrowing func withLock( + _ body: () throws(E) -> sending Result + ) throws(E) -> sending Result { + os_unfair_lock_lock(&_buffer.pointee.lock) + defer { os_unfair_lock_unlock(&_buffer.pointee.lock) } + return try body() + } +}