YYImage
和SDWebIMage
的功能是相同的,通过为系统的UIImageView
、UIButton
、CALayer
添加分类方法继而提供图像的下载、展示、缓存等功能,另外YYImage
还支持GIF
、APNG
、WebP
格式的动画图片。
入口的选择
YYImage
的使用方法同SDWebImage
相同,都可以通过调用原生UI
控件的分类方法获取其提供的功能。
如果你的图像展示区域需要响应UIEvent
事件,可以选择使用UIImageView
或UIButton
,然后调用UI
控件对应的分类方法。
如果你的图像展示区域不需要响应UIEvent
事件,只是单纯的显示图像内容,可以选择使用CALayer
,使用CALayer
可以减少屏幕上UI
控件的层级,进而减少CPU
和GPU
的计算和渲染压力,提高性能,特别对于UIScrollView
及其派生类来说,可提高滑动流畅性。
图片的下载
如何避免重复下载
UITableViewCell
频繁在屏幕中出现时如果不加限制会重复发送下载图片的网络请求,在YYImage
中为避免图片重复下载,每次调用setImageWithURL:
开头的分类方法时,对于每个有效的下载操作,OSAtomicIncrement32
都会以原子方式对_sentinel
递增32位值。
避免重复下载的操作被封装在了分类方法和_YYWebImageSetter
类中。
_YYWebImageSetter
类有几个成员变量:
@implementation _YYWebImageSetter {
dispatch_semaphore_t _lock;
NSURL *_imageURL;
NSOperation *_operation;
int32_t _sentinel;
}
变量_lock
用来控制并发操作保证线程安全。
变量_imageURL
表示当前下载操作所下载图片的URL
。
变量_operation
表示当前下载操作。
变量_sentinel
译为哨兵,用来比对两次下载操作是否为同一个。
_YYWebImageSetter *setter = objc_getAssociatedObject(self, &_YYWebImageHighlightedSetterKey);
if (!setter) {
setter = [_YYWebImageSetter new];
objc_setAssociatedObject(self, &_YYWebImageHighlightedSetterKey, setter, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
int32_t sentinel = [setter cancelWithNewURL:imageURL];
- (int32_t)cancelWithNewURL:(NSURL *)imageURL {
int32_t sentinel;
dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
if (_operation) {
[_operation cancel];
_operation = nil;
}
_imageURL = imageURL;
sentinel = OSAtomicIncrement32(&_sentinel);
dispatch_semaphore_signal(_lock);
return sentinel;
}
分类通过runtime
将_YYWebImageSetter
对象(下称setter
对象)绑定到了自己身上,每次调用setImageWithURL:
方法时都会获取到这个setter
对象,如果setter
对象已经开始了一个下载操作,就会将这个下载操作cancel
。然后更新_imageURL
为新的值,并将_sentinel
递增,返回递增后的新值,新值将用于在后续的创建图片下载操作时与旧值进行比对。
下载图片
接下来,会到YYImageCache
中根据URL
获取UIImage
对象,YYImageCache
封装了YYMemoryCache
和YYDiskCache
,所以UIImage
的查找操作会先到内存缓存中查找,内存缓存里没有会到磁盘缓存里去找。
如果没找到,会将placeholder
赋值给当前分类的image
属性,展示占位图;然后切换到指定的串行队列进行下载任务,在这个任务中,会创建YYWebImageProgressBlock
和YYWebImageCompletionBlock
两个block
用于图片下载中和下载完成的回调,任务的最后,会调用setter
对象的方法,在这个方法中创建一个图片下载操作,方法实现如下:
- (int32_t)setOperationWithSentinel:(int32_t)sentinel
url:(NSURL *)imageURL
options:(YYWebImageOptions)options
manager:(YYWebImageManager *)manager
progress:(YYWebImageProgressBlock)progress
transform:(YYWebImageTransformBlock)transform
completion:(YYWebImageCompletionBlock)completion {
if (sentinel != _sentinel) {
if (completion) completion(nil, imageURL, YYWebImageFromNone, YYWebImageStageCancelled, nil);
return _sentinel;
}
NSOperation *operation = [manager requestImageWithURL:imageURL options:options progress:progress transform:transform completion:completion];
if (!operation && completion) {
NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : @"YYWebImageOperation create failed." };
completion(nil, imageURL, YYWebImageFromNone, YYWebImageStageFinished, [NSError errorWithDomain:@"com.ibireme.yykit.webimage" code:-1 userInfo:userInfo]);
}
dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
if (sentinel == _sentinel) {
if (_operation) [_operation cancel];
_operation = operation;
sentinel = OSAtomicIncrement32(&_sentinel);
} else {
[operation cancel];
}
dispatch_semaphore_signal(_lock);
return sentinel;
}
在方法实现中,首先会比对新旧两个哨兵变量的值(ps:新值是我们在前面说到的通过调用setter
对象的cancelWithNewURL:
方法获取的到的),如果两个值不相等,则会执行completion block
,告诉调用方本次图片下载操作被取消了,如果两个值相等,表明该图片是第一次下载,通过YYWebImageManager
创建一个下载操作(YYWebImageOperation
类型),接下来,会再次判断新旧两个哨兵变量的值(可能存在来回滑动UITableView的操作导致cell频繁出现在屏幕内),如果两次值相同,就会取消上一次的图片下载操作,将_opration
赋值为新的operation
,并原子递增_sentinel
;如果两次值不同,就取消本次下载操作。
在利用YYWebImageManager
生成新的operation
的同时,方法内部创建完operation
后就会将其放入到专门用来做下载任务的队列,然后执行其任务。
在上一步中,每个operation
的类型都是YYWebImageOperation
,在YYWebImageOperation
中,封装了下载操作的具体实现细节,值得一提的是,当前版本的图片下载操作仍然使用的是NSURLConnection
,所以这里利用runloop
开启了一条常驻线程,保证下载图片操作不会中断。
这个类中其他的方法实现,就是根据当前操作的executing
、finished
、cancelled
、started
等状态执行不同的操作,这里有一个值得注意的细节,就是对于下载图片的操作以及取消操作这些任务都是放在runloop
的NSDefaultRunLoopMode
下执行的。
图片缓存
YYImage
的缓存类是YYImageCache
,和SDWebImage
相比,最大的不通是SDWebImage
是基于NSCache
做的图片缓存,YYImageCache
是基于YYKit
的另一个组件库YYCache
做的图片缓存。
YYImageCache
直接内置了YYMemoryCache
和YYDiskCache
,对内存缓存和磁盘缓存的操作都是基于这两个类来做的。
关于YYCache
的源码分析,这里是入口。
图片解码
该支持解码动画WebP,APNG,GIF和系统图像格式,如PNG,JPG,JP2,BMP,TIFF,PIC,ICNS和ICO。 它可以使用解码完整的图像数据,或解码图像期间的增量图像数据下载,并且这个类是线程安全的。
对图像解码有兴趣的可以看看YYImageDecoder
这个类,因为这个类代码较多,并不是所有代码都值得看,这里直接分享一些关于图片解压缩的资源,看完资源相信你就会对YYImageDecoder
所做的事有彻底的了解。
https://github.com/path/FastImageCache
https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/Introduction/Introduction.html
https://www.cocoanetics.com/2011/10/avoiding-image-decompression-sickness/
http://stackoverflow.com/questions/23790837/what-is-byte-alignment-cache-line-alignment-for-core-animation-why-it-matters
YYImage
YYImage
对象是显示动画图像数据的高级方法。
它是一个完全兼容的UIImage
子类。它扩展了UIImage
支持动画WebP
,APNG
和GIF
格式图像数据解码。 它也是支持NSCoding
协议来存档和取消归档多帧图像数据。
如果图像是从多帧图像数据创建的,并且您想要播放动画,尝试用YYAnimatedImageView
替换UIImageView
。
YYImage
有4个类方法:
+ (nullable YYImage *)imageNamed:(NSString *)name; // no cache!
+ (nullable YYImage *)imageWithContentsOfFile:(NSString *)path;
+ (nullable YYImage *)imageWithData:(NSData *)data;
+ (nullable YYImage *)imageWithData:(NSData *)data scale:(CGFloat)scale;
前面3个方法最终都会调用最后一类个方法,最后一个类方法实现如下:
- (instancetype)initWithData:(NSData *)data scale:(CGFloat)scale {
if (data.length == 0) return nil;
if (scale <= 0) scale = [UIScreen mainScreen].scale;
_preloadedLock = dispatch_semaphore_create(1);
@autoreleasepool {
YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];
YYImageFrame *frame = [decoder frameAtIndex:0 decodeForDisplay:YES];
UIImage *image = frame.image;
if (!image) return nil;
self = [self initWithCGImage:image.CGImage scale:decoder.scale orientation:image.imageOrientation];
if (!self) return nil;
_animatedImageType = decoder.type;
if (decoder.frameCount > 1) {
_decoder = decoder;
_bytesPerFrame = CGImageGetBytesPerRow(image.CGImage) * CGImageGetHeight(image.CGImage);
_animatedImageMemorySize = _bytesPerFrame * decoder.frameCount;
}
self.isDecodedForDisplay = YES;
}
return self;
}
其中最核心的两行代码:
YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];
YYImageFrame *frame = [decoder frameAtIndex:0 decodeForDisplay:YES];
在YYImageDecoder
的decoderWithData:scale:
方法中,会经过一系列的方法调用,对UIImage
的二进制数据进行处理,利用YYImageDetectType
函数获取图片类型(png
、jpeg
、webP
等等),对于不同的图片类型,生成不同的_YYImageDecoderFrame
对象,在_YYImageDecoderFrame
对象的_frameAtIndex:decodeForDisplay:
方法调用栈中,会利用CPU
对图像数据进行强制编解码生成位图,根据位图再生成UIImage
对象,这些都是同步操作。
所以,假如你想使用contentOfFile:
方法从沙盒读取图片时,建议使用YYImage
的同名方法,YYImage
会提前对图像数据进行编解码,避免等到真正需要显示的时候才去进行编解码。
网友评论