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

参考一

YYImage: 功能强大的 iOS 图像框架。是YYKit组件之一.

GitHub地址:https://github.com/ibireme/YYImage
这个是GitHub上的说明,写的很详细,就等于再写一遍而已.

 

 

安装

CocoaPods

  1. 将 cocoapods 更新至最新版本.
  2. 在 Podfile 中添加 pod 'YYImage'。
  3. 执行 pod install 或 pod update。
  4. 导入 <YYImage/YYImage.h>。
  5. 注意:pod 配置并没有包含 WebP 组件, 如果你需要支持 WebP,可以在 Podfile 中添加 pod 'YYImage/WebP'。

特性

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

用法

显示动画类型的图片

// 文件: ani@3x.gif
UIImage *image = [YYImage imageNamed:@"ani.gif"];
UIImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image];
[self.view addSubview:imageView];

播放帧动画

// 文件: frame1.png, frame2.png, frame3.png
NSArray *paths = @[@"/ani/frame1.png", @"/ani/frame2.png", @"/ani/frame3.png"];
NSArray *times = @[@0.1, @0.2, @0.1];
UIImage *image = [YYFrameImage alloc] initWithImagePaths:paths frameDurations:times repeats:YES];
UIImageView *imageView = [YYAnimatedImageView alloc] initWithImage:image];
[self.view addSubview:imageView];

播放 sprite sheet 动画

// 8 * 12 sprites in a single sheet image
UIImage *spriteSheet = [UIImage imageNamed:@"sprite-sheet"];
NSMutableArray *contentRects = [NSMutableArray new];
NSMutableArray *durations = [NSMutableArray new];
for (int j = 0; j < 12; j++) {
   for (int i = 0; i < 8; i++) {
       CGRect rect;
       rect.size = CGSizeMake(img.size.width / 8, img.size.height / 12);
       rect.origin.x = img.size.width / 8 * i;
       rect.origin.y = img.size.height / 12 * j;
       [contentRects addObject:[NSValue valueWithCGRect:rect]];
       [durations addObject:@(1 / 60.0)];
   }
}
YYSpriteSheetImage *sprite;
sprite = [[YYSpriteSheetImage alloc] initWithSpriteSheetImage:img
                                                contentRects:contentRects
                                              frameDurations:durations
                                                   loopCount:0];
YYAnimatedImageView *imageView = [YYAnimatedImageView new];
imageView.size = CGSizeMake(img.size.width / 8, img.size.height / 12);
imageView.image = sprite;
[self.view addSubview:imageView];

动画播放控制

YYAnimatedImageView *imageView = ...;
// 暂停:
[imageView stopAnimating];
// 播放:
[imageView startAnimating];
// 设置播放进度:
imageView.currentAnimatedImageIndex = 12;
// 获取播放状态:
image.currentIsPlayingAnimation;
//上面两个属性都支持 KVO。

图片解码

// 解码单帧图片:
NSData *data = [NSData dataWithContentsOfFile:@"/tmp/image.webp"];
YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:2.0];
UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;
    
// 渐进式图片解码 (可用于图片下载显示):
NSMutableData *data = [NSMutableData new];
YYImageDecoder *decoder = [[YYImageDecoder alloc] initWithScale:2.0];
while(newDataArrived) {
   [data appendData:newData];
   [decoder updateData:data final:NO];
   if (decoder.frameCount > 0) {
       UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;
       // progressive display...
   }
}
[decoder updateData:data final:YES];
UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;

图片编码

// 编码静态图 (支持各种常见图片格式):
YYImageEncoder *jpegEncoder = [[YYImageEncoder alloc] initWithType:YYImageTypeJPEG];
jpegEncoder.quality = 0.9;
[jpegEncoder addImage:image duration:0];
NSData jpegData = [jpegEncoder encode];
 
// 编码动态图 (支持 GIF/APNG/WebP):
YYImageEncoder *webpEncoder = [[YYImageEncoder alloc] initWithType:YYImageTypeWebP];
webpEncoder.loopCount = 5;
[webpEncoder addImage:image0 duration:0.1];
[webpEncoder addImage:image1 duration:0.15];
[webpEncoder addImage:image2 duration:0.2];
NSData webpData = [webpEncoder encode];

图片类型探测

// 获取图片类型
YYImageType type = YYImageDetectType(data); 
if (type == YYImageTypePNG) ...

常见问题

Q: 为什么我不能显示 WebP 图片?
A: 确保 WebP.framework 已经被添加到你的工程内了。你可以调用 YYImageWebPAvailable() 来检查一下 WebP 组件是否被正确安装。

Q: 为什么我不能播放 APNG 动画?
A: 你应该禁用 Build Settings 中的 Compress PNG Files 和 Remove Text Metadata From PNG Files. 或者你也可以把 APNG 文件的扩展名改为apng.


作者:iOS_xuanhe
链接:https://www.jianshu.com/p/e4f41dce1960
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

参考二

前言:

要看懂YYImage框架,最好先了解热身部分(具体的自行百度),如果懒得看,直接跨过该部分,等到下面部分有疑问,再回过头看这部分的知识,也是可以。

热身部分

移动端图片格式调研

1、Image I/O

Image I/O 学习笔记
Image I/O官方文档
GIF图添加文字Demo

使用 CGBitmapContextCreate 函数创建一个位图上下文;
使用 CGContextDrawImage 函数将原始位图绘制到上下文中;
使用 CGBitmapContextCreateImage 函数创建一张新的解压缩后的位图。

2、 CGBitmapContextCreate 中的参数

谈谈 iOS 中图片的解压缩

  • data :如果不为 NULL ,那么它应该指向一块大小至少为 bytesPerRow * height 字节的内存;如果 为 NULL ,那么系统就会为我们自动分配和释放所需的内存,所以一般指定 NULL 即可;
  • width 和 height :位图的宽度和高度,分别赋值为图片的像素宽度和像素高度即可;
  • bitsPerComponent :像素的每个颜色分量使用的 bit 数,在 RGB 颜色空间下指定 8 即可;
  • bytesPerRow :位图的每一行使用的字节数,大小至少为 width * bytes per pixel 字节。有意思的是,当我们指定 0 时,系统不仅会为我们自动计算,而且还会进行 cache line alignment 的优化
  • space :颜色空间,一般使用 RGB 即可;
  • bitmapInfo :位图的布局信息。当图片不包含 alpha 的时候使用 kCGImageAlphaNoneSkipFirst ,否则使用 kCGImageAlphaPremultipliedFirst

3、信号量

信号量的讲解

/* 注意,正常的使用顺序是先降低然后再提高,这两个函数通常成对使用。 */
    dispatch_semaphore_wait(_framesLock, DISPATCH_TIME_FOREVER); //等待降低信号量
    // to do
    dispatch_semaphore_signal(_framesLock); //提高信号量

所用到的知识:
复合赋值运算符、Image I/O、CADisplayLink、willChangeValueForKey:、

一、YYImage总体介绍


1、YYImage 源码
2、YYImage 源码的文字解析版本

1、YYImage 功能

  • 显示动画类型的图片
  • 播放帧动画
  • 播放 sprite sheet 动画
  • 图片类型探测
  • 图片解码、编码(最核心功能)

2、YYImage 主要类介绍

YYImage 类

它是一个完全兼容的“UIImage”子类。它扩展了UIImage
支持动画WebP, APNG和GIF格式的图像数据解码。它还
支持NSCoding协议,以存档和反存档多帧图像数据。

a、animatedImageMemorySize

如果所有帧图像都被加载到内存中,那么总内存使用(以字节为单位)。
如果图像不是从多帧图像数据创建的,则该值为0。

b、preloadAllAnimatedImageFrames

将此属性设置为“YES”将阻塞要解码的调用线程
所有动画帧图像到内存,设置为“NO”将释放预装帧。
如果图像被许多图像视图(如emoticon)共享,则预加载所有视图
帧将降低CPU成本。

YYAnimatedImageView 类

用于显示动画图像的图像视图。
可以用来播放多帧动画以及普通动画,可以控制、暂停动画
当设备有足够的空闲内存时,这个视图及时请求帧数据。
这个视图可以在内部缓冲区中缓存一些或所有未来的帧,以降低CPU成本。

3、YYImage 的意义(图片解码的原因)

从磁盘中加载一张图片,并将它显示到屏幕上,这个过程其实经历很多,非常耗性能。随着显示的图片增加,性能下降尤其明显。不管是 JPEG 还是 PNG 等图片,都是一种编码后(压缩)的位图图形格式。我们先看下显示到屏幕这个过程的工作流:

1、我们使用+[UIImage imageWithContentsOfFile:]方法从磁盘中加载一张图片。此时,图片还没有被解码,仍旧是编码状态下。
2、返回的图片被分配给UIImageView
3、接着一个隐式的 CATransaction 捕获到了图层树的变化;
4、在主线程的下一个 run loop到来时,Core Animation 提交了这个隐式的事务,可能会涉及copy这些图片(已经成为图层树中的图层内容的图片)。这个 copy 操作可能会涉及以下部分或全部步骤:

a.分配缓冲区来管理文件IO和解压缩操作。
b.文件数据从磁盘读取到内存。
c.将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作;
d.最后 Core Animation 使用未压缩的位图数据渲染 UIImageView 的图层

图层树:(个人理解)洋葱看过去有很多层,这就是洋葱的图层,而屏幕上显示的文字、图片啊,都可以理解成为图层,很多图层就形成了一个结构,这个很多图层的结构就叫做图层树。

因此,在将磁盘中的图片渲染到屏幕之前,必须先要得到图片的原始像素数据,才能执行后续的绘制操作,这就是为什么需要对图片解码的原因。

二、YYImage主要类调用逻辑

A、渲染GIF/WebP/PNG(APNG)方法调用顺序

1、YYImage *image = [YYImage imageNamed:name]; //传入图片名创建YYImage对象

2、[[self alloc] initWithData:data scale:scale];//用重写的方法初始化图像数据

3、YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];//创建解码类 YYImageDecoder 对象,紧接着更新数据

4、result = [self _updateData:data final:final];//根据图像的data算出图片的type以及其他信息,再根据不同type 的图像去分别更新数据

5、[self _updateSourceImageIO];// 计算出PNG、GIF等图片信息(图片的每一帧的属性,包括宽、高、方向、动画重复次数(gif类型)、持续时间(gif类型))

6、 YYAnimatedImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image]; //把图片添加到 UIImageView 的子类,这个子类后面讲(第7点后都是它的核心),这里暂且当它为普通 ImageView 那样看。

7、[self setImage:image withType:YYAnimatedImageTypeImage];// 设置图片,类似Setter方法

8、[self imageChanged];//判断当前图片类型以及帧数,由CATransaction支持的显示事务去更新图层的 contentsRect,以及重置动画的参数,后面详解该方法。

9、[self resetAnimated];//重置动画多种参数;[self calcMaxBufferCount]; // 动态调整当前内存的缓冲区大小。

10、[self didMoved];// 窗口对象或者父视图对象改变,则开始控制动画的启动(停止),这是动画得以显示的关键

B、渲染帧动画方法调用顺序

1、UIImage *image = [[YYFrameImage alloc] initWithImagePaths:paths oneFrameDuration:0.1 loopCount:0]; //传入图片组的路径、每一个帧(每一个图片)的时间以及循环多少次,计算出总的durations
2、[self initWithImagePaths:paths frameDurations:durations loopCount:loopCount];// 把第一张图片解码后返回,并求出第一帧的大小,作为每一帧的大小
3、YYAnimatedImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image];
后面步骤跟 渲染GIF/WebP/PNG(APNG)方法调用顺序 第7点开始几乎一样

注意:由于代码过多,不可能面面俱到,所以下面只会摘取核心进行讲解。这样,读者看完此文以及看完我标注过的源码(),,去读源代码,也更容易理解。

三、核心代码

// 它接受一个原始的位图参数 imageRef ,最终返回一个新的解压缩后的位图 newImage
CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) {
    if (!imageRef) return NULL;
    size_t width = CGImageGetWidth(imageRef);
    size_t height = CGImageGetHeight(imageRef);
    if (width == 0 || height == 0) return NULL;
    
    // 重新绘制解码(可能会失去一些精度)
    if (decodeForDisplay) { //decode with redraw (may lose some precision)
        CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask;
        BOOL hasAlpha = NO;
        if (alphaInfo == kCGImageAlphaPremultipliedLast ||
            alphaInfo == kCGImageAlphaPremultipliedFirst ||
            alphaInfo == kCGImageAlphaLast ||
            alphaInfo == kCGImageAlphaFirst) {
            hasAlpha = YES;
        }
        // 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); // decode
        CGImageRef newImage = CGBitmapContextCreateImage(context);
        CFRelease(context);
        return newImage; // 返回一个新的解压缩后的位图 newImage
        
    } else {
    
    }
}

YYCGImageCreateDecodedCopy 是解压缩的核心,也就是渲染图片性能显著的原因。该方法首先求出图片的宽高,注意,这里的图片是指编码前的图片的每一帧图片。

- (void)imageChanged {
    YYAnimatedImageType newType = [self currentImageType];
    id newVisibleImage = [self imageForType:newType];
    NSUInteger newImageFrameCount = 0;
    BOOL hasContentsRect = NO;
    if ([newVisibleImage isKindOfClass:[UIImage class]] &&
        [newVisibleImage conformsToProtocol:@protocol(YYAnimatedImage)]) {
        // 求出有多少帧(如果是帧动画(由多张图组合的),相当于有多少张图)
        newImageFrameCount = ((UIImage<YYAnimatedImage> *) newVisibleImage).animatedImageFrameCount;
        if (newImageFrameCount > 1) { // 动态图
            hasContentsRect = [((UIImage<YYAnimatedImage> *) newVisibleImage) respondsToSelector:@selector(animatedImageContentsRectAtIndex:)];
        }
    }
    // 由CATransaction支持的显示事务去更新图层的 contentsRect, 但一般不用走这段代码。大都走的是 CATransaction 的隐式事务自己更新
    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;

    // YYSpriteSheetImage 类用到,先不理
    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]; // 标志需要重绘,会在下一个循环到来时刷新
    [self didMoved]; // 窗口对象或者父视图对象改变,则开始控制动画的启动(停止),这是动画得以显示的关键
}

图片改变的处理核心

主要做了以下几点:

  • 初始化动画参数  resetAniamted
  • 初始化或者重置后求出动画播放循环次数、当前帧、总帧数
  • 调用动态调整缓冲区方法 calcMaxBufferCount 、调用控制动画方法 didMoved
// init the animated params.
- (void)resetAnimated {
    if (!_link) {
        _lock = dispatch_semaphore_create(1);
        _buffer = [NSMutableDictionary new];
        
        // 添加到这种队列中的操作,就会自动放到子线程中执行。
        _requestQueue = [[NSOperationQueue alloc] init];
        /* maxConcurrentOperationCount 默认情况下为-1,表示不进行限制,可进行并发执行。
        为1时,队列为串行队列。只能串行执行。大于1时,队列为并发队列 */
        _requestQueue.maxConcurrentOperationCount = 1;
        /* 初始化一个新的 CADisplayLink 对象,在屏幕更新时调用。为了使显示循环与显示同步,应用程序使用addToRunLoop:forMode:方法将其添加到运行循环中
            一个计时器对象,允许应用程序将其绘图同步到显示的刷新率。
         */
        _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];
    
    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];  // 捕获字典到全局队列,在后台释放这些图像以避免阻塞UI线程。
                 
             });
         }
    );
    _link.paused = YES;
    _time = 0;
    if (_curIndex != 0) {
        [self willChangeValueForKey:@"currentAnimatedImageIndex"];
        _curIndex = 0; // 把索引值重置为0
        [self didChangeValueForKey:@"currentAnimatedImageIndex"];
    }
    _curAnimatedImage = nil; // 当前图像为空
    _curFrame = nil; // 当前帧
    _curLoop = 0; //当前循环次数
    _totalLoop = 0; // 总循环次数
    _totalFrameCount = 1; // 总帧数
    _loopEnd = NO; // 是否循环结尾
    _bufferMiss = NO; // 是否丢帧
    _incrBufferCount = 0; // 当前允许的缓存
}

重置图片的参数;
内存警告时释放内存;
初始化一个新的 CADisplayLink 对象,在屏幕更新时调用。

// 只有屏幕刷新累加时间不小于当前帧的动画播放时间才显示图片,播放下一帧。
// 播放 GIF 的关键
- (void)step:(CADisplayLink *)link {
    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; // link.duration 屏幕刷新的时间,默认1/60 s
        delay = [image animatedImageDurationAtIndex:_curIndex]; // 返回当前帧的持续时间
        if (_time < delay) 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 sleep
                return; // stop at last frame
            }
        }
        delay = [image animatedImageDurationAtIndex:nextIndex]; // 返回下一帧的的持续时间
        
        /**  */
        if (_time > delay) _time = delay; // do not jump over frame
    }
    LOCK(
         bufferedImage = buffer[@(nextIndex)];
         if (bufferedImage) {
             if ((int)_incrBufferCount < _totalFrameCount) {
                 [buffer removeObjectForKey:@(nextIndex)];
             }
             [self willChangeValueForKey:@"currentAnimatedImageIndex"];
             _curIndex = nextIndex; // 用KVO改变 当前索引值
             [self didChangeValueForKey:@"currentAnimatedImageIndex"];
             _curFrame = bufferedImage == (id)[NSNull null] ? nil : bufferedImage;
             
             // 实现YYSpriteSheetImage 的协议方法,才会进入该 if 语句
             if (_curImageHasContentsRect) {
                 _curContentsRect = [image animatedImageContentsRectAtIndex:_curIndex];
                 [self setContentsRect:_curContentsRect forImage:_curFrame];
             }
             nextIndex = (_curIndex + 1) % _totalFrameCount;
             _bufferMiss = NO;
             if (buffer.count == _totalFrameCount) {
                 bufferIsFull = YES; // 缓冲区已经满
             }
         } else {
             // 丢帧,某一帧没有办法找到显示
             _bufferMiss = YES;
         }
    )//LOCK
    
    if (!_bufferMiss) {
        // 刷新显示图像
        [self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep
    }
    
    /* _YYAnimatedImageViewFetchOperation 为 NSOperation 的子类
        还未获取完所有图像,交给它获取下一张图像 */
    if (!bufferIsFull && _requestQueue.operationCount == 0) { // if some work not finished, wait for next opportunity
        _YYAnimatedImageViewFetchOperation *operation = [_YYAnimatedImageViewFetchOperation new];
        operation.view = self;
        operation.nextIndex = nextIndex;
        operation.curImage = image;
        [_requestQueue addOperation:operation]; //
    }
}

这是动画播放的关键,是 CADisplayLink对象 的方法,每 1/60s 也就是屏幕刷新一次就调用一次

- (void)calcMaxBufferCount {
    int64_t bytes = (int64_t)_curAnimatedImage.animatedImageBytesPerFrame; // 求出每一帧的字节数
    if (bytes == 0) bytes = 1024; // 如果为0,则给定1024
    
    int64_t total = _YYDeviceMemoryTotal(); // 获取设备的CPU物理内存
    int64_t free = _YYDeviceMemoryFree(); // 获取设备的容量
    int64_t max = MIN(total * 0.2, free * 0.6); // 比较内存的0.2倍以及容量的0.6倍最小值
    max = MAX(max, BUFFER_SIZE); // 如果不够 10 M,则以 10 M 作为最大缓冲区大小
    
    /** _maxBufferSize 内部帧缓冲区大小
     * 当设备有足够的空闲内存时,这个视图将请求并解码一些或所有未来的帧图像进入一个内部缓冲区。
     * 默认值为0 如果这个属性的值是0,那么最大缓冲区大小将根据当前的状态进行动态调整设备释放内存。否则,缓冲区大小将受到此值的限制。
     * 当收到内存警告或应用程序进入后台时,缓冲区将被立即释放
     */
    if (_maxBufferSize) max = max > _maxBufferSize ? _maxBufferSize : max; //得出缓冲区的最大值
    
    double maxBufferCount = (double)max / (double)bytes;
    if (maxBufferCount < 1) maxBufferCount = 1;
    else if (maxBufferCount > 512) maxBufferCount = 512;
    _maxBufferCount = maxBufferCount; // 最大缓冲数
}

动态求出最大缓冲数—>参考

/* 从自定义的 start 方法中调用 main 方法
 调用[self didMoved]; 从而调用此方法
*/
- (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; // 获取 Operation 中传过来的 下一个索引值
    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)); //  拿索引值去当前缓冲区取图片
            
            // 如果没有取到图片,则在子线程重新解码,得到解码后的图片
            if (miss) {
                // 等到当前还未解码的图片
                UIImage *img = [_curImage animatedImageFrameAtIndex:idx];
                NSLog(@"当前线程---%@", [NSThread currentThread]); // 打印当前线程,每次打印都是 name = (null),说明在异步线程
                // 在异步线程再次调用解码图片,如果无法解码或已经解码就返回self
                img = img.yy_imageByDecoded;
                if ([self isCancelled]) break;
                LOCK_VIEW(view->_buffer[@(idx)] = img ? img : [NSNull null]); // 每次添加一张图片到 _buffer 数组
                view = nil;
            }
        }
    }
}

该方法负责把图片存入缓冲区中。(过程:取未解码图片–>解码存入缓冲区)

在此,对YYImage框架完毕了,希望大家都能从大神源码学到知识。




其他额外收获:

1、是否模拟器

- (BOOL)isSimulator {
    size_t size;
    sysctlbyname("hw.machine", NULL, &size, NULL, 0);
    char *machine = malloc(size);
    sysctlbyname("hw.machine", machine, &size, NULL, 0);
    NSString *model = [NSString stringWithUTF8String:machine];
    free(machine);
    return [model isEqualToString:@"x86_64"] || [model isEqualToString:@"i386"];
}

2、根据不同的系统 scale 选择图片

/** 一个NSNumber对象数组,根据不同的系统scale返回数组内部不同顺序的数字
e.g. iPhone3GS:@[@1,@2,@3] iPhone5:@[@2,@3,@1]  iPhone6 Plus:@[@3,@2,@1]
*/
static NSArray *_NSBundlePreferredScales() {
    static NSArray *scales;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        CGFloat screenScale = [UIScreen mainScreen].scale;
        if (screenScale <= 1) {
            scales = @[@1,@2,@3];
        } else if (screenScale <= 2) {
            scales = @[@2,@3,@1];
        } else {
            scales = @[@3,@2,@1];
        }
    });
    return scales;
}

咋一看,这不是单例吗?保证初始化代码只执行一次,可移步单例相关文章

3、判断图片后缀

    NSArray *exts = ext.length > 0 ? @[ext] : @[@"", @"png", @"jpeg", @"jpg", @"gif", @"webp", @"apng"];
    NSArray *scales = _NSBundlePreferredScales();
    for (int s = 0; s < scales.count; s++) {
        scale = ((NSNumber *)scales[s]).floatValue;
        NSString *scaledName = _NSStringByAppendingNameScale(res, scale);
        for (NSString *e in exts) {
            path = [[NSBundle mainBundle] pathForResource:scaledName ofType:e];
            if (path) break;
        }
        if (path) break;
    }

如果图片没标明后缀,则遍历后缀数组,并添加后缀到传进来的图片名,最后到 mainBundle 里面取图片路径,取到地址则停止

CF_RETURNS_RETAINED 标记返回CF类型的函数,该类型需要调用方释放
NSDefaultRunLoopMode 保持gif 图在scrollView 拉动时不停止
|= 为按位或运算符 eg: a|=b; 相当于 a=a|b;

 


作者:Dwyane_Coding
链接:https://www.imooc.com/article/91430
来源:慕课网
本文原创发布于慕课网 ,转载请注明出处,谢谢合作

参考三

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 each
   pixel is specified by `space', which may also specify a destination color
   profile. 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 bitmap
   consists of `bytesPerRow' bytes, which must be at least `width * bytes
   per pixel' bytes; in addition, `bytesPerRow' must be an integer multiple
   of the number of bytes per pixel. `data', if non-NULL, points to a block
   of memory at least `bytesPerRow * height' bytes. If `data' is NULL, the
   data for context is allocated automatically and freed when the context is
   deallocated. `bitmapInfo' specifies whether the bitmap should contain an
   alpha channel and how it's to be generated, along with whether the
   components 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); // decode
        CGImageRef newImage = CGBitmapContextCreateImage(context);
        CFRelease(context);
        return newImage;
通过CGBitmapContextCreate创建位图上下文
通过CGContextDrawImage把原始位图绘制到上下文
CFDataRef rawData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));该方法可以获取到原始位图信息
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_ENABLED
    WebPDemuxer *_webpSource; // 如果判定图像为 YYImageTypeWebP 则会议 WebP 更新图像源
#endif
    
    UIImageOrientation _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  kCGImagePropertyGIFLoopCount
                    CFTypeRef 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);
            
            // 如果是GIF
            if (_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 {
    //YYAnimatedImageTypeImage
    YYAnimatedImageType 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 sleep
                return; // 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 opportunity
        NSLog(@"添加任务");
        //还未获取所有图像,交给_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];
                // 还是在异步线程再次调用解码  如果没有,就解码,有的话就返回self
                img = img.yy_imageByDecoded;
                if ([self isCancelled]) break;
                // 将解码的图片存储到buffer
                LOCK_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的图片解码以及如何显示的基本逻辑分析,应该很清晰的,核心代码也贴出来了,知识点学习下还是很不错的

 

美团大神

参考二号
--------------------- 
作者:Deft_MKJing宓珂璟 
来源:CSDN 
原文:https://blog.csdn.net/deft_mkjing/article/details/79866973 
版权声明:本文为博主原创文章,转载请附上博文链接!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值