From 85cb2fd6ad96aa3d6cf92ecbe10b26181bd05a97 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 25 Oct 2017 14:05:46 +0800 Subject: [PATCH] Creating FLAnimatedImage in global queue to improve displaying gif performance (#2047) * Creating FLAnimatedImage in global queue to improve displaying gif performance * Added `context` dictionary param to `sd_internalSetImageWithURL` that allows sending context options. For now, one can pass `SDWebImageInternalSetImageInGlobalQueueKey` with true value so the `setImage` op is executed on a global queue. --- .../FLAnimatedImageView+WebCache.m | 20 +++-- SDWebImage/SDWebImageCodersManager.h | 4 +- SDWebImage/SDWebImageGIFCoder.h | 3 +- SDWebImage/SDWebImageImageIOCoder.m | 2 +- SDWebImage/UIView+WebCache.h | 30 ++++++++ SDWebImage/UIView+WebCache.m | 73 ++++++++++++++----- 6 files changed, 102 insertions(+), 30 deletions(-) diff --git a/SDWebImage/FLAnimatedImage/FLAnimatedImageView+WebCache.m b/SDWebImage/FLAnimatedImage/FLAnimatedImageView+WebCache.m index 1a74cfad..ed5db34f 100644 --- a/SDWebImage/FLAnimatedImage/FLAnimatedImageView+WebCache.m +++ b/SDWebImage/FLAnimatedImage/FLAnimatedImageView+WebCache.m @@ -52,17 +52,25 @@ options:options operationKey:nil setImageBlock:^(UIImage *image, NSData *imageData) { + // This setImageBlock may not called from main queue SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:imageData]; + FLAnimatedImage *animatedImage; if (imageFormat == SDImageFormatGIF) { - weakSelf.animatedImage = [FLAnimatedImage animatedImageWithGIFData:imageData]; - weakSelf.image = nil; - } else { - weakSelf.image = image; - weakSelf.animatedImage = nil; + animatedImage = [FLAnimatedImage animatedImageWithGIFData:imageData]; } + dispatch_main_async_safe(^{ + if (animatedImage) { + weakSelf.animatedImage = animatedImage; + weakSelf.image = nil; + } else { + weakSelf.image = image; + weakSelf.animatedImage = nil; + } + }); } progress:progressBlock - completed:completedBlock]; + completed:completedBlock + context:@{SDWebImageInternalSetImageInGlobalQueueKey: @(YES)}]; } @end diff --git a/SDWebImage/SDWebImageCodersManager.h b/SDWebImage/SDWebImageCodersManager.h index cef4ba76..5c3d4b37 100644 --- a/SDWebImage/SDWebImageCodersManager.h +++ b/SDWebImage/SDWebImageCodersManager.h @@ -11,13 +11,13 @@ /** Global object holding the array of coders, so that we avoid passing them from object to object. - Uses a priority queue behind scenes, which means the latest added coders have priority. + Uses a priority queue behind scenes, which means the latest added coders have the highest priority. This is done so when encoding/decoding something, we go through the list and ask each coder if they can handle the current data. That way, users can add their custom coders while preserving our existing prebuilt ones Note: the `coders` getter will return the coders in their reversed order Example: - - by default we internally set coders = `IOCoder`, `WebPCoder` + - by default we internally set coders = `IOCoder`, `WebPCoder`. (`GIFCoder` is not recommended to add only if you want to get GIF support without `FLAnimatedImage`) - calling `coders` will return `@[WebPCoder, IOCoder]` - call `[addCoder:[MyCrazyCoder new]]` - calling `coders` now returns `@[MyCrazyCoder, WebPCoder, IOCoder]` diff --git a/SDWebImage/SDWebImageGIFCoder.h b/SDWebImage/SDWebImageGIFCoder.h index 81af7498..30521f9e 100644 --- a/SDWebImage/SDWebImageGIFCoder.h +++ b/SDWebImage/SDWebImageGIFCoder.h @@ -13,7 +13,8 @@ Built in coder using ImageIO that supports GIF encoding/decoding @note `SDWebImageIOCoder` supports GIF but only as static (will use the 1st frame). @note Use `SDWebImageGIFCoder` for fully animated GIFs - less performant than `FLAnimatedImage` - @note The recommended approach for animated GIFs is using `FLAnimatedImage` + @note If you decide to make all `UIImageView`(including `FLAnimatedImageView`) instance support GIF. You should add this coder to `SDWebImageCodersManager` and make sure that it has a higher priority than `SDWebImageIOCoder` + @note The recommended approach for animated GIFs is using `FLAnimatedImage`. It's more performant than `UIImageView` for GIF displaying */ @interface SDWebImageGIFCoder : NSObject diff --git a/SDWebImage/SDWebImageImageIOCoder.m b/SDWebImage/SDWebImageImageIOCoder.m index b0dc757e..a912f35b 100644 --- a/SDWebImage/SDWebImageImageIOCoder.m +++ b/SDWebImage/SDWebImageImageIOCoder.m @@ -100,7 +100,7 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over SDImageFormat format = [NSData sd_imageFormatForImageData:data]; if (format == SDImageFormatGIF) { - // static single GIF need to be created animated for FLAnimatedImageView logic + // static single GIF need to be created animated for `FLAnimatedImage` logic // GIF does not support EXIF image orientation image = [UIImage animatedImageWithImages:@[image] duration:image.duration]; return image; diff --git a/SDWebImage/UIView+WebCache.h b/SDWebImage/UIView+WebCache.h index 4c575194..00678374 100644 --- a/SDWebImage/UIView+WebCache.h +++ b/SDWebImage/UIView+WebCache.h @@ -12,6 +12,8 @@ #import "SDWebImageManager.h" +FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageInternalSetImageInGlobalQueueKey; + typedef void(^SDSetImageBlock)(UIImage * _Nullable image, NSData * _Nullable imageData); @interface UIView (WebCache) @@ -50,6 +52,34 @@ typedef void(^SDSetImageBlock)(UIImage * _Nullable image, NSData * _Nullable ima progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock; +/** + * Set the imageView `image` with an `url` and optionally a placeholder image. + * + * The download is asynchronous and cached. + * + * @param url The url for the image. + * @param placeholder The image to be set initially, until the image request finishes. + * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. + * @param operationKey A string to be used as the operation key. If nil, will use the class name + * @param setImageBlock Block used for custom set image code + * @param progressBlock A block called while image is downloading + * @note the progress block is executed on a background queue + * @param completedBlock A block called when operation has been completed. This block has no return value + * and takes the requested UIImage as first parameter. In case of error the image parameter + * is nil and the second parameter may contain an NSError. The third parameter is a Boolean + * indicating if the image was retrieved from the local cache or from the network. + * The fourth parameter is the original image url. + * @param context A context with extra information to perform specify changes or processes. + */ +- (void)sd_internalSetImageWithURL:(nullable NSURL *)url + placeholderImage:(nullable UIImage *)placeholder + options:(SDWebImageOptions)options + operationKey:(nullable NSString *)operationKey + setImageBlock:(nullable SDSetImageBlock)setImageBlock + progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock + completed:(nullable SDExternalCompletionBlock)completedBlock + context:(nullable NSDictionary *)context; + /** * Cancel the current download */ diff --git a/SDWebImage/UIView+WebCache.m b/SDWebImage/UIView+WebCache.m index 3e2c6dad..20b944cd 100644 --- a/SDWebImage/UIView+WebCache.m +++ b/SDWebImage/UIView+WebCache.m @@ -13,6 +13,8 @@ #import "objc/runtime.h" #import "UIView+WebCacheOperation.h" +NSString * const SDWebImageInternalSetImageInGlobalQueueKey = @"setImageInGlobalQueue"; + static char imageURLKey; #if SD_UIKIT @@ -34,6 +36,17 @@ static char TAG_ACTIVITY_SHOW; setImageBlock:(nullable SDSetImageBlock)setImageBlock progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock { + return [self sd_internalSetImageWithURL:url placeholderImage:placeholder options:options operationKey:operationKey setImageBlock:setImageBlock progress:progressBlock completed:completedBlock context:nil]; +} + +- (void)sd_internalSetImageWithURL:(nullable NSURL *)url + placeholderImage:(nullable UIImage *)placeholder + options:(SDWebImageOptions)options + operationKey:(nullable NSString *)operationKey + setImageBlock:(nullable SDSetImageBlock)setImageBlock + progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock + completed:(nullable SDExternalCompletionBlock)completedBlock + context:(nullable NSDictionary *)context { NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]); [self sd_cancelImageLoadOperationWithKey:validOperationKey]; objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC); @@ -54,28 +67,48 @@ static char TAG_ACTIVITY_SHOW; id operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { __strong __typeof (wself) sself = wself; [sself sd_removeActivityIndicator]; - if (!sself) { + if (!sself) { return; } + BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage); + BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) || + (!image && !(options & SDWebImageDelayPlaceholder))); + SDWebImageNoParamsBlock callCompletedBlockClojure = ^{ + if (!sself) { return; } + if (!shouldNotSetImage) { + [sself sd_setNeedsLayout]; + } + if (completedBlock && shouldCallCompletedBlock) { + completedBlock(image, error, cacheType, url); + } + }; + + // case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set + // OR + // case 1b: we got no image and the SDWebImageDelayPlaceholder is not set + if (shouldNotSetImage) { + dispatch_main_async_safe(callCompletedBlockClojure); return; } - dispatch_main_async_safe(^{ - if (!sself) { - return; - } - if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) { - completedBlock(image, error, cacheType, url); - return; - } else if (image) { - [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock]; - [sself sd_setNeedsLayout]; - } else { - if ((options & SDWebImageDelayPlaceholder)) { - [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock]; - [sself sd_setNeedsLayout]; - } - } - if (completedBlock && finished) { - completedBlock(image, error, cacheType, url); - } + + UIImage *targetImage = nil; + NSData *targetData = nil; + if (image) { + // case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set + targetImage = image; + targetData = data; + } else if (options & SDWebImageDelayPlaceholder) { + // case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set + targetImage = placeholder; + targetData = nil; + } + BOOL shouldUseGlobalQueue = NO; + if (context && [context valueForKey:SDWebImageInternalSetImageInGlobalQueueKey]) { + shouldUseGlobalQueue = [[context valueForKey:SDWebImageInternalSetImageInGlobalQueueKey] boolValue]; + } + dispatch_queue_t targetQueue = shouldUseGlobalQueue ? dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0) : dispatch_get_main_queue(); + + dispatch_async(targetQueue, ^{ + [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock]; + dispatch_main_async_safe(callCompletedBlockClojure); }); }]; [self sd_setImageLoadOperation:operation forKey:validOperationKey];