diff --git a/mobile/ios/Runner/Images/ImageProcessing.swift b/mobile/ios/Runner/Images/ImageProcessing.swift new file mode 100644 index 0000000000..2270bbffac --- /dev/null +++ b/mobile/ios/Runner/Images/ImageProcessing.swift @@ -0,0 +1,7 @@ +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 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 4f2090443a..c2d4818d8c 100644 --- a/mobile/ios/Runner/Images/LocalImagesImpl.swift +++ b/mobile/ios/Runner/Images/LocalImagesImpl.swift @@ -34,7 +34,6 @@ class LocalImageApiImpl: LocalImageApi { 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 var rgbaFormat = vImage_CGImageFormat( bitsPerComponent: 8, @@ -44,8 +43,6 @@ class LocalImageApiImpl: LocalImageApi { renderingIntent: .defaultIntent )! private static var requests = [Int64: LocalImageRequest]() - private static let cancelledResult = Result<[String: Int64]?, any Error>.success(nil) - private static let concurrencySemaphore = DispatchSemaphore(value: ProcessInfo.processInfo.activeProcessorCount * 2) private static let assetCache = { let assetCache = NSCache() assetCache.countLimit = 10000 @@ -53,7 +50,7 @@ class LocalImageApiImpl: LocalImageApi { }() func getThumbhash(thumbhash: String, completion: @escaping (Result<[String : Int64], any Error>) -> Void) { - Self.processingQueue.async { + ImageProcessing.queue.async { guard let data = Data(base64Encoded: thumbhash) else { return completion(.failure(PigeonError(code: "", message: "Invalid base64 string: \(thumbhash)", details: nil)))} @@ -71,16 +68,16 @@ class LocalImageApiImpl: LocalImageApi { let request = LocalImageRequest(callback: completion) let item = DispatchWorkItem { if request.isCancelled { - return completion(Self.cancelledResult) + return completion(ImageProcessing.cancelledResult) } - Self.concurrencySemaphore.wait() + ImageProcessing.semaphore.wait() defer { - Self.concurrencySemaphore.signal() + ImageProcessing.semaphore.signal() } if request.isCancelled { - return completion(Self.cancelledResult) + return completion(ImageProcessing.cancelledResult) } guard let asset = Self.requestAsset(assetId: assetId) @@ -91,7 +88,7 @@ class LocalImageApiImpl: LocalImageApi { } if request.isCancelled { - return completion(Self.cancelledResult) + return completion(ImageProcessing.cancelledResult) } var image: UIImage? @@ -116,7 +113,7 @@ class LocalImageApiImpl: LocalImageApi { } if request.isCancelled { - return completion(Self.cancelledResult) + return completion(ImageProcessing.cancelledResult) } do { @@ -124,7 +121,7 @@ class LocalImageApiImpl: LocalImageApi { if request.isCancelled { buffer.free() - return completion(Self.cancelledResult) + return completion(ImageProcessing.cancelledResult) } request.callback(.success([ @@ -143,7 +140,7 @@ class LocalImageApiImpl: LocalImageApi { request.workItem = item Self.add(requestId: requestId, request: request) - Self.processingQueue.async(execute: item) + ImageProcessing.queue.async(execute: item) } func cancelRequest(requestId: Int64) { @@ -164,7 +161,7 @@ class LocalImageApiImpl: LocalImageApi { request.isCancelled = true guard let item = request.workItem else { return } if item.isCancelled { - cancelQueue.async { request.callback(Self.cancelledResult) } + cancelQueue.async { request.callback(ImageProcessing.cancelledResult) } } } } diff --git a/mobile/ios/Runner/Images/RemoteImagesImpl.swift b/mobile/ios/Runner/Images/RemoteImagesImpl.swift index f1bd90585a..e4523aa442 100644 --- a/mobile/ios/Runner/Images/RemoteImagesImpl.swift +++ b/mobile/ios/Runner/Images/RemoteImagesImpl.swift @@ -19,7 +19,6 @@ class RemoteImageRequest { class RemoteImageApiImpl: NSObject, RemoteImageApi { private static var lock = os_unfair_lock() private static var requests = [Int64: RemoteImageRequest]() - private static let cancelledResult = Result<[String: Int64]?, any Error>.success(nil) private static var rgbaFormat = vImage_CGImageFormat( bitsPerComponent: 8, bitsPerPixel: 32, @@ -64,45 +63,54 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi { if let error = error { if request.isCancelled || (error as NSError).code == NSURLErrorCancelled { - return request.completion(cancelledResult) + return request.completion(ImageProcessing.cancelledResult) } return request.completion(.failure(error)) } if request.isCancelled { - return request.completion(cancelledResult) + return request.completion(ImageProcessing.cancelledResult) } guard let data = data else { return request.completion(.failure(PigeonError(code: "", message: "No data received", details: nil))) } - guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil), - let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, decodeOptions) else { - return request.completion(.failure(PigeonError(code: "", message: "Failed to decode image for request", details: nil))) - } - - if request.isCancelled { - return request.completion(cancelledResult) - } - - do { - let buffer = try vImage_Buffer(cgImage: cgImage, format: rgbaFormat) + ImageProcessing.queue.async { + ImageProcessing.semaphore.wait() + defer { ImageProcessing.semaphore.signal() } if request.isCancelled { - buffer.free() - return request.completion(cancelledResult) + return request.completion(ImageProcessing.cancelledResult) } - request.completion( - .success([ - "pointer": Int64(Int(bitPattern: buffer.data)), - "width": Int64(buffer.width), - "height": Int64(buffer.height), - "rowBytes": Int64(buffer.rowBytes), - ])) - } catch { - return request.completion(.failure(PigeonError(code: "", message: "Failed to convert image for request: \(error)", details: nil))) + guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil), + let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, decodeOptions) else { + return request.completion(.failure(PigeonError(code: "", message: "Failed to decode image for request", details: nil))) + } + + if request.isCancelled { + return request.completion(ImageProcessing.cancelledResult) + } + + do { + let buffer = try vImage_Buffer(cgImage: cgImage, format: rgbaFormat) + + if request.isCancelled { + buffer.free() + return request.completion(ImageProcessing.cancelledResult) + } + + request.completion( + .success([ + "pointer": Int64(Int(bitPattern: buffer.data)), + "width": Int64(buffer.width), + "height": Int64(buffer.height), + "rowBytes": Int64(buffer.rowBytes), + ])) + } catch { + return request.completion(.failure(PigeonError(code: "", message: "Failed to convert image for request: \(error)", details: nil))) + } } }