YYImage源码分析

YYImageSDWebIMage的功能是相同的,通过为系统的UIImageViewUIButtonCALayer添加分类方法继而提供图像的下载、展示、缓存等功能,另外YYImage还支持GIFAPNGWebP格式的动画图片。

入口的选择


YYImage的使用方法同SDWebImage相同,都可以通过调用原生UI控件的分类方法获取其提供的功能。

如果你的图像展示区域需要响应UIEvent事件,可以选择使用UIImageViewUIButton,然后调用UI控件对应的分类方法。

如果你的图像展示区域不需要响应UIEvent事件,只是单纯的显示图像内容,可以选择使用CALayer,使用CALayer可以减少屏幕上UI控件的层级,进而减少CPUGPU的计算和渲染压力,提高性能,特别对于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封装了YYMemoryCacheYYDiskCache,所以UIImage的查找操作会先到内存缓存中查找,内存缓存里没有会到磁盘缓存里去找。

如果没找到,会将placeholder赋值给当前分类的image属性,展示占位图;然后切换到指定的串行队列进行下载任务,在这个任务中,会创建YYWebImageProgressBlockYYWebImageCompletionBlock两个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开启了一条常驻线程,保证下载图片操作不会中断。

这个类中其他的方法实现,就是根据当前操作的executingfinishedcancelledstarted等状态执行不同的操作,这里有一个值得注意的细节,就是对于下载图片的操作以及取消操作这些任务都是放在runloopNSDefaultRunLoopMode下执行的。

图片缓存


YYImage的缓存类是YYImageCache,和SDWebImage相比,最大的不通是SDWebImage是基于NSCache做的图片缓存,YYImageCache是基于YYKit的另一个组件库YYCache做的图片缓存。

YYImageCache直接内置了YYMemoryCacheYYDiskCache,对内存缓存和磁盘缓存的操作都是基于这两个类来做的。

关于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支持动画WebPAPNGGIF格式图像数据解码。 它也是支持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];

YYImageDecoderdecoderWithData:scale:方法中,会经过一系列的方法调用,对UIImage的二进制数据进行处理,利用YYImageDetectType函数获取图片类型(pngjpegwebP等等),对于不同的图片类型,生成不同的_YYImageDecoderFrame对象,在_YYImageDecoderFrame对象的_frameAtIndex:decodeForDisplay:方法调用栈中,会利用CPU对图像数据进行强制编解码生成位图,根据位图再生成UIImage对象,这些都是同步操作。

所以,假如你想使用contentOfFile:方法从沙盒读取图片时,建议使用YYImage的同名方法,YYImage会提前对图像数据进行编解码,避免等到真正需要显示的时候才去进行编解码。

推荐阅读更多精彩内容