YYImage是由@ibireme开发的一款功能强大的 iOS 图像框架(该项目是 YYKit 组件之一),它支持当前市场主流的静/动态图像编/解码与动态图像的动画播放显示,其主要功能如下:
- 动画类型: WebP, APNG, GIF。
- 静态图像: WebP, PNG, GIF, JPEG, JP2, TIFF, BMP, ICO, ICNS。
- 支持以下类型图片的渐进式/逐行扫描/隔行扫描解码:
PNG, GIF, JPEG, BMP。- 支持多张图片构成的帧动画播放,支持单张图片的 sprite sheet 动画。
- 高效的动态内存缓存管理,以保证高性能低内存的动画播放。
- 完全兼容 UIImage 和 UIImageView,使用方便。
- 保留可扩展的接口,以支持自定义动画。
- 每个类和方法都有完善的文档注释。
-
基础
在分析YYImage框架之前,先了解一下平常图片加载的过程:
- 先获取图片原始数据(
data buffer
),然后解码成图像元素信息(image buffer
),显示到屏幕时变成(frame buffer
)。
用代码说明就是:
UIImage *image = [[UIImage alloc]initWithName: @""];//data buffer
_imageView.image = image;//解码成image buffer,再变成frame buffer
然后就会有下面的过程:
优化图片有两点:一是内存优化,可以通过对图片适当尺寸采样显示进行优化;二是cpu优化,充分利用cpu对图片提前解码,所以也就有了YYImage
。首先看看框架使用:
YYImage *image = [YYImage imageNamed:@"name"];
YYAnimatedImageView *imageView = [[YYAnimatedImageView alloc]initWithImage:image];
[self.view addSubview:imageView];
(一)YYImage
YYImage
继承UIImage
,通过重写初始化方法来实现处理:
@interface YYImage : UIImage
+ (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;
- 直接来看看源码,内部自己实现根据名字查找图片:
@implementation 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;
}
if (path.length == 0) return nil;
NSData *data = [NSData dataWithContentsOfFile:path];//获取
if (data.length == 0) return nil;
return [[self alloc] initWithData:data scale:scale];
}
- 然后初始化解码器:
@implementation YYImage
...
- (instancetype)initWithData:(NSData *)data scale:(CGFloat)scale {
...
@autoreleasepool {
YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];//解码器
YYImageFrame *frame = [decoder frameAtIndex:0 decodeForDisplay:YES];//解码
...
}
return self;
}
@implementation YYImageDecoder {
...
+ (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;
}
...
- (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 {
...
YYImageType type = YYImageDetectType((__bridge CFDataRef)data);//判断图片类型
if (_sourceTypeDetected) {
if (_type != type) {
return NO;
} else {
[self _updateSource];
}
} else { ... }
return YES;
}
...
- (void)_updateSource {
switch (_type) {
case YYImageTypeWebP: ...
case YYImageTypePNG: ...
default: {
[self _updateSourceImageIO];
} break;
}
}
...
- (void)_updateSourceImageIO {
...
if (!_source) {
if (_finalized) {
_source = CGImageSourceCreateWithData((__bridge CFDataRef)_data, NULL);//普通输入源
} else {
_source = CGImageSourceCreateIncremental(NULL);//渐进式数据输入源
if (_source) CGImageSourceUpdateData(_source, (__bridge CFDataRef)_data, false);
}
} else { ... }
...
/*
ICO, GIF, APNG may contains multi-frame.
*/
NSMutableArray *frames = [NSMutableArray new];
for (NSUInteger i = 0; i < _frameCount; i++) {
_YYImageDecoderFrame *frame = [_YYImageDecoderFrame new];
frame.index = i;
frame.blendFromIndex = i;
frame.hasAlpha = YES;
frame.isFullSize = YES;
[frames addObject:frame];
...
}
...
}
最后图像每一帧的信息会被保存在_YYImageDecoderFrame
中:
@interface _YYImageDecoderFrame : YYImageFrame
@property (nonatomic, assign) BOOL hasAlpha; ///< Whether frame has alpha.
@property (nonatomic, assign) BOOL isFullSize; ///< Whether frame fill the canvas.
@property (nonatomic, assign) NSUInteger blendFromIndex; ///< Blend from frame index to current frame.
@end
- 回到第2步,进行解码:
@implementation YYImageDecoder
...
- (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;
}
...
- (YYImageFrame *)_frameAtIndex:(NSUInteger)index decodeForDisplay:(BOOL)decodeForDisplay {
if (index >= _frames.count) return 0;
_YYImageDecoderFrame *frame = [(_YYImageDecoderFrame *)_frames[index] copy];
...
if (!_needBlend) {
CGImageRef imageRef = [self _newUnblendedImageAtIndex:index extendToCanvas:extendToCanvas decoded:&decoded];//
...
UIImage *image = [UIImage imageWithCGImage:imageRef scale:_scale orientation:_orientation];
...
frame.image = image;//已经解码成image buffer的image
return frame;
}
...
}
...
- (CGImageRef)_newUnblendedImageAtIndex:(NSUInteger)index
extendToCanvas:(BOOL)extendToCanvas
decoded:(BOOL *)decoded CF_RETURNS_RETAINED {
...
if (_source) {
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(_source, index, (CFDictionaryRef)@{(id)kCGImageSourceShouldCache:@(YES)});//会立即解码
if (imageRef && extendToCanvas) {
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
if (width == _width && height == _height) {//判断是否一致
CGImageRef imageRefExtended = YYCGImageCreateDecodedCopy(imageRef, YES);//核心
...
} else { ... }
}
return imageRef;
}
}
CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) {
...
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;
} else { ... }
}
-
YYCGImageCreateDecodedCopy
是解压缩的核心,也就是渲染图片性能显著的原因。它接受一个原始的位图参数imageRef
,最终返回一个新的解压缩后的位图newImage
。
(二)YYAnimatedImageView
- 直接从初始化函数探索:
@implementation YYAnimatedImageView
...
- (instancetype)initWithImage:(UIImage *)image {
self = [super init];
_runloopMode = NSRunLoopCommonModes;
_autoPlayAnimatedImage = YES;
self.frame = (CGRect) {CGPointZero, image.size };
self.image = image;//重写setter
return self;
}
...
- (void)setImage:(UIImage *)image {
if (self.image == image) return;
[self setImage:image withType:YYAnimatedImageTypeImage];
}
...
- (void)setImage:(id)image withType:(YYAnimatedImageType)type {
[self stopAnimating];
if (_link) [self resetAnimated];//定时器播放
...
[self imageChanged];//
}
- 接着重置动画:
@implementation YYAnimatedImageView
...
- (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:)];//播放当前图片
if (_runloopMode) {
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:_runloopMode];
}
_link.paused = YES;//默认暂停
...
}
...
}
- 其实
YYAnimatedImageView
就是通过获取动态图的每一帧,利用CADisplayLink
进行播放显示。
- 然后回到第2步,开启动画:
@implementation YYAnimatedImageView
...
- (void)imageChanged {
...
[self setNeedsDisplay];
[self didMoved];
}
...
- (void)didMoved {
if (self.autoPlayAnimatedImage) {
if(self.superview && self.window) {
[self startAnimating];
} else {
[self stopAnimating];
}
}
}
@implementation YYAnimatedImageView
...
- (void)startAnimating {
YYAnimatedImageType type = [self currentImageType];
if (type == YYAnimatedImageTypeImages || type == YYAnimatedImageTypeHighlightedImages) {
...
} else {
if (_curAnimatedImage && _link.paused) {
_curLoop = 0;
_loopEnd = NO;
_link.paused = NO;//启动
self.currentIsPlayingAnimation = YES;
}
}
}
-
CADisplayLink
开启后便执行step
:
@implementation YYAnimatedImageView
...
- (void)step:(CADisplayLink *)link {
...
if (!_bufferMiss) {//核心
_time += link.duration;
delay = [image animatedImageDurationAtIndex:_curIndex];//获取这一帧的播放时间
if (_time < delay) return; //小于
_time -= delay; //大于
...
delay = [image animatedImageDurationAtIndex:nextIndex];
if (_time > delay) _time = delay; // do not jump over frame //避免跳过下一帧
}
...
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];//子线程执行
}
}
最后会启动_YYAnimatedImageViewFetchOperation
:
@implementation _YYAnimatedImageViewFetchOperation
- (void)main {
...
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
@implementation YYImage
...
- (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index {
...
return [_decoder frameAtIndex:index decodeForDisplay:YES].image;//解压缩图片
}
- 总结
-
补充
在resetAnimated
函数中其实有一个普通而厉害的操作:
- (void)resetAnimated {
...
LOCK(
if (_buffer.count) {//因为图片数据比较大,所以用子线程异步释放对象
NSMutableDictionary *holder = _buffer;//释放压力转移到holder,由holder去持有然后子线程释放
_buffer = [NSMutableDictionary new];//清空_buffer;因为释放的不是_buffer,而是_buffer指向的内存空间
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];//就是随便发一条消息以便在该子线程执行一下,然后走向释放,把内容释放;如果在子线程:_buffer=nil,_buffer指向的内存空间还是会在主线程释放
});
}
);
...
}