Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions mobile/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -102,6 +104,8 @@
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>"; };
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>"; };
A01DD6992F7F43B40049AB63 /* UnfairLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnfairLock.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>"; };
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>"; };
Expand Down Expand Up @@ -327,6 +331,8 @@
FED3B1952E253E9B0030FD97 /* Images */ = {
isa = PBXGroup;
children = (
A01DD6982F7F43B40049AB63 /* ImageRequest.swift */,
A01DD6992F7F43B40049AB63 /* UnfairLock.swift */,
FE5FE4AD2F30FBC000A71243 /* ImageProcessing.swift */,
FE5499F72F1198DE006016CB /* RemoteImagesImpl.swift */,
FE5499F52F11980E006016CB /* LocalImagesImpl.swift */,
Expand Down Expand Up @@ -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 */,
Expand Down
91 changes: 91 additions & 0 deletions mobile/ios/Runner/Images/ImageRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import Accelerate
import Foundation

class ImageRequest {
private let lock = UnfairLock()
private var _isCancelled = false
private var callback: ((Result<[String: Int64]?, any Error>) -> Void)?

var isCancelled: Bool {
lock.lock()
defer { lock.unlock() }
Comment on lines +10 to +11
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need a lock?

return _isCancelled
}

init(callback: @escaping (Result<[String: Int64]?, any Error>) -> Void) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably shouldn't be nullable.

self.callback = callback
}

func cancel() {
lock.lock()
_isCancelled = true
let cb = callback
callback = nil
lock.unlock()

onCancel()
cb?(ImageProcessing.cancelledResult)
}

func finish(with result: Result<[String: Int64]?, any Error>) {
lock.lock()
let cb = callback
callback = nil
lock.unlock()

cb?(result)
}

func onCancel() {}

func encodeToPointer(_ data: Data) {
let length = data.count
let pointer = malloc(length)!
data.copyBytes(to: pointer.assumingMemoryBound(to: UInt8.self), count: length)
if isCancelled {
free(pointer)
return
}
finish(with: .success([
"pointer": Int64(Int(bitPattern: pointer)),
"length": Int64(length),
]))
}

func cgImageToPointer(_ cgImage: CGImage, format: vImage_CGImageFormat) throws {
var fmt = format
Comment thread
LeLunZ marked this conversation as resolved.
let buffer = try vImage_Buffer(cgImage: cgImage, format: fmt)
if isCancelled {
buffer.free()
return
}
finish(with: .success([
"pointer": Int64(Int(bitPattern: buffer.data)),
"width": Int64(buffer.width),
"height": Int64(buffer.height),
"rowBytes": Int64(buffer.rowBytes),
]))
}
}

class RequestRegistry<T: ImageRequest> {
private let lock = UnfairLock()
private var requests = [Int64: T]()

func add(requestId: Int64, request: T) {
lock.lock()
requests[requestId] = request
lock.unlock()
}

@discardableResult
func remove(requestId: Int64) -> T? {
lock.lock()
defer { lock.unlock() }
return requests.removeValue(forKey: requestId)
}

func cancel(requestId: Int64) {
remove(requestId: requestId)?.cancel()
}
}
113 changes: 32 additions & 81 deletions mobile/ios/Runner/Images/LocalImagesImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ import Flutter
import MobileCoreServices
import Photos

class LocalImageRequest {
class LocalImageRequest: ImageRequest {
weak var operation: Operation?
var isCancelled = false
let callback: (Result<[String: Int64]?, any Error>) -> Void

init(callback: @escaping (Result<[String: Int64]?, any Error>) -> Void) {
self.callback = callback
override func onCancel() {
operation?.cancel()
}
}

Expand All @@ -31,18 +29,15 @@ 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<LocalImageRequest>()

private static var rgbaFormat = vImage_CGImageFormat(
private static let rgbaFormat = vImage_CGImageFormat(
bitsPerComponent: 8,
bitsPerPixel: 32,
colorSpace: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue),
renderingIntent: .defaultIntent
)!
private static var requests = [Int64: LocalImageRequest]()
private static let assetCache = {
let assetCache = NSCache<NSString, PHAsset>()
assetCache.countLimit = 10000
Expand All @@ -67,19 +62,17 @@ 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 operation = BlockOperation {
if request.isCancelled {
return completion(ImageProcessing.cancelledResult)
}
if request.isCancelled { return }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does it respond to Pigeon in this case?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always in the cancel method of the request.


guard let asset = Self.requestAsset(assetId: assetId)
else {
Self.remove(requestId: requestId)
completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil)))
return
Self.registry.remove(requestId: requestId)
return request.finish(with: .failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil)))
}

if request.isCancelled {
return completion(ImageProcessing.cancelledResult)
Self.registry.remove(requestId: requestId)
return
}

if preferEncoded {
Expand All @@ -98,30 +91,17 @@ class LocalImageApiImpl: LocalImageApi {
)

if request.isCancelled {
Self.remove(requestId: requestId)
return completion(ImageProcessing.cancelledResult)
Self.registry.remove(requestId: requestId)
return
}

guard let data = imageData else {
Self.remove(requestId: requestId)
return completion(.failure(PigeonError(code: "", message: "Could not get image data for \(assetId)", details: nil)))
}

let length = data.count
let pointer = malloc(length)!
data.copyBytes(to: pointer.assumingMemoryBound(to: UInt8.self), count: length)

if request.isCancelled {
free(pointer)
Self.remove(requestId: requestId)
return completion(ImageProcessing.cancelledResult)
Self.registry.remove(requestId: requestId)
return request.finish(with: .failure(PigeonError(code: "", message: "Could not get image data for \(assetId)", details: nil)))
}

request.callback(.success([
"pointer": Int64(Int(bitPattern: pointer)),
"length": Int64(length),
]))
Self.remove(requestId: requestId)
request.encodeToPointer(data)
Self.registry.remove(requestId: requestId)
return
}

Expand All @@ -137,76 +117,47 @@ class LocalImageApiImpl: LocalImageApi {
)

if request.isCancelled {
return completion(ImageProcessing.cancelledResult)
Self.registry.remove(requestId: requestId)
return
}

guard let image = image,
let cgImage = image.cgImage else {
Self.remove(requestId: requestId)
return completion(.failure(PigeonError(code: "", message: "Could not get pixel data for \(assetId)", details: nil)))
Self.registry.remove(requestId: requestId)
return request.finish(with: .failure(PigeonError(code: "", message: "Could not get pixel data for \(assetId)", details: nil)))
}

if request.isCancelled {
return completion(ImageProcessing.cancelledResult)
Self.registry.remove(requestId: requestId)
return
}

do {
let buffer = try vImage_Buffer(cgImage: cgImage, format: Self.rgbaFormat)

if request.isCancelled {
buffer.free()
return completion(ImageProcessing.cancelledResult)
}

request.callback(.success([
"pointer": Int64(Int(bitPattern: buffer.data)),
"width": Int64(buffer.width),
"height": Int64(buffer.height),
"rowBytes": Int64(buffer.rowBytes)
]))
Self.remove(requestId: requestId)
try request.cgImageToPointer(cgImage, format: Self.rgbaFormat)
Self.registry.remove(requestId: requestId)
} catch {
Self.remove(requestId: requestId)
return completion(.failure(PigeonError(code: "", message: "Failed to convert image for \(assetId): \(error)", details: nil)))
Self.registry.remove(requestId: requestId)
return request.finish(with: .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
Comment on lines +154 to 161
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dictionary isn't thread-safe.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn’t the NSCache thread save, the docs are linked above?

}
}
Loading
Loading