SDWebImage icon indicating copy to clipboard operation
SDWebImage copied to clipboard

SWIFT TASK CONTINUATION MISUSE: tried to resume its continuation more than once, returning nil!

Open hp1909 opened this issue 2 years ago • 6 comments

New Issue Checklist

Issue Info

Info Value
Platform Name ios
Platform Version 15.0 and above
SDWebImage Version e.g. 5.14.0
Integration Method manually
Xcode Version Xcode 15
Repro rate Usually the first time install the application
Repro with our demo prj e.g. does it happen with our demo project?
Demo project link e.g. link to a demo project that highlights the issue

Issue Description and Steps

When I wrap the sd_setImage in Swift Concurrency context, the crash happens, usually the first time I load and scroll the pretty large image list

public func loadImage(
    imageView: UIImageView,
    url: URL?,
    additionalView: UIImageView?,
    placeholderImage: UIImage?
) async -> UIImage? {
    await withCheckedContinuation { continuation in
        Task {
            await MainActor.run {
                guard let url else {
                    imageView.image = nil
                    continuation.resume(returning: nil)
                    return
                }

                imageView.sd_setImage(
                    with: url,
                    placeholderImage: placeholderImage,
                    options: [.retryFailed, .continueInBackground]
                ) { image, _, _, _ in
                    additionalView?.image = image
                    continuation.resume(returning: image)
                }
            }
        }
    }
}

Crash log:

image

image

image

The crash happens after I update SDWebImage from 5.9.1 to 5.14.0 (to use lazyDecoding config). I tried to use the latest version (5.18.7) but it still happens.

hp1909 avatar Dec 18 '23 04:12 hp1909

Didn't you use that callbackQueue context to avoid queue switch ?

I think this is indeed for that advanced control

dreampiggy avatar Dec 18 '23 06:12 dreampiggy

@dreampiggy Ohh I didn't aware of that context, how can I use it correctly? Thank you in advance.

hp1909 avatar Dec 18 '23 06:12 hp1909

@dreampiggy I set callbackQueue but it still crashes. I think it's about calling completion block twice, not related to the callbackQueue.

image

hp1909 avatar Dec 18 '23 07:12 hp1909

Using un-attached Task with main actor and wrap that sd_setImage API is not a good idea, actually, bad idea. Becaues that API does not ensure the Main actor isolation.

Swift Actor and async is actually, different concept. They should not just hack to match the exists Objc API like your wrapper.

A proper solution is to wait my #2980

Or you can do a wrapper by direclly use API in SDWebImageManager level, and provide the extension on UIImageView as well.

dreampiggy avatar Dec 29 '23 09:12 dreampiggy

It is not only with sd_setImage that this problem occurs.

@discardableResult
func loadImage(url: URL,
               options: SDWebImageOptions,
               context: [SDWebImageContextOption: Any]?) async throws -> UIImage {

    try Task.checkCancellation()

    return try await withCheckedThrowingContinuation { [url, options, context] continuation in
        guard Task.isCancelled == false else {
            continuation.resume(throwing: CancellationError())
            return
        }

        var context: [SDWebImageContextOption: Any] = context ?? [:]

        // Select queue for work
        let queue = SDCallbackQueue.main // .current also has trouble
        queue.policy = .safeExecute // also tried without this
        context[SDWebImageContextOption.callbackQueue] = queue

        manager.loadImage(with: url,
                          options: options,
                          context: context,
                          progress: nil) { [continuation] image, _, error, _, _, _ in

            // Cancelled
            guard Task.isCancelled == false else {
                continuation.resume(throwing: CancellationError())
                return
            }

            // Error thrown
            guard let image = image else {
                // MISSUSE continuation here :)
                continuation.resume(throwing: ImageLoaderError.imageLoadFailed.error(underlyingError: error, info: ["url": url]))
                return
            }

            // Result exist
            continuation.resume(returning: image)
        }
    }
}

iDevPro avatar Apr 09 '24 07:04 iDevPro

The bug exists because this API (loadImageWithURL) has many possible interruption usage, like.cancel, nil url, and no actual queue guarantee (sometime it will callback on caller queue, sometimes on main queue)

So, actually , pay attention:

There are no guarantee in any cases, that the call to SDWebImageManager.loadImageWithURL always callback the completionBlock on specify queue

This is why this can not be simple to wrap into a async function

dreampiggy avatar Apr 09 '24 08:04 dreampiggy