特性
- 支持以下类型动画图像的播放/编码/解码:
WebP, APNG, GIF。 - 支持以下类型静态图像的显示/编码/解码:
WebP, PNG, GIF, JPEG, JP2, TIFF, BMP, ICO, ICNS。 - 支持以下类型图片的渐进式/逐行扫描/隔行扫描解码:
PNG, GIF, JPEG, BMP。 - 支持多张图片构成的帧动画播放,支持单张图片的 sprite sheet 动画。
- 高效的动态内存缓存管理,以保证高性能低内存的动画播放。
- 完全兼容 UIImage 和 UIImageView,使用方便。
- 保留可扩展的接口,以支持自定义动画。
- 每个类和方法都有完善的文档注释。
基础用法
显示动画类型的图片
UIImage *image = [YYImage imageNamed:@"ani.gif"];
UIImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image];
[self.view addSubview:imageView];
播放帧动画
// 文件: 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];
播放 sprite sheet 动画
// 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];
动画播放控制
YYAnimatedImageView *imageView = ...;
// 暂停:
[imageView stopAnimating];
// 播放:
[imageView startAnimating];
// 设置播放进度:
imageView.currentAnimatedImageIndex = 12;
// 获取播放状态:
image.currentIsPlayingAnimation;
//上面两个属性都支持 KVO。
图片解码
// 解码单帧图片:
NSData *data = [NSData dataWithContentsOfFile:@"/tmp/image.webp"];
YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:2.0];
UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;
// 渐进式图片解码 (可用于图片下载显示):
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;
图片编码
// 编码静态图 (支持各种常见图片格式):
YYImageEncoder *jpegEncoder = [[YYImageEncoder alloc] initWithType:YYImageTypeJPEG];
jpegEncoder.quality = 0.9;
[jpegEncoder addImage:image duration:0];
NSData jpegData = [jpegEncoder encode];
// 编码动态图 (支持 GIF/APNG/WebP):
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];
图片类型探测
// 获取图片类型
YYImageType type = YYImageDetectType(data);
if (type == YYImageTypePNG) ...
类结构
类功能
YYFrameImage (普通图片序列动画图片)
YYFrameImage 是动画帧存储对象,属于一个模型类,类属性结构如下
// 动画帧
@interface YYImageFrame : NSObject
// 帧的索引
@property (nonatomic) NSUInteger index; ///< Frame index (zero based)
// 帧的宽度
@property (nonatomic) NSUInteger width; ///< Frame width
// 帧的高度
@property (nonatomic) NSUInteger height; ///< Frame height
// 帧的水平偏移量
@property (nonatomic) NSUInteger offsetX; ///< Frame origin.x in canvas (left-bottom based)
// 帧的垂直偏移量
@property (nonatomic) NSUInteger offsetY; ///< Frame origin.y in canvas (left-bottom based)
// 帧的持续时间
@property (nonatomic) NSTimeInterval duration; ///< Frame duration in seconds
// 在呈现下一帧之前如何处理当前帧显示的区域
@property (nonatomic) YYImageDisposeMethod dispose; ///< Frame dispose method.
// 当前帧与原画布之间的透明区域混合模式
@property (nonatomic) YYImageBlendOperation blend; ///< Frame blend operation.
// 源图片
@property (nullable, nonatomic, strong) UIImage *image; ///< The image.
// 初始化方法
+ (instancetype)frameWithImage:(UIImage *)image;
@end
YYImageDisposeMethod 枚举
/**
Dispose method specifies how the area used by the current frame is to be treated
before rendering the next frame on the canvas.
*/
typedef NS_ENUM(NSUInteger, YYImageDisposeMethod) {
/**
No disposal is done on this frame before rendering the next; the contents
of the canvas are left as is.
*/
// 在呈现下一个帧之前,不需要对该帧进行任何处理;画布的内容保持原样。
YYImageDisposeNone = 0,
/**
The frame's region of the canvas is to be cleared to fully transparent black
before rendering the next frame.
*/
// 在绘制下一帧之前,画布的框架区域将被清除为完全透明的黑色。
YYImageDisposeBackground,
/**
The frame's region of the canvas is to be reverted to the previous contents
before rendering the next frame.
*/
// 画布的框架区域将在呈现下一帧之前恢复到先前的内容。
YYImageDisposePrevious,
};
YYImageBlendOperation 图像透明区域的混合方式
/**
Blend operation specifies how transparent pixels of the current frame are
blended with those of the previous canvas.
*/
typedef NS_ENUM(NSUInteger, YYImageBlendOperation) {
/**
All color components of the frame, including alpha, overwrite the current
contents of the frame's canvas region.
*/
// 框架的所有颜色组件,包括alpha,覆盖框架画布区域的当前内容。
YYImageBlendNone = 0,
/**
The frame should be composited onto the output buffer based on its alpha.
*/
// 帧应该基于alpha值合成到输出缓冲区中。
YYImageBlendOver,
};
YYImageDecoder (图片解码器)
图片类型,根据不同类型进行不同的解码
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
};
YYImageDecoder 类属性结构
// 图片解码器
@interface YYImageDecoder : NSObject
// 图片原始为解码的二进制数据
@property (nullable, nonatomic, readonly) NSData *data; ///< Image data.
// 图片类型
@property (nonatomic, readonly) YYImageType type; ///< Image data type.
// 图片倍数
@property (nonatomic, readonly) CGFloat scale; ///< Image scale.
// 总帧数
@property (nonatomic, readonly) NSUInteger frameCount; ///< Image frame count.
// 循环次数。如果为 0,表示无限循环
@property (nonatomic, readonly) NSUInteger loopCount; ///< Image loop count, 0 means infinite.
// 图片宽
@property (nonatomic, readonly) NSUInteger width; ///< Image canvas width.
// 图片高
@property (nonatomic, readonly) NSUInteger height; ///< Image canvas height.
// 是否解码结束
@property (nonatomic, readonly, getter=isFinalized) BOOL finalized;
@end
YYImageDecoder 为 YYImage 框架的核心类,用于针对不同格式图片的解码,动画的帧解码。
初始化方法
/**
* 根据一个图片数据和解码图片倍数实例化解码器
* data 需解码的数据
* scale 解码的倍数
*/
+ (instancetype)decoderWithData:(NSData *)data scale:(CGFloat)scale {
if (!data) return nil;
// 初始化解码器
YYImageDecoder *decoder = [[YYImageDecoder alloc] initWithScale:scale];
// 更新数据
[decoder updateData:data final:YES];
// 判断解码之后的数据帧的数量
if (decoder.frameCount == 0) return nil;
// 返回解码器
return decoder;
}
初始化方法 initWithScale: 中记录类 scale,并且初始化了一个信号量以及一个 c语言的互斥锁
- (instancetype)initWithScale:(CGFloat)scale {
self = [super init];
if (scale <= 0) scale = 1;
// 初始化图的倍数
_scale = scale;
// 初始化信号量
_framesLock = dispatch_semaphore_create(1);
// 声明互斥锁属性对象
pthread_mutexattr_t attr;
// 初始化互斥锁属性对象
pthread_mutexattr_init (&attr);
// 互斥锁类型
// PTHREAD_MUTEX_NORMAL 描述: 此类型的互斥锁不会检测死锁
// PTHREAD_MUTEX_ERRORCHECK 描述: 此类型的互斥锁可提供错误检查
// PTHREAD_MUTEX_RECURSIVE 描述: 如果线程在不首先解除锁定互斥锁的情况下尝试重新锁定该互斥锁,则可成功锁定该互斥锁
// PTHREAD_MUTEX_DEFAULT 描述: 如果尝试以递归方式锁定此类型的互斥锁,则会产生不确定的行为
pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE);
// 初始化互斥锁
pthread_mutex_init (&_lock, &attr);
// 销毁互斥锁属性对象
pthread_mutexattr_destroy (&attr);
return self;
}
紧接着 进入 “updateData: final:” 方法,updateData: final: 方法内为了避免多线程带来的数据错乱,加锁调用了“_updateData: final:” 方法,_updateData: final: 干了两件事
- 类型检测
- 更新数据源
内部的实现如下:
- (BOOL)_updateData:(NSData *)data final:(BOOL)final {
// 如果已经完成 就返回失败
if (_finalized) return NO;
// 如果 更新的 date 数据大小 小于,就不用更新了,直接返回失败
if (data.length < _data.length) return NO;
//
_finalized = final;
// 赋值 data
_data = data;
// 获取图片格式
YYImageType type = YYImageDetectType((__bridge CFDataRef)data);
// 数据源类型检测,初始化时,方法第一次调用这个值应该是为 NO,此后都为 YES
if (_sourceTypeDetected) {
// 如果更新的的图片数据格式跟之前的的图片格式不一致,直接返回格式错误
if (_type != type) {
return NO;
// 如果更新的图片格式跟之前的图片格式一致,则更新数据源
} else {
[self _updateSource];
}
} else {
//
if (_data.length > 16) {
// 赋值图片格式
_type = type;
// 数据源类型检测完成
_sourceTypeDetected = YES;
// 更新数据源
[self _updateSource];
}
}
return YES;
}
紧接着进入 _updateSource 方法内对不同格式的图片做了分发,一共分为三组,YYImageTypeWebP、YYImageTypePNG、YYImageTypeImageIO,这里我主要介绍一下,更新数据源中 YYImageTypeImageIO 对应 _updateSourceImageIO方法
- (void)_updateSourceImageIO {
// 高度
_width = 0;
// 宽度
_height = 0;
// 方向
_orientation = UIImageOrientationUp;
// 循环次数
_loopCount = 0;
// 上锁
dispatch_semaphore_wait(_framesLock, DISPATCH_TIME_FOREVER);
_frames = nil;
// 解锁
dispatch_semaphore_signal(_framesLock);
// 处理图像源
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;
// 获取图像帧数
_frameCount = CGImageSourceGetCount(_source);
if (_frameCount == 0) return;
if (!_finalized) { // ignore multi-frame before finalized
_frameCount = 1;
} else {
// PNG一帧
if (_type == YYImageTypePNG) { // use custom apng decoder and ignore multi-frame
_frameCount = 1;
}
// GIF多帧
if (_type == YYImageTypeGIF) { // get gif loop count
// 获取数据源属性字典
CFDictionaryRef properties = CGImageSourceCopyProperties(_source, NULL);
if (properties) {
// 根据Key kCGImagePropertyGIFDictionary 获取到GIF下的字典属性
CFDictionaryRef gif = CFDictionaryGetValue(properties, kCGImagePropertyGIFDictionary);
// 获取到gif 字典
if (gif) {
// 获取循环次数 根据Key kCGImagePropertyGIFLoopCount
CFTypeRef loop = CFDictionaryGetValue(gif, kCGImagePropertyGIFLoopCount);
// _loopCount 地址进去赋值
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++) {
// 每一帧对象属性 都继承自 YYImageFrame
_YYImageDecoderFrame *frame = [_YYImageDecoderFrame new];
// 当前索引
frame.index = i;
frame.blendFromIndex = i;
frame.hasAlpha = YES;
frame.isFullSize = YES;
[frames addObject:frame];
// 根据数据源获取属性字典 (刚才上面的获取方式是拿循环次数的时候GIF专用key,这里有多种情况,就根据下标拿)
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);
// 如果是 GIF 图片
if (_type == YYImageTypeGIF) {
// 依然获取 GIF 图的属性
CFDictionaryRef gif = CFDictionaryGetValue(properties, kCGImagePropertyGIFDictionary);
if (gif) {
// Use the unclamped frame delay if it exists.
// 延迟
value = CFDictionaryGetValue(gif, kCGImagePropertyGIFUnclampedDelayTime);
if (!value) {
// Fall back to the clamped frame delay if the unclamped frame delay does not exist.
value = CFDictionaryGetValue(gif, kCGImagePropertyGIFDelayTime);
}
// 获取每一帧持续时间
if (value) CFNumberGetValue(value, kCFNumberDoubleType, &duration);
}
}
// 赋值宽/高/单帧持续时长
frame.width = width;
frame.height = height;
frame.duration = duration;
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);
}
这个方法执行完成,解码器简单初始化是为了搜集每一帧的宽度、高度、持续时间,原始方向、循环次数一些简单属性,这里每一帧的对象是以_YYImageDecoderFrame继承于YYImageFrame的私有类保存,然后统一存储到frames数组里面,这里的图片只是显示了每一帧的基本信息保存而已,图片还是没有解码的。
提取单帧图片
解码器提取单针图片的对外开发的方法名为 “frameAtIndex:decodeForDisplay:”,其内部加锁调用了私有方法“ _frameAtIndex:index decodeForDisplay:”
- (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;
}
// 根据 index 提取对应帧的解码图片,下面方法就是解码提取图片的核心方法
- (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) { // ICO contains multi-size frame and should not extend to canvas.
extendToCanvas = YES;
}
// 非APNG、WebP混合,_needBlend 为一个私有成员变量,只有在 “_updateSourceWebP”和“_updateSourceAPNG” 方法中才会用对其值进行更改,默认为 NO,平常在使用过程中基本上都会走入这个判断
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;
}
// blend
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把原始位图绘制到上下文
// CFDataRef rawData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));该方法可以获取到原始位图信息
CGContextDrawImage(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height), unblendedImage);
CFRelease(unblendedImage);
}
// CGBitmapContextCreateImage创建一个新的解压后的位图
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;
}
而实际核心图片解码部分是在 YYCGImageCreateDecodedCopy() 函数内完成的
/**
把图片用 CGContextDrawImage() 绘制到画布上,然后把画布的数据取出来当作图片
*/
CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) {
if (!imageRef) return NULL;
// 获取图片宽
size_t width = CGImageGetWidth(imageRef);
// 获取图片高
size_t height = CGImageGetHeight(imageRef);
// 如果宽高等于 0 ,返回 NULL
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;
// 通过CGBitmapContextCreate创建位图上下文
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, YYCGColorSpaceGetDeviceRGB(), bitmapInfo);
if (!context) return NULL;
// 通过CGContextDrawImage 把原始位图绘制到上下文 CFDataRef rawData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));该方法可以获取到原始位图信息
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); // decode
// CGBitmapContextCreateImage 创建一个新的解压后的位图
CGImageRef newImage = CGBitmapContextCreateImage(context);
// 释放图形上下文
CFRelease(context);
// 返回图片
return newImage;
} else {
CGColorSpaceRef space = CGImageGetColorSpace(imageRef);
size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef);
size_t bitsPerPixel = CGImageGetBitsPerPixel(imageRef);
size_t bytesPerRow = CGImageGetBytesPerRow(imageRef);
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
if (bytesPerRow == 0 || width == 0 || height == 0) return NULL;
CGDataProviderRef dataProvider = CGImageGetDataProvider(imageRef);
if (!dataProvider) return NULL;
CFDataRef data = CGDataProviderCopyData(dataProvider); // decode
if (!data) return NULL;
CGDataProviderRef newProvider = CGDataProviderCreateWithCFData(data);
CFRelease(data);
if (!newProvider) return NULL;
CGImageRef newImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, space, bitmapInfo, newProvider, NULL, false, kCGRenderingIntentDefault);
CFRelease(newProvider);
return newImage;
}
}
解码图片大致分为四步
- 取出帧数据以及详细信息,使用 YYFrameImage 包装起来
- 函数创建一个与图片等宽等高的图形上下文对象,
- 函数将帧数据绘制到刚创建的 图形上下文中
- 从图形上下文中取出绘制的图片,并销毁释放图形上下文
YYImage (Webp、APNG、GIF 动画图片)
YYImage 是继承自 UIImage,是对动画图像数据的高级封装,同时实现了动画协议
下面来从“imageNamed:”方法看一下 YYImage 的具体实现
+ (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;
// If no extension, guess by system supported (same as 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;
}
// 如果路径为 nil,return nil,初始化失败,参数有误
if (path.length == 0) return nil;
// 取出图片数据,如果数据为空,初始化失败,数据有误
NSData *data = [NSData dataWithContentsOfFile:path];
if (data.length == 0) return nil;
// 调用初始化方法
return [[self alloc] initWithData:data scale:scale];
}
/**
* 实例化方法
* data 图片二进制数据
* scale 图片的倍数,也就是几倍图中的倍,了解了吗
*/
- (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 {
// 创建一个解码器
YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];
// 根据index从解码器的数组里面提取出 _YYImageDecoderFrame 然后对图片源根据index解码出对应的帧图片存储到frame的image字段返回
YYImageFrame *frame = [decoder frameAtIndex:0 decodeForDisplay:YES];
// 获得上一个方法的解码图片,赋值。
UIImage *image = frame.image;
// 如果解码图片为空,表示解码失败,返回 nil
if (!image) return nil;
// 使用一张 CGImage 初始化 一张scale倍的 UIImage
self = [self initWithCGImage:image.CGImage scale:decoder.scale orientation:image.imageOrientation];
// 初始化失败
if (!self) return nil;
// 图片类型
_animatedImageType = decoder.type;
// 如果 decoder 中的帧数 大于 1,表示的是一个动态图,这里没毛病的吧
if (decoder.frameCount > 1) {
// 记录下这个解码器以及解码器里面的数据
_decoder = decoder;
// 获取整张动态图片单帧所占用的字节数
_bytesPerFrame = CGImageGetBytesPerRow(image.CGImage) * CGImageGetHeight(image.CGImage);
// 计算出整张动态图所有帧所需的自己空间大小
_animatedImageMemorySize = _bytesPerFrame * decoder.frameCount;
}
//图片解码成功
self.yy_isDecodedForDisplay = YES;
//结束自动释放池
}
return self;
}
可以看到 YYImage 在初始化的时候就已经把图片的第一帧数据给取了出来,YYImage 对象本身就是代表图片的第一帧数据,其他的帧数据存储在 _decoder 成员变量当中。其他帧数据可以通过
// 总帧数
- (NSUInteger)animatedImageFrameCount {
return _decoder.frameCount;
}
// 总循环次数
- (NSUInteger)animatedImageLoopCount {
return _decoder.loopCount;
}
// 总大小(字节)
- (NSUInteger)animatedImageBytesPerFrame {
return _bytesPerFrame;
}
// 取得图片第 index 帧数据
- (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index {
if (index >= _decoder.frameCount) return nil;
dispatch_semaphore_wait(_preloadedLock, DISPATCH_TIME_FOREVER);
UIImage *image = _preloadedFrames[index];
dispatch_semaphore_signal(_preloadedLock);
if (image) return image == (id)[NSNull null] ? nil : image;
return [_decoder frameAtIndex:index decodeForDisplay:YES].image;
}
// 图片对应 index 帧的持续时间
- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index {
NSTimeInterval duration = [_decoder frameDurationAtIndex:index];
/*
http://opensource.apple.com/source/WebCore/WebCore-7600.1.25/platform/graphics/cg/ImageSourceCG.cpp
Many annoying ads specify a 0 duration to make an image flash as quickly as
possible. We follow Safari and Firefox's behavior and use a duration of 100 ms
for any frames that specify a duration of <= 10 ms.
See and for more information.
See also: http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser.
*/
if (duration < 0.011f) return 0.100f;
return duration;
}
YYAnimatedImageView (动画展示视图)
YYAnimatedImageView 继承自 UIImageView, 在 UIImageView 基础之上实现了图片异步解码,以及对 YYImage、YYSpriteSheetImage、YYFrameImage 动图的支持。下面就来开始对 YYAnimatedImageView 的实现一探究竟
新增属性
@interface YYAnimatedImageView : UIImageView
// 自动播放动画 默认为YES
@property (nonatomic) BOOL autoPlayAnimatedImage;
// 当前播放的动画第几帧
@property (nonatomic) NSUInteger currentAnimatedImageIndex;
// 当前播放动画的状态
@property (nonatomic, readonly) BOOL currentIsPlayingAnimation;
// 加入的运行循环的模式
@property (nonatomic, copy) NSString *runloopMode;
// 缓冲区最大容量(字节)
@property (nonatomic) NSUInteger maxBufferSize;
@end
从一个例子开始
YYImage *image = [YYImage imageNamed:@"ani"];
YYAnimatedImageView *imageView = [YYAnimatedImageView alloc] initWithImage:image];
[view addSubView:imageView];
- 进入到 “initWithImage:” 的实现
- (instancetype)initWithImage:(UIImage *)image {
// 初始化父类
self = [super init];
// 指定到 runloop 的特定模式运行
_runloopMode = NSRunLoopCommonModes;
// 自动播放动画
_autoPlayAnimatedImage = YES;
// 大小
self.frame = (CGRect) {CGPointZero, image.size };
// 赋值图片内容
self.image = image;
return self;
}
- 进入 “setImage:” 的实现,实质上是调用了私有方法“setImage: withType:”
- (void)setImage:(UIImage *)image {
// 容错
if (self.image == image) return;
//
[self setImage:image withType:YYAnimatedImageTypeImage];
}
- 进入到 “setImage: withType:” 的实现
- (void)setImage:(id)image withType:(YYAnimatedImageType)type {
// 停止动画
[self stopAnimating];
// 重置动画
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;
// 判断 newVisibleImage 是否是 属于 UIImage 类 并且遵循了 YYAnimatedImage
if ([newVisibleImage isKindOfClass:[UIImage class]] &&
[newVisibleImage conformsToProtocol:@protocol(YYAnimatedImage)]) {
// 根据之前Decode之后的参数 返回frame帧数
newImageFrameCount = ((UIImage *) newVisibleImage).animatedImageFrameCount;
if (newImageFrameCount > 1) {
//判断 newVisibleImage 是否实现了 animatedImageContentsRectAtIndex: 方法,只有 动画精灵才会实现这个方法
hasContentsRect = [((UIImage *) newVisibleImage) respondsToSelector:@selector(animatedImageContentsRectAtIndex:)];
}
}
// 当新数据内容 不是 YYSpriteSheetImage (精灵动画),并且旧的数据内容是YYSpriteSheetImage (精灵动画)就会z进入判断体内
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];
}
}
// 记录当前的 hasContentsRect
_curImageHasContentsRect = hasContentsRect;
// 如果为新的数据为YYSpriteSheetImage(精灵动画),进入判断体内
if (hasContentsRect) {
// 获取第一帧图片显示的区域
CGRect rect = [((UIImage *) newVisibleImage) animatedImageContentsRectAtIndex:0];
// 赋值新内容的区域
[self setContentsRect:rect forImage:newVisibleImage];
}
// 多张图
if (newImageFrameCount > 1) {
// 重置动画
[self resetAnimated];
// 当前动画图片为 新的 图片
_curAnimatedImage = newVisibleImage;
// 当前显示的 帧 为 newVisibleImage
_curFrame = newVisibleImage;
// 取得总循环次数
_totalLoop = _curAnimatedImage.animatedImageLoopCount;
// 取得总帧数
_totalFrameCount = _curAnimatedImage.animatedImageFrameCount;
[self calcMaxBufferCount];
}
// 标记下一个Runloop进行刷新
[self setNeedsDisplay];
// 添加到父视图的时候是否动画
[self didMoved];
}
- 这里得特别注意得就是这个重置动画方法 “resetAnimated”
- (void)resetAnimated {
// 加入尚未初始化 link, 也就代表对象初次调用这个 reset animated 方法
if (!_link) {
// 初始化信号量
_lock = dispatch_semaphore_create(1);
// 缓冲区
_buffer = [NSMutableDictionary new];
// 初始化请求队列
_requestQueue = [[NSOperationQueue alloc] init];
// 初始化最大并发量为 1
_requestQueue.maxConcurrentOperationCount = 1;
// 监听主线程UI 的刷新
_link = [CADisplayLink displayLinkWithTarget:[_YYImageWeakProxy proxyWithTarget:self] selector:@selector(step:)];
// 指定监听 主运行循环的 _runloopMode 模式下的UI刷新频率
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];
// 加锁 小技巧 后台线程释放资源 (用局部变量捕获,把类的成员变量重新分配新的空间,然后丢到异步全局队列发送一条消息,在后台线程释放buffer资源)
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];
});
}
);
// Link 没你事了,一边呆着
_link.paused = YES;
// 初始化时间
_time = 0;
// 把当前帧索引重置 并且发出KVO通知
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;
}
- 进入到 “didMoved” 方法
- (void)didMoved {
if (self.autoPlayAnimatedImage) {
// 判断 自身有没有被添加到父视图上并且展示到窗口上时,就让动画跑起来,否则就停止动画
if(self.superview && self.window) {
[self startAnimating];
} else {
[self stopAnimating];
}
}
}
- 进入到 “startAnimating” 方法
- (void)startAnimating {
YYAnimatedImageType type = [self currentImageType];
// 系统的动画
if (type == YYAnimatedImageTypeImages || type == YYAnimatedImageTypeHighlightedImages) {
NSArray *images = [self imageForType:type];
if (images.count > 0) {
[super startAnimating];
self.currentIsPlayingAnimation = YES;
}
// YY 自定义动画
} else {
//如果有图片,而且定时器暂停了,就开启定时器,并且标记状态 currentIsPlayingAnimation 为 Yes
if (_curAnimatedImage && _link.paused) {
_curLoop = 0;
_loopEnd = NO;
_link.paused = NO;
self.currentIsPlayingAnimation = YES;
}
}
}
- 每次UI刷新都会触发 “step:”方法
// 动画的播放方法 每当监听的运行循环触发UI刷新 都会触发这个方法
- (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++;
// 如果当前循环次数 大于等于 总循环次数 并且 总循环次数不等于 0 就结束循环
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];
// 如果当前累加时间还是大于下张显示时间,设置累加时间为delay,避免直接跳过下张图像显示
if (_time > delay) _time = delay; // do not jump over frame
}
// 加锁 读取缓冲区下一张图片
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) {
[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];
}
}
- 每次UI刷新都会触发 “displayLayer:”方法,因为在刷新UI的时候使用 “[self.layer setNeedsDisplay]”标记了当前的 Layer
// 当layer被标记为 setNeedsDisplay 的时候,系统会在下一个Runloop休眠周期调用下面方法赋值
- (void)displayLayer:(CALayer *)layer {
if (_curFrame) {
// 把当前帧赋值给 Layer 的寄宿图
layer.contents = (__bridge id)_curFrame.CGImage;
}
}
_YYAnimatedImageViewFetchOperation (YYAnimatedImageView 内部匿名类)
_YYAnimatedImageViewFetchOperation 继承自 NSOperation, 负责 YYAnimatedImageView 动画帧的异步解码任务,把解码后的帧放入 YYAnimatedImageView 对象的 _buffer (缓冲区)内,解码时的线程安全使用最大并发数为 1 来控制。
/// 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
// 自定义NSOperation任务被加入到队列的时候,重写main函数会在异步线程执行任务(这里是在异步解码,线程安全操作_buffer,而且队列最大并发数是1,是串行的)
- (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) {
// 读取丢失的缓存 根据index拿出_YYImageDecoderFrame 进行帧图片解码
UIImage *img = [_curImage animatedImageFrameAtIndex:idx];
// 还是在异步线程再次调用解码 如果没有,就解码,有的话就返回self
img = img.yy_imageByDecoded;
if ([self isCancelled]) break;
// 将解码的图片存储到buffer
LOCK_VIEW(view->_buffer[@(idx)] = img ? img : [NSNull null]);
view = nil;
}
}
}
}
@end
重点
YYAnimatedImageView 设计思想,通过监听主运行循环的UI刷新来驱动动画的运行,通过当前显示的帧驱动下一帧的解码,帧解码完成后把放入帧缓冲区,当前一帧播放完成之后,就从缓冲区中取下一帧(已解码的帧,如果没有取到,就会出现掉帧的情况),并用当前帧指向从缓冲区里取出来的帧,UI在刷新的时候把当前帧数据(已解码的图片)赋值给 Layer.contents(寄宿图)属性,完成帧数据的异步解码,达到的性能优化。
核心思想 -- 总结
我们首先要知道,如果最普通的UIImageView的图片UIImage创建资源赋值,图片是没有解码的,只有当图片被被赋值给UIImageView的时候,Runloop捕获到事件,才会进行解压缩,其中会把二进制压缩的数据,解压成没有压缩的位图,这里就是最耗时的操作
既然那么耗时,为什么一定要解压缩才可以显示?那你得明白位图数据和二进制数据的区别了。
比如一张10kb的图,我们有data信息,也就是平时看到的PNG或者JPEG等后缀的格式,其中PNG支持alpha通道,无损压缩,JPEG是支持有损压缩的图片压缩格式介绍,有损无损无非就是把多余的通过代码压进去。
那么PNG还是JPEG,只是位图的压缩形式罢了。一张PNG的图,解压缩出来就是原始位图,里面装载着像素信息,颜色信息等,这才是最原始的解压后的图,只有这样,所有的信息具备,才能被渲染到屏幕上,因此拿到的图片只能解压缩才可以显示(就是必然要耗时),既然一定要解压,耗时,不能卡在主线程,那就拿到子线程解压,把解压完的图片返回之后,再次渲染的时候,捕捉到已经解压了,就不需要在主线程解压了,直接显示。这也是所有第三方图片框架下载的核心。平时如果你不在意,你压根不知道他做了什么性能优化。
在 YYImage 的图片数据类(YYImage、YYSpriteSheetImage、YYFrameImage)继承自 UIImage ,同时重写了 “initWithCoder:” 和 “encodeWithCoder:” 方法,也就是意味着这些图片数据可以当成文件存入文件系统中或者作为二进制存入本地数据库中。为后面的YYWebImage 打下了坚实的基础。
参考文献
- iOS 开发中自旋和互斥锁的理解以及所有锁的性能比较
- iOS 常见知识点(三):Lock
- 关于 @synchronized,这儿比你想知道的还要多
- 百度孙冰的runloop分享
- Y神的深入理解runloop
- iOS 处理图片的一些小 Tip
- 移动端图片格式调研