YYWebImage,SDWebImage和PINRemoteImage比较
共同的特性
- 以类别 api 下载远程图片。
- 图片缓存
- 图片提前解码
- 其他
图片框架比较
图片后处理
根据下面的比较,可以看出图片后处理方面,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 界面需要小幅度的圆角),尤为有用。
-
(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 可以参考此文。
- YYWebImage:
- GIF: YYImage
- WebP: libwebp
- SDWebImage:
- GIF: FLAnimatedImage
- WebP: libwebp
- PINRemoteImage:
- GIF: FLAnimatedImage
- WebP: libwebp
图片解码控制:
根据下面的比较,可以看出图片解码控制方面,PINRemoteImage > YYWebImage > SDWebImage
- YYWebImage:
- 下载完图片,会自动解码,可根据参数
来控制不解码。YYWebImageOptionIgnoreImageDecoding
代码位置: YYWebImageOperation(connectionDidFinishLoading:) BOOL shouldDecode = (self.options & YYWebImageOptionIgnoreImageDecoding) == ; BOOL allowAnimation = (self.options & YYWebImageOptionIgnoreAnimatedImage) == ; 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] > ) { hasAnimation = YES; } } else { YYImageDecoder *decoder = [YYImageDecoder decoderWithData:self.data scale:[UIScreen mainScreen].scale]; image = [decoder frameAtIndex: 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:- 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 == )) 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) == ; BOOL skipDecode = (options & PINRemoteImageManagerDownloadOptionsSkipDecode) != ; __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 == ? : (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, ), ^{
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库并添加其他到工程中
- 本地编译WebP库并添加其他到工程中1
-
[UIWebView 使用 WebP
](http://blog.devzeng.com/blog/ios-webp-usage.html
)