YYKit组件之一---->YYImage 图像处理

  • 移动端图片格式调研
  • 图片处理的小技巧

YYWebImage源码分析

YYModel源码分析

YYText源码分析

核心思路--->图片解码 (二进制数据<-->位图)

雷纯峰的分析

这段是前言,介绍下图片是如何解码的。不想看到的可以直接无视

核心代码:

[_decoder frameAtIndex:index decodeForDisplay:YES]----->YYCGImageCreateDecodedCopy左边是外部暴露的解码方法,右边是核心解码方法

我们首先要知道,如果最普通的UIImageView的图片UIImage创建资源赋值,图片是没有解码的,只有当图片被被赋值给UIImageView的时候,Runloop捕获到事件,才会进行解压缩,其中会把二进制压缩的数据,解压成没有压缩的位图,这里就是最耗时的操作

我这里只是简单的说下我自己理解的流程,具体验证可以看雷哥的博客

既然那么耗时,为什么一定要解压缩才可以显示?那你得明白位图数据和二进制数据的区别了。

比如一张10kb的图,我们有data信息,也就是平时看到的PNG或者JPEG等后缀的格式,其中PNG支持alpha通道,无损压缩,JPEG是支持有损压缩的 图片压缩格式介绍 ,有损无损无非就是把多余的通过代码压进去,可以看看这个文章

那么PNG还是JPEG,只是位图的压缩形式罢了。一张PNG的图,解压缩出来就是原始位图,里面装载着像素信息,颜色信息等,这才是最原始的解压后的图,只有这样,所有的信息具备,才能被渲染到屏幕上,因此拿到的图片只能解压缩才可以显示(就是必然要耗时),既然一定要解压,耗时,不能卡在主线程,那就拿到子线程解压,把解压完的图片返回之后,再次渲染的时候,捕捉到已经解压了,就不需要在主线程解压了,直接显示。这也是所有第三方图片框架下载的核心。平时如果你不在意,你压根不知道他做了什么性能优化。

以上就是原理知识点

解压位图官方核心代码

/* Create a bitmap context. The context draws into a bitmap which is `width'pixels wide and `height' pixels high. The number of components for eachpixel is specified by `space', which may also specify a destination colorprofile. The number of bits for each component of a pixel is specified by`bitsPerComponent'. The number of bytes per pixel is equal to`(bitsPerComponent * number of components + 7)/8'. Each row of the bitmapconsists of `bytesPerRow' bytes, which must be at least `width * bytesper pixel' bytes; in addition, `bytesPerRow' must be an integer multipleof the number of bytes per pixel. `data', if non-NULL, points to a blockof memory at least `bytesPerRow * height' bytes. If `data' is NULL, thedata for context is allocated automatically and freed when the context isdeallocated. `bitmapInfo' specifies whether the bitmap should contain analpha channel and how it's to be generated, along with whether thecomponents are floating-point or integer. */CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(void * __nullable data,size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow,CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo)CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
  • data :如果不为 NULL ,那么它应该指向一块大小至少为 bytesPerRow * height 字节的内存;如果 为 NULL ,那么系统就会为我们自动分配和释放所需的内存,所以一般指定 NULL 即可;
  • width 和 height :位图的宽度和高度,分别赋值为图片的像素宽度和像素高度即可;
  • bitsPerComponent :像素的每个颜色分量使用的 bit 数,在 RGB 颜色空间下指定 8 即可;
  • bytesPerRow :位图的每一行使用的字节数,大小至少为 width * bytes per pixel 字节。有意思的是,当我们指定 0 时,系统不仅会为我们自动计算,而且还会进行 cache line alignment 的优化
  • space :就是我们前面提到的颜色空间,一般使用 RGB 即可;
  • bitmapInfo :就是我们前面提到的位图的布局信息。

以上是雷哥总结出来的参数介绍,下面看看YY里面的调用解压

// BGRA8888 (premultiplied) or BGRX8888// same as UIGraphicsBeginImageContext() and -[UIView drawRect:]CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, YYCGColorSpaceGetDeviceRGB(), bitmapInfo);if (!context) return NULL;CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); // decodeCGImageRef newImage = CGBitmapContextCreateImage(context);CFRelease(context);return newImage;
  1. 通过CGBitmapContextCreate创建位图上下文
  2. 通过CGContextDrawImage把原始位图绘制到上下文
    CFDataRef rawData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));该方法可以获取到原始位图信息
  3. CGBitmapContextCreateImage创建一个新的解压后的位图

通过资源的读取到屏幕渲染之间,我们不做处理,系统的解压是在主线程的,因此我们穿插了强制解压,放在异步线程处理,会让性能有着显著的提升。YY,SD,FLA都是这个思路

特性

  • 支持以下类型动画图像的播放/编码/解码:
  • WebP, APNG, GIF。
  • 支持以下类型静态图像的显示/编码/解码:
  • WebP, PNG, GIF, JPEG, JP2, TIFF, BMP, ICO, ICNS。
  • 支持以下类型图片的渐进式/逐行扫描/隔行扫描解码:
  • PNG, GIF, JPEG, BMP。
  • 支持多张图片构成的帧动画播放,支持单张图片的 sprite sheet 动画。
  • 高效的动态内存缓存管理,以保证高性能低内存的动画播放。
  • 完全兼容 UIImage 和 UIImageView,使用方便。
  • 保留可扩展的接口,以支持自定义动画。
  • 每个类和方法都有完善的文档注释。

概览

流程

  • YYImage : UIImage的子类,遵守 YYAnimatedImage 协议,帧图片,编解码,帧预加载等高级特性,支持WebP,APNG和GIF的编解码
  • YYFrameImage : 能够显示帧动画,仅支持png,jpeg 格式
  • YYSpriteSheetImage : 是用来做Spritesheet动画显示的图像类,也是UIImage的子类
  • YYImageCoder : 图像的编码和解码功能类,YYImage底层支持,YYImageEncoder负责编码,YYImageDecoder 负责解码,YYImageFrame 负责管理帧图像信息,_YYImageDecoderFrame 内部私有类是其子类,UIImage+YYImageCoder提供了一些便利方法
  • YYAnimatedImageView: UIImageView 子类,用于播放图像动画

1.第一步 YYImage的初始化

根据流程走一遍,以YYImage为例,先创建一个YYImage的对象供后续使用

YYImage *image = [YYImage imageNamed:name];

这里入口函数,接着会根据一些后缀,或者没有给出来判断出扩展名

@[@"", @"png", @"jpeg", @"jpg", @"gif", @"webp", @"apng"]

最后来根据以下方法初始化YYImage对象

- (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];// 根据index从解码器的数组里面提取出 _YYImageDecoderFrame 然后对图片源根据index解码出对应的帧图片存储到frame的image字段返回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.yy_isDecodedForDisplay = YES;}return self;
}

先来看一下YYImageDecoder的私有变量

@implementation YYImageDecoder {pthread_mutex_t _lock; // recursive lock 递归锁 初始化调用 更新图像数据源加递归锁BOOL _sourceTypeDetected; // 是否推测图像源类型CGImageSourceRef _source; // 图像源yy_png_info *_apngSource; // 如果判定图像为 YYImageTypePNG 则会以 APNG 更新图像源
#if YYIMAGE_WEBP_ENABLEDWebPDemuxer *_webpSource; // 如果判定图像为 YYImageTypeWebP 则会议 WebP 更新图像源
#endifUIImageOrientation _orientation; // 绘制方向dispatch_semaphore_t _framesLock; // 针对于图像帧的锁 这种不长时间阻塞线程的线程安全可以用信号量  frame操作锁NSArray *_frames; ///< Array<GGImageDecoderFrame>, without image 每一帧属性BOOL _needBlend; // 是否需要混合  WebP 和 APNG来用的NSUInteger _blendFrameIndex; // 从帧索引混合到当前帧CGContextRef _blendCanvas; // 混合画布
}

解码器根据Data源初始化的核心代码

- (void)_updateSourceImageIO {// 宽  高   初始方向 循环次数_width = 0;_height = 0;_orientation = UIImageOrientationUp;_loopCount = 0;// 清楚原先解码器的数据dispatch_semaphore_wait(_framesLock, DISPATCH_TIME_FOREVER);_frames = nil;dispatch_semaphore_signal(_framesLock);// 处理图像源if (!_source) {if (_finalized) {_source = CGImageSourceCreateWithData((__bridge CFDataRef)_data, NULL);} else {_source = CGImageSourceCreateIncremental(NULL);if (_source) CGImageSourceUpdateData(_source, (__bridge CFDataRef)_data, false);}} else {CGImageSourceUpdateData(_source, (__bridge CFDataRef)_data, _finalized);}if (!_source) return;// 获取图像帧数_frameCount = CGImageSourceGetCount(_source);if (_frameCount == 0) return;if (!_finalized) { // ignore multi-frame before finalized_frameCount = 1;} else {// PNG一帧if (_type == YYImageTypePNG) { // use custom apng decoder and ignore multi-frame_frameCount = 1;}// GIF多帧if (_type == YYImageTypeGIF) { // get gif loop count// 获取数据源属性字典CFDictionaryRef properties = CGImageSourceCopyProperties(_source, NULL);// 属性字典获取到if (properties) {// 根据Key kCGImagePropertyGIFDictionary 获取到GIF下的字典属性CFDictionaryRef gif = CFDictionaryGetValue(properties, kCGImagePropertyGIFDictionary);// 获取到gif 字典if (gif) {// 获取循环次数  根据Key  kCGImagePropertyGIFLoopCountCFTypeRef loop = CFDictionaryGetValue(gif, kCGImagePropertyGIFLoopCount);// _loopCount 地址进去赋值if (loop) CFNumberGetValue(loop, kCFNumberNSIntegerType, &_loopCount);}CFRelease(properties);}}}/*ICO, GIF, APNG may contains multi-frame.多帧的情况下才会进来*/NSMutableArray *frames = [NSMutableArray new];for (NSUInteger i = 0; i < _frameCount; i++) {// 每一帧的对象属性  继承于YYImageFrame_YYImageDecoderFrame *frame = [_YYImageDecoderFrame new];frame.index = i; // 当前索引frame.blendFromIndex = i;frame.hasAlpha = YES;frame.isFullSize = YES;[frames addObject:frame];// 根据数据源的索引获取属性字典  (刚才上面的获取方式是拿循环次数的时候GIF专用key,这里有多种情况,就根据下标拿)CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_source, i, NULL);if (properties) {NSTimeInterval duration = 0;NSInteger orientationValue = 0, width = 0, height = 0;CFTypeRef value = NULL;// 获取宽度value = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);if (value) CFNumberGetValue(value, kCFNumberNSIntegerType, &width);// 获取高度value = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);if (value) CFNumberGetValue(value, kCFNumberNSIntegerType, &height);// 如果是GIFif (_type == YYImageTypeGIF) {// 依旧获取到对应的gif属性字典CFDictionaryRef gif = CFDictionaryGetValue(properties, kCGImagePropertyGIFDictionary);if (gif) {// Use the unclamped frame delay if it exists.value = CFDictionaryGetValue(gif, kCGImagePropertyGIFUnclampedDelayTime);if (!value) {// Fall back to the clamped frame delay if the unclamped frame delay does not exist.value = CFDictionaryGetValue(gif, kCGImagePropertyGIFDelayTime);}// 获取到每一帧的持续时间if (value) CFNumberGetValue(value, kCFNumberDoubleType, &duration);}}frame.width = width;frame.height = height;frame.duration = duration;if (i == 0 && _width + _height == 0) { // init first frame_width = width;_height = height;value = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);if (value) {CFNumberGetValue(value, kCFNumberNSIntegerType, &orientationValue);_orientation = YYUIImageOrientationFromEXIFValue(orientationValue);}}CFRelease(properties);}}// NSArray *_frames; ///< Array<GGImageDecoderFrame>, without image  _YYImageDecoderFrame(每一帧对象存进数组)dispatch_semaphore_wait(_framesLock, DISPATCH_TIME_FOREVER);_frames = frames;dispatch_semaphore_signal(_framesLock);
}

这里以GIF为例,解码器的简单初始化是为了搜集每一帧的宽度,高度,duration,原始方向以及循环次数一些简单属性,这里每一帧的对象是以_YYImageDecoderFrame继承于YYImageFrame的私有类保存,然后统一存储到frames数组里面,记住,这里的图片只是显示了每一帧的基本信息保存而已,图片还是没有解码的。

这里YYImageDecoder就是管理类,里面的参数frames保存上面的DecoderFrame,也就是每一帧的信息,然后把数据源通过CG函数转换存入_source字段备用,后续frames(YYImageDecoderFrame)里面的每一帧都会被解码存储到image字段,通过定时器去显示,可以理解为Decoder管理数据源以及每一帧的信息和解码后的image资源

_source = CGImageSourceCreateWithData((__bridge CFDataRef)_data, NULL);

以上最基本的属性获取提取完之后调用

YYImageFrame *frame = [decoder frameAtIndex:0 decodeForDisplay:YES];

根据index,提取出对应帧的解码图片 下面这个方法就是解码提取图片的核心方法

- (YYImageFrame *)_frameAtIndex:(NSUInteger)index decodeForDisplay:(BOOL)decodeForDisplay {if (index >= _frames.count) return 0;_YYImageDecoderFrame *frame = [(_YYImageDecoderFrame *)_frames[index] copy];BOOL decoded = NO;BOOL extendToCanvas = NO;if (_type != YYImageTypeICO && decodeForDisplay) { // ICO contains multi-size frame and should not extend to canvas.extendToCanvas = YES;}if (!_needBlend) {// 核心CGImageRef imageRef = [self _newUnblendedImageAtIndex:index extendToCanvas:extendToCanvas decoded:&decoded];if (!imageRef) return nil;if (decodeForDisplay && !decoded) {// 解码的图片CGImageRef imageRefDecoded = YYCGImageCreateDecodedCopy(imageRef, YES);if (imageRefDecoded) {CFRelease(imageRef);imageRef = imageRefDecoded;decoded = YES;}}UIImage *image = [UIImage imageWithCGImage:imageRef scale:_scale orientation:_orientation];CFRelease(imageRef);if (!image) return nil;image.yy_isDecodedForDisplay = decoded;frame.image = image;return frame;}if (!imageRef) return nil;UIImage *image = [UIImage imageWithCGImage:imageRef scale:_scale orientation:_orientation];CFRelease(imageRef);if (!image) return nil;image.yy_isDecodedForDisplay = YES;frame.image = image;if (extendToCanvas) {frame.width = _width;frame.height = _height;frame.offsetX = 0;frame.offsetY = 0;frame.dispose = YYImageDisposeNone;frame.blend = YYImageBlendNone;}return frame;
}

从上面标记核心的方法开始,开始解码图片,点进去有一个方法CG下面的绘图

YYCGImageCreateDecodedCopy

概括下就是把图片用 CGContextDrawImage() 绘制到画布上,然后把画布的数据取出来当作图片,把CGRef下面的图片转换成UIImage图片资源,然后保存到之前解码器Decoder每一帧里面Frame对应的_YYImageDecoderFrame的属性Image里面保存。

然后就是在初始化YYImage的时候取出第一帧的图片的Image赋值并返回

        UIImage *image = frame.image;if (!image) return nil;self = [self initWithCGImage:image.CGImage scale:decoder.scale orientation:image.imageOrientation];

到这里,无论是你播放的PNG还是GIF,如果没有后续代码,就显示第一帧图片在UI上

2.第二步YYAnimatedImageView创建

- (instancetype)initWithImage:(UIImage *)image {self = [super init];_runloopMode = NSRunLoopCommonModes;_autoPlayAnimatedImage = YES;self.frame = (CGRect) {CGPointZero, image.size };self.image = image;return self;
}

初始化一些简单的属性,这里的有个Runloop是标记为NSRunLoopCOmmondModes,为后续的定时器做铺垫,让线程无论处于DefaultMode还是TrackingMode都能播放动图,_autoPlayAnimatedImage默认是YES,,后面重写DidMove到window层或者SuperView的时候,会根据该字段,自动调用startAnimation方法,后面再展开。这里的Setter方法self.image继续往下走

- (void)setImage:(id)image withType:(YYAnimatedImageType)type {[self stopAnimating];if (_link) [self resetAnimated];_curFrame = nil;switch (type) {case YYAnimatedImageTypeNone: break;case YYAnimatedImageTypeImage: super.image = image; break;case YYAnimatedImageTypeHighlightedImage: super.highlightedImage = image; break;case YYAnimatedImageTypeImages: super.animationImages = image; break;case YYAnimatedImageTypeHighlightedImages: super.highlightedAnimationImages = image; break;}[self imageChanged];
}
- (void)imageChanged {//YYAnimatedImageTypeImageYYAnimatedImageType newType = [self currentImageType];id newVisibleImage = [self imageForType:newType];NSUInteger newImageFrameCount = 0;BOOL hasContentsRect = NO;if ([newVisibleImage isKindOfClass:[UIImage class]] &&[newVisibleImage conformsToProtocol:@protocol(YYAnimatedImage)]) {// 根据之前Decode之后的参数 返回frame帧数newImageFrameCount = ((UIImage<YYAnimatedImage> *) newVisibleImage).animatedImageFrameCount;if (newImageFrameCount > 1) {hasContentsRect = [((UIImage<YYAnimatedImage> *) newVisibleImage) respondsToSelector:@selector(animatedImageContentsRectAtIndex:)];}}// 这坨代码一般不会走if (!hasContentsRect && _curImageHasContentsRect) {if (!CGRectEqualToRect(self.layer.contentsRect, CGRectMake(0, 0, 1, 1)) ) {[CATransaction begin];[CATransaction setDisableActions:YES];self.layer.contentsRect = CGRectMake(0, 0, 1, 1);[CATransaction commit];}}_curImageHasContentsRect = hasContentsRect;if (hasContentsRect) {CGRect rect = [((UIImage<YYAnimatedImage> *) newVisibleImage) animatedImageContentsRectAtIndex:0];[self setContentsRect:rect forImage:newVisibleImage];}// 多张图if (newImageFrameCount > 1) {[self resetAnimated]; // 重置动画_curAnimatedImage = newVisibleImage; // 当前图像_curFrame = newVisibleImage; // 当前帧图像_totalLoop = _curAnimatedImage.animatedImageLoopCount; // 循环次数_totalFrameCount = _curAnimatedImage.animatedImageFrameCount; // 总帧数[self calcMaxBufferCount]; // 最大缓存}[self setNeedsDisplay]; // 标记下一个Runloop进行刷新[self didMoved]; // 添加到父视图的时候是否动画
}

通过ImageChange函数,这里可以简单概括下四步

  • 改变图片 setter改变
  • 重置动画  resetAniamted
  • 初始化动画参数
  • 重绘  setNeedsDisplay
// init the animated params.
- (void)resetAnimated {// 懒加载,第一次的时候初始化 锁,buffer,队列 和 CADisplayLink  添加两个通知if (!_link) {_lock = dispatch_semaphore_create(1);_buffer = [NSMutableDictionary new];_requestQueue = [[NSOperationQueue alloc] init];_requestQueue.maxConcurrentOperationCount = 1;// 知识点_link = [CADisplayLink displayLinkWithTarget:[_YYImageWeakProxy proxyWithTarget:self] selector:@selector(step:)];if (_runloopMode) {[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:_runloopMode];}_link.paused = YES;[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];}// 串行队列删除任务[_requestQueue cancelAllOperations];// 加锁 小技巧 后台线程释放资源 (用局部变量捕获,把类的成员变量重新分配新的空间,然后丢到异步全局队列发送一条消息,在后台线程释放buffer资源)LOCK(if (_buffer.count) {NSMutableDictionary *holder = _buffer;_buffer = [NSMutableDictionary new];dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{// Capture the dictionary to global queue,// release these images in background to avoid blocking UI thread.[holder class];});});// 暂停_link.paused = YES;_time = 0;// 把当前帧索引重置 并且发出KVO通知if (_curIndex != 0) {[self willChangeValueForKey:@"currentAnimatedImageIndex"];_curIndex = 0;[self didChangeValueForKey:@"currentAnimatedImageIndex"];}_curAnimatedImage = nil; // 当前图像为空_curFrame = nil; // 当前帧_curLoop = 0; //当前循环次数_totalLoop = 0; // 总循环次数_totalFrameCount = 1; // 总帧数_loopEnd = NO; // 是否循环结尾_bufferMiss = NO; // 是否丢帧_incrBufferCount = 0; // 当前允许的缓存
}

以下来自美团一个大神的分析:

YYAnimatedImageView 内部设计了 _YYImageWeakProxy 来避免使用 NSTimer 或者 CADisplayLink 可能造成的循环引用问题,_YYImageWeakProxy 内部实现也比较简单,继承自 NSProxy

既然都消息重定向给 target 了还要消息转发干嘛?因为要避免循环引用问题所以对 target 使用弱引用,期间无法保证 target 一定存在,所以 forwardingTargetForSelector: 方法可能返回 nil,接着在 Runtime 消息转发中借用 init 消息返回空以“吞掉”异常。

网上资料查询简单了解了以下,NSProxy对于无法识别的消息更容易处理转发,也不会报错

这里有个小技巧,由于重新setter了Image资源,那么后台线程释放资源 (用局部变量捕获,把类的成员变量重新分配新的空间,然后丢到异步全局队列发送一条消息,在后台线程释放buffer资源)之后Buffer资源重新分配空间,这种GIF帧数多的话都丢到后台去释放,还是能优化不少性能的

继续往下

YYAnimatedImageView里面重写了didMoveToWindow和didMoveToSuperView,里面这两个函数会在UI被添加到其他父控件上面之后触发,然后会根据条件执行StartAnimation或者StopAnimation两个不同的函数

- (void)startAnimating {YYAnimatedImageType type = [self currentImageType];// 系统if (type == YYAnimatedImageTypeImages || type == YYAnimatedImageTypeHighlightedImages) {NSArray *images = [self imageForType:type];if (images.count > 0) {[super startAnimating];self.currentIsPlayingAnimation = YES;}} else {// 自定义动画if (_curAnimatedImage && _link.paused) {_curLoop = 0;_loopEnd = NO;_link.paused = NO;self.currentIsPlayingAnimation = YES;}}
}

启动函数,在else那里,如果有图片,而且定时器暂停了,就开启定时器,并且标记状态currentIsPlayingAnimation为Yes

下面看看到底是如何播放GIF动画的(定时器方法)

- (void)step:(CADisplayLink *)link {// 当前显示的图像 必须遵循 <YYAnimatedImage>UIImage <YYAnimatedImage> *image = _curAnimatedImage;// 获取当前图像数据字典NSMutableDictionary *buffer = _buffer;// 下张要显示的图像UIImage *bufferedImage = nil;// 下一张要显示的索引NSUInteger nextIndex = (_curIndex + 1) % _totalFrameCount;// 是否获取所有图像数据BOOL bufferIsFull = NO;// 当前无图像显示 返回if (!image) return;// 结束循环 停留在最后帧if (_loopEnd) { // view will keep in last frame[self stopAnimating];return;}NSTimeInterval delay = 0;// 下张图存在,没跳帧if (!_bufferMiss) {// 每一帧读完之后的时间 累加_time += link.duration;// 当前帧需要多少时间播放delay = [image animatedImageDurationAtIndex:_curIndex];if (_time < delay){NSLog(@"下一帧还没到来都会进入这里 上一帧的时间都会小于当前帧的开始播放时间,继续等待下一帧时间到来");return;}// 减去当前图像的时间,保证下张图显示时间正确_time -= delay;if (nextIndex == 0) {// 一次循环结束_curLoop++;// 总循环结束if (_curLoop >= _totalLoop && _totalLoop != 0) {_loopEnd = YES;[self stopAnimating];[self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleepreturn; // stop at last frame}}// 如果当前累加时间还是大于下张显示时间,设置累加时间为delay,避免直接跳过下张图像显示delay = [image animatedImageDurationAtIndex:nextIndex];if (_time > delay) _time = delay; // do not jump over frame}// 加锁 读取缓冲区下一张图片LOCK(bufferedImage = buffer[@(nextIndex)];// 缓存区读取到图片 不丢帧if (bufferedImage) {NSLog(@"加锁读取缓存 命中");if ((int)_incrBufferCount < _totalFrameCount) {[buffer removeObjectForKey:@(nextIndex)];}// 一次播放往后移动索引[self willChangeValueForKey:@"currentAnimatedImageIndex"];_curIndex = nextIndex;[self didChangeValueForKey:@"currentAnimatedImageIndex"];// 更新当前帧图像_curFrame = bufferedImage == (id)[NSNull null] ? nil : bufferedImage;// YYSpriteSheetImage 可以先不看if (_curImageHasContentsRect) {_curContentsRect = [image animatedImageContentsRectAtIndex:_curIndex];[self setContentsRect:_curContentsRect forImage:_curFrame];}nextIndex = (_curIndex + 1) % _totalFrameCount;_bufferMiss = NO;if (buffer.count == _totalFrameCount) {bufferIsFull = YES;}} else {// 丢帧NSLog(@"加锁读取缓存 未命中,丢帧");_bufferMiss = YES;})//LOCK// 未丢帧 绘制if (!_bufferMiss) {//更新图像  layer.contents = (__bridge id)_curFrame.CGImage;[self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep}// 缓冲区没有满 而且没有任务if (!bufferIsFull && _requestQueue.operationCount == 0) { // if some work not finished, wait for next opportunityNSLog(@"添加任务");//还未获取所有图像,交给_YYAnimatedImageViewFetchOperation 获取下一张图像_YYAnimatedImageViewFetchOperation *operation = [_YYAnimatedImageViewFetchOperation new];operation.view = self;operation.nextIndex = nextIndex;operation.curImage = image;[_requestQueue addOperation:operation];}
}
  • 根据当前帧index读取出下一帧图片的nextIndex
  • 根据nextIndex加锁去缓存区字典中作为key去拿(加锁取的前提是时间到了当前帧的时间或者跳帧)
  • 取到把_curFrame当前帧替换成下一帧的图像,没有丢帧,标记setNeedsDisplay,没取到就丢帧,不标记
  • 无论取到与否,当缓存数量没有满而且任务队列空了,就根据nextIndex去解码对应帧的图片并缓存起来

这里有两个知识点

1.当layer被标记为setNeedsDisplay的时候,系统会在下一个Runloop休眠周期调用下面方法赋值

- (void)displayLayer:(CALayer *)layer {if (_curFrame) {layer.contents = (__bridge id)_curFrame.CGImage;}
}

2.自定义NSOperation任务被加入到队列的时候,重写main函数会在异步线程执行任务(这里是在异步解码,线程安全操作_buffer,而且队列最大并发数是1,是串行的)

这里用NSOperation串行队列实现了异步图片解码GIF,每次都会根据Index去解码对应的帧图像,然后缓存到_buffer字典里面,在CADisplayLink跑的时候不断的去缓存中查找,由于任务可能会很多,NSOperation会在适当的时候cancel掉,因此循环解码存储的时候都要进行cancel判断,用来及时停止任务。

- (void)main {__strong YYAnimatedImageView *view = _view;if (!view) return;if ([self isCancelled]) return;view->_incrBufferCount++; // 缓存计数增加if (view->_incrBufferCount == 0) [view calcMaxBufferCount];if (view->_incrBufferCount > (NSInteger)view->_maxBufferCount) {view->_incrBufferCount = view->_maxBufferCount;}NSUInteger idx = _nextIndex;// 缓存个数 最多也就图像数量NSUInteger max = view->_incrBufferCount < 1 ? 1 : view->_incrBufferCount;NSUInteger total = view->_totalFrameCount;view = nil;for (int i = 0; i < max; i++, idx++) {@autoreleasepool {if (idx >= total) idx = 0;if ([self isCancelled]) break;__strong YYAnimatedImageView *view = _view;if (!view) break;// 判断是否已经有缓存LOCK_VIEW(BOOL miss = (view->_buffer[@(idx)] == nil));// miss = YES  没有缓存if (miss) {// 读取丢失的缓存 根据index拿出_YYImageDecoderFrame 进行帧图片解码UIImage *img = [_curImage animatedImageFrameAtIndex:idx];// 还是在异步线程再次调用解码  如果没有,就解码,有的话就返回selfimg = img.yy_imageByDecoded;if ([self isCancelled]) break;// 将解码的图片存储到bufferLOCK_VIEW(view->_buffer[@(idx)] = img ? img : [NSNull null]);view = nil;}}}
}

该方法是解码缓存图像的方法

主要逻辑是根据idx读取缓存字典里面的资源,读取到了说明有缓存,不处理,没读取到,执行核心代码,上面已经有注释了

概括下

核心一代理根据索引获取解码图像资源

- (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index {if (index >= _decoder.frameCount) return nil;dispatch_semaphore_wait(_preloadedLock, DISPATCH_TIME_FOREVER);UIImage *image = _preloadedFrames[index];dispatch_semaphore_signal(_preloadedLock);if (image) return image == (id)[NSNull null] ? nil : image;return [_decoder frameAtIndex:index decodeForDisplay:YES].image;
}

该方法是YYImage的代理实现,其内部逻辑是根据YYImage的属性是否预加载把所有帧的解码图片预先加载到预加载数组里面方便直接读取,如果没有开启的话就依然调用我们刚开始YYImage初始化第一帧的时候的核心方法 frameAtIndex:decodeForDIsplay去根据idx解码帧图像返回,原理最上面介绍YYImage初始化的时候也有提到了,细节不列出了,可以看源码CG下面的实现

这里作者还调用了便利方法yy_imageByDecoded,再次解码,这方法的介绍是,如果未解码,就解码,解码了就返回自身,看来是确保一定解码返回UIImage资源,然后继续加锁访问_buffer字典,根据index作为key把UIImage缓存起来

这个时候,缓存任务已经结束,根据1/60s的执行时间,会无线循环step的CADisplaylink执行,命中缓存,标记setNeedsDisplay,在Runloop周期给layerContent赋值

这里用的了递归锁和信号量锁,简单提一下,细节可以看点击打开链接

YYImageDecoder里面 pthread_mutexattr_t 和 dispatch_semaphore_wait两个的选择

解码器可以一直创建,当前一个锁还没有释放的时候就再创建新的,就会导致死循环,因此需要递归锁,只是需要把

pthread_mutexattr_t的类型标记为PTHREAD_MUTEX_RECURSIVE就可以了,而作为图像帧Frames数组的锁用信号量是因为更加轻量,对于这种数组字典操作,不耗时阻塞的操作用信号量性能更好,如果需要等待的操作,个人觉得OSSPinLock自旋锁可以拿来用,虽然被证明有优先级翻转问题。。。。。。需要了解细节的可以看上面的链接

以上就是YYImage的图片解码以及如何显示的基本逻辑分析,应该很清晰的,核心代码也贴出来了,知识点学习下还是很不错的

美团大神

参考二号

YYImage实现思路源码分析(图片解压缩原理)相关推荐

  1. 手撸spring源码分析IOC实现原理

    手撸spring源码分析IOC实现原理 文章出处:https://github.com/fuzhengwei/small-spring 根据小付哥的手撸spring核心源码一步步学习出来的结果收货总结 ...

  2. Vue3源码分析之打包原理

    Vue3源码分析之打包原理 如果之前你已经看过我的<Vue3源码分析之入门>,那么你可以直接阅读此篇文章 Vue3源码分析之入门 一.配置环境 1. 全局安装yarn Monorepo 管 ...

  3. cocos源码分析--SpriteBatchNode绘图原理(转--侵删)

    cocos源码分析–SpriteBatchNode绘图原理 https://www.cnblogs.com/xiaonanxia/p/9199737.html

  4. Dubbo 源码分析 - 自适应拓展原理

    1.原理 我在上一篇文章中分析了 Dubbo 的 SPI 机制,Dubbo SPI 是 Dubbo 框架的核心.Dubbo 中的很多拓展都是通过 SPI 机制进行加载的,比如 Protocol.Clu ...

  5. [Vue源码分析]自定义事件原理及事件总线的实现

    最近小组有个关于vue源码分析的分享会,提前准备一下- 前言: 我们都知道Vue中父组件可以通过 props 向下传数据给子组件:子组件可以通过向$emit触发一个事件,在父组件中执行回调函数,从而实 ...

  6. [Vue源码分析] v-model实现原理

    最近小组有个关于vue源码分析的分享会,提前准备一下- 前言: 我们都知道使用v-model可以实现数据的双向绑定,及实现数据的变化驱动dom的更新,dom的更新影响数据的变化.那么v-model是怎 ...

  7. Springmvc源码分析、底层原理

    1.Springmvc是如何找到Controller的? 首先在请求过来时,会先进入DispatcherServlet进行请求分发,执行DispatcherServlet类中的doDispatch() ...

  8. zlib源码分析—DEFLATE算法原理及实现

    从上一篇博客zlib源码分析-compress函数学习了compress函数的代码,这一篇我们来详细分析一下deflate算法的流程.先从compress代码中所体现出来的deflate函数的返回值和 ...

  9. JVM源码分析之javaagent原理完全解读

    转载地址:https://yq.aliyun.com/articles/2946?spm=5176.100239.yqblog1.45 摘要: 前言 本系列文章都是基于Hotspot/JDK源码,从源 ...

最新文章

  1. 猜数游戏的Java程序
  2. hdu2830 可交换行的最大子矩阵
  3. android vcard解析代码,Android使用vcard文件的方法简单实例
  4. python3解释器安装过程 2022
  5. 【图像配准】基于灰度的模板匹配算法(一):MAD、SAD、SSD、MSD、NCC、SSDA、SATD算法
  6. solr java score_java-Apache Solr:按位运算来过滤搜索结果
  7. @Scheduled(cron=) spring定时任务时间设置
  8. 无线通信技术-NB-IoT
  9. 一文搞懂Android抓包
  10. linux-ubuntu16.04下搭建java运行环境
  11. 最新《圣思园JavaSE实地培训系列教程》
  12. getSreenWH()
  13. mysql5.5免安装版教程_mysql 5.5.56免安装版配置方法
  14. Jersey搭建restFul形式接口
  15. 计算机段落格式解释,职称计算机考试Word教程:Word段落格式
  16. forEach和$.each()以及$().each()的用法
  17. Swift UIView代码控制隐藏与显示
  18. html 按钮吸底,CSS实现footer“吸底”效果
  19. 2020-10-16 js实现模拟双色球摇号
  20. 大数据时代统计学面临的机遇与挑战

热门文章

  1. 中科红旗开始新一轮招聘,服务器、桌面研发工程师、测试工程师
  2. Today今天便利店的梦想:准独角兽的雄心与挑战
  3. Android Studio做登录界面
  4. Python数据分析与机器学习45- 股票预测
  5. java线程堆栈nid.tid_java排查一个线上死循环cpu暴涨的过程分析
  6. 从程序员到项目经理(5):程序员加油站 -- 不是人人都懂的学习要点--------转自西西吹雪...
  7. ssl证书是什么?为什么需要部署ssl证书?
  8. ssl证书怎么购买?买多少钱的ssl证书合适?
  9. 跟着老猫来搞GO——工欲善其事必先利器
  10. 再探 set/map