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

本文主要是介绍YYWebImage流程源码分析(YYCache和YYImage设计思路)附带所有YYKit组件源码分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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

前言

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

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

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


流程

步骤一:外部API调用
[_webImageView yy_setImageWithURL:urlplaceholder:niloptions:YYWebImageOptionProgressiveBlur | YYWebImageOptionShowNetworkActivity | YYWebImageOptionSetImageWithFadeAnimationprogress:^(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:nilcompletion:^(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 *)imageURLplaceholder:(UIImage *)placeholderoptions:(YYWebImageOptions)optionsmanager:(YYWebImageManager *)managerprogress:(YYWebImageProgressBlock)progresstransform:(YYWebImageTransformBlock)transformcompletion:(YYWebImageCompletionBlock)completion {if ([imageURL isKindOfClass:[NSString class]]) imageURL = [NSURL URLWithString:(id)imageURL];// 生成管理类单例manager = manager ? manager : [YYWebImageManager sharedManager];// 私有类_YYWebImageSetter *setter = objc_getAssociatedObject(self, &_YYWebImageSetterKey);// 通过Category挂载 _YYWebImageSetterKeyif (!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)sentinelurl:(NSURL *)imageURLoptions:(YYWebImageOptions)optionsmanager:(YYWebImageManager *)managerprogress:(YYWebImageProgressBlock)progresstransform:(YYWebImageTransformBlock)transformcompletion:(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;// 任务生成 计数+1sentinel = 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 cacheif (_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) YESBOOL 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) {// 多帧 例如 GIFhasAnimation = 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 cacheif (!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) {// 多帧 例如 GIFhasAnimation = 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 cacheif (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 {@packageCFMutableDictionaryRef _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取Nodepthread_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 valueif (_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) {// 数据大于 20kif (value.length > _inlineThreshold) {// 文件名  MD5filename = [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 MD5sqlite3_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_WATCHCGFloat scale = 1;scale = [WKInterfaceDevice currentDevice].screenScale;
#elif SD_UIKITCGFloat scale = 1;scale = [UIScreen mainScreen].scale;
#endifCGImageRef CGImage = CGImageSourceCreateImageAtIndex(source, 0, NULL);
#if SD_UIKIT || SD_WATCHUIImage *frameImage = [UIImage imageWithCGImage:CGImage scale:scale orientation:UIImageOrientationUp];staticImage = [UIImage animatedImageWithImages:@[frameImage] duration:0.0f];
#elif SD_MACstaticImage = [[UIImage alloc] initWithCGImage:CGImage size:NSZeroSize];
#endifCGImageRelease(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源码分析


这篇关于YYWebImage流程源码分析(YYCache和YYImage设计思路)附带所有YYKit组件源码分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/189967

相关文章

Go标准库常见错误分析和解决办法

《Go标准库常见错误分析和解决办法》Go语言的标准库为开发者提供了丰富且高效的工具,涵盖了从网络编程到文件操作等各个方面,然而,标准库虽好,使用不当却可能适得其反,正所谓工欲善其事,必先利其器,本文将... 目录1. 使用了错误的time.Duration2. time.After导致的内存泄漏3. jsO

Python实现无痛修改第三方库源码的方法详解

《Python实现无痛修改第三方库源码的方法详解》很多时候,我们下载的第三方库是不会有需求不满足的情况,但也有极少的情况,第三方库没有兼顾到需求,本文将介绍几个修改源码的操作,大家可以根据需求进行选择... 目录需求不符合模拟示例 1. 修改源文件2. 继承修改3. 猴子补丁4. 追踪局部变量需求不符合很

Spring事务中@Transactional注解不生效的原因分析与解决

《Spring事务中@Transactional注解不生效的原因分析与解决》在Spring框架中,@Transactional注解是管理数据库事务的核心方式,本文将深入分析事务自调用的底层原理,解释为... 目录1. 引言2. 事务自调用问题重现2.1 示例代码2.2 问题现象3. 为什么事务自调用会失效3

找不到Anaconda prompt终端的原因分析及解决方案

《找不到Anacondaprompt终端的原因分析及解决方案》因为anaconda还没有初始化,在安装anaconda的过程中,有一行是否要添加anaconda到菜单目录中,由于没有勾选,导致没有菜... 目录问题原因问http://www.chinasem.cn题解决安装了 Anaconda 却找不到 An

Spring定时任务只执行一次的原因分析与解决方案

《Spring定时任务只执行一次的原因分析与解决方案》在使用Spring的@Scheduled定时任务时,你是否遇到过任务只执行一次,后续不再触发的情况?这种情况可能由多种原因导致,如未启用调度、线程... 目录1. 问题背景2. Spring定时任务的基本用法3. 为什么定时任务只执行一次?3.1 未启用

Vue中组件之间传值的六种方式(完整版)

《Vue中组件之间传值的六种方式(完整版)》组件是vue.js最强大的功能之一,而组件实例的作用域是相互独立的,这就意味着不同组件之间的数据无法相互引用,针对不同的使用场景,如何选择行之有效的通信方式... 目录前言方法一、props/$emit1.父组件向子组件传值2.子组件向父组件传值(通过事件形式)方

Python实现将MySQL中所有表的数据都导出为CSV文件并压缩

《Python实现将MySQL中所有表的数据都导出为CSV文件并压缩》这篇文章主要为大家详细介绍了如何使用Python将MySQL数据库中所有表的数据都导出为CSV文件到一个目录,并压缩为zip文件到... python将mysql数据库中所有表的数据都导出为CSV文件到一个目录,并压缩为zip文件到另一个

利用Go语言开发文件操作工具轻松处理所有文件

《利用Go语言开发文件操作工具轻松处理所有文件》在后端开发中,文件操作是一个非常常见但又容易出错的场景,本文小编要向大家介绍一个强大的Go语言文件操作工具库,它能帮你轻松处理各种文件操作场景... 目录为什么需要这个工具?核心功能详解1. 文件/目录存javascript在性检查2. 批量创建目录3. 文件

C++ 各种map特点对比分析

《C++各种map特点对比分析》文章比较了C++中不同类型的map(如std::map,std::unordered_map,std::multimap,std::unordered_multima... 目录特点比较C++ 示例代码 ​​​​​​代码解释特点比较1. std::map底层实现:基于红黑

Spring AI ectorStore的使用流程

《SpringAIectorStore的使用流程》SpringAI中的VectorStore是一种用于存储和检索高维向量数据的数据库或存储解决方案,它在AI应用中发挥着至关重要的作用,本文给大家介... 目录一、VectorStore的基本概念二、VectorStore的核心接口三、VectorStore的