具有以下代码并继续收到此错误消息。
var mediaimage = Image("blank")
let imageManager = PHImageManager()
group.enter()
imageManager.requestImage(for: asset, targetSize: imageSize, contentMode: .aspectFill, options: requestImageOptions, resultHandler: { (image, info) in
if image != nil {
mediaimage = Image(uiImage: image!)
group.leave()
} else {
group.leave()
}
})
group.wait()
已尝试使用 Task{} ,其他错误显示使用 Xcode 15 构建的 iOS 16。如果我不执行 group.enter() ,则图像不会添加到 mediaimage 变量中。
使用遗留的
wait
函数(无论是调度组还是信号量或者其他什么)是一种反模式。如果从主线程调用,问题可能包括 UI 故障到应用程序灾难性终止。即使从后台线程调用,这也是一个坏主意,因为它会阻塞工作线程池中数量非常有限的线程之一,这不仅效率低下,而且如果耗尽该池,可能会导致更严重的问题。
错误消息中提到的“使用任务组”有点误导。他们假设,如果您使用的调度组用于其预期目的,即管理任务的group,其中
TaskGroup
是现代的替代方案。
但是您没有一组任务,而只有一个任务。所以我们不会使用任务组。相反,我们只需将遗留异步 API 包装在
withCheckedThrowingContinuation
:
extension PHImageManager {
func image(
for asset: PHAsset,
targetSize: CGSize,
contentMode: PHImageContentMode = .default,
options: PHImageRequestOptions? = nil
) async throws -> UIImage {
assert(!(options?.isSynchronous ?? false))
return try await withCheckedThrowingContinuation { continuation in
requestImage(for: asset, targetSize: targetSize, contentMode: contentMode, options: options) { image, _ in
if let image {
continuation.resume(returning: image)
} else {
continuation.resume(throwing: ImageManagerError.noImage)
}
}
}
}
}
extension PHImageManager {
enum ImageManagerError: Error {
case noImage
}
}
那么你可以这样做:
func fetch(asset: PHAsset, imageSize: CGSize) async throws -> Image {
let uiImage = try await imageManager.image(for: asset, targetSize: imageSize)
return Image(uiImage: uiImage)
}
通过遵循
async
-await
模式,我们可以避免调用旧版 wait
API,从而避免阻塞线程。
虽然我试图保持示例的简单性,但有两个注意事项:
像您原来的示例一样,它不处理取消。
您可以修改上面的内容以通过将其包装在
withTaskCancellationHandler
: 中来处理取消
extension PHImageManager {
func image(
for asset: PHAsset,
targetSize: CGSize,
contentMode: PHImageContentMode = .default,
options: PHImageRequestOptions? = nil
) async throws -> UIImage {
assert(!(options?.isSynchronous ?? false))
let request = ImageRequest(manager: self)
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
guard !request.isCancelled else {
continuation.resume(throwing: CancellationError())
return
}
request.id = requestImage(for: asset, targetSize: targetSize, contentMode: contentMode, options: options) { image, _ in
if let image {
continuation.resume(returning: image)
} else {
continuation.resume(throwing: ImageManagerError.noImage)
}
}
}
} onCancel: {
request.cancel()
}
}
}
private extension PHImageManager {
class ImageRequest: @unchecked Sendable {
private weak var manager: PHImageManager?
private let lock = NSLock()
private var _id: PHImageRequestID?
private var _isCancelled = false
init(manager: PHImageManager) {
self.manager = manager
}
var id: PHImageRequestID? {
get { lock.withLock { _id } }
set { lock.withLock { _id = newValue } }
}
var isCancelled: Bool {
get { lock.withLock { _isCancelled } }
}
func cancel() {
lock.withLock {
_isCancelled = true
if let id = _id {
manager?.cancelImageRequest(id)
}
}
}
}
}
requestImage
会多次调用其完成处理程序闭包(除非您使用 deliveryMode
或 highQualityFormat
的 fastFormat
)。
就像您的调度组示例一样,
withCheckedContinuation
要求您resume
恰好一次且仅一次。如果我们想要支持多个图像(例如,检索本地低质量图像并随后检索远程高质量图像),我们将使用 AsyncSequence
,即 AsyncStream
:
extension PHImageManager {
func images(
for asset: PHAsset,
targetSize: CGSize,
contentMode: PHImageContentMode = .default,
options: PHImageRequestOptions? = nil
) -> AsyncStream<UIImage> {
assert(!(options?.isSynchronous ?? false))
let request = ImageRequest(manager: self)
return AsyncStream { continuation in
request.id = requestImage(for: asset, targetSize: targetSize, contentMode: contentMode, options: options) { image, info in
if let image {
continuation.yield(image)
}
// don't finish, yet, if current result is degraded (and we didn't ask for `fastFormat`)
guard
let isDegraded = info?[PHImageResultIsDegradedKey] as? Bool,
isDegraded,
options?.deliveryMode != .fastFormat
else {
return
}
// otherwise, go ahead and finish
continuation.finish()
}
continuation.onTermination = { reason in
guard case .cancelled = reason else { return }
request.cancel()
}
}
}
}
然后你会做类似的事情:
func fetchImages(for asset: PHAsset, imageSize: CGSize) async {
for await uiImage in imageManager.images(for: asset, targetSize: imageSize) {
let image = Image(uiImage: uiImage)
// do something with `image`
}
}
我必须承认,我发现上面的“完成了吗”逻辑有点脆弱。 (Apple怎么可能不提供一个简单的属性来检查请求是否完成呢?)
我承认我只是快速地将其组合在一起,并没有进行任何详尽的测试,但希望它说明了一些与将遗留异步 API 包装在 Swift 并发模式中并避免调用遗留
wait
函数相关的概念。