YYImage
YYImage是由@ibireme 开发的一款功能强大的 iOS 图像框架(该项目是 YYKit 组件之一),它支持当前市场主流的静/动态图像编/解码与动态图像的动画播放显示,其主要功能如下:
- 动画类型: WebP, APNG, GIF。
- 静态图像: WebP, PNG, GIF, JPEG, JP2, TIFF, BMP, ICO, ICNS。
- 支持以下类型图片的渐进式/逐行扫描/隔行扫描解码:
PNG, GIF, JPEG, BMP。 - 支持多张图片构成的帧动画播放,支持单张图片的 sprite sheet 动画。
- 高效的动态内存缓存管理,以保证高性能低内存的动画播放。
- 完全兼容 UIImage 和 UIImageView,使用方便。
- 保留可扩展的接口,以支持自定义动画。
- 每个类和方法都有完善的文档注释。
sprite sheet: 由一张图片组成,图片中有很多小块区域,在显示的时候,读取指定区域内元素,进行显示的图片。
sprite sheet
YYImage 架构分析
阅读YYImage 源码可以很清晰地得出其划分层级如下:
- 图像层,把不同类型的图像信息封装成类并提供初始化和其他便捷接口。
- 视图层,负责图像层内容的显示(包含动态图像的动画播放)工作。其中YYAnimatedImageView类中定义的YYAnimatedImage协议连接了图像层和视图层。
- 编/解码层,提供对图像数据的编解码支持。
YYImage示例使用
DEMO
使用YYImage和YYImageAnimatedImageView代替UIImage和UIImageView,与我们平时的用法几乎一致,符合我们平时的编码习惯。
// 使用YYImage去创建image对象
UIImage *image = [YYImage imageNamed:@"ani.gif"];
// 外界无需关心图片类型,当做正常的ImageView来使用即可
UIImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image];
[self.view addSubview:imageView];
动画类型Image效果展示
YYAnimatedImage
动画图片协议
遵循这个协议的类,都可以使用YYAnimateImageView去进行动画播放
在YYImage中,所有的图像层类都遵循于这个协议
// 动画帧总数
- (NSUInteger)animatedImageFrameCount;
// 动画循环次数,0 表示无限循环
- (NSUInteger)animatedImageLoopCount;
// 每帧字节数(在内存中),可能用于优化内存缓冲区大小
- (NSUInteger)animatedImageBytesPerFrame;
// 返回给定特殊索引对应的帧图像,这个方法可能在异步线程中调用
- (nullable UIImage *)animatedImageFrameAtIndex:(NSUInteger)index;
// 返回给定特殊索引对应的帧图像对应的显示持续时长
- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index;
@optional
// 针对 Spritesheet 动画的方法,用于显示某一帧图像在 Spritesheet 画布中的位置
- (CGRect)animatedImageContentsRectAtIndex:(NSUInteger)index;
YYImage
YYImage主要用来显示动画类型的图片,它继承自 UIImage 并对 UIImage 做了扩展以支持 WebP/APNG/GIF 格式的图片解码。它还支持 NSCoding 协议可以对多帧图像数据进行 archive 和 unarchive 操作,即支持缓存image对象到缓存,以便下次使用。
由下面头文件内容可以看出,YYImage 提供了类似 UIImage 的初始化方法,符合我们一贯的使用思维这一点比较好。另外值得一提的是 YYImage 的 imageNamed: 初始化方法并不支持缓存,而是通过参数所提供的图片名称/路径/数据直接查找到对应数据解码获取对应图片。
YYImage 头文件函数以及属性
@interface YYImage : UIImage
{
// Private
YYImageDecoder *_decoder; // YYImage自定义的解码器
NSArray *_preloadedFrames; // 预先加载的UIImage实例数组
dispatch_semaphore_t _preloadedLock; // 锁,YYImage所有对于Frame的访问都是在加锁的情况下进行的
NSUInteger _bytesPerFrame; // 每一帧图片的资源消耗大小
}
// 几个初始化方法,都不提供缓存功能,no cache!
+ (nullable YYImage *)imageNamed:(NSString *)name; // no cache!
+ (nullable YYImage *)imageWithContentsOfFile:(NSString *)path;
+ (nullable YYImage *)imageWithData:(NSData *)data;
+ (nullable YYImage *)imageWithData:(NSData *)data scale:(CGFloat)scale;
// 图像类型:调用上述类方法的时候即会解析header得到,即第一次decoder实例创建时
@property (nonatomic, readonly) YYImageType animatedImageType;
// 图片的data,_decoder.data,并不是只有动态图像才会有。普通类型也会有
@property (nullable, nonatomic, readonly) NSData *animatedImageData;
// 图片的内存消耗
// 计算公式_bytesPerFrame(每一帧图片大小) * decoder.frameCount(总共多少帧图片);
@property (nonatomic, readonly) NSUInteger animatedImageMemorySize;
// 是否要预先加载图片
@property (nonatomic) BOOL preloadAllAnimatedImageFrames;
@end
区别于系统的[UIImage imageNamed:],YYImage所提供的imageNamed:是没有系统缓存的
YYImage的几个重要的函数
初始化函数:所有YYImage的初始化都会进入该方法
步骤:
- 初始化解码器,获取图片基本信息(图片类型,循环次数,图片宽高等)
- 取出第一帧(动画类型图片)图片/普通图片
- 赋值给UIImage的image属性
- 计算内存消耗
- (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 {
// 使用YYImage自己的解码去进行图片解析
// 值得注意的是这边并没有将data转换成image,只是解析处理图片的基本信息
YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];
// 取出第一帧(动画类型图片)图片/普通图片
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.isDecodedForDisplay = YES;
}
return self;
}
图片读取函数:动画类图片,可以根据index获取,index帧的图片。
- (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index {
if (index >= _decoder.frameCount) return nil;
dispatch_semaphore_wait(_preloadedLock, DISPATCH_TIME_FOREVER);
// 加载preload的图片
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;
}
Q&A
【问】 preloadAllAnimatedImageFrames这个属性适合在什么场景下使用
【答】 在需要用控件换时间的情况下。1. TextView这种图文混排的时候使用。2. QQ表情之类的初始化,需要提前加载
YYFrameImage
YYFrameImage主要用来播放帧动画,该类可以把静态图片类型如 png 和 jpeg 格式的静态图像用帧切换的方式以动态图片的形式显示,
DEMO
// 文件: 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];
YYFrameImage初始化
相对于YYImage来说帧动画图片,只是继承UIImage,并且遵循动画图片的协议。
其中初始化方法大致可以分为以下步骤:
- 入参校验
- 根据入参取到首张图片
- 用首图初始化 _oneFrameBytes ,如入参初始化 _imageDatas ,_frameDurations 和 _loopCount
- 用 UIImage 的 initWithCGImage:scale:orientation: 初始化并返回初始化结果
- (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 = firstPath.pathScale;
UIImage *firstCG = [[[UIImage alloc] initWithData:firstData] 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;
}
YYFrameImage获取index下图片的方式
- (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index {
if (_imagePaths) {
if (index >= _imagePaths.count) return nil;
NSString *path = _imagePaths[index];
CGFloat scale = [path pathScale];
NSData *data = [NSData dataWithContentsOfFile:path];
return [[UIImage imageWithData:data scale:scale] imageByDecoded];
} else if (_imageDatas) {
if (index >= _imageDatas.count) return nil;
NSData *data = _imageDatas[index];
return [[UIImage imageWithData:data scale:[UIScreen mainScreen].scale] imageByDecoded];
} else {
return index == 0 ? self : nil;
}
}
Q&A
UIImage *firstCG = [[[UIImage alloc] initWithData:firstData] imageByDecoded];
【问】为什么要这么使用imageByDecoded这个函数?
【答】imageByDecoded内原理是处理原图片成新图片,但是去除了Alpha通道
YYSpriteSheetImage
YYSpriteSheetImage主要用来做 Spritesheet 动画显示的图像类,Spritesheet 动画原理是把一个动画过程分解为多个动画帧,按照顺序将这些动画帧排布在一张大的画布中,播放动画时只需要按照每一帧图像的尺寸大小以及对应索引去画布中提取对应的帧替换显示以达到人眼判定动画的效果。
DEMO
步骤:
- 获取一张雪碧图创建UIImage实例
- 创建图片中的对应子frame的数组,播放时间
- 通过image,frame数组,时间,播放次数来创建雪碧图实例
- 赋值给YYAnimatedImageView进行播放
// 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 *imgView = [YYAnimatedImageView new];
imgView.size = CGSizeMake(img.size.width / 8, img.size.height / 12);
imgView.image = sprite;
含有的属性
@interface YYSpriteSheetImage : UIImage
/** 创建并返回一个图像
@param image 雪碧图整张大图
@param contentRects 每一帧图像相对于整张大图的坐标位置,注意此范围不能超出大图的边界
@param frameDurations 每一帧图像播放持续时长
@param loopCount 循环次数
@return 返回图像/若有错误发生返回nil*/
// 这个函数只做了简单的赋值操作,不做过多解析
- (nullable instancetype)initWithSpriteSheetImage:(UIImage *)image
contentRects:(NSArray *)contentRects
frameDurations:(NSArray *)frameDurations
loopCount:(NSUInteger)loopCount;
@property (nonatomic, readonly) NSArray *contentRects;
@property (nonatomic, readonly) NSArray *frameDurations;
@property (nonatomic, readonly) NSUInteger loopCount;
// index下图片对应原图的位置
- (CGRect)contentsRectForCALayerAtIndex:(NSUInteger)index;
@end
重要的函数
// 根据索引找到对应帧 CALayer 的位置
- (CGRect)contentsRectForCALayerAtIndex:(NSUInteger)index {
CGRect layerRect = CGRectMake(0, 0, 1, 1);
if (index >= _contentRects.count) return layerRect;
CGSize imageSize = self.size;
CGRect rect = [self animatedImageContentsRectAtIndex:index];
if (imageSize.width > 0.01 && imageSize.height > 0.01) {
layerRect.origin.x = rect.origin.x / imageSize.width;
layerRect.origin.y = rect.origin.y / imageSize.height;
layerRect.size.width = rect.size.width / imageSize.width;
layerRect.size.height = rect.size.height / imageSize.height;
layerRect = CGRectIntersection(layerRect, CGRectMake(0, 0, 1, 1));
if (CGRectIsNull(layerRect) || CGRectIsEmpty(layerRect)) {
layerRect = CGRectMake(0, 0, 1, 1);
}
}
return layerRect;
}
// 根据index区寻找对应原图对应的位置
- (CGRect)animatedImageContentsRectAtIndex:(NSUInteger)index {
if (index >= _contentRects.count) return CGRectZero;
return ((NSValue *)_contentRects[index]).CGRectValue;
}
该方法会根据索引找到对应帧的 CALayer 位置,该接口返回一个由 0.0~1.0 之间的数值组成的图层定位 LayerRect,如果在查找位置过程中发现异常则返回 CGRectMake(0, 0, 1, 1),其内部实现大体步骤:
- 校验入参索引是否超过 SpriteSheet 分割帧总数,超过返回 CGRectMake(0, 0, 1, 1)
- 没超过则通过 YYAnimatedImage 协议的animatedImageContentsRectAtIndex: 方法找到对应索引的真实位置 RealRect
- 通过真实位置 RealRect 与 SpriteSheet 画布的比算错 0.0~1.0 之间的值,得到指定索引帧的逻辑定位 LogicRect
- 通过 CGRectIntersection 方法计算逻辑定位 LogicRect 与CGRectMake(0, 0, 1, 1) 的交集,确保逻辑定位没有超出画布的部分
- 将处理后的逻辑定位 LogicRect 作为图层定位 LayerRect 返回
返回的 LayerRect 作为对应索引帧的画布内相对位置存在,结合画布就可以定位到对应帧图像的具体尺寸和位置。
Q&A
【问】 为什么要设计contentsRectForCALayerAtIndex:这个函数,函数存在的意义是什么
【答】 在CALayer类中有一个contentsReact的属性,为了配合这个属性的使用而设计的。
YYSpriteSheetImage *sheet = ...;
UIImageView *imageView = ...;
imageView.image = sheet;
imageView.layer.contentsRect = [sheet contentsRectForCALayerAtIndex:6];
YYAnimatedImageView
YYAnimatedImageView类被用来进行动画播放控制,其内部包含 YYAnimatedImage 协议以及 YYAnimatedImageView 自身两部分。
其类本身是对于UIImageView的继承,对于原有的UIImageView进行增强,其内部复写了一系列重要的函数(startAnimating/stopAnimating)来控制播放
DEMO
// [email protected]
YYImage *image = [YYImage imageNamed:@"ani"];
YYAnimatedImageView *imageView = [YYAnimatedImageView alloc] initWithImage:image];
[view addSubView:imageView];
YYAnimatedImageView工作原理
YYAnimatedImageView 内部的初始化没什么特别之处,初始化函数中会设置图片,当判定图片有更改时会依照下面 4 步去处理:
【ps】 这样可以保证 YYAnimatedImageView的图片更改时都会执行上面的步骤为新的图片初始化配套的新动画参数并且重绘,而重置动画实现中会使用到上面的 dispatch_once_t _onceToken; 以确保某些内部变量的创建以及对 App 内存警告和进入后台的通知观察代码只执行一次。
YYAnimatedImageView 使图片动起来是依靠 CADisplayLink *_link; 变量切换帧图像,其内部的实现逻辑流程可以简单理解为:
- (void)step:(CADisplayLink *)link {
UIImage *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) 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
}
// 用信号量对于buffer的操作进行加锁
LOCK(
bufferedImage = buffer[@(nextIndex)];
if (bufferedImage) {
if ((int)_incrBufferCount < _totalFrameCount) {
[buffer removeObjectForKey:@(nextIndex)];
}
// KVO
[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
if (!_bufferMiss) {
// 调用setNeedsDisplay进行重绘操作
[self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep
}
if (!bufferIsFull && _requestQueue.operationCount == 0) { // if some work not finished, wait for next opportunity
// 真正图片的data转换成image的解码在这里发生的
_YYAnimatedImageViewFetchOperation *operation = [_YYAnimatedImageViewFetchOperation new];
operation.view = self;
operation.nextIndex = nextIndex;
operation.curImage = image;
[_requestQueue addOperation:operation];
}
}
// 进行图片设置
- (void)displayLayer:(CALayer *)layer {
if (_curFrame) {
layer.contents = (__bridge id)_curFrame.CGImage;
}
}
实现细节(需注意)
- YYAnimatedImageView 实现中当 _curIndex 即当前帧索引修改时在修改代码前后加入了 willChangeValueForKey: 与 didChangeValueForKey: 方法以支持 KVO
- 对帧缓冲区 _buffer 的操作都使用 _lock 上锁
- 通过将图片请求队列 _requestQueue 的 maxConcurrentOperationCount 设置为 1 使图片请求队列成为串行队列(最大并发数为 1)
- 图片请求队列中加入的操作均为 _YYAnimatedImageViewFetchOperation
- 为了避免使用 CADisplayLink 可能造成的循环引用设计了 _YYImageWeakProxy
_YYAnimatedImageViewFetchOperation
获取图片的队列_YYAnimatedImageViewFetchOperation 继承自 NSOperation 类,是自定义操作类,作者将其操作内容实现写在了 main 中,其 main 函数内部实现逻辑如下:
/// An operation for image fetch
@interface _YYAnimatedImageViewFetchOperation : NSOperation
@property (nonatomic, weak) YYAnimatedImageView *view;
@property (nonatomic, assign) NSUInteger nextIndex;
@property (nonatomic, strong) UIImage *curImage;
@end
@implementation _YYAnimatedImageViewFetchOperation
- (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));
if (miss) {
// 触发解码操作
UIImage *img = [_curImage animatedImageFrameAtIndex:idx];
// 如果不是YYImage类型,可能会存在没有decoded情况
// 其他类型用ImageIO的解析方式
img = img.imageByDecoded;
if ([self isCancelled]) break;
LOCK_VIEW(view->_buffer[@(idx)] = img ? img : [NSNull null]);
view = nil;
}
}
}
}
@end
需要注意的实现细节:
- 操作中对于 view 缓冲区的操作也都上了锁
- 操作由于是放入图片请求队列中进行的,内部有对isCancelled做判断,如果操作已经被取消(发生在更改图片、停止动画、手动更改当前帧、收到内存警告或 App 进入后台等)则需要及时跳出
- 对于新的线程优先级只在 main 方法范围内有效,所以推荐把操作的实现放在 main 中而非 start(如需覆盖 start 方法时,需要关注 isExecuting 和 isFinished 两个 key paths)
Q&A
【问题】为什么是用了main函数,而不是去使用start函数
【答案】使用 main 方法非常简单,开发者不需要管理一些状态属性(例如 isExecuting 和 isFinished),当 main 方法返回的时候,这个operation 就结束了。这种方式使用起来非常简单,但是灵活性相对重写start来说要少一些,因为main方法执行完就认为operation结束了,所以一般可以用来执行同步任务。
【问题】在YYAnimatedImageView类的resetAnimated方法中有这样一段代码:
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];
});
}
);
其中[holder class]的具体作用是什么?这边怎么实现了异步释放。
【答案】对象的销毁虽然消耗资源不多,但累积起来也是不容忽视的。通常当容器类持有大量对象时,其销毁时的资源消耗就非常明显。同样的,如果对象可以放到后台线程去释放,那就挪到后台线程去。这里有个小 Tip:把对象捕获到 block 中,然后扔到后台队列去随便发送个消息以避免编译器警告,就可以让对象在后台线程销毁了。
YYImageCoder
YYImageCoder 是 YYImage 的编/解码类,也是YYImage 框架的底层核心。
YYImageCoder 内部定义了许多 YYImage 中用到的核心数据结构:
- YYImageType,所有的支持的图片格式做了枚举定义
- YYImageDisposeMethod,指定在画布上渲染下一个帧之前如何处理当前帧所使用的区域方法
- YYImageBlendOperation,指定当前帧的透明像素如何与前一个画布的透明像素混合操作
- YYImageFrame,一帧图像数据
- YYImageEncoder,图像编码器
- YYImageDecoder,图像解码器
- UIImage+YYImageCoder,UIImage 的分类,里面提供了一些方便使用的方法
其中 YYImageFrame 是对一帧图像数据的封装,便于在 YYImageCoder 编/解码过程中使用。
YYImageEncoder
demo示例:
// 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];
根据示例代码可以看出,YYImageEncoder主要根据当前图片的类型选择对应的底层编码方法以执行编码操作。
YYImageDecoder
demo示例:
// 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;
可以看出YYImageDecoder使用- (BOOL)updateData:(nullable NSData *)data final:(BOOL)final方法来区分是否数据装载完毕,以实现渐进式解码数据的功能。
解析部分
;) 太多了,大家自给自足吧。哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈
Q&A
【问题】YYImageCoder类中手动添加@autoreleasepool的作用
- (void)_encodeImageWithDestination:(CGImageDestinationRef)destination imageCount:(NSUInteger)count {
// balabala
for (int i = 0; i < count; i++) {
@autoreleasepool {
// balabala
}
}
}
【答案】@autoreleasepool加了之后,会创建一个新的释放池,等@autoreleasepool{}右括号结束以后,会立即释放以减少资源占用。
参考链接
- YYImage解析
- 图片编码解码YYImageCoder的设计流程概述
作者:奚山,红纸