YYImage探究

2,807 阅读8分钟

前言

YYImageYYKit 框架中一款强大的图片处理工具,作者 ibireme,其中包括了图片的编解码以及动图的播放等功能

而网络图片的加载是YYWebImage,其逻辑和 SDWebImage 类似,使用了 YYCache 编写缓存框架,YYImage 作为图片编解码工具编,使用方式和 SDWebImage 类似,由于依赖关系,使用 pod 导入 YYWebImage 会自动导入依赖库,即 YYCacheYYImage

YYImage简介

YYImage框架主要有下面几个文件组成:

YYImage: 继承自 UIImage,遵循 YYAnimatedImage 协议,作为基本图片加载的入口,并重写了UIImage的类方法imageNamed:,能加载png、jpg、gif等多种格式图片

YYFrameImage: 继承自 UIImage ,遵循 YYAnimatedImage 协议,主要负责帧动画图片的加载,而帧动画为一组图片(就想ppt一样,一旦快速翻起来就成了动画),而帧动画由图片帧和图片每帧时间段组成,最后根据这些参数形成动画

YYSpriteSheetImage: 继承自 UIImage ,遵循 YYAnimatedImage 协议,主要负责精灵动画的加载,而精灵动画与其他动画不一样的是,精灵动画由一张图片组成(即使gif解压出来也是一组图片),这一张图片包含了很多内容,例如:一个角色的多个动作,分别保存到图片的不同位置,每个位置的图片信息都会被保存到一个json文件中,其中包含了每一个图片的frame等信息,一般在游戏中使用此方案,避免了多张图片的管理 参考地址

YYAnimatedImageView: 继承自 UIImageView,是UIKit显示图片的接口,其主要负责动画图片播放等,配合UIImageYYImageYYFrameImageYYSpriteSheetImage使用,且由于内部动画是使用 CADisplayLink 编写,动画可以放runloopDefaultMode中执行,避免了滚动视图滑动时性能的消耗,默认为CommontMode,若想成功播放gif等动图,必须使用该类,而不是UIImageView

YYImageCoder: 包含了YYImageEncoderYYImageDecoderYYImageFrame等类,主要负责图片编码、解码,以及图片相关信息的保存

YYImage特性

支持以下类型动画图像的播放/编码/解码:WebP, APNG, GIF

支持以下类型静态图像的显示/编码/解码:WebP, PNG, GIF, JPEG, JP2, TIFF, BMP, ICO, ICNS

支持以下类型图片的渐进式/逐行扫描/隔行扫描解码:PNG, GIF, JPEG, BMP

支持多张图片构成的帧动画播放,支持单张图片的 sprite sheet 动画

高效的动态内存缓存管理,以保证高性能低内存的动画播放

完全兼容 UIImage 和 UIImageView,使用方便

保留可扩展的接口,以支持自定义动画

每个类和方法都有完善的文档注释

YYImage源码探究

YYImage主要由 YYImageYYFrameImageYYSpriteSheetImageYYAnimatedImageViewYYImageCoder文件组成,下面来循序渐进介绍

其中ImageIO 为 apple 处理图片信息的核心框架,在YYImageDecoder文件中会使用到

图片知识简介

研究源码之间先了解一下图片的一些信息

位图(dataBuffer): 就是一个像素数组,里面每一个像素代表着图片中的一个点,我们在应用中常见的png、jpeg就是位图

像素: 组成图片的基本元素,每一个像素里面只放置一个颜色块

图像缓冲区: 表示一种特定的缓冲区,缓冲区内的元素描述了图片中的每一个像素的颜色值(RGBA四个向量组成red、green、blue、alpha),因此图像缓冲区的大小和图片大小成正比

帧缓冲区: 负责保存APP实际渲染的内容,当视图发生改变时,UIKit会重新渲染内容保存到帧缓区中,帧缓冲区保存着每个像素的颜色信息,硬件会从帧缓冲区中读取信息并渲染内容

图片的渲染流程大致分为下面几步:

获取加载压缩图片data信息解压缩图片信息处理图片渲染内容

YYFrameImage、YYSpriteSheetImage

YYFrameImage用于加载帧动画的Image类,即一帧一帧的图片组合成的动画,使用方式如下所示:

// 文件: 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];

初始化方法默认先加载一张图片并解压,使用 YYAnimatedImageView 播放动画的时候回解压后续图片,后面介

- (instancetype)initWithImagePaths:(NSArray *)paths 
    frameDurations:(NSArray *)frameDurations 
    loopCount:(NSUInteger)loopCount {
    if (paths.count == 0) return nil;
    if (paths.count != frameDurations.count) return nil;
    
    NSString *firstPath = paths[0];
    NSData *firstData = [NSData dataWithContentsOfFile:firstPath];
    CGFloat scale = _NSStringPathScale(firstPath);
    //加载完图片后copy一份,并调用yy_imageByDecoded解压出来一份新图片
    //最终调用YYCGImageCreateDecodedCopy方法生成图片
    UIImage *firstCG = [[[UIImage alloc] initWithData:firstData] 
        yy_imageByDecoded];
    self = [self initWithCGImage:firstCG.CGImage scale:scale orientation:UIImageOrientationUp];
    if (!self) return nil;
    long frameByte = CGImageGetBytesPerRow(firstCG.CGImage) * CGImageGetHeight(firstCG.CGImage);
    _oneFrameBytes = (NSUInteger)frameByte;
    _imagePaths = paths.copy;
    _frameDurations = frameDurations.copy;
    _loopCount = loopCount;
    
    return self;
}

YYSpriteSheetImage 使用方式也和前面介绍的一样,需要拿到图片对应内容的frame相关信息,案例是使用的同等大小块,每次间隔一定块取出内容,只是一种方式

// 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];

YYSpriteSheetImage 初始化方式也类似,初始化第一张图片,并记录下相关信息

- (instancetype)initWithSpriteSheetImage:(UIImage *)image
                            contentRects:(NSArray *)contentRects
                          frameDurations:(NSArray *)frameDurations
                               loopCount:(NSUInteger)loopCount {
    if (!image.CGImage) return nil;
    if (contentRects.count < 1 || frameDurations.count < 1) return nil;
    if (contentRects.count != frameDurations.count) return nil;
    
    self = [super initWithCGImage:image.CGImage 
        scale:image.scale orientation:image.imageOrientation];
    if (!self) return nil;
    
    _contentRects = contentRects.copy;
    _frameDurations = frameDurations.copy;
    _loopCount = loopCount;
    return self;
}

YYImage、YYImageCode

上面两个对于图片的处理比较少,我们就从YYImage来入手,看看图片的解压缩图片代码做了什么

首先YYImage继承自UIImage,并重写了了其类方法 imageNamed:,开始准备图片信息的准备工作,即加载data数据

+ (YYImage *)imageNamed:(NSString *)name {
    if (name.length == 0) return nil;
    if ([name hasSuffix:@"/"]) return nil;
    
    NSString *res = name.stringByDeletingPathExtension;
    NSString *ext = name.pathExtension;
    NSString *path = nil;
    CGFloat scale = 1;
    
    //如果没有扩展名,则根据系统支持情况猜测,和UIImage一样
    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;
    }
    if (path.length == 0) return nil;
    
    NSData *data = [NSData dataWithContentsOfFile:path];
    if (data.length == 0) return nil;
    //调用 initWithData:scale方法开始准备解压图片相关操作
    return [[self alloc] initWithData:data scale:scale];
}

紧接着最终会走到 initWithData: scale:方法开始解压缩图片相关操作

- (instancetype)initWithData:(NSData *)data scale:(CGFloat)scale {
    if (data.length == 0) return nil;
    //获取屏幕的scale
    if (scale <= 0) scale = [UIScreen mainScreen].scale;
    _preloadedLock = dispatch_semaphore_create(1);
    //
    @autoreleasepool {
        //使用YYImageDecoder类开始解压图片相关信息
        //获取图片的一些信息、图片宽高、帧数、图片类型
        YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];
        //通过解压后的YYImageDecoder对象,获取某一帧的数据信息(每一帧信息中保存UIImage信息)
        //可用于第一帧或者静态效果的显示
        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 调用 decoderWithData:方法初始化后,最终会调用下面的方法

可以看到先调用YYImageDetectType检测图片格式,然后开始调用_updateSource更新数据源

- (BOOL)updateData:(NSData *)data final:(BOOL)final {
    BOOL result = NO;
    //互斥锁,避免多线程操作干扰
    pthread_mutex_lock(&_lock);
    result = [self _updateData:data final:final];
    pthread_mutex_unlock(&_lock);
    return result;
}
//内部实际的处理方法
- (BOOL)_updateData:(NSData *)data final:(BOOL)final {
    if (_finalized) return NO;
    if (data.length < _data.length) return NO;
    _finalized = final;
    _data = data;
    //检测图片的格式
    YYImageType type = YYImageDetectType((__bridge CFDataRef)data);
    if (_sourceTypeDetected) {
        if (_type != type) {
            return NO;
        } else {
            //解压缩更新数据
            [self _updateSource];
        }
    } else {
        if (_data.length > 16) {
            _type = type;
            _sourceTypeDetected = YES;
            //解压缩更新数据
            [self _updateSource];
        }
    }
    return YES;
}

YYImageDetectType检查图片格式, 图片的前8个字节中包含着图片的格式信息,下面为辨别图片格式的方法

//c1、c2、c3、c4是1字节大小的内容,里面保存这图片的类型信息
//此枚举通过 移位 + 按位与的方式,使用4字节(32位)大小保存了图片的类型信息,避免格式重复
#define YY_FOUR_CC(c1,c2,c3,c4) ((uint32_t)(((c4) << 24) | ((c3) << 16) | ((c2) << 8) | (c1)))
YYImageType YYImageDetectType(CFDataRef data) {
    if (!data) return YYImageTypeUnknown;
    //uint64_t = 8个字节,拿到data数据中前8个字节长度的数据
    uint64_t length = CFDataGetLength(data);
    if (length < 16) return YYImageTypeUnknown;
    
    const char *bytes = (char *)CFDataGetBytePtr(data);
    //前四个字节
    uint32_t magic4 = *((uint32_t *)bytes);
    switch (magic4) {
        case YY_FOUR_CC(0x4D, 0x4D, 0x00, 0x2A): { // big endian TIFF
            return YYImageTypeTIFF;
        } break;
            
        case YY_FOUR_CC(0x49, 0x49, 0x2A, 0x00): { // little endian TIFF
            return YYImageTypeTIFF;
        } break;
            
        case YY_FOUR_CC(0x00, 0x00, 0x01, 0x00): { // ICO
            return YYImageTypeICO;
        } break;
            
        case YY_FOUR_CC(0x00, 0x00, 0x02, 0x00): { // CUR
            return YYImageTypeICO;
        } break;
            
        case YY_FOUR_CC('i', 'c', 'n', 's'): { // ICNS
            return YYImageTypeICNS;
        } break;
            
        case YY_FOUR_CC('G', 'I', 'F', '8'): { // GIF
            return YYImageTypeGIF;
        } break;
        // 89 50 4E 47 (. P  N  G)
        case YY_FOUR_CC(0x89, 'P', 'N', 'G'): {  // PNG
            uint32_t tmp = *((uint32_t *)(bytes + 4));
            if (tmp == YY_FOUR_CC('\r', '\n', 0x1A, '\n')) {
                return YYImageTypePNG;
            }
        } break;
            
        case YY_FOUR_CC('R', 'I', 'F', 'F'): { // WebP
            uint32_t tmp = *((uint32_t *)(bytes + 8));
            if (tmp == YY_FOUR_CC('W', 'E', 'B', 'P')) {
                return YYImageTypeWebP;
            }
        } break;
        /*
        case YY_FOUR_CC('B', 'P', 'G', 0xFB): { // BPG
            return YYImageTypeBPG;
        } break;
        */
    }
    
    uint16_t magic2 = *((uint16_t *)bytes);
    switch (magic2) {
        case YY_TWO_CC('B', 'A'):
        case YY_TWO_CC('B', 'M'):
        case YY_TWO_CC('I', 'C'):
        case YY_TWO_CC('P', 'I'):
        case YY_TWO_CC('C', 'I'):
        case YY_TWO_CC('C', 'P'): { // BMP
            return YYImageTypeBMP;
        }
        case YY_TWO_CC(0xFF, 0x4F): { // JPEG2000
            return YYImageTypeJPEG2000;
        }
    }
    
    // JPG             FF D8 FF
    if (memcmp(bytes,"\377\330\377",3) == 0) return YYImageTypeJPEG;
    
    // JP2
    if (memcmp(bytes + 4, "\152\120\040\040\015", 5) == 0) return YYImageTypeJPEG2000;
    
    return YYImageTypeUnknown;
}

_updateSource为图片的解压缩方案处理,代码如下所示

平时加载的动图无非三种 WebPAPNGGIF
- (void)_updateSource {
    switch (_type) {
        case YYImageTypeWebP: {
            [self _updateSourceWebP];
        } break;
            
        case YYImageTypePNG: {
            [self _updateSourceAPNG];
        } break;
            
        default: {
            [self _updateSourceImageIO];
        } break;
    }
}

_updateSource处理图片时,我们就从最常用gif格式走的_updateSourceImageIO方法探究

- (void)_updateSourceImageIO {
    _width = 0;
    _height = 0;
    _orientation = UIImageOrientationUp; //默认图片方向设置向上
    _loopCount = 0; //GIF图片循环次数
    dispatch_semaphore_wait(_framesLock, DISPATCH_TIME_FOREVER);
    _frames = nil;
    dispatch_semaphore_signal(_framesLock);
    //ImageIO根据数据data获取ImageSource参数
    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;
    //通过source获取图片帧数
    _frameCount = CGImageSourceGetCount(_source);
    if (_frameCount == 0) return;
    
    if (!_finalized) { // ignore multi-frame before finalized
        _frameCount = 1;
    } else {
        if (_type == YYImageTypePNG) { // use custom apng decoder and ignore multi-frame
            _frameCount = 1;
        }
        图片类型为GIF,则通过 ImageIO 框架中的方法获取图片相关信息
        if (_type == YYImageTypeGIF) { // get gif loop count
            //获取图片的属性信息
            CFDictionaryRef properties = CGImageSourceCopyProperties(_source, NULL);
            if (properties) {
                CFDictionaryRef gif = CFDictionaryGetValue(properties,
                    kCGImagePropertyGIFDictionary);
                if (gif) {
                    //需要循环的次数
                    CFTypeRef loop = CFDictionaryGetValue(gif, kCGImagePropertyGIFLoopCount);
                    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++) {
        //获取动图每一帧的数据(普通的图片只有一帧,frameCount为1)
        //_YYImageDecoderFrame为YYImageFrame子类,保存每一帧图片的宽高等基本信息
        _YYImageDecoderFrame *frame = [_YYImageDecoderFrame new];
        frame.index = i;
        frame.blendFromIndex = i; //渲染的第几帧
        frame.hasAlpha = YES;
        frame.isFullSize = YES;
        [frames addObject:frame];
        
        //获取图片的信息,保存到_YYImageDecoderFrame对象 中去
        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);
            if (_type == YYImageTypeGIF) {
                CFDictionaryRef gif = CFDictionaryGetValue(properties, 
                    kCGImagePropertyGIFDictionary);
                if (gif) {
                    // Use the unclamped frame delay if it exists.
                    // 获取该帧图片的播放时间(key=kCGImagePropertyGIFUnclampedDelayTime);
                    value = CFDictionaryGetValue(gif, kCGImagePropertyGIFUnclampedDelayTime);
                    if (!value) {
                        //如果通过kCGImagePropertyGIFUnclampedDelayTime没有获取到播放时长
                        //就通过kCGImagePropertyGIFDelayTime来获取,两者的含义是相同的
                        value = CFDictionaryGetValue(gif, kCGImagePropertyGIFDelayTime);
                    }
                    if (value) CFNumberGetValue(value, kCFNumberDoubleType, &duration);
                }
            }
            
            frame.width = width;
            frame.height = height;
            frame.duration = duration;
            
            //第一帧的处理,通过第一帧更新本图片的_width和_height信息,并且获取图片的旋转信息
            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);
        }
    }
    dispatch_semaphore_wait(_framesLock, DISPATCH_TIME_FOREVER);
    _frames = frames;
    dispatch_semaphore_signal(_framesLock);
}

创建获取压缩图片基本信息的decoderWithData:scale方法介绍完毕之后,开始介绍frameAtIndex:decodeForDisplay方法来解压缩某一帧图片了,其调用后最终后走到私有方法_frameAtIndex:decodeForDisplay:中去解压出图片到_YYImageDecoderFrame类中去

- (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) {
        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 (![self _createBlendContextIfNeeded]) return nil;
    CGImageRef imageRef = NULL;
    
    //根据渲染帧和给定帧解压出图片信息
    if (_blendFrameIndex + 1 == frame.index) {
        imageRef = [self _newBlendedImageWithFrame:frame];
        _blendFrameIndex = index;
    } else { // should draw canvas from previous frame
        _blendFrameIndex = NSNotFound;
        CGContextClearRect(_blendCanvas, CGRectMake(0, 0, _width, _height));
        
        if (frame.blendFromIndex == frame.index) {
            CGImageRef unblendedImage = [self _newUnblendedImageAtIndex:index 
                extendToCanvas:NO decoded:NULL];
            if (unblendedImage) {
                CGContextDrawImage(_blendCanvas, CGRectMake(frame.offsetX, 
                    frame.offsetY, frame.width, frame.height), unblendedImage);
                CFRelease(unblendedImage);
            }
            imageRef = CGBitmapContextCreateImage(_blendCanvas);
            if (frame.dispose == YYImageDisposeBackground) {
                CGContextClearRect(_blendCanvas, CGRectMake(frame.offsetX, 
                    frame.offsetY, frame.width, frame.height));
            }
            _blendFrameIndex = index;
        } else { // canvas is not ready
            for (uint32_t i = (uint32_t)frame.blendFromIndex; i <= (uint32_t)frame.index; i++) {
                if (i == frame.index) {
                    if (!imageRef) imageRef = [self _newBlendedImageWithFrame:frame];
                } else {
                    [self _blendImageWithFrame:_frames[i]];
                }
            }
            _blendFrameIndex = index;
        }
    }
    
    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;
}

解压缩的_newUnblendedImageAtIndex:extendToCanvas:decoded方法实现如下所示

- (CGImageRef)_newUnblendedImageAtIndex:(NSUInteger)index
                         extendToCanvas:(BOOL)extendToCanvas
                                decoded:(BOOL *)decoded CF_RETURNS_RETAINED {
    
    if (!_finalized && index > 0) return NULL;
    if (_frames.count <= index) return NULL;
    _YYImageDecoderFrame *frame = _frames[index];
    
    if (_source) {
        //通过source和index获取CGImageRef图片信息
        CGImageRef imageRef = CGImageSourceCreateImageAtIndex(_source, index, 
            (CFDictionaryRef)@{(id)kCGImageSourceShouldCache:@(YES)});
        if (imageRef && extendToCanvas) {
            //获取当前索引图片宽高信息
            size_t width = CGImageGetWidth(imageRef);
            size_t height = CGImageGetHeight(imageRef);
             //图片大大小一致是直接copy图片保存,标记解压成功
            if (width == _width && height == _height) {
                CGImageRef imageRefExtended = YYCGImageCreateDecodedCopy(imageRef, YES);
                if (imageRefExtended) {
                    CFRelease(imageRef);
                    imageRef = imageRefExtended;
                    if (decoded) *decoded = YES;
                }
            } else {
            //图片大小与设定不一致开始重绘图片保持一致
                //RGBA ( 4 * 8位)
                //解压缩图片 w * h * s
                //data : 内存空间 (),NULL 系统自动分配
                //w h:像素的宽度和高度
                //bitmapINfo : 位图布局信息,一般给8
                //ARGB RGBA (指定向量顺序) kCGImageAlphaPremultipliedFirst
                //大小端模式:小端 低地址存放高位、高地址存放低位
                CGContextRef context = CGBitmapContextCreate(NULL, _width, _height, 8, 
                    0, YYCGColorSpaceGetDeviceRGB(), 
                    kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst);
                if (context) {
                    重绘图片,并标记解压成功
                    CGContextDrawImage(context, 
                        CGRectMake(0, _height - height, width, height), imageRef);
                    CGImageRef imageRefExtended = CGBitmapContextCreateImage(context);
                    CFRelease(context);
                    if (imageRefExtended) {
                        CFRelease(imageRef);
                        imageRef = imageRefExtended;
                        if (decoded) *decoded = YES;
                    }
                }
            }
        }
        return imageRef;
    }
    //其他类型图片处理,这里不介绍了
    ...
    
#endif
    
    return NULL;
}

YYAnimatedImageView

YYImage等加载的图片信息,最终需要递交给 YYAnimatedImageView 才能渲染出来,因此 YYAnimatedImageView处理着图片的渲染逻辑

其init方法都类似,下面介绍一个

默认使用的渲染runloopMode为 CommonModes

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

当使用set方法设置image才真正开始动起来,最终调用 setImage:withType方法

- (void)setImage:(UIImage *)image {
    if (self.image == image) return;
    [self setImage:image withType:YYAnimatedImageTypeImage];
}
- (void)setImage:(id)image withType:(YYAnimatedImageType)type {
    //停止动画
    [self stopAnimating];
    //重置动画操作
    //link为CADisplayLink,每一帧会回调的计时器,非常适合图片动画的处理
    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];
}

imageChanged 主要获取动图需要的信息,包括渲染区域等,并初始化计时器相关内容

- (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:)];
        }
    }
    //获取渲染的区域,如果不是精灵类型的则不需要
    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];
    }
    
    //如果帧数大于1,开始初始化动画参数
    if (newImageFrameCount > 1) {
        [self resetAnimated];
        _curAnimatedImage = newVisibleImage;
        _curFrame = newVisibleImage;
        _totalLoop = _curAnimatedImage.animatedImageLoopCount;
        _totalFrameCount = _curAnimatedImage.animatedImageFrameCount;
        [self calcMaxBufferCount];
    }
    //标记 RunLoop,定时器(CADisplayLink)
    [self setNeedsDisplay];
    [self didMoved];
}

resetAnimated初始化了动画执行必须的空间

- (void)resetAnimated {
    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:)];
        //加入到指定的runloopMode中去
        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(
        //子线程释放buffer信息,减少开销
         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;
    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;
}

当初始化完毕基本参数后,通过CADisplayLink开始我们的动画了, CADisplayLink的loop与屏幕刷新频率有关,一般 60次(帧)/s

//刷新频率有关
- (void)step:(CADisplayLink *)link {
    //拿到遵循动图协议的图片对象
    UIImage <YYAnimatedImage> *image = _curAnimatedImage;
    NSMutableDictionary *buffer = _buffer;
    UIImage *bufferedImage = nil;
    //下一帧的index,取模为了保证动画能够循环播放
    NSUInteger nextIndex = (_curIndex + 1) % _totalFrameCount;
    BOOL bufferIsFull = NO;
    
    if (!image) return;
    //如果已经播放完毕,结束动画
    if (_loopEnd) { // view will keep in last frame
        [self stopAnimating];
        return;
    }
    //YYImageFrame:image
    NSTimeInterval delay = 0;
    //正常获取到了图片
    if (!_bufferMiss) {
        //
        _time += link.duration;
        //获取下一帧的播放时长
        delay = [image animatedImageDurationAtIndex:_curIndex];
        如果播放时长不够,则继续播放,结束
        if (_time < delay) return; //如果小于,return
        _time -= delay; //如果播放时长足够了,准备切换下一帧播放,做好准备
        //下一轮的开始,在里面判断是否动画应该结束了
        if (nextIndex == 0) {
            _curLoop++;
            //一轮循环次数增加,如果已经达到播放次数,则结束动画
            if (_curLoop >= _totalLoop && _totalLoop != 0) {
                _loopEnd = YES;
                [self stopAnimating];
                [self.layer setNeedsDisplay]; //更新画面
                return; // stop at last frame
            }
        }
        //获取下一帧的时长,继续累计此帧的播放时间,重复操作
        delay = [image animatedImageDurationAtIndex:nextIndex];
        if (_time > delay) _time = delay; // do not jump over frame
    }
    LOCK(
        //从buffer缓存中获取下一帧图片信息
         bufferedImage = buffer[@(nextIndex)];
         if (bufferedImage) {
             //从缓存中找到了下一帧图片
             if ((int)_incrBufferCount < _totalFrameCount) {
                 [buffer removeObjectForKey:@(nextIndex)];
             }
             [self willChangeValueForKey:@"currentAnimatedImageIndex"];
             _curIndex = nextIndex;
             [self didChangeValueForKey:@"currentAnimatedImageIndex"];
             _curFrame = bufferedImage == (id)[NSNull null] ? nil : bufferedImage;
             //精灵动画需要的渲染区间
             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
    
    //图片拿到了更新layer
    if (!_bufferMiss) {
        [self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep
    }
    //缓存中没有相关图片,立即开启线程去获取图片信息,然后继续下一轮
    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];
    }
}

最后

YYImage就介绍到这里,如果想要了解更细节的内容,可以查看 源码, 如果还不知道怎么使用的,或者有使用问题,里面也有案例介绍

最后每次分享都是一份收货