原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的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
(读取图片指定区域内元素进行显示)动画 - 高效的动态内存缓存管理,以保证高性能低内存的动画播放
- 完全兼容
UIImage
和UIImageView
,使用方便 - 保留可扩展的接口,以支持自定义动画
- 每个类和方法都有完善的文档注释
2、架构分析
- 图像层:把不同类型的图像信息封装成类并提供初始化和其他便捷接口
- 视图层:负责图像层内容的显示(包含动态图像的动画播放)工作。其中YYAnimatedImageView类中定义的YYAnimatedImage协议连接了图像层和视图层
- 编/解码层:提供对图像数据的编解码支持
二、YYImage使用示例
1、YYImage的使用
使用YYImage
和YYImageAnimatedImageView
代替UIImage
和UIImageView
,与我们平时的用法几乎一致,符合我们平时的编码习惯。
// 使用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
做了扩展以支持 WebP
/APNG
/GIF
格式的图片解码。它还支持 NSCoding
协议可以对多帧图像数据进行 archive
和 unarchive
操作,即支持缓存image
对象到缓存,以便下次使用。由下面头文件内容可以看出,YYImage
提供了类似 UIImage
的初始化方法,符合我们一贯的使用思维这一点比较好。另外值得一提的是 YYImage
的 imageNamed:
初始化方法并不支持缓存,而是通过参数所提供的图片名称/路径/数据直接查找到对应数据解码获取对应图片。
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:播放帧动画
该类可以把静态图片类型如 png
和 jpeg
格式的静态图像用帧切换的方式以动态图片的形式显示。
// 相对于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
方法非常简单,开发者不需要管理一些状态属性(例如 isExecuting
和 isFinished
),当 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
是线程安全的图片解码器,支持的格式有PNG
、JPG
、BMP
、TIFF
、ICNS
等,可以用来解码完整的图片数据,也可以用来增量地解码数据。
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