YYWebImage流程源码分析(YYCache和YYImage设计思路)附带所有YYKit组件源码分析

以下所有的介绍不想看源码,可以直接看文字介绍,一样的逻辑,不妨碍阅读

前言

首先,所有的源码和作者提供的基本资料在这里都能找到点击打开链接

YYWebImage是网络图片下载的Category,其中YYImage是编码解码的基石,YYImage已经单独拉出一篇分析过了

YYImage分析,非常重要的编码解码思路,可以看看,还有一个就是YYCache,这里就和YYWebImage一起分析了。大家熟知的就是SDWebImage,该库的思路也已经分析过了,最新的版本和以前旧版本都有分析 SDWebImage源码分析,YYWebImage和SDWebImage一般情况下都能很好的使用,两者的实现思路大致一直,但是细节和性能上还是有差异的,SDWebImage很明显有个GIF显示的问题,这个问题他自己在源码中也有写出,这个问题我们最后来分析,到底是什么缺陷。老套路,跟着YYWebimage网络下载图片的流程走一遍源码,把所有的知识点都打通。


流程

步骤一:外部API调用
[_webImageView yy_setImageWithURL:url
                          placeholder:nil
                          options:YYWebImageOptionProgressiveBlur | YYWebImageOptionShowNetworkActivity | YYWebImageOptionSetImageWithFadeAnimation
                          progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                              if (expectedSize > 0 && receivedSize > 0) {
                                  CGFloat progress = (CGFloat)receivedSize / expectedSize;
                                  progress = progress < 0 ? 0 : progress > 1 ? 1 : progress;
                                  if (_self.progressLayer.hidden) _self.progressLayer.hidden = NO;
                                  _self.progressLayer.strokeEnd = progress;
                              }
                          }
                          transform:nil
                          completion:^(UIImage *image, NSURL *url, YYWebImageFromType from, YYWebImageStage stage, NSError *error) {
                              if (stage == YYWebImageStageFinished) {
                                  _self.progressLayer.hidden = YES;
                                  [_self.indicator stopAnimating];
                                  _self.indicator.hidden = YES;
                                  if (!image) _self.label.hidden = NO;
                              }
                         }];

  • 参数1 URL
  • 参数2 placeHolder
  • 参数3 显示枚举 (位符号 可以用 | )
  • 下载过程回调
  • transferBlock
  • completeBlock 结束的时候回调出来
步骤二:调用核心API

这个方法看起来有点长,不想看注释的可以跳过,下面给你来个精简版本的 这东西展开来好多东西,开始吧

// 核心API
- (void)yy_setImageWithURL:(NSURL *)imageURL
               placeholder:(UIImage *)placeholder
                   options:(YYWebImageOptions)options
                   manager:(YYWebImageManager *)manager
                  progress:(YYWebImageProgressBlock)progress
                 transform:(YYWebImageTransformBlock)transform
                completion:(YYWebImageCompletionBlock)completion {
    if ([imageURL isKindOfClass:[NSString class]]) imageURL = [NSURL URLWithString:(id)imageURL];
    // 生成管理类单例
    manager = manager ? manager : [YYWebImageManager sharedManager];
    
    // 私有类
    _YYWebImageSetter *setter = objc_getAssociatedObject(self, &_YYWebImageSetterKey);
    // 通过Category挂载 _YYWebImageSetterKey
    if (!setter) {
        setter = [_YYWebImageSetter new];
        objc_setAssociatedObject(self, &_YYWebImageSetterKey, setter, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    // 取消之前的未现在完成任务  全局计数递增
    int32_t sentinel = [setter cancelWithNewURL:imageURL];
    
    // 永远保证主线程回调Block inline 方法 
    _yy_dispatch_sync_on_main_queue(^{
        if ((options & YYWebImageOptionSetImageWithFadeAnimation) &&
            !(options & YYWebImageOptionAvoidSetImage)) {
            if (!self.highlighted) {
                [self.layer removeAnimationForKey:_YYWebImageFadeAnimationKey];
            }
        }
        
        if (!imageURL) {
            if (!(options & YYWebImageOptionIgnorePlaceHolder)) {
                self.image = placeholder;
            }
            return;
        }
        
        // get the image from memory as quickly as possible
        // 通过内存YYMemory缓存拿  尽快拿,这里内存是用自制链表链接
        UIImage *imageFromMemory = nil;
        if (manager.cache &&
            !(options & YYWebImageOptionUseNSURLCache) &&
            !(options & YYWebImageOptionRefreshImageCache)) {
            imageFromMemory = [manager.cache getImageForKey:[manager cacheKeyForURL:imageURL] withType:YYImageCacheTypeMemory];
        }
        // 拿到返回
        if (imageFromMemory) {
            if (!(options & YYWebImageOptionAvoidSetImage)) {
                self.image = imageFromMemory;
            }
            if(completion) completion(imageFromMemory, imageURL, YYWebImageFromMemoryCacheFast, YYWebImageStageFinished, nil);
            return;
        }
        
        if (!(options & YYWebImageOptionIgnorePlaceHolder)) {
            self.image = placeholder;
        }
        
        __weak typeof(self) _self = self;
        dispatch_async([_YYWebImageSetter setterQueue], ^{
            
            // Progress 任务
            YYWebImageProgressBlock _progress = nil;
            if (progress) _progress = ^(NSInteger receivedSize, NSInteger expectedSize) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    progress(receivedSize, expectedSize);
                });
            };
            
            
            // 完成任务
            __block int32_t newSentinel = 0;
            __block __weak typeof(setter) weakSetter = nil;
            YYWebImageCompletionBlock _completion = ^(UIImage *image, NSURL *url, YYWebImageFromType from, YYWebImageStage stage, NSError *error) {
//                _completion(image, _request.URL, YYWebImageFromRemote, YYWebImageStageProgress, nil);
                __strong typeof(_self) self = _self;
                BOOL setImage = (stage == YYWebImageStageFinished || stage == YYWebImageStageProgress) && image && !(options & YYWebImageOptionAvoidSetImage);
                dispatch_async(dispatch_get_main_queue(), ^{
                    BOOL sentinelChanged = weakSetter && weakSetter.sentinel != newSentinel;
                    if (setImage && self && !sentinelChanged) {
                        BOOL showFade = ((options & YYWebImageOptionSetImageWithFadeAnimation) && !self.highlighted);
                        if (showFade) {
                            CATransition *transition = [CATransition animation];
                            transition.duration = stage == YYWebImageStageFinished ? _YYWebImageFadeTime : _YYWebImageProgressiveFadeTime;
                            transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
                            transition.type = kCATransitionFade;
                            [self.layer addAnimation:transition forKey:_YYWebImageFadeAnimationKey];
                        }
                        // 超级核心代码 别看就这么点,这句话就是精髓 通过四步骤,GIF情况下让第一帧启动定时器,缓存策略解码缓存/预解码缓存进行播放
                        self.image = image;
                    }
                    if (completion) {
                        if (sentinelChanged) {
                            completion(nil, url, YYWebImageFromNone, YYWebImageStageCancelled, nil);
                        } else {
                            completion(image, url, from, stage, error);
                        }
                    }
                });
            };
            // _YYWebImageSetter 私有类
            newSentinel = [setter setOperationWithSentinel:sentinel url:imageURL options:options manager:manager progress:_progress transform:transform completion:_completion];
            weakSetter = setter;
        });
    });
}
  1. 生成管理类YYWebImageManager 单例,它管理YYImageCache 也是单例,YYImageCache有两个小弟YYMemory和YYDiskCache 初始化工作完毕,顺便初始化了一个NSOperationQueue
  2. 该类是UIImageView的Category,因此作者通过挂载objc_getAssociatedObject了一个对象_YYWebImageSetter 该类管理几个字段,分别用来创建NSOperation任务和取消任务,线程安全用dispatch_semaphore来保护,具体后面展开
  3. 通过YYWebImageSetter取消任务 用新的ImageURL替代,等下开启新的下载任务
  4. 这里有个标记,作者会立马调用manager的cache类,去内存中查找对应的图片资源,注意是只在内存中查找,这里是卡线程的,否则磁盘也查找就会卡,所以,as soon as possible去找
  5. 没有拿到,马上开启异步线程,在自己的串行队列里面添加了两个Block任务,一个是过程回调,还有一个是完成回调,还有拿出刚才挂载上去的_YYWebImageSetter对象去创建一个下载NSOperation任务

注: 这里保留一个如何查找缓存的介绍,等最后下载完成之后如何缓存进去的一起分析


步骤三:挂载对象_YYWebImageSetter创建下载任务
/// Create new operation for web image and return a sentinel value.
/// 生成一个新的任务队列 下载webImage  返回全局计数递增
- (int32_t)setOperationWithSentinel:(int32_t)sentinel
                                url:(NSURL *)imageURL
                            options:(YYWebImageOptions)options
                            manager:(YYWebImageManager *)manager
                           progress:(YYWebImageProgressBlock)progress
                          transform:(YYWebImageTransformBlock)transform
                         completion:(YYWebImageCompletionBlock)completion {
    // 例如取消的时候是10,那么进来的时候也应该是10,如果不同,说明有其他任务让计数器增加了,直接返回
    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.webimage" code:-1 userInfo:userInfo]);
    }
    
    // 加锁
    dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
    // 相等 说明是同一个操作
    if (sentinel == _sentinel) {
        // 取消之前的下载操作
        if (_operation) [_operation cancel];
        // 把刚才生成的任务添加YYWebImageSetter字段里面
        _operation = operation;
        // 任务生成 计数+1
        sentinel = OSAtomicIncrement32(&_sentinel);
    } else {
        [operation cancel];
    }
    dispatch_semaphore_signal(_lock);
    return sentinel;
}

  1. OSAtomicIncrement32(&_sentinel)该方法维护全局计数器,该计数器只有在取消任务和创建任务的时候会+1,由于这个方法是异步的,因此这里一进来就会检测,你想想,如果你刚开始下载,又马上赋值,虽然一进来会取消之前的任务,这里还有一种就是通过比较这个全局计时器,是否和进来的一致,如果一致,才开始下载任务,如果不一致就不需要下载了。说明已经被取消或者已经被其他任务提前替代了。保证唯一性
  2. YYWebImageSetter这个东西的方法,带了manager参数进来,创建任务其实是由manager来执行的,这里这个方法就不列出来了,无非就是manager的方法里面,自定义NSOperation,初始化,加入到队列里面,并返回
  3. 继续回到YYWebImageSetter挂载的这个对象,简单的赋值修改不阻塞线程,开启性能最优的信号量锁,然后把之前的任务取消,替换,重新赋值,这样子,新的任务开启了。


步骤四:YYWebImageOperation里面重写Start开启下载  NSURLConnection代理回调接收数据

重写Start isFinished isExcuting isCanceled几个方法

- (void)start {
    @autoreleasepool {
        [_lock lock];
        self.started = YES;
        if ([self isCancelled]) {
            [self performSelector:@selector(_cancelOperation) onThread:[[self class] _networkThread] withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
            self.finished = YES;
        } else if ([self isReady] && ![self isFinished] && ![self isExecuting]) {
            if (!_request) {
                self.finished = YES;
                if (_completion) {
                    NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:@{NSLocalizedDescriptionKey:@"request in nil"}];
                    _completion(nil, _request.URL, YYWebImageFromNone, YYWebImageStageFinished, error);
                }
            } else {
                // 任务开始
                self.executing = YES;
                // 后台线程  开启下载任务  NSURLConnection
                [self performSelector:@selector(_startOperation) onThread:[[self class] _networkThread] withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
                
                if ((_options & YYWebImageOptionAllowBackgroundTask) && _YYSharedApplication()) {
                    __weak __typeof__ (self) _self = self;
                    if (_taskID == UIBackgroundTaskInvalid) {
                        _taskID = [_YYSharedApplication() beginBackgroundTaskWithExpirationHandler:^{
                            __strong __typeof (_self) self = _self;
                            if (self) {
                                [self cancel];
                                self.finished = YES;
                            }
                        }];
                    }
                }
            }
        }
        [_lock unlock];
    }
}
/// Network thread entry point.
+ (void)_networkThreadMain:(id)object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"com.ibireme.webimage.request"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

/// Global image request network thread, used by NSURLConnection delegate.
+ (NSThread *)_networkThread {
    static NSThread *thread = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        thread = [[NSThread alloc] initWithTarget:self selector:@selector(_networkThreadMain:) object:nil];
        if ([thread respondsToSelector:@selector(setQualityOfService:)]) {
            thread.qualityOfService = NSQualityOfServiceBackground;
        }
        [thread start];
    });
    return thread;
}

1.开始任务,标记execting为YES,然后调用_startOperation,这里依然是AF那会儿线程保活,在异步线程中给Runloop add一个port消息源,激活线程跑起来,全局图片网络下载线程,用来NSURLConnection代理回调,这个问题NSURLSession已经维护了自己的线程,所以AF和SD都去掉了维护自己的线程,保活的操作,由NSURLSession自己来维护下载的线程,上面就是自己维护的全局图片下载线程,通过addport让线程Runloop跑起来

// runs on network thread
// 开启Operation任务
- (void)_startOperation {
    if ([self isCancelled]) return;
    @autoreleasepool {
        // get image from cache
        if (_cache &&
            !(_options & YYWebImageOptionUseNSURLCache) &&
            !(_options & YYWebImageOptionRefreshImageCache)) {
            // 先从内存中拿  有就返回
            UIImage *image = [_cache getImageForKey:_cacheKey withType:YYImageCacheTypeMemory];
            if (image) {
                [_lock lock];
                if (![self isCancelled]) {
                    if (_completion) _completion(image, _request.URL, YYWebImageFromMemoryCache, YYWebImageStageFinished, nil);
                }
                [self _finish];
                [_lock unlock];
                return;
            }
            
            // 内存中没有,在Disk中拿
            if (!(_options & YYWebImageOptionIgnoreDiskCache)) {
                __weak typeof(self) _self = self;
                dispatch_async([self.class _imageQueue], ^{
                    __strong typeof(_self) self = _self;
                    if (!self || [self isCancelled]) return;
                    UIImage *image = [self.cache getImageForKey:self.cacheKey withType:YYImageCacheTypeDisk];
                    // 拿到了,直接缓存到内存中
                    if (image) {
                        [self.cache setImage:image imageData:nil forKey:self.cacheKey withType:YYImageCacheTypeMemory];
                        [self performSelector:@selector(_didReceiveImageFromDiskCache:) onThread:[self.class _networkThread] withObject:image waitUntilDone:NO];
                    } else {
                        // 没拿到,网络下载
                        [self performSelector:@selector(_startRequest:) onThread:[self.class _networkThread] withObject:nil waitUntilDone:NO];
                    }
                });
                return;
            }
        }
    }
    [self performSelector:@selector(_startRequest:) onThread:[self.class _networkThread] withObject:nil waitUntilDone:NO];
}
// runs on network thread
// 开启网路下载请求
- (void)_startRequest:(id)object {
    if ([self isCancelled]) return;
    @autoreleasepool {
        // 黑名单 返回
        。。。
        // 文件URL
        。。。
        // request image from web
        // 网络请求
        [_lock lock];
        if (![self isCancelled]) {
            // NSURLCOnnection 下载任务开启 代理回调
            _connection = [[NSURLConnection alloc] initWithRequest:_request delegate:[_YYWebImageWeakProxy proxyWithTarget:self]];
            if (![_request.URL isFileURL] && (_options & YYWebImageOptionShowNetworkActivity)) {
                [YYWebImageManager incrementNetworkActivityCount];
            }
        }
        [_lock unlock];
    }
}

2.省略了部分代码,先从内存拿,再从磁盘拿,无论哪里有,拿到了都要二级缓存起来,没拿到再进行_startRequest进行网络下载,这里用的是NSURLConnection进行资源下载

/**
 NSURLConnection回调代理
 总之,这个代理是持续回调的,这里无论多少帧的图片,都是返回第一帧给外部先显示
 */
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    @autoreleasepool {
        [_lock lock];
        BOOL canceled = [self isCancelled];
        [_lock unlock];
        if (canceled) return;
        
        if (data) [_data appendData:data];
        if (_progress) {
            [_lock lock];
            if (![self isCancelled]) {
                _progress(_data.length, _expectedSize);
            }
            [_lock unlock];
        }
        
        /*--------------------------- progressive ----------------------------*/

        
        // 解码器
        if (!_progressiveDecoder) {
            _progressiveDecoder = [[YYImageDecoder alloc] initWithScale:[UIScreen mainScreen].scale];
        }
        // 关键代码-----> 解码器每一帧的图像用frames数组保存 _YYImageDecoderFrame 每一帧的图像
        [_progressiveDecoder updateData:_data final:NO];
        if ([self isCancelled]) return;
        
        
        
            // 核心代码------> 注意每一次回到的时候无论多少帧,都是返回第一帧给外部先显示用
            YYImageFrame *frame = [_progressiveDecoder frameAtIndex:0 decodeForDisplay:YES];
            if (frame.image) {
                [_lock lock];
                if (![self isCancelled]) {
                    _completion(frame.image, _request.URL, YYWebImageFromRemote, YYWebImageStageProgress, nil);
                    _lastProgressiveDecodeTimestamp = now;
                }
                [_lock unlock];
            }
            return;
        
            
            // 同上
            YYImageFrame *frame = [_progressiveDecoder frameAtIndex:0 decodeForDisplay:YES];
            UIImage *image = frame.image;
            if (!image) return;
            if ([self isCancelled]) return;
            
            if (!YYCGImageLastPixelFilled(image.CGImage)) return;
            _progressiveDisplayCount++;
            
        
            image = [image yy_imageByBlurRadius:radius tintColor:nil tintMode:0 saturation:1 maskImage:nil];
            
            if (image) {
                [_lock lock];
                if (![self isCancelled]) {
                    _completion(image, _request.URL, YYWebImageFromRemote, YYWebImageStageProgress, nil);
                    _lastProgressiveDecodeTimestamp = now;
                }
                [_lock unlock];
            }
        }
    }
}

/**
 代理数据传输完成 回调  
 */
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    @autoreleasepool {
        [_lock lock];
        _connection = nil;
        if (![self isCancelled]) {
            __weak typeof(self) _self = self;
            dispatch_async([self.class _imageQueue], ^{
                __strong typeof(_self) self = _self;
                if (!self) return;
                
                BOOL shouldDecode = (self.options & YYWebImageOptionIgnoreImageDecoding) == 0;
                // 知识点 没有 YYWebImageOptionIgnoreAnimatedImage  就是allowAnimation = (0==0) YES
                BOOL allowAnimation = (self.options & YYWebImageOptionIgnoreAnimatedImage) == 0;
                UIImage *image;
                BOOL hasAnimation = NO;
                // 允许动画 多帧的图像初始化处理  上面的帧显示而已,不管是什么图片 finish的时候就不同了
                if (allowAnimation) {
                    // 这里的data是所有接收完整的Image图像资源data 该方法自带生成解码器和所有帧
//                    YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];
//                    YYImageFrame *frame = [decoder frameAtIndex:0 decodeForDisplay:YES];
//                    UIImage *image = frame.image;
                    // 注意看上面,所有的数据data进去,生成返回的图片都是第一帧的  这里为什么要用YYImage调用,是多帧,需要和第一帧绑定Decoder解码器进行定时器播放的 重点,划重点
                    image = [[YYImage alloc] initWithData:self.data scale:[UIScreen mainScreen].scale];
                    // 依旧是第一帧解码
                    if (shouldDecode) image = [image yy_imageByDecoded];
                    if ([((YYImage *)image) animatedImageFrameCount] > 1) {
                        // 多帧 例如 GIF
                        hasAnimation = YES;
                    }
                } else {
                    // 单帧的不需要调用上面的YYImage便利方法了,直接解码即可 上面多余的参数也是给GIF多帧图的
                    // 单帧就不需要绑定了,直接解码器解出第一帧即可
                    YYImageDecoder *decoder = [YYImageDecoder decoderWithData:self.data scale:[UIScreen mainScreen].scale];
                    image = [decoder frameAtIndex:0 decodeForDisplay:shouldDecode].image;
                }
                
                /*
                 If the image has animation, save the original image data to disk cache. 动画,保存原始data到磁盘
                 If the image is not PNG or JPEG, re-encode the image to PNG or JPEG for 不是png或者jpeg,encode成这两个
                 better decoding performance.
                 这里如果image不属于自己的类型  清除data
                 */
                YYImageType imageType = YYImageDetectType((__bridge CFDataRef)self.data);
                switch (imageType) {
                    case YYImageTypeJPEG:
                    case YYImageTypeGIF:
                    case YYImageTypePNG:
                    case YYImageTypeWebP: { // save to disk cache
                        if (!hasAnimation) {
                            if (imageType == YYImageTypeGIF ||
                                imageType == YYImageTypeWebP) {
                                self.data = nil; // clear the data, re-encode for disk cache
                            }
                        }
                    } break;
                    default: {
                        self.data = nil; // clear the data, re-encode for disk cache
                    } break;
                }
                if ([self isCancelled]) return;
                
                // transfer Block 外部是否需要传新的图片替换下载下来的图片
                if (self.transform && image) {
                    UIImage *newImage = self.transform(image, self.request.URL);
                    if (newImage != image) {
                        self.data = nil;
                    }
                    // 由外部赋值
                    image = newImage;
                    if ([self isCancelled]) return;
                }
                
                [self performSelector:@selector(_didReceiveImageFromWeb:) onThread:[self.class _networkThread] withObject:image waitUntilDone:NO];
            });
            if (![self.request.URL isFileURL] && (self.options & YYWebImageOptionShowNetworkActivity)) {
                [YYWebImageManager decrementNetworkActivityCount];
            }
        }
        [_lock unlock];
    }
}

3.这里的介绍都是针对上面连接的代码的,当开始NSURLConnection的时候,这两个代理就是核心,一个是不断接受数据用的,另一个是接受完成数据之后所有数据的回调

_progressiveDecoder这个解码器又来了,如果想仔细了解如何解码图片的可以参考YYImage分析

data是慢慢拼接的,把data传进去给解码器把相关所有帧的数据都解出来,存储在解码器的frames里面,每个帧对应的帧图片都是未解码的,主要看下这两句代码

            YYImageFrame *frame = [_progressiveDecoder frameAtIndex:0 decodeForDisplay:YES];
            UIImage *image = frame.image;
其中根据解码器,获取到第一帧的图片并返回,因此,无论下载到什么程度,获取到的image都是第一帧的静态图片,因此GIF为例,没下载完,都是显示第一帧图片在那里给用户看,而且这种调用方式,只是单纯获取到第一帧图片资源,而没有把第一帧Image图片资源关联对应的解码器,这里真的有点绕,但是我个人觉得要真的理解透,就要知道这两个方法的区别,因为YYImage的通过Data初始化,都是返回第一帧的图片使用的,因此如果是PNF或者JPEG,直接拿第一帧即可,无需其他操作,但是如果GIF为例,你拿到了第一帧,那你怎么拿到后面帧通过CADisplayLink进行帧播放?这里又是YYImage的只是,单独拿出来真的是很重要的,需要的朋友,先看了YYImage分析在来看就会明白很多,多帧和单帧是不同的操作,具体这里也有体现,看下面代码
// 允许动画 多帧的图像初始化处理  上面的帧显示而已,不管是什么图片 finish的时候就不同了
                if (allowAnimation) {
                    // 这里的data是所有接收完整的Image图像资源data 该方法自带生成解码器和所有帧
//                    YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];
//                    YYImageFrame *frame = [decoder frameAtIndex:0 decodeForDisplay:YES];
//                    UIImage *image = frame.image;
                    // 注意看上面,所有的数据data进去,生成返回的图片都是第一帧的  这里为什么要用YYImage调用,是多帧,需要和第一帧绑定Decoder解码器进行定时器播放的 重点,划重点
                    image = [[YYImage alloc] initWithData:self.data scale:[UIScreen mainScreen].scale];
                    // 依旧是第一帧解码
                    if (shouldDecode) image = [image yy_imageByDecoded];
                    if ([((YYImage *)image) animatedImageFrameCount] > 1) {
                        // 多帧 例如 GIF
                        hasAnimation = YES;
                    }
                } else {
                    // 单帧的不需要调用上面的YYImage便利方法了,直接解码即可 上面多余的参数也是给GIF多帧图的
                    // 单帧就不需要绑定了,直接解码器解出第一帧即可
                    YYImageDecoder *decoder = [YYImageDecoder decoderWithData:self.data scale:[UIScreen mainScreen].scale];
                    image = [decoder frameAtIndex:0 decodeForDisplay:shouldDecode].image;
                }

可以看到如果allowAnimation,就是多帧动图,这里的初始化方式是通过YYImage初始化的

// data 解码 存储
        YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];
        // 解码第一帧图像资源出来
        YYImageFrame *frame = [decoder frameAtIndex:0 decodeForDisplay:YES];
        // 第一帧图像返回
        UIImage *image = frame.image;
        if (!image) return nil;
        // self 对象就是第一帧图像 UIImage  父类可以指向子类  UIImage = YYimage new 子类调用父类的,返回子类对象
        self = [self initWithCGImage:image.CGImage scale:decoder.scale orientation:image.imageOrientation];
这段代码是YYImage初始化的时候内部自带的解码器,把所有的帧都解出来,返回第一帧YYImage的方法,因此,通过这种方式初始化,YYImage第一帧是有个Decoder解码器属性的,所以后面的动画都可以根据这个YYImage对象带的解码器,逐帧解码显示出来,但是下面那种直接没有用YYImage包装,直接用YYImageDecoder解码器直接解码返回,然后通过index获取到第1帧的图像解码,这个时候是针对单帧图片的。握草,我打字都打累了,这个真的很关键啊,不然你无法理解为什么会这样。解码器播放动图那里会用到的。。。。敲黑板划重点啊有木有,咳咳咳

你妹。为了讲清楚,累死我了,喝个旺仔压压惊


明白了这个,你就能知道为什么didReceiveData代理方法里面如果是动态图GIF资源,还是只是显示第一帧,不会继续播放,因为他是直接用解码器解出来来的第一帧,而不是通过YYImage包装再解的,直接解就会让定时器执行的时候,解码器是空的,获取到就可以当做单帧图片显示,因此还没下载完之前就是静态一帧图片而已


步骤5:接受完data以及解码出第一帧图片之后进行缓存再回调出去
// finish的代理方法那里调用该方法接收网络图片数据
- (void)_didReceiveImageFromWeb:(UIImage *)image {
    @autoreleasepool {
        [_lock lock];
        if (![self isCancelled]) {
            if (_cache) {
                // 有图片  或者需要刷新缓存
                if (image || (_options & YYWebImageOptionRefreshImageCache)) {
                    NSData *data = _data;
                    dispatch_async([YYWebImageOperation _imageQueue], ^{
                        // 判断缓存类型
                        YYImageCacheType cacheType = (_options & YYWebImageOptionIgnoreDiskCache) ? YYImageCacheTypeMemory : YYImageCacheTypeAll;
                        // 磁盘缓存  file + db
                        [_cache setImage:image imageData:data forKey:_cacheKey withType:cacheType];
                    });
                }
            }
            _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];
    }
}

下面主要介绍下内存缓存以及磁盘缓存

// GIF WebP imageData是nil
- (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) {
            // 该字段标识  是否图片资源可以直接展示到屏幕上而不需要任何解码操作 YES代表直接可以展示
            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;
                    // NO 代表不能展示  调用我们上一期将提到的图片解码,解码没有在后台,需要放到异步解码 递归锁 线程安全的
                    [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
        // 有imgData的情况是有规定数据类型的
        if (imageData) {
            if (image) {
                // 关联 扩展的Data
                [YYDiskCache setExtendedData:[NSKeyedArchiver archivedDataWithRootObject:@(image.scale)] toObject:imageData];
            }
            [_diskCache setObject:imageData forKey:key];
        } else if (image) {
            // 没有data的情况,格式对,把图像转换成
            // If the image is not PNG or JPEG, re-encode the image to PNG or JPEG for better decoding performance. 不是png或者jpeg,encode成这两个
            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];
            });
        }
    }
}

先看看YYMemoryCache的结构,其中里面有个YYLinkedMap双向链表,下面是双向链表和每个链表Node的结构

@interface _YYLinkedMap : NSObject {
    @package
    CFMutableDictionaryRef _dic; // do not set object directly 链表存储实际key(url) 和 value(Node)
    NSUInteger _totalCost;
    NSUInteger _totalCount;
    _YYLinkedMapNode *_head; // MRU, do not change it directly // 头
    _YYLinkedMapNode *_tail; // LRU, do not change it directly 尾
    BOOL _releaseOnMainThread;
    BOOL _releaseAsynchronously;
}
@interface _YYLinkedMapNode : NSObject {
    @package
    __unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic  节点头指针
    __unsafe_unretained _YYLinkedMapNode *_next; // retained by dic  节点尾指针
    id _key;
    id _value;
    NSUInteger _cost;
    NSTimeInterval _time;
}

再来看看YYMemoryCache如何进行存取的

- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
    if (!key) return;
    // key 存在  object没有的话 移除
    if (!object) {
        [self removeObjectForKey:key];
        return;
    }
    // 加锁锁 YYLinkedMap中的字典中根据Key取Node
    pthread_mutex_lock(&_lock);
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    NSTimeInterval now = CACurrentMediaTime();
    // 存在 替换
    if (node) {
        _lru->_totalCost -= node->_cost;
        _lru->_totalCost += cost;
        node->_cost = cost;
        node->_time = now;
        node->_value = object;
        // 把节点移动到链表头部
        [_lru bringNodeToHead:node];
    } else {
        // 不存在 第一次 赋值 创建一个新的
        node = [_YYLinkedMapNode new];
        node->_cost = cost;
        node->_time = now;
        node->_key = key;
        node->_value = object;
        // 把节点插入到链表头部
        [_lru insertNodeAtHead:node];
    }
    if (_lru->_totalCost > _costLimit) {
        dispatch_async(_queue, ^{
            [self trimToCost:_costLimit];
        });
    }
    // 如果超过最大内存缓存 优先移除链表尾部节点  而且从链表对象的Dic中移除Key value
    if (_lru->_totalCount > _countLimit) {
        _YYLinkedMapNode *node = [_lru removeTailNode];
        if (_lru->_releaseAsynchronously) {
            dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
            dispatch_async(queue, ^{
                [node class]; //hold and release in queue
            });
        } else if (_lru->_releaseOnMainThread && !pthread_main_np()) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [node class]; //hold and release in queue
            });
        }
    }
    pthread_mutex_unlock(&_lock);
}
- (id)objectForKey:(id)key {
    if (!key) return nil;
    pthread_mutex_lock(&_lock);
    // 根据YYMemoryCache中的_YYLinkedMap *_lru;(可以理解为链表)链表中的字典 读取对应的节点
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    // 如果节点存在,就把节点移动到链表头部
    if (node) {
        node->_time = CACurrentMediaTime();
        [_lru bringNodeToHead:node];
    }
    pthread_mutex_unlock(&_lock);
    return node ? node->_value : nil;
}

先来看看存,首先pthread_mutex_init由于OSSPinLock自旋锁的问题,可以用信号量和pthread_mutex_init来追求性能的最优。

加锁通过YYMemoryCache字段NodeMap链表的dict根据对应的key去拿,有的话就替换,淘汰算法,(bringNodeToHead)把该Node拿到链表头部,如果没有,就创建一个新的Node,然后调用insertNodeAtHead插入到链表头部,这里注意的是,如果是插入操作,就需要在链表的dict字典中把对应的key和value(Node对象 里面包含url和data)存入字典。由于作者用的是LRU淘汰算法点击打开链接,可以概括为如果数据最近被访问过,那么将来被访问的几率也更高。当临界内存到的时候,就把链表尾部节点优先淘汰,这就解决了如何处理内存超过预期值的时候如何清理内存的策略。想一下,如果用数组,那么你要把用到的值拿出来,再插入到头部,显然没有双向链表高效,直接移动就好了。那么平时内存正常的情况下存取都是在NodeMap的Dict里面操作的,只有超负荷了,才会有链表淘汰策略。对应的SD用的是NSCache,系统自带的策略,具体我也没研究过,知道的可以留下言,取的时候就简单了,直接根据key,去链表的dict拿就行了,拿到的Node里面value字段就是值


握草有完没完,那么多知识点。。。。。。

下面看看磁盘缓存

- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
    if (!key) return;
    if (!object) {
        [self removeObjectForKey:key];
        return;
    }
    
    // 扩展 数据 可先不看
    if (!value) return;
    NSString *filename = nil;
    // 敲黑板 划重点  _inlineThreshold 默认 20k
    // iPhone 6 64G 下,SQLite 写入性能比直接写文件要高,但读取性能取决于数据大小:当单条数据小于 20K 时,数据越小 SQLite 读取性能越高;单条数据大于 20K 时,直接写为文件速度会更快一些。
    /*
     YYKVStorage 不是数据库存储  而且大于20k 文件名不为空  优先写入文件
     存在filetype 和 mixType 前者很容易理解,直接写入 一般都是mixtype 后续判断是有文件名写文件,否则写入数据库  因此这里文件名的判断是  >20 写文件,  小于的话就写数据库 原因上面YY大神已经测评过了,重点 性能优化得益于此
     */
    if (_kv.type != YYKVStorageTypeSQLite) {
        // 数据大于 20k
        if (value.length > _inlineThreshold) {
            // 文件名  MD5
            filename = [self _filenameForKey:key];
        }
    }
    
    // 加锁访问数据库写入  写入的都是value元数据 未解码
    Lock();
    [_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
    Unlock();
}
// YYKVStorageTypeFile 写文件 文件名不能为空
// YYKVStorageTypeSQLite 忽略文件名
// YYKVStorageTypeMixed 当文件名不为空就写入文件,否则写数据库
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData {
    if (key.length == 0 || value.length == 0) return NO;
    if (_type == YYKVStorageTypeFile && filename.length == 0) {
        return NO;
    }
    
    if (filename.length) {
        // 写文件
        if (![self _fileWriteWithName:filename data:value]) {
            return NO;
        }
        // 写数据库
        if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
            [self _fileDeleteWithName:filename];
            return NO;
        }
        return YES;
    } else {
        if (_type != YYKVStorageTypeSQLite) {
            NSString *filename = [self _dbGetFilenameWithKey:key];
            if (filename) {
                [self _fileDeleteWithName:filename];
            }
        }
        // 写数据库
        return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
    }
}
- (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData {
    NSString *sql = @"insert or replace into manifest (key, filename, size, inline_data, modification_time, last_access_time, extended_data) values (?1, ?2, ?3, ?4, ?5, ?6, ?7);";
    sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];
    if (!stmt) return NO;
    
    int timestamp = (int)time(NULL);
    sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); // URL MD5
    sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL); // 写入文件名
    sqlite3_bind_int(stmt, 3, (int)value.length); // 元数据size
    // 文件名不存在  会写入数据库 inline_data ---> data.bytes 我打印出来是内存地址 data是二进制  
    if (fileName.length == 0) {
        sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0);
    } else {
        sqlite3_bind_blob(stmt, 4, NULL, 0, 0);
    }
    // 编辑时间
    sqlite3_bind_int(stmt, 5, timestamp);
    // 最后写入时间
    sqlite3_bind_int(stmt, 6, timestamp);
    // 扩展元数据
    sqlite3_bind_blob(stmt, 7, extendedData.bytes, (int)extendedData.length, 0);
    
    int result = sqlite3_step(stmt);
    if (result != SQLITE_DONE) {
        if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite insert error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
        return NO;
    }
    return YES;
}

磁盘缓存用到的是sqllite3和写文件的方式混合使用,上面的代码依旧不想看可以不看,直接看我的分析就可以了

首先明白,filename什么时候会有值?当value.length大于20k的时候,filename就有值

然后一般都是混合枚举类型,当有filenam值的时候,写入文件,写入失败,写入数据库

写入数据库部分里面的inline_data存储的就是data.bytes,这个值我打印出来是地址,那这个20k临界值性能最好?下面是作者测评说的

为此我评测了一下 SQLite 在真机上的表现。iPhone 6 64G 下,SQLite 写入性能比直接写文件要高,但读取性能取决于数据大小:当单条数据小于 20K 时,数据越小 SQLite 读取性能越高;单条数据大于 20K 时,直接写为文件速度会更快一些。




步骤6:回调出去,调用 self.image = image


终于搞清楚了,这句代码总很简单吧。。。。。。太年轻了兄弟,如果是GIF,这句代码里面很复杂


这才是核心代码啊,内部会调用刷新定时器缓存等操作,具体概括起来

  • 改变图片 setter改变
  • 重置动画  resetAniamted
  • 初始化动画参数  
  • 重绘  setNeedsDisplay

在重置之后,所有的参数定时器重新开启,进行GIF的图片播放,详细情况这里不说了,可以查看传送门


总结

可以看到,按作者所说,和之前的其他框架有一些简单的区别和性能上的优化,虽然还在用NSURLConnection。

1.通过pthread_metux和dispatch_semaphore性能极好的锁来保证线程安全

2.内存缓存层面通过双向链表和NSDictionary实现LRU淘汰算法,清理缓存策略

3.磁盘缓存通过写文件和sqllite3来进行不同大小数据的选择优化

4.针对SD而言,更好的实现GIF图片的播放


设计一个优秀的缓存必要的几点

  1. 内存缓存和磁盘缓存
  2. 线程安全  内存缓存用pthread_mutex  磁盘缓存用dipatch_semaphore
  3. 缓存控制  cost count age
  4. 缓存策略  LRU 双向链表 淘汰算法
  5. 性能
    异步线程释放对象
    锁的选择
    使用 NSMapTable 单例管理的 YYDiskCache
    CF框架下的字典访问 CFDictionarySetValue
    SQLite3的缓存


SDWebImage对比YYWebImage

内存NSCache和磁盘FileManager内存双向链表 + dict + LRU  和 磁盘 FileManager 和Sqlite3
NSURLCOnnectionNSURLSession
不支持GIF支持GIF
@synchronize锁pthread和dispatch_semaphore
下载任务全局管理,barrier队列一个个执行挂载的方式一个UI对应一个任务


对了,这里有个小知识点

SDWebImage对GIF播放是支持的不好的,可以看解码GIF的时

+ (UIImage *)sd_animatedGIFWithData:(NSData *)data {
    if (!data) {
        return nil;
    }

    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);

    size_t count = CGImageSourceGetCount(source);

    UIImage *staticImage;

    if (count <= 1) {
        staticImage = [[UIImage alloc] initWithData:data];
    } else {
        // we will only retrieve the 1st frame. the full GIF support is available via the FLAnimatedImageView category.
        // this here is only code to allow drawing animated images as static ones
#if SD_WATCH
        CGFloat scale = 1;
        scale = [WKInterfaceDevice currentDevice].screenScale;
#elif SD_UIKIT
        CGFloat scale = 1;
        scale = [UIScreen mainScreen].scale;
#endif
        
        CGImageRef CGImage = CGImageSourceCreateImageAtIndex(source, 0, NULL);
#if SD_UIKIT || SD_WATCH
        UIImage *frameImage = [UIImage imageWithCGImage:CGImage scale:scale orientation:UIImageOrientationUp];
        staticImage = [UIImage animatedImageWithImages:@[frameImage] duration:0.0f];
#elif SD_MAC
        staticImage = [[UIImage alloc] initWithCGImage:CGImage size:NSZeroSize];
#endif
        CGImageRelease(CGImage);
    }

    CFRelease(source);

    return staticImage;
}

可以看到这里SD作者都有些注释,说只支持显示第一帧的图片,如果要很好的GIF支持,请用FLAnimatedImage

这个框架能很好的显示GIF,可以简单看下实现思路,和YYImage实现的基本一致,都是算好每一帧的时间,根据时间通过CADisplayLink来播放,应该没理解错的话,如果有问题,请留言指正。

那么有三个解决方法

1.GIF直接用YYWebImage

2.用SDWebImage和FLAAnimationImage混合 这里有介绍点击打开链接,无非就是在SD的API下面,有一个setImageBlock,如果有实现,SD就不不会帮我们赋值,需要我们自己实现赋值,我们让SD下载图片,然后用FLA异步解码显示,记住要异步解码啊

3.点击打开链接这个哥们有个替代UIImage+GIF的M文件替换SDWebImage框架里面的,也行

不过三个方法都摆在这里了,用哪个看个人喜好喽


早些时间就看过,只是没那么认真分析,这次全部记下来了,吃透,妥妥的


SDWebImage源码分析

YYImage源码分析

YYModel源码分析

YYText源码分析


  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值