前言
YYImage 是 YYKit 框架中一款强大的图片处理工具,作者 ibireme,其中包括了图片的编解码以及动图的播放等功能
而网络图片的加载是YYWebImage
,其逻辑和 SDWebImage
类似,使用了 YYCache 编写缓存框架,YYImage 作为图片编解码工具编,使用方式和 SDWebImage
类似,由于依赖关系,使用 pod
导入 YYWebImage
会自动导入依赖库,即 YYCache
和 YYImage
YYImage简介
YYImage框架主要有下面几个文件组成:
YYImage: 继承自 UIImage
,遵循 YYAnimatedImage
协议,作为基本图片加载的入口,并重写了UIImage的类方法imageNamed
:,能加载png、jpg、gif等多种格式图片
YYFrameImage: 继承自 UIImage
,遵循 YYAnimatedImage
协议,主要负责帧动画图片的加载,而帧动画为一组图片(就想ppt一样,一旦快速翻起来就成了动画),而帧动画由图片帧和图片每帧时间段组成,最后根据这些参数形成动画
YYSpriteSheetImage: 继承自 UIImage
,遵循 YYAnimatedImage
协议,主要负责精灵动画的加载,而精灵动画与其他动画不一样的是,精灵动画由一张图片组成(即使gif解压出来也是一组图片),这一张图片包含了很多内容,例如:一个角色的多个动作,分别保存到图片的不同位置,每个位置的图片信息都会被保存到一个json文件中,其中包含了每一个图片的frame等信息,一般在游戏中使用此方案,避免了多张图片的管理 参考地址
YYAnimatedImageView: 继承自 UIImageView
,是UIKit显示图片的接口,其主要负责动画图片播放等,配合UIImage
、YYImage
、YYFrameImage
、YYSpriteSheetImage
使用,且由于内部动画是使用 CADisplayLink
编写,动画可以放runloop
的DefaultMode
中执行,避免了滚动视图滑动时性能的消耗,默认为CommontMode
,若想成功播放gif等动图
,必须使用该类,而不是UIImageView
YYImageCoder: 包含了YYImageEncoder
、YYImageDecoder
、YYImageFrame
等类,主要负责图片编码、解码,以及图片相关信息的保存
YYImage特性
支持以下类型动画图像的播放/编码/解码:WebP, APNG, GIF
支持以下类型静态图像的显示/编码/解码:WebP, PNG, GIF, JPEG, JP2, TIFF, BMP, ICO, ICNS
支持以下类型图片的渐进式/逐行扫描/隔行扫描解码:PNG, GIF, JPEG, BMP
支持多张图片构成的帧动画播放,支持单张图片的 sprite sheet 动画
高效的动态内存缓存管理,以保证高性能低内存的动画播放
完全兼容 UIImage 和 UIImageView,使用方便
保留可扩展的接口,以支持自定义动画
每个类和方法都有完善的文档注释
YYImage源码探究
YYImage主要由 YYImage
、YYFrameImage
、 YYSpriteSheetImage
、 YYAnimatedImageView
、 YYImageCoder
文件组成,下面来循序渐进介绍
其中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
为图片的解压缩方案处理,代码如下所示
平时加载的动图无非三种 WebP、APNG、GIF
- (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就介绍到这里,如果想要了解更细节的内容,可以查看 源码, 如果还不知道怎么使用的,或者有使用问题,里面也有案例介绍
最后每次分享都是一份收货