源码剖析学习系列:(不断更新)
1、FBKVOController源码剖析与学习
2、MJRefresh源码剖析与学习
3、YYImage源码剖析与学习
前言:
要看懂YYImage框架,最好先了解热身部分(具体的自行百度),如果懒得看,直接跨过该部分,等到下面部分有疑问,再回过头看这部分的知识,也是可以。
热身部分
移动端图片格式调研
1、Image I/O
Image I/O 学习笔记
Image I/O官方文档
GIF图添加文字Demo
使用
CGBitmapContextCreate
函数创建一个位图上下文; 使用CGContextDrawImage
函数将原始位图绘制到上下文中; 使用CGBitmapContextCreateImage
函数创建一张新的解压缩后的位图。
2、 CGBitmapContextCreate
中的参数
谈谈 iOS 中图片的解压缩
data
:如果不为NULL
,那么它应该指向一块大小至少为bytesPerRow * height
字节的内存;如果 为NULL
,那么系统就会为我们自动分配和释放所需的内存,所以一般指定NULL
即可;width
和height
:位图的宽度和高度,分别赋值为图片的像素宽度和像素高度即可;bitsPerComponent
:像素的每个颜色分量使用的 bit 数,在 RGB 颜色空间下指定 8 即可;bytesPerRow
:位图的每一行使用的字节数,大小至少为width * bytes per pixel
字节。有意思的是,当我们指定 0 时,系统不仅会为我们自动计算,而且还会进行 cache line alignment 的优化space
:颜色空间,一般使用 RGB 即可;bitmapInfo
:位图的布局信息。当图片不包含 alpha 的时候使用kCGImageAlphaNoneSkipFirst
,否则使用kCGImageAlphaPremultipliedFirst
3、信号量
信号量的讲解
/* 注意,正常的使用顺序是先降低然后再提高,这两个函数通常成对使用。 */
dispatch_semaphore_wait(_framesLock, DISPATCH_TIME_FOREVER); //等待降低信号量
// to do
dispatch_semaphore_signal(_framesLock); //提高信号量
复制代码
所用到的知识: 复合赋值运算符、Image I/O、CADisplayLink、willChangeValueForKey:、
一、YYImage总体介绍
1、YYImage 源码 2、YYImage 源码的文字解析版本1、YYImage 功能
- 显示动画类型的图片
- 播放帧动画
- 播放 sprite sheet 动画
- 图片类型探测
- 图片解码、编码(最核心功能)
2、YYImage 主要类介绍
YYImage 类
它是一个完全兼容的“UIImage”子类。它扩展了UIImage 支持动画WebP, APNG和GIF格式的图像数据解码。它还 支持NSCoding协议,以存档和反存档多帧图像数据。
a、animatedImageMemorySize
如果所有帧图像都被加载到内存中,那么总内存使用(以字节为单位)。 如果图像不是从多帧图像数据创建的,则该值为0。
b、preloadAllAnimatedImageFrames
将此属性设置为“YES”将阻塞要解码的调用线程 所有动画帧图像到内存,设置为“NO”将释放预装帧。 如果图像被许多图像视图(如emoticon)共享,则预加载所有视图 帧将降低CPU成本。
YYAnimatedImageView 类
用于显示动画图像的图像视图。 可以用来播放多帧动画以及普通动画,可以控制、暂停动画 当设备有足够的空闲内存时,这个视图及时请求帧数据。 这个视图可以在内部缓冲区中缓存一些或所有未来的帧,以降低CPU成本。
3、YYImage 的意义(图片解码的原因)
从磁盘中加载一张图片,并将它显示到屏幕上,这个过程其实经历很多,非常耗性能。随着显示的图片增加,性能下降尤其明显。不管是 JPEG 还是 PNG 等图片,都是一种编码后(压缩)的位图图形格式。我们先看下显示到屏幕这个过程的工作流:
1、我们使用
+[UIImage imageWithContentsOfFile:]
方法从磁盘中加载一张图片。此时,图片还没有被解码,仍旧是编码状态下。 2、返回的图片被分配给UIImageView
3、接着一个隐式的CATransaction
捕获到了图层树的变化; 4、在主线程的下一个run loop
到来时,Core Animation
提交了这个隐式的事务,可能会涉及copy这些图片(已经成为图层树中的图层内容的图片)。这个 copy 操作可能会涉及以下部分或全部步骤:a.分配缓冲区来管理文件IO和解压缩操作。 b.文件数据从磁盘读取到内存。 c.将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作; d.最后
Core Animation
使用未压缩的位图数据渲染UIImageView
的图层
图层树:(个人理解)洋葱看过去有很多层,这就是洋葱的图层,而屏幕上显示的文字、图片啊,都可以理解成为图层,很多图层就形成了一个结构,这个很多图层的结构就叫做图层树。
因此,在将磁盘中的图片渲染到屏幕之前,必须先要得到图片的原始像素数据,才能执行后续的绘制操作,这就是为什么需要对图片解码的原因。
二、YYImage主要类调用逻辑
A、渲染GIF/WebP/PNG(APNG)方法调用顺序
1、YYImage *image = [YYImage imageNamed:name];
//传入图片名创建YYImage对象
2、[[self alloc] initWithData:data scale:scale];
//用重写的方法初始化图像数据
3、YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];
//创建解码类 YYImageDecoder 对象,紧接着更新数据
4、result = [self _updateData:data final:final];
//根据图像的data算出图片的type以及其他信息,再根据不同type 的图像去分别更新数据
5、[self _updateSourceImageIO];
// 计算出PNG、GIF等图片信息(图片的每一帧的属性,包括宽、高、方向、动画重复次数(gif类型)、持续时间(gif类型))
6、 YYAnimatedImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image];
//把图片添加到 UIImageView 的子类,这个子类后面讲(第7点后都是它的核心),这里暂且当它为普通 ImageView 那样看。
7、[self setImage:image withType:YYAnimatedImageTypeImage];
// 设置图片,类似Setter方法
8、[self imageChanged];
//判断当前图片类型以及帧数,由CATransaction支持的显示事务去更新图层的 contentsRect
,以及重置动画的参数,后面详解该方法。
9、[self resetAnimated];
//重置动画多种参数;[self calcMaxBufferCount];
// 动态调整当前内存的缓冲区大小。
10、[self didMoved];
// 窗口对象或者父视图对象改变,则开始控制动画的启动(停止),这是动画得以显示的关键
B、渲染帧动画方法调用顺序
1、UIImage *image = [[YYFrameImage alloc] initWithImagePaths:paths oneFrameDuration:0.1 loopCount:0];
//传入图片组的路径、每一个帧(每一个图片)的时间以及循环多少次,计算出总的durations 2、[self initWithImagePaths:paths frameDurations:durations loopCount:loopCount];
// 把第一张图片解码后返回,并求出第一帧的大小,作为每一帧的大小 3、YYAnimatedImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image];
后面步骤跟 渲染GIF/WebP/PNG(APNG)方法调用顺序 第7点开始几乎一样
注意:由于代码过多,不可能面面俱到,所以下面只会摘取核心进行讲解。这样,读者看完此文以及看完我标注过的源码(),,去读源代码,也更容易理解。
三、核心代码
// 它接受一个原始的位图参数 imageRef ,最终返回一个新的解压缩后的位图 newImage
CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) {
if (!imageRef) return NULL;
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
if (width == 0 || height == 0) return NULL;
// 重新绘制解码(可能会失去一些精度)
if (decodeForDisplay) { //decode with redraw (may lose some precision)
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask;
BOOL hasAlpha = NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaFirst) {
hasAlpha = YES;
}
// BGRA8888 (premultiplied) or BGRX8888
// same as UIGraphicsBeginImageContext() and -[UIView drawRect:]
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, YYCGColorSpaceGetDeviceRGB(), bitmapInfo);
if (!context) return NULL;
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); // decode
CGImageRef newImage = CGBitmapContextCreateImage(context);
CFRelease(context);
return newImage; // 返回一个新的解压缩后的位图 newImage
} else {
}
}
复制代码
YYCGImageCreateDecodedCopy
是解压缩的核心,也就是渲染图片性能显著的原因。该方法首先求出图片的宽高,注意,这里的图片是指编码前的图片的每一帧图片。
- (void)imageChanged {
YYAnimatedImageType newType = [self currentImageType];
id newVisibleImage = [self imageForType:newType];
NSUInteger newImageFrameCount = 0;
BOOL hasContentsRect = NO;
if ([newVisibleImage isKindOfClass:[UIImage class]] &&
[newVisibleImage conformsToProtocol:@protocol(YYAnimatedImage)]) {
// 求出有多少帧(如果是帧动画(由多张图组合的),相当于有多少张图)
newImageFrameCount = ((UIImage *) newVisibleImage).animatedImageFrameCount;
if (newImageFrameCount > 1) { // 动态图
hasContentsRect = [((UIImage *) newVisibleImage) respondsToSelector:@selector(animatedImageContentsRectAtIndex:)];
}
}
// 由CATransaction支持的显示事务去更新图层的 contentsRect, 但一般不用走这段代码。大都走的是 CATransaction 的隐式事务自己更新
if (!hasContentsRect && _curImageHasContentsRect) {
if (!CGRectEqualToRect(self.layer.contentsRect, CGRectMake(0, 0, 1, 1)) ) {
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.layer.contentsRect = CGRectMake(0, 0, 1, 1);
[CATransaction commit];
}
}
_curImageHasContentsRect = hasContentsRect;
// YYSpriteSheetImage 类用到,先不理
if (hasContentsRect) {
CGRect rect = [((UIImage *) newVisibleImage) animatedImageContentsRectAtIndex:0];
[self setContentsRect:rect forImage:newVisibleImage];
}
if (newImageFrameCount > 1) {
[self resetAnimated]; // 重置动画多种参数,包括在后台释放图像,下面再赋值已经被重置过的动画参数
_curAnimatedImage = newVisibleImage; // 当前动画图片
_curFrame = newVisibleImage; // 当前帧
_totalLoop = _curAnimatedImage.animatedImageLoopCount; // 总循环次数
_totalFrameCount = _curAnimatedImage.animatedImageFrameCount; // 总帧数
[self calcMaxBufferCount]; // 动态调整当前内存的缓冲区大小。
}
[self setNeedsDisplay]; // 标志需要重绘,会在下一个循环到来时刷新
[self didMoved]; // 窗口对象或者父视图对象改变,则开始控制动画的启动(停止),这是动画得以显示的关键
}
复制代码
图片改变的处理核心
主要做了以下几点:
- 初始化动画参数
resetAniamted
- 初始化或者重置后求出动画播放循环次数、当前帧、总帧数
- 调用动态调整缓冲区方法
calcMaxBufferCount
、调用控制动画方法didMoved
// init the animated params.
- (void)resetAnimated {
if (!_link) {
_lock = dispatch_semaphore_create(1);
_buffer = [NSMutableDictionary new];
// 添加到这种队列中的操作,就会自动放到子线程中执行。
_requestQueue = [[NSOperationQueue alloc] init];
/* maxConcurrentOperationCount 默认情况下为-1,表示不进行限制,可进行并发执行。
为1时,队列为串行队列。只能串行执行。大于1时,队列为并发队列 */
_requestQueue.maxConcurrentOperationCount = 1;
/* 初始化一个新的 CADisplayLink 对象,在屏幕更新时调用。为了使显示循环与显示同步,应用程序使用addToRunLoop:forMode:方法将其添加到运行循环中
一个计时器对象,允许应用程序将其绘图同步到显示的刷新率。
*/
_link = [CADisplayLink displayLinkWithTarget:[_YYImageWeakProxy proxyWithTarget:self] selector:@selector(step:)];
if (_runloopMode) {
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:_runloopMode];
}
// 禁用通知
_link.paused = YES;
// 接受内存警告的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
// 接受返回后台的通知,返回后台时,记录即将显示的下一帧
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
}
[_requestQueue cancelAllOperations];
LOCK(
if (_buffer.count) {
NSMutableDictionary *holder = _buffer;
_buffer = [NSMutableDictionary new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
// Capture the dictionary to global queue,
// release these images in background to avoid blocking UI thread.
[holder class]; // 捕获字典到全局队列,在后台释放这些图像以避免阻塞UI线程。
});
}
);
_link.paused = YES;
_time = 0;
if (_curIndex != 0) {
[self willChangeValueForKey:@"currentAnimatedImageIndex"];
_curIndex = 0; // 把索引值重置为0
[self didChangeValueForKey:@"currentAnimatedImageIndex"];
}
_curAnimatedImage = nil; // 当前图像为空
_curFrame = nil; // 当前帧
_curLoop = 0; //当前循环次数
_totalLoop = 0; // 总循环次数
_totalFrameCount = 1; // 总帧数
_loopEnd = NO; // 是否循环结尾
_bufferMiss = NO; // 是否丢帧
_incrBufferCount = 0; // 当前允许的缓存
}
复制代码
重置图片的参数; 内存警告时释放内存; 初始化一个新的 CADisplayLink 对象,在屏幕更新时调用。
// 只有屏幕刷新累加时间不小于当前帧的动画播放时间才显示图片,播放下一帧。
// 播放 GIF 的关键
- (void)step:(CADisplayLink *)link {
UIImage *image = _curAnimatedImage;
NSMutableDictionary *buffer = _buffer;
// 下一张的图片
UIImage *bufferedImage = nil;
// 下一张要显示的索引
NSUInteger nextIndex = (_curIndex + 1) % _totalFrameCount;
BOOL bufferIsFull = NO;
// // 当前无图像显示 返回
if (!image) return;
if (_loopEnd) { // view will keep in last frame // 结束循环 停留在最后帧
[self stopAnimating]; // 如果动画播放循环结束了,就停止动画
return;
}
NSTimeInterval delay = 0;
if (!_bufferMiss) {
// 屏幕刷新时间的累加
_time += link.duration; // link.duration 屏幕刷新的时间,默认1/60 s
delay = [image animatedImageDurationAtIndex:_curIndex]; // 返回当前帧的持续时间
if (_time < delay) return;
_time -= delay; // 减去上一帧播放的时间
if (nextIndex == 0) {
_curLoop++; // 增加一轮循环次数
if (_curLoop >= _totalLoop && _totalLoop != 0) { // 已经到了循环次数,停止播放
_loopEnd = YES;
[self stopAnimating];
[self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep
return; // stop at last frame
}
}
delay = [image animatedImageDurationAtIndex:nextIndex]; // 返回下一帧的的持续时间
/** */
if (_time > delay) _time = delay; // do not jump over frame
}
LOCK(
bufferedImage = buffer[@(nextIndex)];
if (bufferedImage) {
if ((int)_incrBufferCount < _totalFrameCount) {
[buffer removeObjectForKey:@(nextIndex)];
}
[self willChangeValueForKey:@"currentAnimatedImageIndex"];
_curIndex = nextIndex; // 用KVO改变 当前索引值
[self didChangeValueForKey:@"currentAnimatedImageIndex"];
_curFrame = bufferedImage == (id)[NSNull null] ? nil : bufferedImage;
// 实现YYSpriteSheetImage 的协议方法,才会进入该 if 语句
if (_curImageHasContentsRect) {
_curContentsRect = [image animatedImageContentsRectAtIndex:_curIndex];
[self setContentsRect:_curContentsRect forImage:_curFrame];
}
nextIndex = (_curIndex + 1) % _totalFrameCount;
_bufferMiss = NO;
if (buffer.count == _totalFrameCount) {
bufferIsFull = YES; // 缓冲区已经满
}
} else {
// 丢帧,某一帧没有办法找到显示
_bufferMiss = YES;
}
)//LOCK
if (!_bufferMiss) {
// 刷新显示图像
[self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep
}
/* _YYAnimatedImageViewFetchOperation 为 NSOperation 的子类
还未获取完所有图像,交给它获取下一张图像 */
if (!bufferIsFull && _requestQueue.operationCount == 0) { // if some work not finished, wait for next opportunity
_YYAnimatedImageViewFetchOperation *operation = [_YYAnimatedImageViewFetchOperation new];
operation.view = self;
operation.nextIndex = nextIndex;
operation.curImage = image;
[_requestQueue addOperation:operation]; //
}
}
复制代码
这是动画播放的关键,是 CADisplayLink对象 的方法,每 1/60s 也就是屏幕刷新一次就调用一次
- (void)calcMaxBufferCount {
int64_t bytes = (int64_t)_curAnimatedImage.animatedImageBytesPerFrame; // 求出每一帧的字节数
if (bytes == 0) bytes = 1024; // 如果为0,则给定1024
int64_t total = _YYDeviceMemoryTotal(); // 获取设备的CPU物理内存
int64_t free = _YYDeviceMemoryFree(); // 获取设备的容量
int64_t max = MIN(total * 0.2, free * 0.6); // 比较内存的0.2倍以及容量的0.6倍最小值
max = MAX(max, BUFFER_SIZE); // 如果不够 10 M,则以 10 M 作为最大缓冲区大小
/** _maxBufferSize 内部帧缓冲区大小
* 当设备有足够的空闲内存时,这个视图将请求并解码一些或所有未来的帧图像进入一个内部缓冲区。
* 默认值为0 如果这个属性的值是0,那么最大缓冲区大小将根据当前的状态进行动态调整设备释放内存。否则,缓冲区大小将受到此值的限制。
* 当收到内存警告或应用程序进入后台时,缓冲区将被立即释放
*/
if (_maxBufferSize) max = max > _maxBufferSize ? _maxBufferSize : max; //得出缓冲区的最大值
double maxBufferCount = (double)max / (double)bytes;
if (maxBufferCount < 1) maxBufferCount = 1;
else if (maxBufferCount > 512) maxBufferCount = 512;
_maxBufferCount = maxBufferCount; // 最大缓冲数
}
复制代码
动态求出最大缓冲数--->参考
/* 从自定义的 start 方法中调用 main 方法
调用[self didMoved]; 从而调用此方法
*/
- (void)main {
__strong YYAnimatedImageView *view = _view;
if (!view) return;
if ([self isCancelled]) return;
view->_incrBufferCount++;
//动态调整当前内存的缓冲区大小。
if (view->_incrBufferCount == 0) [view calcMaxBufferCount];
if (view->_incrBufferCount > (NSInteger)view->_maxBufferCount) {
view->_incrBufferCount = view->_maxBufferCount;
}
NSUInteger idx = _nextIndex; // 获取 Operation 中传过来的 下一个索引值
NSUInteger max = view->_incrBufferCount < 1 ? 1 : view->_incrBufferCount; // 当前的缓冲区计数
NSUInteger total = view->_totalFrameCount; // 总图片帧数
view = nil;
for (int i = 0; i < max; i++, idx++) {
@autoreleasepool {
if (idx >= total) idx = 0;
if ([self isCancelled]) break;
__strong YYAnimatedImageView *view = _view;
if (!view) break;
LOCK_VIEW(BOOL miss = (view->_buffer[@(idx)] == nil)); // 拿索引值去当前缓冲区取图片
// 如果没有取到图片,则在子线程重新解码,得到解码后的图片
if (miss) {
// 等到当前还未解码的图片
UIImage *img = [_curImage animatedImageFrameAtIndex:idx];
NSLog(@"当前线程---%@", [NSThread currentThread]); // 打印当前线程,每次打印都是 name = (null),说明在异步线程
// 在异步线程再次调用解码图片,如果无法解码或已经解码就返回self
img = img.yy_imageByDecoded;
if ([self isCancelled]) break;
LOCK_VIEW(view->_buffer[@(idx)] = img ? img : [NSNull null]); // 每次添加一张图片到 _buffer 数组
view = nil;
}
}
}
}
复制代码
该方法负责把图片存入缓冲区中。(过程:取未解码图片-->解码存入缓冲区)
在此,对YYImage框架完毕了,希望大家都能从大神源码学到知识。
其他额外收获:
1、是否模拟器
- (BOOL)isSimulator {
size_t size;
sysctlbyname("hw.machine", NULL, &size, NULL, 0);
char *machine = malloc(size);
sysctlbyname("hw.machine", machine, &size, NULL, 0);
NSString *model = [NSString stringWithUTF8String:machine];
free(machine);
return [model isEqualToString:@"x86_64"] || [model isEqualToString:@"i386"];
}
复制代码
2、根据不同的系统 scale 选择图片
/** 一个NSNumber对象数组,根据不同的系统scale返回数组内部不同顺序的数字
e.g. iPhone3GS:@[@1,@2,@3] iPhone5:@[@2,@3,@1] iPhone6 Plus:@[@3,@2,@1]
*/
static NSArray *_NSBundlePreferredScales() {
static NSArray *scales;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
CGFloat screenScale = [UIScreen mainScreen].scale;
if (screenScale <= 1) {
scales = @[@1,@2,@3];
} else if (screenScale <= 2) {
scales = @[@2,@3,@1];
} else {
scales = @[@3,@2,@1];
}
});
return scales;
}
复制代码
咋一看,这不是单例吗?保证初始化代码只执行一次,可移步单例相关文章
3、判断图片后缀
NSArray *exts = ext.length > 0 ? @[ext] : @[@"", @"png", @"jpeg", @"jpg", @"gif", @"webp", @"apng"];
NSArray *scales = _NSBundlePreferredScales();
for (int s = 0; s < scales.count; s++) {
scale = ((NSNumber *)scales[s]).floatValue;
NSString *scaledName = _NSStringByAppendingNameScale(res, scale);
for (NSString *e in exts) {
path = [[NSBundle mainBundle] pathForResource:scaledName ofType:e];
if (path) break;
}
if (path) break;
}
复制代码
如果图片没标明后缀,则遍历后缀数组,并添加后缀到传进来的图片名,最后到
mainBundle
里面取图片路径,取到地址则停止
CF_RETURNS_RETAINED
标记返回CF类型的函数,该类型需要调用方释放 NSDefaultRunLoopMode
保持gif 图在scrollView 拉动时不停止 |= 为按位或运算符 eg: a|=b;
相当于 a=a|b;
参考: 快速解决GIF图的锯齿问题