mirror of
https://github.com/immich-app/immich.git
synced 2026-07-15 01:21:29 +00:00
refactor(mobile): introduce image request registry on iOS (#27486)
* refactor: replace DispatchQueue + DispatchSemaphore with OperationQueue for image processing * implement RequestRegistry and UnfairLock for managing cancellable requests * implement requests registry for local and remote image processing * remove Cancellable protocol and cancel method from request registry * use mutex --------- Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
This commit is contained in:
parent
f29c06799f
commit
cd86a83c33
5 changed files with 125 additions and 61 deletions
|
|
@ -3,7 +3,7 @@
|
||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 54;
|
objectVersion = 77;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
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 */; };
|
B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */; };
|
||||||
B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34AB2E5B09100031FDB9 /* BackgroundWorker.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 */; };
|
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 = "<group>"; };
|
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
A01DD6982F7F43B40049AB63 /* ImageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRequest.swift; sourceTree = "<group>"; };
|
||||||
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 = "<group>"; };
|
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 = "<group>"; };
|
||||||
B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorkerApiImpl.swift; sourceTree = "<group>"; };
|
B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorkerApiImpl.swift; sourceTree = "<group>"; };
|
||||||
B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = "<group>"; };
|
B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -137,20 +139,23 @@
|
||||||
);
|
);
|
||||||
target = F0B57D372DF764BD00DC5BCC /* WidgetExtension */;
|
target = F0B57D372DF764BD00DC5BCC /* WidgetExtension */;
|
||||||
};
|
};
|
||||||
|
FE1BB4572F83196E0087DBF9 /* Exceptions for "Utility" folder in "Runner" target */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
|
membershipExceptions = (
|
||||||
|
Mutex.swift,
|
||||||
|
);
|
||||||
|
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||||
|
};
|
||||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
B231F52D2E93A44A00BC45D1 /* Core */ = {
|
B231F52D2E93A44A00BC45D1 /* Core */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
exceptions = (
|
|
||||||
);
|
|
||||||
path = Core;
|
path = Core;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
|
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
exceptions = (
|
|
||||||
);
|
|
||||||
path = Sync;
|
path = Sync;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
|
@ -162,10 +167,16 @@
|
||||||
path = WidgetExtension;
|
path = WidgetExtension;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
FEE084F22EC172080045228E /* Schemas */ = {
|
FE1BB4562F8319560087DBF9 /* Utility */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
exceptions = (
|
exceptions = (
|
||||||
|
FE1BB4572F83196E0087DBF9 /* Exceptions for "Utility" folder in "Runner" target */,
|
||||||
);
|
);
|
||||||
|
path = Utility;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
FEE084F22EC172080045228E /* Schemas */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
path = Schemas;
|
path = Schemas;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
|
@ -273,6 +284,7 @@
|
||||||
97C146F01CF9000F007C117D /* Runner */ = {
|
97C146F01CF9000F007C117D /* Runner */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
FE1BB4562F8319560087DBF9 /* Utility */,
|
||||||
FEE084F22EC172080045228E /* Schemas */,
|
FEE084F22EC172080045228E /* Schemas */,
|
||||||
B231F52D2E93A44A00BC45D1 /* Core */,
|
B231F52D2E93A44A00BC45D1 /* Core */,
|
||||||
B25D37792E72CA15008B6CA7 /* Connectivity */,
|
B25D37792E72CA15008B6CA7 /* Connectivity */,
|
||||||
|
|
@ -327,6 +339,7 @@
|
||||||
FED3B1952E253E9B0030FD97 /* Images */ = {
|
FED3B1952E253E9B0030FD97 /* Images */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
A01DD6982F7F43B40049AB63 /* ImageRequest.swift */,
|
||||||
FE5FE4AD2F30FBC000A71243 /* ImageProcessing.swift */,
|
FE5FE4AD2F30FBC000A71243 /* ImageProcessing.swift */,
|
||||||
FE5499F72F1198DE006016CB /* RemoteImagesImpl.swift */,
|
FE5499F72F1198DE006016CB /* RemoteImagesImpl.swift */,
|
||||||
FE5499F52F11980E006016CB /* LocalImagesImpl.swift */,
|
FE5499F52F11980E006016CB /* LocalImagesImpl.swift */,
|
||||||
|
|
@ -558,10 +571,14 @@
|
||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||||
|
|
@ -590,10 +607,14 @@
|
||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
|
|
@ -608,6 +629,7 @@
|
||||||
files = (
|
files = (
|
||||||
65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */,
|
65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */,
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||||
|
A01DD69B2F7F43B40049AB63 /* ImageRequest.swift in Sources */,
|
||||||
B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */,
|
B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */,
|
||||||
FE5499F32F1197D8006016CB /* LocalImages.g.swift in Sources */,
|
FE5499F32F1197D8006016CB /* LocalImages.g.swift in Sources */,
|
||||||
FE5499F62F11980E006016CB /* LocalImagesImpl.swift in Sources */,
|
FE5499F62F11980E006016CB /* LocalImagesImpl.swift in Sources */,
|
||||||
|
|
|
||||||
14
mobile/ios/Runner/Images/ImageRequest.swift
Normal file
14
mobile/ios/Runner/Images/ImageRequest.swift
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct RequestRegistry<T: AnyObject & Sendable>: ~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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,11 @@ class LocalImageRequest {
|
||||||
init(callback: @escaping (Result<[String: Int64]?, any Error>) -> Void) {
|
init(callback: @escaping (Result<[String: Int64]?, any Error>) -> Void) {
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cancel() {
|
||||||
|
isCancelled = true
|
||||||
|
operation?.cancel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LocalImageApiImpl: LocalImageApi {
|
class LocalImageApiImpl: LocalImageApi {
|
||||||
|
|
@ -31,9 +36,7 @@ class LocalImageApiImpl: LocalImageApi {
|
||||||
return requestOptions
|
return requestOptions
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private static let assetQueue = DispatchQueue(label: "thumbnail.assets", qos: .userInitiated)
|
private static let registry = RequestRegistry<LocalImageRequest>()
|
||||||
private static let requestQueue = DispatchQueue(label: "thumbnail.requests", qos: .userInitiated)
|
|
||||||
private static let cancelQueue = DispatchQueue(label: "thumbnail.cancellation", qos: .default)
|
|
||||||
|
|
||||||
private static var rgbaFormat = vImage_CGImageFormat(
|
private static var rgbaFormat = vImage_CGImageFormat(
|
||||||
bitsPerComponent: 8,
|
bitsPerComponent: 8,
|
||||||
|
|
@ -42,7 +45,6 @@ class LocalImageApiImpl: LocalImageApi {
|
||||||
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue),
|
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue),
|
||||||
renderingIntent: .defaultIntent
|
renderingIntent: .defaultIntent
|
||||||
)!
|
)!
|
||||||
private static var requests = [Int64: LocalImageRequest]()
|
|
||||||
private static let assetCache = {
|
private static let assetCache = {
|
||||||
let assetCache = NSCache<NSString, PHAsset>()
|
let assetCache = NSCache<NSString, PHAsset>()
|
||||||
assetCache.countLimit = 10000
|
assetCache.countLimit = 10000
|
||||||
|
|
@ -73,7 +75,7 @@ class LocalImageApiImpl: LocalImageApi {
|
||||||
|
|
||||||
guard let asset = Self.requestAsset(assetId: assetId)
|
guard let asset = Self.requestAsset(assetId: assetId)
|
||||||
else {
|
else {
|
||||||
Self.remove(requestId: requestId)
|
Self.registry.remove(requestId: requestId)
|
||||||
completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil)))
|
completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -98,12 +100,11 @@ class LocalImageApiImpl: LocalImageApi {
|
||||||
)
|
)
|
||||||
|
|
||||||
if request.isCancelled {
|
if request.isCancelled {
|
||||||
Self.remove(requestId: requestId)
|
|
||||||
return completion(ImageProcessing.cancelledResult)
|
return completion(ImageProcessing.cancelledResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let data = imageData else {
|
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)))
|
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 {
|
if request.isCancelled {
|
||||||
free(pointer)
|
free(pointer)
|
||||||
Self.remove(requestId: requestId)
|
|
||||||
return completion(ImageProcessing.cancelledResult)
|
return completion(ImageProcessing.cancelledResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,7 +121,7 @@ class LocalImageApiImpl: LocalImageApi {
|
||||||
"pointer": Int64(Int(bitPattern: pointer)),
|
"pointer": Int64(Int(bitPattern: pointer)),
|
||||||
"length": Int64(length),
|
"length": Int64(length),
|
||||||
]))
|
]))
|
||||||
Self.remove(requestId: requestId)
|
Self.registry.remove(requestId: requestId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,7 +142,7 @@ class LocalImageApiImpl: LocalImageApi {
|
||||||
|
|
||||||
guard let image = image,
|
guard let image = image,
|
||||||
let cgImage = image.cgImage else {
|
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)))
|
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)),
|
"pointer": Int64(Int(bitPattern: buffer.data)),
|
||||||
"width": Int64(buffer.width),
|
"width": Int64(buffer.width),
|
||||||
"height": Int64(buffer.height),
|
"height": Int64(buffer.height),
|
||||||
"rowBytes": Int64(buffer.rowBytes)
|
"rowBytes": Int64(buffer.rowBytes),
|
||||||
]))
|
]))
|
||||||
Self.remove(requestId: requestId)
|
Self.registry.remove(requestId: requestId)
|
||||||
} catch {
|
} 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)))
|
return completion(.failure(PigeonError(code: "", message: "Failed to convert image for \(assetId): \(error)", details: nil)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
request.operation = operation
|
request.operation = operation
|
||||||
Self.add(requestId: requestId, request: request)
|
Self.registry.add(requestId: requestId, request: request)
|
||||||
ImageProcessing.queue.addOperation(operation)
|
ImageProcessing.queue.addOperation(operation)
|
||||||
}
|
}
|
||||||
|
|
||||||
func cancelRequest(requestId: Int64) {
|
func cancelRequest(requestId: Int64) {
|
||||||
Self.cancel(requestId: requestId)
|
Self.registry.remove(requestId: requestId)?.cancel()
|
||||||
}
|
|
||||||
|
|
||||||
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) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func requestAsset(assetId: String) -> PHAsset? {
|
private static func requestAsset(assetId: String) -> PHAsset? {
|
||||||
var asset: PHAsset?
|
if let cached = assetCache.object(forKey: assetId as NSString) {
|
||||||
assetQueue.sync { asset = assetCache.object(forKey: assetId as NSString) }
|
return cached
|
||||||
if asset != nil { return asset }
|
}
|
||||||
|
|
||||||
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: Self.fetchOptions).firstObject
|
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: Self.fetchOptions).firstObject
|
||||||
else { return nil }
|
else { return nil }
|
||||||
assetQueue.async { assetCache.setObject(asset, forKey: assetId as NSString) }
|
assetCache.setObject(asset, forKey: assetId as NSString)
|
||||||
return asset
|
return asset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,15 @@ class RemoteImageRequest {
|
||||||
self.task = task
|
self.task = task
|
||||||
self.completion = completion
|
self.completion = completion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cancel() {
|
||||||
|
isCancelled = true
|
||||||
|
task?.cancel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RemoteImageApiImpl: NSObject, RemoteImageApi {
|
class RemoteImageApiImpl: NSObject, RemoteImageApi {
|
||||||
private static var lock = os_unfair_lock()
|
private static let registry = RequestRegistry<RemoteImageRequest>()
|
||||||
private static var requests = [Int64: RemoteImageRequest]()
|
|
||||||
private static var rgbaFormat = vImage_CGImageFormat(
|
private static var rgbaFormat = vImage_CGImageFormat(
|
||||||
bitsPerComponent: 8,
|
bitsPerComponent: 8,
|
||||||
bitsPerPixel: 32,
|
bitsPerPixel: 32,
|
||||||
|
|
@ -43,20 +47,15 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
|
||||||
|
|
||||||
let request = RemoteImageRequest(id: requestId, task: task, completion: completion)
|
let request = RemoteImageRequest(id: requestId, task: task, completion: completion)
|
||||||
|
|
||||||
os_unfair_lock_lock(&Self.lock)
|
Self.registry.add(requestId: requestId, request: request)
|
||||||
Self.requests[requestId] = request
|
|
||||||
os_unfair_lock_unlock(&Self.lock)
|
|
||||||
|
|
||||||
task.resume()
|
task.resume()
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func handleCompletion(requestId: Int64, encoded: Bool, data: Data?, response: URLResponse?, error: Error?) {
|
private static func handleCompletion(requestId: Int64, encoded: Bool, data: Data?, response: URLResponse?, error: Error?) {
|
||||||
os_unfair_lock_lock(&Self.lock)
|
guard let request = registry.remove(requestId: requestId) else {
|
||||||
guard let request = requests[requestId] else {
|
return
|
||||||
return os_unfair_lock_unlock(&Self.lock)
|
|
||||||
}
|
}
|
||||||
requests[requestId] = nil
|
|
||||||
os_unfair_lock_unlock(&Self.lock)
|
|
||||||
|
|
||||||
if let error = error {
|
if let error = error {
|
||||||
if request.isCancelled || (error as NSError).code == NSURLErrorCancelled {
|
if request.isCancelled || (error as NSError).code == NSURLErrorCancelled {
|
||||||
|
|
@ -127,13 +126,7 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
func cancelRequest(requestId: Int64) {
|
func cancelRequest(requestId: Int64) {
|
||||||
os_unfair_lock_lock(&Self.lock)
|
Self.registry.remove(requestId: requestId)?.cancel()
|
||||||
let request = Self.requests[requestId]
|
|
||||||
os_unfair_lock_unlock(&Self.lock)
|
|
||||||
|
|
||||||
guard let request = request else { return }
|
|
||||||
request.isCancelled = true
|
|
||||||
request.task?.cancel()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearCache(completion: @escaping (Result<Int64, any Error>) -> Void) {
|
func clearCache(completion: @escaping (Result<Int64, any Error>) -> Void) {
|
||||||
|
|
|
||||||
54
mobile/ios/Runner/Utility/Mutex.swift
Normal file
54
mobile/ios/Runner/Utility/Mutex.swift
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import Darwin
|
||||||
|
|
||||||
|
// Can be replaced with std Mutex when the deployment target is iOS 18+
|
||||||
|
struct Mutex<Value: ~Copyable>: ~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<Result: ~Copyable, E: Error>(
|
||||||
|
_ 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<Void>
|
||||||
|
|
||||||
|
extension Mutex where Value == Void {
|
||||||
|
init() {
|
||||||
|
self.init(())
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
borrowing func withLock<Result: ~Copyable, E: Error>(
|
||||||
|
_ 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue