YYWebImage,SDWebImage和PINRemoteImage比較

卡洛斯發表於2018-02-28

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;

<!–more–>

圖片格式支援

根據下面的比較,可以看出圖片格式支援方面,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 崩潰。

所以針對這個問題,目前還是保留使用兩套圖片庫。

總結

這裡只是可用度方面進行了比較,並沒有做全面的評估,所以實際專案還是需要根據自己的需求來決定。

參考

  • [本地編譯WebP庫並新增其他到工程中

](https://github.com/ibireme/YY…

](http://blog.devzeng.com/blog/…
)

相關文章