ios fixes and improvements

This commit is contained in:
mertalev 2025-08-12 22:42:43 -04:00
parent 0f772ea875
commit e639041b84
No known key found for this signature in database
GPG Key ID: DF6ABC77AAD98C95

View File

@ -3,18 +3,22 @@ import Flutter
import MobileCoreServices import MobileCoreServices
import Photos import Photos
struct Request { class Request {
var managerId: Int32?
weak var workItem: DispatchWorkItem? weak var workItem: DispatchWorkItem?
var isCancelled = false var isCancelled = false
let callback: (Result<[String: Int64], any Error>) -> Void let callback: (Result<[String: Int64], any Error>) -> Void
init(callback: @escaping (Result<[String: Int64], any Error>) -> Void) {
self.callback = callback
}
} }
class ThumbnailApiImpl: ThumbnailApi { class ThumbnailApiImpl: ThumbnailApi {
private static let cacheManager = PHImageManager.default() private static let imageManager = PHImageManager.default()
private static let fetchOptions = { private static let fetchOptions = {
let fetchOptions = PHFetchOptions() let fetchOptions = PHFetchOptions()
fetchOptions.fetchLimit = 1 fetchOptions.fetchLimit = 1
fetchOptions.wantsIncrementalChangeDetails = false
return fetchOptions return fetchOptions
}() }()
private static let requestOptions = { private static let requestOptions = {
@ -26,38 +30,71 @@ class ThumbnailApiImpl: ThumbnailApi {
requestOptions.version = .current requestOptions.version = .current
return requestOptions 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 processingQueue = DispatchQueue(label: "thumbnail.processing", qos: .userInteractive, attributes: .concurrent) private static let processingQueue = DispatchQueue(label: "thumbnail.processing", qos: .userInteractive, attributes: .concurrent)
private static let rgbColorSpace = CGColorSpaceCreateDeviceRGB() private static let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
private static let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).rawValue private static let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).rawValue
private static var requests = [Int64: Request]() private static var requests = [Int64: Request]()
private static let cancelledResult = Result<[String: Int64], any Error>.success([:]) private static let cancelledResult = Result<[String: Int64], any Error>.success([:])
private static let concurrencySemaphore = DispatchSemaphore(value: ProcessInfo.processInfo.activeProcessorCount * 2)
private static let assetCache = {
let assetCache = NSCache<NSString, PHAsset>()
assetCache.countLimit = 10000
return assetCache
}()
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, completion: @escaping (Result<[String: Int64], any Error>) -> Void) { func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, completion: @escaping (Result<[String: Int64], any Error>) -> Void) {
var request = Request(callback: completion) let request = Request(callback: completion)
let item = DispatchWorkItem { let item = DispatchWorkItem {
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: Self.fetchOptions).firstObject if request.isCancelled {
else { return completion(Self.cancelledResult)
Self.requests[requestId] = nil
completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil)))
return
} }
request.managerId = Self.cacheManager.requestImage( Self.concurrencySemaphore.wait()
for: asset, defer {
targetSize: CGSize(width: Double(width), height: Double(height)), Self.concurrencySemaphore.signal()
contentMode: .aspectFit,
options: Self.requestOptions,
resultHandler: { (image, info) -> Void in
defer { Self.requests[requestId] = nil }
guard let image = image,
let cgImage = image.cgImage else {
return completion(.failure(PigeonError(code: "", message: "Could not get pixel data for \(assetId)", details: nil)))
} }
if request.isCancelled { if request.isCancelled {
return completion(Self.cancelledResult) return completion(Self.cancelledResult)
} }
guard let asset = Self.requestAsset(assetId: assetId)
else {
Self.removeRequest(requestId: requestId)
completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil)))
return
}
if request.isCancelled {
return completion(Self.cancelledResult)
}
var image: UIImage?
Self.imageManager.requestImage(
for: asset,
targetSize: CGSize(width: Double(width), height: Double(height)),
contentMode: .aspectFit,
options: Self.requestOptions,
resultHandler: { (_image, info) -> Void in
image = _image
}
)
if request.isCancelled {
return completion(Self.cancelledResult)
}
guard let image = image,
let cgImage = image.cgImage else {
Self.removeRequest(requestId: requestId)
return completion(.failure(PigeonError(code: "", message: "Could not get pixel data for \(assetId)", details: nil)))
}
let pointer = UnsafeMutableRawPointer.allocate( let pointer = UnsafeMutableRawPointer.allocate(
byteCount: Int(cgImage.width) * Int(cgImage.height) * 4, byteCount: Int(cgImage.width) * Int(cgImage.height) * 4,
alignment: MemoryLayout<UInt8>.alignment alignment: MemoryLayout<UInt8>.alignment
@ -78,6 +115,7 @@ class ThumbnailApiImpl: ThumbnailApi {
bitmapInfo: Self.bitmapInfo bitmapInfo: Self.bitmapInfo
) else { ) else {
pointer.deallocate() pointer.deallocate()
Self.removeRequest(requestId: requestId)
return completion(.failure(PigeonError(code: "", message: "Could not create context for \(assetId)", details: nil))) return completion(.failure(PigeonError(code: "", message: "Could not create context for \(assetId)", details: nil)))
} }
@ -95,24 +133,45 @@ class ThumbnailApiImpl: ThumbnailApi {
} }
completion(.success(["pointer": Int64(Int(bitPattern: pointer)), "width": Int64(cgImage.width), "height": Int64(cgImage.height)])) completion(.success(["pointer": Int64(Int(bitPattern: pointer)), "width": Int64(cgImage.width), "height": Int64(cgImage.height)]))
} Self.removeRequest(requestId: requestId)
)
} }
request.workItem = item request.workItem = item
Self.requests[requestId] = request Self.addRequest(requestId: requestId, request: request)
Self.processingQueue.async(execute: item) Self.processingQueue.async(execute: item)
} }
func cancelImageRequest(requestId: Int64) { func cancelImageRequest(requestId: Int64) {
guard var request = Self.requests.removeValue(forKey: requestId) else { return } Self.cancelRequest(requestId: requestId)
}
private static func addRequest(requestId: Int64, request: Request) -> Void {
requestQueue.sync { requests[requestId] = request }
}
private static func removeRequest(requestId: Int64) -> Void {
requestQueue.sync { requests[requestId] = nil }
}
private static func cancelRequest(requestId: Int64) -> Void {
requestQueue.async {
guard let request = requests.removeValue(forKey: requestId) else { return }
request.isCancelled = true request.isCancelled = true
guard let item = request.workItem else { return } guard let item = request.workItem else { return }
item.cancel()
if item.isCancelled { if item.isCancelled {
request.callback(Self.cancelledResult) request.callback(Self.cancelledResult)
} else if let managerId = request.managerId {
Self.cacheManager.cancelImageRequest(managerId)
} }
} }
} }
private static func requestAsset(assetId: String) -> PHAsset? {
var asset: PHAsset?
assetQueue.sync { asset = assetCache.object(forKey: assetId as NSString) }
if asset != nil { return asset }
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: Self.fetchOptions).firstObject
else { return nil }
assetQueue.async { assetCache.setObject(asset, forKey: assetId as NSString) }
return asset
}
}