YYWebImage,SDWebImage和PINRemoteImage比较

YYWebImage,SDWebImage和PINRemoteImage比较

共同的特性

  1. 以类别 api 下载远程图片。
  2. 图片缓存
  3. 图片提前解码
  4. 其他

图片框架比较

图片后处理

根据下面的比较,可以看出图片后处理方面,PINRemoteImage > YYWebImage > SDWebImage

  • YYWebImage:
    • 支持不带标记的后处理。
    /**
     Set the view's `image` with a specified URL.
     
     @param imageURL    The image url (remote or local file path).
     @param placeholder he image to be set initially, until the image request finishes.
     @param options     The options to use when request the image.
     @param manager     The manager to create image request operation.
     @param progress    The block invoked (on main thread) during image request.
     @param transform   The block invoked (on background thread) to do additional image process.
     @param completion  The block invoked (on main thread) when image request completed.
     */
    - (void)yy_setImageWithURL:(nullable NSURL *)imageURL
                   placeholder:(nullable UIImage *)placeholder
                       options:(YYWebImageOptions)options
                       manager:(nullable YYWebImageManager *)manager
                      progress:(nullable YYWebImageProgressBlock)progress
                     transform:(nullable YYWebImageTransformBlock)transform
                    completion:(nullable YYWebImageCompletionBlock)completion;
  • SDWebImage: 不支持图片后处理。

  • PINRemoteImage:

    • 支持带标记的图片后处理。对于同一张图片,当需要不同的后处理方式时(a 界面需要正圆角,b 界面需要小幅度的圆角),尤为有用。
    /**
 Set placeholder on view and retrieve the image from the given URL, process it using the passed in processor block and set result on view. Call completion after image has been fetched, processed and set on view.
 
 @param url NSURL to fetch from.
 @param placeholderImage PINImage to set on the view while the image at URL is being retrieved.
 @param processorKey NSString key to uniquely identify processor. Used in caching.
 @param processor PINRemoteImageManagerImageProcessor processor block which should return the processed image.
 @param completion Called when url has been retrieved and set on view.
 */
- (void)pin_setImageFromURL:(nullable NSURL *)url placeholderImage:(nullable PINImage *)placeholderImage processorKey:(nullable NSString *)processorKey processor:(nullable PINRemoteImageManagerImageProcessor)processor completion:(nullable PINRemoteImageManagerImageCompletion)completion;

图片格式支持

根据下面的比较,可以看出图片格式支持方面,YYWebImage = SDWebImage = PINRemoteImage

另外对于 WebP 的支持,需要下载 google 的 libwebp pod,这就需要先配置命令行代理了,才能安装此 pod,命令行代理的配置就不在此说明了。

而 YYImage 是提前先把编译 webp 的源码,并打包成了 framework,直接引入到了项目里了,避免了配置代理的繁琐工作。编译 webp 成 framework 可以参考此文

图片解码控制:

根据下面的比较,可以看出图片解码控制方面,PINRemoteImage > YYWebImage > SDWebImage

  • YYWebImage:
    • 下载完图片,会自动解码,可根据参数 YYWebImageOptionIgnoreImageDecoding 来控制不解码。
       代码位置:
       YYWebImageOperation(connectionDidFinishLoading:)
       
       BOOL shouldDecode = (self.options & YYWebImageOptionIgnoreImageDecoding) == 0;
       BOOL allowAnimation = (self.options & YYWebImageOptionIgnoreAnimatedImage) == 0;
       UIImage *image;
       BOOL hasAnimation = NO;
       if (allowAnimation) {
           image = [[YYImage alloc] initWithData:self.data scale:[UIScreen mainScreen].scale];
           if (shouldDecode) image = [image yy_imageByDecoded];
           if ([((YYImage *)image) animatedImageFrameCount] > 1) {
               hasAnimation = YES;
           }
       } else {
           YYImageDecoder *decoder = [YYImageDecoder decoderWithData:self.data scale:[UIScreen mainScreen].scale];
           image = [decoder frameAtIndex:0 decodeForDisplay:shouldDecode].image;
       }   
             
  • 当我们在传入 YYWebImageOptionIgnoreImageDecoding,是期望图片不被解码,确实图片在下载完成的时候,不会被解码,但是当把图片存入到内存缓存的时候,图片还是一样会被解码一次并存入到缓存。代码 [_cache setImage:image imageData:data forKey:_cacheKey withType:YYImageCacheTypeAll]; 会针对没有解码的图片,进行一次解码,并存入缓存。所以对于大图的解码,还是会占用很大的内存。
      代码位置:
      YYWebImageOperation(_didReceiveImageFromWeb:)
            
      - (void)_didReceiveImageFromWeb:(UIImage *)image {
        @autoreleasepool {
            [_lock lock];
            if (![self isCancelled]) {
                if (_cache) {
                    if (image || (_options & YYWebImageOptionRefreshImageCache)) {
                        NSData *data = _data;
                        dispatch_async([YYWebImageOperation _imageQueue], ^{
                            // 保存图片到缓存中
                            [_cache setImage:image imageData:data forKey:_cacheKey withType:YYImageCacheTypeAll];
                        });
                    }
                }
                _data = nil;
                NSError *error = nil;
                if (!image) {
                    error = [NSError errorWithDomain:@"com.ibireme.image" code:-1 userInfo:@{ NSLocalizedDescriptionKey : @"Web image decode fail." }];
                    if (_options & YYWebImageOptionIgnoreFailedURL) {
                        if (URLBlackListContains(_request.URL)) {
                            error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:@{ NSLocalizedDescriptionKey : @"Failed to load URL, blacklisted." }];
                        } else {
                            URLInBlackListAdd(_request.URL);
                        }
                    }
                }
                if (_completion) _completion(image, _request.URL, YYWebImageFromRemote, YYWebImageStageFinished, error);
                [self _finish];
            }
            [_lock unlock];
        }
    }
     
       代码位置:
       YYImageCache(setImage:imageData:)
       
       - (void)setImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key withType:(YYImageCacheType)type {
           if (!key || (image == nil && imageData.length == 0)) return;
           
           __weak typeof(self) _self = self;
           if (type & YYImageCacheTypeMemory) { // add to memory cache
               if (image) {
                   if (image.yy_isDecodedForDisplay) {
                       [_memoryCache setObject:image forKey:key withCost:[_self imageCost:image]];
                   } else {
                       // 如果没有解码过,解码图片,并保存到内存缓存 
                       dispatch_async(YYImageCacheDecodeQueue(), ^{
                           __strong typeof(_self) self = _self;
                           if (!self) return;
                           [self.memoryCache setObject:[image yy_imageByDecoded] forKey:key withCost:[self imageCost:image]];
                       });
                   }
               } else if (imageData) {
                   dispatch_async(YYImageCacheDecodeQueue(), ^{
                       __strong typeof(_self) self = _self;
                       if (!self) return;
                       UIImage *newImage = [self imageFromData:imageData];
                       [self.memoryCache setObject:newImage forKey:key withCost:[self imageCost:newImage]];
                   });
               }
           }
           if (type & YYImageCacheTypeDisk) { // add to disk cache
               if (imageData) {
                   if (image) {
                       [YYDiskCache setExtendedData:[NSKeyedArchiver archivedDataWithRootObject:@(image.scale)] toObject:imageData];
                   }
                   [_diskCache setObject:imageData forKey:key];
               } else if (image) {
                   dispatch_async(YYImageCacheIOQueue(), ^{
                       __strong typeof(_self) self = _self;
                       if (!self) return;
                       NSData *data = [image yy_imageDataRepresentation];
                       [YYDiskCache setExtendedData:[NSKeyedArchiver archivedDataWithRootObject:@(image.scale)] toObject:data];
                       [self.diskCache setObject:data forKey:key];
                   });
               }
           }
       }
  • SDWebImage: 下载完图片,会自动解码,没有 api 暴露去控制是否解码下载的图片。所以对于大图的解码,还是会占用很大的内存。

  • PINRemoteImage:

    • 下载完图片,会自动解码,可根据参数 PINRemoteImageManagerDownloadOptionsSkipDecode 来控制不解码。并且在不解码时缓存不解码的图片数据,解码时缓存解码的图片数据。可以根据场景来决定是否需要提前解码图片。
    代码位置:
    PINRemoteImageManager(materializeAndCacheObject:(id)object
                      cacheInDisk:(NSData *)diskData
                   additionalCost:(NSUInteger)additionalCost
                              url:(NSURL *)url
                              key:(NSString *)key
                          options:(PINRemoteImageManagerDownloadOptions)options
                         outImage:(PINImage **)outImage
                        outAltRep:(id *)outAlternateRepresentation)
       
    //takes the object from the cache and returns an image or animated image.
    //if it's a non-alternative representation and skipDecode is not set it also decompresses the image.
    - (BOOL)materializeAndCacheObject:(id)object
                          cacheInDisk:(NSData *)diskData
                       additionalCost:(NSUInteger)additionalCost
                                  url:(NSURL *)url
                                  key:(NSString *)key
                              options:(PINRemoteImageManagerDownloadOptions)options
                             outImage:(PINImage **)outImage
                            outAltRep:(id *)outAlternateRepresentation
    {
        NSAssert(object != nil, @"Object should not be nil.");
        if (object == nil) {
            return NO;
        }
        BOOL alternateRepresentationsAllowed = (PINRemoteImageManagerDisallowAlternateRepresentations & options) == 0;
        BOOL skipDecode = (options & PINRemoteImageManagerDownloadOptionsSkipDecode) != 0;
        __block id alternateRepresentation = nil;
        __block PINImage *image = nil;
        __block NSData *data = nil;
        __block BOOL updateMemoryCache = NO;
        
        PINRemoteImageMemoryContainer *container = nil;
        if ([object isKindOfClass:[PINRemoteImageMemoryContainer class]]) {
            container = (PINRemoteImageMemoryContainer *)object;
            [container.lock lockWithBlock:^{
                data = container.data;
            }];
        } else {
        // 缓存图片原始数据
            updateMemoryCache = YES;
            
            // don't need to lock the container here because we just init it.
            container = [[PINRemoteImageMemoryContainer alloc] init];
            
            if ([object isKindOfClass:[PINImage class]]) {
                data = diskData;
                container.image = (PINImage *)object;
            } else if ([object isKindOfClass:[NSData class]]) {
                data = (NSData *)object;
            } else {
                //invalid item in cache
                updateMemoryCache = NO;
                data = nil;
                container = nil;
            }
            
            container.data = data;
        }
        
        if (alternateRepresentationsAllowed) {
            alternateRepresentation = [_alternateRepProvider alternateRepresentationWithData:data options:options];
        }
        
        if (alternateRepresentation == nil) {
            //we need the image
            [container.lock lockWithBlock:^{
                image = container.image;
            }];
            if (image == nil && container.data) {
                image = [PINImage pin_decodedImageWithData:container.data skipDecodeIfPossible:skipDecode];
                
                if (url != nil) {
                    image = [PINImage pin_scaledImageForImage:image withKey:key];
                }
                
                if (skipDecode == NO) {
                    [container.lock lockWithBlock:^{
                
                // 需要缓存图片解码后的数据        updateMemoryCache = YES;
                        container.image = image;
                    }];
                }
            }
        }
        
        if (updateMemoryCache) {
            [container.lock lockWithBlock:^{
                NSUInteger cacheCost = additionalCost;
                cacheCost += [container.data length];
                CGImageRef imageRef = container.image.CGImage;
                NSAssert(container.image == nil || imageRef != NULL, @"We only cache a decompressed image if we decompressed it ourselves. In that case, it should be backed by a CGImageRef.");
                if (imageRef) {
                    cacheCost += CGImageGetHeight(imageRef) * CGImageGetBytesPerRow(imageRef);
                }
                [self.cache setObjectInMemory:container forKey:key withCost:cacheCost];
            }];
        }
        
        if (diskData) {
        // 缓存原始图片数据到磁盘
            [self.cache setObjectOnDisk:diskData forKey:key];
        }
        
        if (outImage) {
            *outImage = image;
        }
        
        if (outAlternateRepresentation) {
            *outAlternateRepresentation = alternateRepresentation;
        }
        
        if (image == nil && alternateRepresentation == nil) {
            PINLog(@"Invalid item in cache");
            [self.cache removeObjectForKey:key completion:nil];
            return NO;
        }
        return YES;
    }

网络请求:

根据下面的比较,可以看出网络请求方面,PINRemoteImage = SDWebImage > YYWebImage

  • YYWebImage: 使用还是老的网络请求对象 NSURLConnection

  • SDWebImage: 使用的是新的网络请求对象 NSURLSession

  • PINRemoteImage: 使用的是新的网络请求对象 NSURLSession

性能比较

关于性能比较,只是简单的使用 FPS 工具,在列表页面,分别使用三种图片库去看效果,滑动效果比较结果如下:
YYWebImage > SDWebImage ~= PINRemoteImage

使用 YYWebImage 加载图片时的滑动效果是最好的。SDWebImage 和 PINRemoteImage 加载图片时的滑动效果是差不多的。

遇到的问题

由于想要尝试使用 ASDisplayKit 去做局部列表页面的优化,但是这个库使用的图片库是 PINRemoteImage 库,而项目中使用的是 YYWebImage 库去加载图片的,这样的话就会有两套图片库,所以为了统一就需要让ASNetworkImageNode
也要使用 YYWebImage 库去加载图片。这也是我为什么要做这个比较的原因。

代码如下:

#import <AsyncDisplayKit/AsyncDisplayKit.h>
#import <YYWebImage/YYWebImage.h>

@interface YYWebImageManager(ASNetworkImageNode)<ASImageCacheProtocol, ASImageDownloaderProtocol>

@end

@implementation YYWebImageManager(ASNetworkImageNode)

- (nullable id)downloadImageWithURL:(NSURL *)URL
                      callbackQueue:(dispatch_queue_t)callbackQueue
                   downloadProgress:(nullable ASImageDownloaderProgress)downloadProgress
                         completion:(ASImageDownloaderCompletion)completion {
                         // 这里传入的  YYWebImageOptionIgnoreImageDecoding,是期望图片不需要解码,因为 ASNetworkImageNode 内部自己会在合适的时机进行解码的。但是正如我上面说的,YYWebImage 不能很好的控制解码,所以这个参数是不起作用的。    
                         __weak typeof(YYWebImageOperation) *operation = nil;
    operation = [self requestImageWithURL:URL options:YYWebImageOptionIgnoreImageDecoding progress:^(NSInteger receivedSize, NSInteger expectedSize) {
        dispatch_async(callbackQueue, ^{
            CGFloat progress = expectedSize == 0 ? 0.0 : (CGFloat)receivedSize / expectedSize;

            if (downloadProgress) {
                downloadProgress(progress);
            }
        });
    } transform:nil completion:^(UIImage * _Nullable image, NSURL * _Nonnull url, YYWebImageFromType from, YYWebImageStage stage, NSError * _Nullable error) {
        dispatch_async(callbackQueue, ^{
            if (completion) {
                completion(image, error, operation);
            }
        });
    }];

    return operation;
}

- (void)cancelImageDownloadForIdentifier:(id)downloadIdentifier {
    if ([downloadIdentifier isKindOfClass:[YYWebImageOperation class]]) {
        [(YYWebImageOperation *)downloadIdentifier cancel];
    }
}

- (void)cancelImageDownloadWithResumePossibilityForIdentifier:(id)downloadIdentifier
{
    if ([downloadIdentifier isKindOfClass:[YYWebImageOperation class]]) {
        [(YYWebImageOperation *)downloadIdentifier cancel];
    }
}

- (void)clearFetchedImageFromCacheWithURL:(NSURL *)URL
{
    YYImageCache *cache = self.cache;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSString *key = [self cacheKeyForURL:URL transformKey:nil];
        [cache.memoryCache removeObjectForKey:key];
    });
}

- (void)cachedImageWithURL:(NSURL *)URL
             callbackQueue:(dispatch_queue_t)callbackQueue
                completion:(ASImageCacherCompletion)completion {
    [self.cache getImageForKey:[self cacheKeyForURL:URL transformKey:nil] withType:YYImageCacheTypeAll withBlock:^(UIImage * _Nullable image, YYImageCacheType type) {
        dispatch_async(callbackQueue, ^{
            if (completion) {
                completion(image);
            }
        });
    }];
}

@end

@interface ASNetworkImageNode(YYWebImageManager)

+ (ASNetworkImageNode *)lk_imageNode;

@end

@implementation ASNetworkImageNode(YYWebImageManager)

+ (ASNetworkImageNode *)lk_imageNode {
    return [[ASNetworkImageNode alloc] initWithCache:[YYWebImageManager sharedManager] downloader:[YYWebImageManager sharedManager]];
}

@end

// 使用方式如下:
ASNetworkImageNode *node = [ASNetworkImageNode lk_imageNode];
node.URL = [NSURL URLWithString:@""];

可以看到我在下载的回调方法里传入了 YYWebImageOptionIgnoreImageDecoding,是期望图片不需要解码,因为 ASNetworkImageNode 内部自己会在合适的时机进行解码的。但是正如我上面说的,YYWebImage 不能很好的控制解码,所以这个参数是不起作用的。

由于这个不能控制解码的问题,会导致在快速滑动的过程中,几十张图片同时解码,内存会飙升,YYMemoryCache 的内存警告也来不及回收,最终很容易引起 OOM 而让 app 崩溃。

所以针对这个问题,目前还是保留使用两套图片库。

总结

这里只是可用度方面进行了比较,并没有做全面的评估,所以实际项目还是需要根据自己的需求来决定。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 用到的组件 1、通过CocoaPods安装 2、第三方类库安装 3、第三方服务 友盟社会化分享组件 友盟用户反馈 ...
    SunnyLeong阅读 14,598评论 1 180
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • •文/蕉下客陈却 原创 前记: 断桥不断,一转眼,又是几十年过去了,不知苏堤上来来往往,行过了多少人,这纷纷攘攘...
    蕉下客陈却阅读 507评论 2 7
  • 人生目标:心,脑,身健康。 途径: 1 报恩以净心 2 学习与实践以练脑,或接近真理 3 吃和锻炼以存活 具体途径...
    0bf5d40cc5c6阅读 844评论 0 0
  • 黑夜里 只能做黑夜的事 白天里 只能做白天的事 一旦 黑夜里的事放纵于光天化日之下 白天里的事却见不得光 如此颠倒...
    任若水阅读 387评论 0 1