美文网首页收藏ios
YYImage源码分析

YYImage源码分析

作者: fou7 | 来源:发表于2018-09-14 17:59 被阅读85次

    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会提前对图像数据进行编解码,避免等到真正需要显示的时候才去进行编解码。

    相关文章

      网友评论

        本文标题:YYImage源码分析

        本文链接:https://www.haomeiwen.com/subject/ioougftx.html