From de9c217d51628a284fec457e37857bdcf5430042 Mon Sep 17 00:00:00 2001 From: LeLunZ <31982496+LeLunZ@users.noreply.github.com> Date: Thu, 2 Apr 2026 20:04:24 +0200 Subject: [PATCH 1/5] refactor: replace DispatchQueue + DispatchSemaphore with OperationQueue for image processing --- .../ios/Runner/Images/ImageProcessing.swift | 9 ++++++-- .../ios/Runner/Images/LocalImagesImpl.swift | 23 ++++++------------- .../ios/Runner/Images/RemoteImagesImpl.swift | 5 +--- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/mobile/ios/Runner/Images/ImageProcessing.swift b/mobile/ios/Runner/Images/ImageProcessing.swift index 2270bbffac871..686a3464a702f 100644 --- a/mobile/ios/Runner/Images/ImageProcessing.swift +++ b/mobile/ios/Runner/Images/ImageProcessing.swift @@ -1,7 +1,12 @@ import Foundation enum ImageProcessing { - static let queue = DispatchQueue(label: "thumbnail.processing", qos: .userInitiated, attributes: .concurrent) - static let semaphore = DispatchSemaphore(value: ProcessInfo.processInfo.activeProcessorCount * 2) + static let queue = { + let q = OperationQueue() + q.name = "thumbnail.processing" + q.qualityOfService = .userInitiated + q.maxConcurrentOperationCount = ProcessInfo.processInfo.activeProcessorCount * 2 + return q + }() static let cancelledResult = Result<[String: Int64]?, any Error>.success(nil) } diff --git a/mobile/ios/Runner/Images/LocalImagesImpl.swift b/mobile/ios/Runner/Images/LocalImagesImpl.swift index 303ff5bc338d8..f0710bf75f14a 100644 --- a/mobile/ios/Runner/Images/LocalImagesImpl.swift +++ b/mobile/ios/Runner/Images/LocalImagesImpl.swift @@ -4,7 +4,7 @@ import MobileCoreServices import Photos class LocalImageRequest { - weak var workItem: DispatchWorkItem? + weak var operation: Operation? var isCancelled = false let callback: (Result<[String: Int64]?, any Error>) -> Void @@ -50,7 +50,7 @@ class LocalImageApiImpl: LocalImageApi { }() func getThumbhash(thumbhash: String, completion: @escaping (Result<[String : Int64], any Error>) -> Void) { - ImageProcessing.queue.async { + ImageProcessing.queue.addOperation { guard let data = Data(base64Encoded: thumbhash) else { return completion(.failure(PigeonError(code: "", message: "Invalid base64 string: \(thumbhash)", details: nil)))} @@ -66,16 +66,7 @@ class LocalImageApiImpl: LocalImageApi { func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, preferEncoded: Bool, completion: @escaping (Result<[String: Int64]?, any Error>) -> Void) { let request = LocalImageRequest(callback: completion) - let item = DispatchWorkItem { - if request.isCancelled { - return completion(ImageProcessing.cancelledResult) - } - - ImageProcessing.semaphore.wait() - defer { - ImageProcessing.semaphore.signal() - } - + let operation = BlockOperation { if request.isCancelled { return completion(ImageProcessing.cancelledResult) } @@ -180,9 +171,9 @@ class LocalImageApiImpl: LocalImageApi { } } - request.workItem = item + request.operation = operation Self.add(requestId: requestId, request: request) - ImageProcessing.queue.async(execute: item) + ImageProcessing.queue.addOperation(operation) } func cancelRequest(requestId: Int64) { @@ -201,8 +192,8 @@ class LocalImageApiImpl: LocalImageApi { requestQueue.async { guard let request = requests.removeValue(forKey: requestId) else { return } request.isCancelled = true - guard let item = request.workItem else { return } - if item.isCancelled { + guard let operation = request.operation else { return } + if operation.isCancelled { cancelQueue.async { request.callback(ImageProcessing.cancelledResult) } } } diff --git a/mobile/ios/Runner/Images/RemoteImagesImpl.swift b/mobile/ios/Runner/Images/RemoteImagesImpl.swift index f2a0c37254b87..7c69acba7673f 100644 --- a/mobile/ios/Runner/Images/RemoteImagesImpl.swift +++ b/mobile/ios/Runner/Images/RemoteImagesImpl.swift @@ -73,10 +73,7 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi { return request.completion(.failure(PigeonError(code: "", message: "No data received", details: nil))) } - ImageProcessing.queue.async { - ImageProcessing.semaphore.wait() - defer { ImageProcessing.semaphore.signal() } - + ImageProcessing.queue.addOperation { if request.isCancelled { return request.completion(ImageProcessing.cancelledResult) } From d71afee0c325312f9270462d155d119e5acca547 Mon Sep 17 00:00:00 2001 From: LeLunZ <31982496+LeLunZ@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:19:06 +0200 Subject: [PATCH 2/5] implement RequestRegistry and UnfairLock for managing cancellable requests --- mobile/ios/Runner/Images/ImageRequest.swift | 23 +++++++++++++++++++++ mobile/ios/Runner/Images/UnfairLock.swift | 23 +++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 mobile/ios/Runner/Images/ImageRequest.swift create mode 100644 mobile/ios/Runner/Images/UnfairLock.swift diff --git a/mobile/ios/Runner/Images/ImageRequest.swift b/mobile/ios/Runner/Images/ImageRequest.swift new file mode 100644 index 0000000000000..a3ab8490d35c7 --- /dev/null +++ b/mobile/ios/Runner/Images/ImageRequest.swift @@ -0,0 +1,23 @@ +import Foundation + +protocol Cancellable: AnyObject { + func cancel() +} + +class RequestRegistry { + private let lock = UnfairLock() + private var requests = [Int64: T]() + + func add(requestId: Int64, request: T) { + lock.withLock { requests[requestId] = request } + } + + @discardableResult + func remove(requestId: Int64) -> T? { + lock.withLock { requests.removeValue(forKey: requestId) } + } + + func cancel(requestId: Int64) { + remove(requestId: requestId)?.cancel() + } +} diff --git a/mobile/ios/Runner/Images/UnfairLock.swift b/mobile/ios/Runner/Images/UnfairLock.swift new file mode 100644 index 0000000000000..c94b013557ef9 --- /dev/null +++ b/mobile/ios/Runner/Images/UnfairLock.swift @@ -0,0 +1,23 @@ +import Foundation + +// Can be replaced with OSAllocatedUnfairLock when the deployment target is iOS 16+ +final class UnfairLock { + private let _lock: UnsafeMutablePointer + + init() { + _lock = .allocate(capacity: 1) + _lock.initialize(to: os_unfair_lock()) + } + + deinit { + _lock.deinitialize(count: 1) + _lock.deallocate() + } + + @discardableResult + func withLock(_ body: () throws -> T) rethrows -> T { + os_unfair_lock_lock(_lock) + defer { os_unfair_lock_unlock(_lock) } + return try body() + } +} From 272d0cea0896b7e5153eead41e8f5c90b775e8b7 Mon Sep 17 00:00:00 2001 From: LeLunZ <31982496+LeLunZ@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:32:48 +0200 Subject: [PATCH 3/5] implement requests registry for local and remote image processing --- mobile/ios/Runner.xcodeproj/project.pbxproj | 8 +++ .../ios/Runner/Images/LocalImagesImpl.swift | 59 +++++++------------ .../ios/Runner/Images/RemoteImagesImpl.swift | 29 ++++----- 3 files changed, 39 insertions(+), 57 deletions(-) diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 22a7abcbac8e9..3153c7140b8b2 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -16,6 +16,8 @@ 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 */; }; + A01DD69A2F7F43B40049AB63 /* UnfairLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A01DD6992F7F43B40049AB63 /* UnfairLock.swift */; }; + 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 +104,8 @@ 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 = ""; }; + A01DD6992F7F43B40049AB63 /* UnfairLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnfairLock.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 = ""; }; @@ -327,6 +331,8 @@ FED3B1952E253E9B0030FD97 /* Images */ = { isa = PBXGroup; children = ( + A01DD6982F7F43B40049AB63 /* ImageRequest.swift */, + A01DD6992F7F43B40049AB63 /* UnfairLock.swift */, FE5FE4AD2F30FBC000A71243 /* ImageProcessing.swift */, FE5499F72F1198DE006016CB /* RemoteImagesImpl.swift */, FE5499F52F11980E006016CB /* LocalImagesImpl.swift */, @@ -608,6 +614,8 @@ files = ( 65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + A01DD69A2F7F43B40049AB63 /* UnfairLock.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/LocalImagesImpl.swift b/mobile/ios/Runner/Images/LocalImagesImpl.swift index f0710bf75f14a..6604a2902f1e2 100644 --- a/mobile/ios/Runner/Images/LocalImagesImpl.swift +++ b/mobile/ios/Runner/Images/LocalImagesImpl.swift @@ -3,7 +3,7 @@ import Flutter import MobileCoreServices import Photos -class LocalImageRequest { +class LocalImageRequest: Cancellable { weak var operation: Operation? var isCancelled = false let callback: (Result<[String: Int64]?, any Error>) -> Void @@ -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.cancel(requestId: requestId) } 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..c34f9a56e51f4 100644 --- a/mobile/ios/Runner/Images/RemoteImagesImpl.swift +++ b/mobile/ios/Runner/Images/RemoteImagesImpl.swift @@ -3,7 +3,7 @@ import Flutter import MobileCoreServices import Photos -class RemoteImageRequest { +class RemoteImageRequest: Cancellable { weak var task: URLSessionDataTask? let id: Int64 var isCancelled = false @@ -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.cancel(requestId: requestId) } func clearCache(completion: @escaping (Result) -> Void) { From f5f3368501a8d7263ce9c02f7d68db3e070e8ebd Mon Sep 17 00:00:00 2001 From: LeLunZ <31982496+LeLunZ@users.noreply.github.com> Date: Sun, 5 Apr 2026 19:52:42 +0200 Subject: [PATCH 4/5] remove Cancellable protocol and cancel method from request registry --- mobile/ios/Runner/Images/ImageRequest.swift | 10 +--------- mobile/ios/Runner/Images/LocalImagesImpl.swift | 4 ++-- mobile/ios/Runner/Images/RemoteImagesImpl.swift | 4 ++-- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/mobile/ios/Runner/Images/ImageRequest.swift b/mobile/ios/Runner/Images/ImageRequest.swift index a3ab8490d35c7..a5553083a91b7 100644 --- a/mobile/ios/Runner/Images/ImageRequest.swift +++ b/mobile/ios/Runner/Images/ImageRequest.swift @@ -1,10 +1,6 @@ import Foundation -protocol Cancellable: AnyObject { - func cancel() -} - -class RequestRegistry { +class RequestRegistry { private let lock = UnfairLock() private var requests = [Int64: T]() @@ -16,8 +12,4 @@ class RequestRegistry { func remove(requestId: Int64) -> T? { lock.withLock { requests.removeValue(forKey: requestId) } } - - func cancel(requestId: Int64) { - remove(requestId: requestId)?.cancel() - } } diff --git a/mobile/ios/Runner/Images/LocalImagesImpl.swift b/mobile/ios/Runner/Images/LocalImagesImpl.swift index 6604a2902f1e2..713a24a2dd594 100644 --- a/mobile/ios/Runner/Images/LocalImagesImpl.swift +++ b/mobile/ios/Runner/Images/LocalImagesImpl.swift @@ -3,7 +3,7 @@ import Flutter import MobileCoreServices import Photos -class LocalImageRequest: Cancellable { +class LocalImageRequest { weak var operation: Operation? var isCancelled = false let callback: (Result<[String: Int64]?, any Error>) -> Void @@ -177,7 +177,7 @@ class LocalImageApiImpl: LocalImageApi { } func cancelRequest(requestId: Int64) { - Self.registry.cancel(requestId: requestId) + Self.registry.remove(requestId: requestId)?.cancel() } private static func requestAsset(assetId: String) -> PHAsset? { diff --git a/mobile/ios/Runner/Images/RemoteImagesImpl.swift b/mobile/ios/Runner/Images/RemoteImagesImpl.swift index c34f9a56e51f4..37d37f597be60 100644 --- a/mobile/ios/Runner/Images/RemoteImagesImpl.swift +++ b/mobile/ios/Runner/Images/RemoteImagesImpl.swift @@ -3,7 +3,7 @@ import Flutter import MobileCoreServices import Photos -class RemoteImageRequest: Cancellable { +class RemoteImageRequest { weak var task: URLSessionDataTask? let id: Int64 var isCancelled = false @@ -126,7 +126,7 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi { } func cancelRequest(requestId: Int64) { - Self.registry.cancel(requestId: requestId) + Self.registry.remove(requestId: requestId)?.cancel() } func clearCache(completion: @escaping (Result) -> Void) { From 9ddace5753491c54930c34a6eac48d055fed2516 Mon Sep 17 00:00:00 2001 From: mertalev <101130780+mertalev@users.noreply.github.com> Date: Sun, 5 Apr 2026 18:31:30 -0400 Subject: [PATCH 5/5] use mutex --- mobile/ios/Runner.xcodeproj/project.pbxproj | 34 +++++++++---- mobile/ios/Runner/Images/ImageRequest.swift | 9 ++-- mobile/ios/Runner/Images/UnfairLock.swift | 23 --------- mobile/ios/Runner/Utility/Mutex.swift | 54 +++++++++++++++++++++ 4 files changed, 82 insertions(+), 38 deletions(-) delete mode 100644 mobile/ios/Runner/Images/UnfairLock.swift create mode 100644 mobile/ios/Runner/Utility/Mutex.swift diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 3153c7140b8b2..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,7 +16,6 @@ 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 */; }; - A01DD69A2F7F43B40049AB63 /* UnfairLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A01DD6992F7F43B40049AB63 /* UnfairLock.swift */; }; 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 */; }; @@ -105,7 +104,6 @@ 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 = ""; }; - A01DD6992F7F43B40049AB63 /* UnfairLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnfairLock.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 = ""; }; @@ -141,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 = ""; }; @@ -166,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 = ""; }; @@ -277,6 +284,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + FE1BB4562F8319560087DBF9 /* Utility */, FEE084F22EC172080045228E /* Schemas */, B231F52D2E93A44A00BC45D1 /* Core */, B25D37792E72CA15008B6CA7 /* Connectivity */, @@ -332,7 +340,6 @@ isa = PBXGroup; children = ( A01DD6982F7F43B40049AB63 /* ImageRequest.swift */, - A01DD6992F7F43B40049AB63 /* UnfairLock.swift */, FE5FE4AD2F30FBC000A71243 /* ImageProcessing.swift */, FE5499F72F1198DE006016CB /* RemoteImagesImpl.swift */, FE5499F52F11980E006016CB /* LocalImagesImpl.swift */, @@ -564,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"; @@ -596,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"; @@ -614,7 +629,6 @@ files = ( 65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - A01DD69A2F7F43B40049AB63 /* UnfairLock.swift in Sources */, A01DD69B2F7F43B40049AB63 /* ImageRequest.swift in Sources */, B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */, FE5499F32F1197D8006016CB /* LocalImages.g.swift in Sources */, diff --git a/mobile/ios/Runner/Images/ImageRequest.swift b/mobile/ios/Runner/Images/ImageRequest.swift index a5553083a91b7..5c05bafb8379d 100644 --- a/mobile/ios/Runner/Images/ImageRequest.swift +++ b/mobile/ios/Runner/Images/ImageRequest.swift @@ -1,15 +1,14 @@ import Foundation -class RequestRegistry { - private let lock = UnfairLock() - private var requests = [Int64: T]() +struct RequestRegistry: ~Copyable, Sendable { + private let requests = Mutex<[Int64: T]>([:]) func add(requestId: Int64, request: T) { - lock.withLock { requests[requestId] = request } + requests.withLock { $0[requestId] = request } } @discardableResult func remove(requestId: Int64) -> T? { - lock.withLock { requests.removeValue(forKey: requestId) } + requests.withLock { $0.removeValue(forKey: requestId) } } } diff --git a/mobile/ios/Runner/Images/UnfairLock.swift b/mobile/ios/Runner/Images/UnfairLock.swift deleted file mode 100644 index c94b013557ef9..0000000000000 --- a/mobile/ios/Runner/Images/UnfairLock.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation - -// Can be replaced with OSAllocatedUnfairLock when the deployment target is iOS 16+ -final class UnfairLock { - private let _lock: UnsafeMutablePointer - - init() { - _lock = .allocate(capacity: 1) - _lock.initialize(to: os_unfair_lock()) - } - - deinit { - _lock.deinitialize(count: 1) - _lock.deallocate() - } - - @discardableResult - func withLock(_ body: () throws -> T) rethrows -> T { - os_unfair_lock_lock(_lock) - defer { os_unfair_lock_unlock(_lock) } - return try body() - } -} 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() + } +}