IOS框架:YYImage源码解析

原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 一、YYImage的简介
    • 1、主要功能
    • 2、架构分析
  • 二、YYImage使用示例
    • 1、YYImage的使用
    • 2、YYFrameImage的使用
    • 3、YYAnimatedImageView的使用
    • 4、YYImageEncoder的使用
    • 5、YYImageDecoderd的使用
  • 三、YYAnimatedImage:动画图片协议
  • 四、YYImage:获取图片相关信息
    • 1、头文件的函数以及属性
    • 2、初始化函数
    • 3、读取图片函数
  • 五、YYFrameImage:播放帧动画
    • 1、初始化方法
    • 2、获取index下图片的方法
  • 六、YYAnimatedImageView:动画播放控制
    • 1、初始化函数
    • 2、Image的Setter方法
    • 3、重置动画
    • 4、根据当前帧索引推出下一帧索引
  • 七、YYAnimatedImageViewFetchOperation:获取图片的队列
  • 八、YYImageCoder:编/解码类
    • 1、声明的枚举和类
    • 2、YYImageDecoder
    • 3、解析支持动画的APNG图片
    • 4、使用ImageIO解码
    • 5、获取帧图片
    • 6、创建混合图片
  • Demo
  • 参考文献

一、YYImage的简介

1、主要功能

YYImage是由ibireme 开发的一款功能强大的 iOS 图像框架(该项目是 YYKit 组件之一),它支持当前市场主流的静/动态图像编/解码与动态图像的动画播放显示。

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

2、架构分析

  • 图像层:把不同类型的图像信息封装成类并提供初始化和其他便捷接口
  • 视图层:负责图像层内容的显示(包含动态图像的动画播放)工作。其中YYAnimatedImageView类中定义的YYAnimatedImage协议连接了图像层和视图层
  • 编/解码层:提供对图像数据的编解码支持

二、YYImage使用示例

1、YYImage的使用

使用YYImageYYImageAnimatedImageView代替UIImageUIImageView,与我们平时的用法几乎一致,符合我们平时的编码习惯。

// 使用YYImage去创建image对象
UIImage *image = [YYImage imageNamed:@"ani.gif"];

// 外界无需关心图片类型,当做正常的ImageView来使用即可
UIImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image];
[self.view addSubview:imageView];   

2、YYFrameImage的使用

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

3、YYAnimatedImageView的使用

YYImage *image = [YYImage imageNamed:@"ani"];
YYAnimatedImageView *imageView = [YYAnimatedImageView alloc] initWithImage:image];
[view addSubView:imageView];

4、YYImageEncoder的使用

// Encode still image:
YYImageEncoder *jpegEncoder = [[YYImageEncoder alloc]   initWithType:YYImageTypeJPEG];
jpegEncoder.quality = 0.9;
[jpegEncoder addImage:image duration:0];
NSData jpegData = [jpegEncoder encode];
 
// Encode animated image:
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];

5、YYImageDecoderd的使用

// Decode single frame:
NSData *data = [NSData dataWithContentsOfFile:@"/tmp/image.webp"];
YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:2.0];
UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;
    
// Progressive:
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;

三、YYAnimatedImage:动画图片协议

遵循这个协议的类,都可以使用YYAnimateImageView去进行动画播放。在YYImage中,所有的图像层类都遵循于这个协议。

@protocol YYAnimatedImage 
@required

// 动画帧总数
- (NSUInteger)animatedImageFrameCount;

// 动画循环次数,0 表示无限循环
- (NSUInteger)animatedImageLoopCount;

// 每帧字节数(在内存中),可能用于优化内存缓冲区大小
- (NSUInteger)animatedImageBytesPerFrame;

// 返回给定特殊索引对应的帧图像,这个方法可能在异步线程中调用
- (nullable UIImage *)animatedImageFrameAtIndex:(NSUInteger)index;

// 返回给定特殊索引对应的帧图像对应的显示持续时长
- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index;

@optional

// 针对 Spritesheet 动画的方法,用于显示某一帧图像在 Spritesheet 画布中的位置
- (CGRect)animatedImageContentsRectAtIndex:(NSUInteger)index;

@end

四、YYImage:获取图片相关信息

它继承自 UIImage 并对 UIImage 做了扩展以支持 WebPAPNGGIF 格式的图片解码。它还支持 NSCoding 协议可以对多帧图像数据进行 archiveunarchive 操作,即支持缓存image对象到缓存,以便下次使用。由下面头文件内容可以看出,YYImage 提供了类似 UIImage 的初始化方法,符合我们一贯的使用思维这一点比较好。另外值得一提的是 YYImageimageNamed: 初始化方法并不支持缓存,而是通过参数所提供的图片名称/路径/数据直接查找到对应数据解码获取对应图片。

1、头文件的函数以及属性

成员变量
@implementation YYImage
{
    YYImageDecoder *_decoder;// YYImage自定义的解码器
    NSArray *_preloadedFrames;// 预先加载的UIImage实例数组
    dispatch_semaphore_t _preloadedLock;// 锁,YYImage所有对于Frame的访问都是在加锁的情况下进行的
    NSUInteger _bytesPerFrame;// 每一帧图片的资源消耗大小
}
初始化方法列表
// 使用图片名称加载图片,区别于系统的imageNamed方法,此方法不会缓存图片
+ (nullable YYImage *)imageNamed:(NSString *)name;
// 使用图片路径加载图片
+ (nullable YYImage *)imageWithContentsOfFile:(NSString *)path;
// 使用NSData加载图片
+ (nullable YYImage *)imageWithData:(NSData *)data;
// 使用NSData和图片缩放比例加载图片
+ (nullable YYImage *)imageWithData:(NSData *)data scale:(CGFloat)scale;
头文件属性
// 图像类型:调用上述类方法的时候即会解析header得到,即第一次decoder实例创建时
@property (nonatomic, readonly) YYImageType animatedImageType;

// 动态图像的data
@property (nullable, nonatomic, readonly) NSData *animatedImageData;

// 图片的内存消耗:计算公式_bytesPerFrame(每一帧图片大小) * decoder.frameCount(总共多少帧图片)
// 多帧图片数据所占用的内存,如果不是由多帧图片数据创建则返回0
@property (nonatomic, readonly) NSUInteger animatedImageMemorySize;

// 是否要预先加载图片所有的帧。这个属性适合在需要用空间换时间的场景下使用
// 1. TextView这种图文混排的时候使用
// 2. QQ表情之类的初始化,需要提前加载
@property (nonatomic) BOOL preloadAllAnimatedImageFrames;

2、初始化函数

所有YYImage的初始化都会进入该方法。

- (instancetype)initWithData:(NSData *)data scale:(CGFloat)scale
{
    ...
}
a、准备工作
❶ 数据合理性检查
if (data.length == 0) return nil;
if (scale <= 0) scale = [UIScreen mainScreen].scale;
❷ 加锁
_preloadedLock = dispatch_semaphore_create(1);
❸ 释放大量临时对象
@autoreleasepool
{
    ...
}

b、autoreleasepool内部分
❶ 创建图片解码器:使用YYImage自己的解码去进行图片解析

这里并没有将data转换成image,只是解析处理图片的基本信息(图片类型,循环次数,图片宽高等)。

YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];
❷ 解码出图片数据:取出动画类型图片的第一帧图片或者普通图片
YYImageFrame *frame = [decoder frameAtIndex:0 decodeForDisplay:YES];
❸ 赋值给UIImage的image属性并实例化自身
UIImage *image = frame.image;
self = [self initWithCGImage:image.CGImage scale:decoder.scale orientation:image.imageOrientation];
❹ 存储图片类型信息
_animatedImageType = decoder.type;
❺ 当为动画类型图片存在有多帧时
if (decoder.frameCount > 1)
{
    // 当有多帧时复用解码器
    _decoder = decoder;
    // 每帧大小
    _bytesPerFrame = CGImageGetBytesPerRow(image.CGImage) * CGImageGetHeight(image.CGImage);
    // 计算内存消耗:多帧图片的总大小
    _animatedImageMemorySize = _bytesPerFrame * decoder.frameCount;
}
❻ 标记解码成功
self.yy_isDecodedForDisplay = YES;

3、读取图片函数

- (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index
{
    ...
}
❶ 加载preload的图片
dispatch_semaphore_wait(_preloadedLock, DISPATCH_TIME_FOREVER);
UIImage *image = _preloadedFrames[index];
dispatch_semaphore_signal(_preloadedLock);
if (image) return image == (id)[NSNull null] ? nil : image;
❷ 没有preload,通过decoder的frames去查询加载
return [_decoder frameAtIndex:index decodeForDisplay:YES].image;

五、YYFrameImage:播放帧动画

该类可以把静态图片类型如 pngjpeg 格式的静态图像用帧切换的方式以动态图片的形式显示。

// 相对于YYImage来说,帧动画图片只是继承UIImage并且遵循动画图片的协议
@interface YYFrameImage : UIImage 

1、YYFrameImage的初始化方法

- (instancetype)initWithImagePaths:(NSArray *)paths frameDurations:(NSArray *)frameDurations loopCount:(NSUInteger)loopCount
{
    ...
}
❶ 根据入参取到首张图片
NSString *firstPath = paths[0];
NSData *firstData = [NSData dataWithContentsOfFile:firstPath];
❷ imageByDecoded会将原图片处理成新图片,但是去除了Alpha通道
UIImage *firstCG = [[[UIImage alloc] initWithData:firstData] yy_imageByDecoded];
CGFloat scale = _NSStringPathScale(firstPath);
self = [self initWithCGImage:firstCG.CGImage scale:scale orientation:UIImageOrientationUp];
❸ 用首图初始化以下属性
_oneFrameBytes = (NSUInteger)frameByte;
_imagePaths = paths.copy;
_frameDurations = frameDurations.copy;
_loopCount = loopCount;

2、获取index下图片的方法

- (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index
{
    if (_imagePaths)
    {
        if (index >= _imagePaths.count) return nil;
        NSString *path = _imagePaths[index];
        CGFloat scale = _NSStringPathScale(path);
        NSData *data = [NSData dataWithContentsOfFile:path];
        return [[UIImage imageWithData:data scale:scale] yy_imageByDecoded];
    }
    else if (_imageDatas)
    {
        if (index >= _imageDatas.count) return nil;
        NSData *data = _imageDatas[index];
        return [[UIImage imageWithData:data scale:[UIScreen mainScreen].scale] yy_imageByDecoded];
    }
    else
    {
        return index == 0 ? self : nil;
    }
}

六、YYAnimatedImageView:动画播放控制

其类本身是对于UIImageView的继承,重写了一系列重要的函数(startAnimating/stopAnimating)来控制播放。

1、初始化函数

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

2、Image的Setter方法

- (void)setImage:(id)image withType:(YYAnimatedImageType)type
{
    ...
}
❶ 停止之前的动画
[self stopAnimating];
❷ 重置动画
if (_link) [self resetAnimated];
❸ 这样可以保证YYAnimatedImageView的图片更改时都会为新的图片初始化配套的动画参数并且重绘
[self imageChanged];

3、重置动画

- (void)resetAnimated
{
    ...
}
❶ YYAnimatedImageView 使图片动起来是依靠CADisplayLink *_link变量切换帧图像。为了避免使用 CADisplayLink 可能造成的循环引用设计了 _YYImageWeakProxy。
_link = [CADisplayLink displayLinkWithTarget:[_YYImageWeakProxy proxyWithTarget:self] selector:@selector(step:)];

if (_runloopMode)
{
    [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:_runloopMode];
}
_link.paused = YES;
❷ App内存警告和进入后台的通知观察代码
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
❸ 通过将图片请求队列 _requestQueue 的 maxConcurrentOperationCount 设置为 1,使图片请求队列成为串行队列(最大并发数为 1)
_requestQueue.maxConcurrentOperationCount = 1;
[_requestQueue cancelAllOperations];
❹ 异步释放

[holder class]的具体作用是什么?这边怎么实现了异步释放。对象的销毁虽然消耗资源不多,但累积起来也是不容忽视的。通常当容器类持有大量对象时,其销毁时的资源消耗就非常明显。同样的,如果对象可以放到后台线程去释放,那就挪到后台线程去。这里有个小 Tip:把对象捕获到 block 中,然后扔到后台队列去随便发送个消息以避免编译器警告,就可以让对象在后台线程销毁了。

LOCK(
     if (_buffer.count) {
         NSMutableDictionary *holder = _buffer;
         _buffer = [NSMutableDictionary new];
         dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
             [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;

4、根据当前帧索引推出下一帧索引

- (void)step:(CADisplayLink *)link
{
    ...
}
❶ 图片帧存储字典
NSMutableDictionary *buffer = _buffer;
❷ 根据当前帧索引推出下一帧索引
NSUInteger nextIndex = (_curIndex + 1) % _totalFrameCount;
❸ 用信号量对于帧缓冲区buffer的操作进行加锁
LOCK(...)

#define LOCK(...) dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER); \__VA_ARGS__; \
dispatch_semaphore_signal(self->_lock);
❹ 使用下一帧索引去帧缓冲区尝试获取对应帧图像
bufferedImage = buffer[@(nextIndex)];
❺ 当_curIndex即当前帧索引修改时手动调用KVO进行观察
[self willChangeValueForKey:@"currentAnimatedImageIndex"];
_curIndex = nextIndex;
[self didChangeValueForKey:@"currentAnimatedImageIndex"];
❻ 设置当前帧的图片
_curFrame = bufferedImage == (id)[NSNull null] ? nil : bufferedImage;
❼ 找到了对应帧图像就使用其重绘
if (!_bufferMiss)
{
    [self.layer setNeedsDisplay]; 
}
❽ 未找到对应帧图像,则根据条件向图片请求队列加入请求操作
if (!bufferIsFull && _requestQueue.operationCount == 0)
{
    // 图片请求队列中加入的操作均为 _YYAnimatedImageViewFetchOperation 类型
    _YYAnimatedImageViewFetchOperation *operation = [_YYAnimatedImageViewFetchOperation new];
    operation.view = self;
    operation.nextIndex = nextIndex;
    operation.curImage = image;
    [_requestQueue addOperation:operation];
}

七、YYAnimatedImageViewFetchOperation:获取图片的队列

继承自 NSOperation 类,是自定义操作类,作者将其操作内容实现写在了 main 中。

- (void)main
{
    ...
}

使用 main 方法非常简单,开发者不需要管理一些状态属性(例如 isExecutingisFinished),当 main 方法返回的时候,这个 operation 就结束了。

❶ 由于操作是放入图片请求队列中进行的,所以内部有对isCancelled做判断。如果操作已经被取消(发生在更改图片、停止动画、手动更改当前帧、收到内存警告或 App 进入后台等)则需要及时跳出
if ([self isCancelled]) return;
❷ 判断缓冲区的大小,如有必要则重新赋值缓冲区的大小
if (view->_incrBufferCount > (NSInteger)view->_maxBufferCount)
{
    view->_incrBufferCount = view->_maxBufferCount;
}
❸ 扫描下一帧以及当前缓冲范围内之后的帧图片
NSUInteger idx = _nextIndex;
NSUInteger max = view->_incrBufferCount < 1 ? 1 : view->_incrBufferCount;
❹ 是否存在丢失的帧图像
// 对于 view 缓冲区的操作也都上了锁
LOCK_VIEW(BOOL miss = (view->_buffer[@(idx)] == nil));
❺ 存在丢失的帧图像就尝试重新获取帧图像并加入到帧缓冲
if (miss)
{
    UIImage *img = [_curImage animatedImageFrameAtIndex:idx];
    img = img.yy_imageByDecoded;// 触发解码操作
    if ([self isCancelled]) break;
    LOCK_VIEW(view->_buffer[@(idx)] = img ? img : [NSNull null]);
    view = nil;
}

八、YYImageCoder:编/解码类

1、声明的枚举和类

YYImageType:所有支持的图片格式
typedef NS_ENUM(NSUInteger, YYImageType) {
    YYImageTypeUnknown = 0, ///< unknown
    YYImageTypeJPEG,        ///< jpeg, jpg
    YYImageTypeJPEG2000,    ///< jp2
    YYImageTypeTIFF,        ///< tiff, tif
    YYImageTypeBMP,         ///< bmp
    YYImageTypeICO,         ///< ico
    YYImageTypeICNS,        ///< icns
    YYImageTypeGIF,         ///< gif
    YYImageTypePNG,         ///< png
    YYImageTypeWebP,        ///< webp
    YYImageTypeOther,       ///< other image format
};
YYImageDisposeMethod:指定在画布上渲染下一个帧之前如何处理当前帧所使用的区域方法
typedef NS_ENUM(NSUInteger, YYImageDisposeMethod) {
    YYImageDisposeNone = 0,
    YYImageDisposeBackground,
    YYImageDisposePrevious,
}
YYImageBlendOperation:指定当前帧的透明像素如何与前一个画布的透明像素混合
typedef NS_ENUM(NSUInteger, YYImageBlendOperation) {
    YYImageBlendNone = 0,
    YYImageBlendOver,
}
类声明
// 一帧图像数据
@interface YYImageFrame : NSObject 

// 图像编码器
@interface YYImageEncoder : NSObject

// 图像解码器
@interface YYImageDecoder : NSObject

2、YYImageDecoder

YYImageDecoder是线程安全的图片解码器,支持的格式有PNGJPGBMPTIFFICNS等,可以用来解码完整的图片数据,也可以用来增量地解码数据。

a、接口属性和方法
提供的属性
@property (nullable, nonatomic, readonly) NSData *data;// 图片二进制数据
@property (nonatomic, readonly) YYImageType type;// 图片类型:png、jpeg、webP、gif...
@property (nonatomic, readonly) CGFloat scale;// 图片缩放比率
@property (nonatomic, readonly) NSUInteger frameCount;// 图片帧数
@property (nonatomic, readonly) NSUInteger loopCount;// 图片循环数,0是无限循环
@property (nonatomic, readonly) NSUInteger width;// 图片宽度
@property (nonatomic, readonly) NSUInteger height;// 图片高度
@property (nonatomic, readonly, getter=isFinalized) BOOL finalized;// 是否解码完成
提供的方法
// 使用图片缩放比率初始化
- (instancetype)initWithScale:(CGFloat)scale NS_DESIGNATED_INITIALIZER;

// 使用累计数据更新图片
- (BOOL)updateData:(nullable NSData *)data final:(BOOL)final;

// 使用图片数据和缩放比率进行初始化
+ (nullable instancetype)decoderWithData:(NSData *)data scale:(CGFloat)scale;

// 解码一帧图片。decodeForDisplay为YES时会解码为bitmap以供显示
- (nullable YYImageFrame *)frameAtIndex:(NSUInteger)index decodeForDisplay:(BOOL)decodeForDisplay;

// 获取某一帧的持续时长
- (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index;

// 获取某一帧的属性
- (nullable NSDictionary *)framePropertiesAtIndex:(NSUInteger)index;

// 获取图片的属性
- (nullable NSDictionary *)imageProperties;

b、使用图片数据和缩放比率进行初始化
+ (instancetype)decoderWithData:(NSData *)data scale:(CGFloat)scale
{
    // 调用实例初始化方法
    YYImageDecoder *decoder = [[YYImageDecoder alloc] initWithScale:scale];
    // 添加待解码数据
    [decoder updateData:data final:YES];
    return decoder;
}

c、更新待解码数据
- (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;
}

d、私有更新待解码数据
- (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];
    }
}
❹ 图片数据长度大于16的情况下才能解析出图片类型
else
{
    if (_data.length > 16)
    {
        // 存储图片类型
        _type = type;
        // 已经解析过图片类型
        _sourceTypeDetected = YES;
        // 调用私有更新资源方法
        [self _updateSource];
    }
}

e、私有更新资源方法
- (void)_updateSource
{
    // 解析不同的图片类型,调用私有特定类型的更新数据方法
    switch (_type)
    {
        case YYImageTypeWebP:
        {
            [self _updateSourceWebP];// 更新WebP资源
        } break;
            
        case YYImageTypePNG:
        {
            [self _updateSourceAPNG];// 更新PNG资源
        } break;
            
        default:
        {
            [self _updateSourceImageIO];// 更新其他类型
        } break;
    }
}

3、解析支持动画的APNG图片

- (void)_updateSourceAPNG
{
    ...
}
❶ 释放之前的png数据
yy_png_info_release(_apngSource);
_apngSource = nil;
❷ 解码第一帧
[self _updateSourceImageIO];
// 解码失败返回
if (_frameCount == 0) return;
❸ 解码PNG数据
yy_png_info *apng = yy_png_info_create(_data.bytes, (uint32_t)_data.length);
❹ 获取画布宽度和高度并创建帧数组
uint32_t canvasWidth = apng->header.width;
uint32_t canvasHeight = apng->header.height;
NSMutableArray *frames = [NSMutableArray new];
❺ 遍历APNG的每一帧
for (uint32_t i = 0; i < apng->apng_frame_num; i++) {...}
❻ 创建_YYImageDecoderFrame对象并添加到帧数组
_YYImageDecoderFrame *frame = [_YYImageDecoderFrame new];
[frames addObject:frame];
❼ 取出一帧数据为 _YYImageDecoderFrame 对象设置信息
yy_png_frame_info *fi = apng->apng_frames + I;
frame.index = I;
frame.duration = 
...
❽ 设置帧的绘制方式
switch (fi->frame_control.dispose_op)
{
    case YY_PNG_DISPOSE_OP_BACKGROUND:
    {
        frame.dispose = YYImageDisposeBackground;// 绘制下一帧前清空画布
    } break;
    case YY_PNG_DISPOSE_OP_PREVIOUS:
    {
        frame.dispose = YYImageDisposePrevious;// 绘制下一帧前保存画布之前的状态
    } break;
    default:
    {
        frame.dispose = YYImageDisposeNone;// 绘制下一帧前不做任何额外操作
    } break;
}
❾ 设置混合方式
switch (fi->frame_control.blend_op)
{
    case YY_PNG_BLEND_OP_OVER:
    {
        frame.blend = YYImageBlendOver;// 基于alpha混合
    } break;
        
    default:
    {
        frame.blend = YYImageBlendNone;
    } break;
}
❿ 保存信息
dispatch_semaphore_wait(_framesLock, DISPATCH_TIME_FOREVER);
_frames = frames;
dispatch_semaphore_signal(_framesLock);

4、使用ImageIO解码

- (void)_updateSourceImageIO
{
    ...
}
❶ 清空之前的信息
_width = 0;
_height = 0;
_orientation = UIImageOrientationUp;
_loopCount = 0;
dispatch_semaphore_wait(_framesLock, DISPATCH_TIME_FOREVER);
_frames = nil;
dispatch_semaphore_signal(_framesLock);
❷ 当_sourcesource不存在时创建_source对象
if (!_source)
{
    if (_finalized)
    {
        // 创建CGImageSourceRef对象
        _source = CGImageSourceCreateWithData((__bridge CFDataRef)_data, NULL);
    }
    else
    {
        // 创建CGImageSourceRef对象用于增量渲染
        _source = CGImageSourceCreateIncremental(NULL);
        if (_source) CGImageSourceUpdateData(_source, (__bridge CFDataRef)_data, false);
    }
}
else
{
    // _source存在直接更新数据
    CGImageSourceUpdateData(_source, (__bridge CFDataRef)_data, _finalized);
}
// _source创建失败直接返回
if (!_source) return;

❸ 获取图片帧数,如果为0则直接返回
_frameCount = CGImageSourceGetCount(_source);
if (_frameCount == 0) return;

if (_type == YYImageTypePNG)
{
    _frameCount = 1;
}
❹ 获取GIF图片信息
if (_type == YYImageTypeGIF)
{
    // 获取GIF图片信息
    CFDictionaryRef properties = CGImageSourceCopyProperties(_source, NULL);
    if (properties)
    {
        CFDictionaryRef gif = CFDictionaryGetValue(properties, kCGImagePropertyGIFDictionary);
        if (gif)
        {
            // 获取GIF图片循环次数
            CFTypeRef loop = CFDictionaryGetValue(gif, kCGImagePropertyGIFLoopCount);
            if (loop) CFNumberGetValue(loop, kCFNumberNSIntegerType, &_loopCount);
        }
        // 释放资源
        CFRelease(properties);
    }
}
❺ GIF、APNG存在多帧,所以需要创建帧数组并遍历每一帧
NSMutableArray *frames = [NSMutableArray new];
// 遍历每一帧
for (NSUInteger i = 0; i < _frameCount; i++)
{
    ...
}
❻ 获取像素宽度和高度
// 获取图片信息
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_source, i, NULL);
value = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
if (value) CFNumberGetValue(value, kCFNumberNSIntegerType, &width);
value = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
if (value) CFNumberGetValue(value, kCFNumberNSIntegerType, &height);
❼ 当图片类型是GIF时设置duration
if (_type == YYImageTypeGIF)
{
    // 获取gif图片相关的信息
    CFDictionaryRef gif = CFDictionaryGetValue(properties, kCGImagePropertyGIFDictionary);
    if (gif)
    {
        // 设置duration
        value = CFDictionaryGetValue(gif, kCGImagePropertyGIFUnclampedDelayTime);
        if (!value)
        {
            value = CFDictionaryGetValue(gif, kCGImagePropertyGIFDelayTime);
        }
        if (value) CFNumberGetValue(value, kCFNumberDoubleType, &duration);
    }
}
❽ 第一帧图片时保存宽高信息并获取图片方向信息
if (i == 0 && _width + _height == 0)
{
    // 保存宽高信息
    _width = width;
    _height = height;
    value = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
    if (value)
    {
        // 获取图片方向信息
        CFNumberGetValue(value, kCFNumberNSIntegerType, &orientationValue);
        _orientation = YYUIImageOrientationFromEXIFValue(orientationValue);
    }
}
❾ 保存帧数组
dispatch_semaphore_wait(_framesLock, DISPATCH_TIME_FOREVER);
_frames = frames;
dispatch_semaphore_signal(_framesLock);

5、获取帧图片

- (YYImageFrame *)frameAtIndex:(NSUInteger)index decodeForDisplay:(BOOL)decodeForDisplay
{
    YYImageFrame *result = nil;
    pthread_mutex_lock(&_lock);
    result = [self _frameAtIndex:index decodeForDisplay:decodeForDisplay];// 调用私有获取方法
    pthread_mutex_unlock(&_lock);
    return result;
}
a、私有获取图片帧方法
- (YYImageFrame *)_frameAtIndex:(NSUInteger)index decodeForDisplay:(BOOL)decodeForDisplay
{
    ...
}
取出 _YYImageDecoderFrame对象
_YYImageDecoderFrame *frame = [(_YYImageDecoderFrame *)_frames[index] copy];

b、不需要混合
if (!_needBlend)
❶ 获取不需要混合的图片数据,此时还没有解码
CGImageRef imageRef = [self _newUnblendedImageAtIndex:index extendToCanvas:extendToCanvas decoded:&decoded];
❷ 解码为CGImageRef
CGImageRef imageRefDecoded = YYCGImageCreateDecodedCopy(imageRef, YES);
❸ 转换为UIImage
UIImage *image = [UIImage imageWithCGImage:imageRef scale:_scale orientation:_orientation];
❹ 赋值给_YYImageDecoderFrame
frame.image = image;
return frame;

c、需要混合
❶ 创建混合上下文
if (![self _createBlendContextIfNeeded]) return nil;
❷ 清空CGContext
CGContextClearRect(_blendCanvas, CGRectMake(0, 0, _width, _height));
❸ 创建要混合的前一帧图片
CGImageRef unblendedImage = [self _newUnblendedImageAtIndex:index extendToCanvas:NO decoded:NULL];
❹ 绘制到CGContext
CGContextDrawImage(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height), unblendedImage);
❺ 解码获取图片
imageRef = CGBitmapContextCreateImage(_blendCanvas);

c、返回结果
❶ 创建UIImage
UIImage *image = [UIImage imageWithCGImage:imageRef scale:_scale orientation:_orientation];
❷ 赋值给帧对象
frame.image = image;
return frame;

6、创建混合图片

a、创建图片混合上下文
- (BOOL)_createBlendContextIfNeeded
{
    if (!_blendCanvas)
    {
        // 清空缓存索引
        _blendFrameIndex = NSNotFound;
        // 创建位图上下文
        _blendCanvas = CGBitmapContextCreate(NULL, _width, _height, 8, 0, YYCGColorSpaceGetDeviceRGB(), kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst);
    }
    // 返回成功与否
    BOOL suc = _blendCanvas != NULL;
    return suc;
}

b、创建混合图片
- (CGImageRef)_newBlendedImageWithFrame:(_YYImageDecoderFrame *)frame CF_RETURNS_RETAINED
{
    如果画布上的帧区域在绘制之前应该保持之前的状态
    ...
}
帧是基于其alpha合成到输出缓冲区上
if (frame.blend == YYImageBlendOver)
{
    // 获取混合画布上之前的内容
    CGImageRef previousImage = CGBitmapContextCreateImage(_blendCanvas);
    // //使用当前帧生成一张图片
    CGImageRef unblendImage = [self _newUnblendedImageAtIndex:frame.index extendToCanvas:NO decoded:NULL];
    if (unblendImage)
    {
        // 将当前帧图片绘制在混合画布上
        CGContextDrawImage(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height), unblendImage);
        CFRelease(unblendImage);
    }
    // 从混合画布上获取当前图片
    imageRef = CGBitmapContextCreateImage(_blendCanvas);
    // 清空画布
    CGContextClearRect(_blendCanvas, CGRectMake(0, 0, _width, _height));
    if (previousImage)
    {
        // 将之前的状态重新绘制在混合画布上
        CGContextDrawImage(_blendCanvas, CGRectMake(0, 0, _width, _height), previousImage);
        CFRelease(previousImage);
    }
}
覆盖当前的区域使用帧的所有颜色分量
else
{
    // 保存混合画布的当前状态
    CGImageRef previousImage = CGBitmapContextCreateImage(_blendCanvas);
    // 使用当前帧生成图片
    CGImageRef unblendImage = [self _newUnblendedImageAtIndex:frame.index extendToCanvas:NO decoded:NULL];
    if (unblendImage)
    {
        // 清空画布
        CGContextClearRect(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height));
        // 绘制当前帧
        CGContextDrawImage(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height), unblendImage);
        CFRelease(unblendImage);
    }
    // 从混合画布中取出解码后的当前帧
    imageRef = CGBitmapContextCreateImage(_blendCanvas);
    // 清空画布
    CGContextClearRect(_blendCanvas, CGRectMake(0, 0, _width, _height));
    // 在画布上保存之前的状态
    if (previousImage)
    {
        CGContextDrawImage(_blendCanvas, CGRectMake(0, 0, _width, _height), previousImage);
        CFRelease(previousImage);
    }
}

Demo

Demo在我的Github上,欢迎下载。
SourceCodeAnalysisDemo

参考文献

你可能感兴趣的:(IOS框架:YYImage源码解析)