YYImage 是一个强大的iOS图像框架,是YYKit 的组件之一。具体用法可以参考Demo。
YYImageEncoder
负责编码, YYImageDecoder
负责解码, YYImageFrame
负责管理帧图像信息, _YYImageDecoderFrame
内部私有类是其子类 运行Demo,进入Animated Image 。有五种播放的图片动画,前三个图像格式分别是GIF,WebP,APNG,后两个分别转化成 YYFrameImage,YYSpriteSheetImage ,全部都是通过YYAnimatedImageView 播放动画的,我们进入 YYAnimatedImageView 来分析
- (instancetype)initWithImage:(UIImage *)image {
self = [super init];
_runloopMode = NSRunLoopCommonModes;
_autoPlayAnimatedImage = YES;
self.frame = (CGRect) {CGPointZero, image.size };
self.image = image;
return self;
}
初始化方法只是一些简单的属性设置,默认 _autoPlayAnimatedImage
为YES,自动开启动画, _runloopMode
为 NSRunLoopCommonModes
,避免滑动时动画停止,因为以上的五种动画都是通过 CADisplayLink
来实现的动画, 滑动时 mode 会切换到 UITrackingRunLoopMode
导致 _link
失效,代码中实现如下 ,保证滑动时依然有效
if (_runloopMode) {
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:_runloopMode];
}
通过设置 self.image = image;
传入 YYAnimatedImageTypeImage
type
- (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];
}
- (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) {
//是否只显示部分图像,YYImage中只有YYSpriteSheetImage 实现了该协议方法
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) {
//获取首张尺寸大小,Demo中为 {{0, 0}, {32, 32}}
CGRect rect = [((UIImage<YYAnimatedImage> *) 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];
}
如果当前是 YYSpriteSheetImage
会进入到 [self setContentsRect:rect forImage:newVisibleImage];
此时设置 self.layer.contentsRect = CGRectMake(0, 0, 32 / image.size.width, 32 / image.size.height);
即裁剪的第一张图像
- (void)stopAnimating {
[super stopAnimating];
[_requestQueue cancelAllOperations];
_link.paused = YES;
self.currentIsPlayingAnimation = NO;
}
- (void)startAnimating {
YYAnimatedImageType type = [self currentImageType];
if (type == YYAnimatedImageTypeImages || type == YYAnimatedImageTypeHighlightedImages) {
// UIImageView 原始动画
NSArray *images = [self imageForType:type];
if (images.count > 0) {
[super startAnimating];
self.currentIsPlayingAnimation = YES;
}
} else {
// 自定义动画
if (_curAnimatedImage && _link.paused) {
_curLoop = 0;
_loopEnd = NO;
_link.paused = NO;
self.currentIsPlayingAnimation = YES;
}
}
}
准备工作,调用 resetAnimated
, 在 dispatch_once
中初始化 _requestQueue
, 设置 maxConcurrentOperationCount
为1,串行执行请求图像任务,初始化 _link
,设置target为 _YYImageWeakProxy
,传入 self
赋值给 weak 属性 target,避免循环引用,添加到 mainRunLoop,mode 设置为 NSRunLoopCommonModes
// init the animated params.
- (void)resetAnimated {
dispatch_once(&_onceToken, ^{
_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;
[[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];
});
}
);
//初始化属性设置
_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;
}
开启 _link
,以大约每秒60次的频率(屏幕刷新频率)调用 step:(CADisplayLink *)link
其中 _buffer
存储图像数据, _YYAnimatedImageViewFetchOperation
用于获取图像数据 实现代码在 main
方法中的 UIImage *img = [_curImage animatedImageFrameAtIndex:idx];
- (void)step:(CADisplayLink *)link {
//当前显示的图像, 必须遵守 YYAnimatedImage 协议
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;
}
NSTimeInterval delay = 0;
//下张图像存在
if (!_bufferMiss) {
// 累加时间,保证当前图像的显示时间为delay
_time += link.duration;
delay = [image animatedImageDurationAtIndex:_curIndex];
if (_time < delay) return;
//减去当前图像时间,保证下张图像显示时间正确
_time -= delay;
if (nextIndex == 0) {
//循环次数加1
_curLoop++;
if (_curLoop >= _totalLoop && _totalLoop != 0) {
//总循环次数不为0时,且当前循环次数大于总循环次数,关闭动画
_loopEnd = YES;
[self stopAnimating];
[self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep
return; // stop at last frame
}
}
// 如果当前累加时间还是大于下张显示时间,设置累加时间为delay,避免直接跳过下张图像显示
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)];
}
//更新当前Index
[self willChangeValueForKey:@"currentAnimatedImageIndex"];
_curIndex = nextIndex;
[self didChangeValueForKey:@"currentAnimatedImageIndex"];
// 更新当前图像
_curFrame = bufferedImage == (id)[NSNull null] ? nil : bufferedImage;
if (_curImageHasContentsRect) {
// YYSpriteSheetImage :获取当前Index下的部分图像 Rect,设置 对应的 self.layer.contentsRect
_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) {
//更新图像 layer.contents = (__bridge id)_curFrame.CGImage;
[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 获取下一张图像
_YYAnimatedImageViewFetchOperation *operation = [_YYAnimatedImageViewFetchOperation new];
operation.view = self;
operation.nextIndex = nextIndex;
operation.curImage = image;
[_requestQueue addOperation:operation];
}
}
_YYAnimatedImageViewFetchOperation
是 NSOperation
的子类, 用来执行获取图像操作。通过自定义main 方法实现,每添加一个 operation
, _incrBufferCount ++
, 遍历 _buffer
, 获取丢失的图像。具体获取图像通过协议方法 animatedImageFrameAtIndex:
获取
- (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++) {
//遍历当前所需的图像 Index,按下一张显示图像Index 开始查找
@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];
img = img.yy_imageByDecoded;
if ([self isCancelled]) break;
LOCK_VIEW(view->_buffer[@(idx)] = img ? img : [NSNull null]);
view = nil;
}
}
}
}
获取图像
YYImage
YYFrameImage
YYSpriteSheetImage
都实现了 YYAnimatedImage
协议方法,后两个比较简单,主要来看 YYImage
, 内部获取图像通过 _decoder
实现, 其中 preloadAllAnimatedImageFrames
属性可以用来预先加载所有图像数据
- (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;
}
最终获取图像调用到 YYImageDecoder
内部,返回一个 YYImageFrame
实例,存储了图像信息。可以参考文章 移动端图片格式调研
- (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;
}
图像解码主要方法
根据传入的图像data 更新数据到 _frames
数组 ,里面存储的是 _YYImageDecoderFrame
。大致有三类图片数据,webP,APNG, 其他。
webP 图片通过Google的 WebP.framework 实现,APNG是 自定义实现的图像解码,其他的通过 ImageIO 框架实现的
- (void)_updateSource {
switch (_type) {
case YYImageTypeWebP: {
[self _updateSourceWebP];
} break;
case YYImageTypePNG: {
[self _updateSourceAPNG];
} break;
default: {
[self _updateSourceImageIO];
} break;
}
}
主要看一下 _updateSourceImageIO
- (void)_updateSourceImageIO {
//图像宽,高,显示方向初始化, 循环次数, 0 代表无限
_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 {
if (_type == YYImageTypePNG) { // use custom apng decoder and ignore multi-frame
_frameCount = 1;
}
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++) {
_YYImageDecoderFrame *frame = [_YYImageDecoderFrame new];
frame.index = i;
frame.blendFromIndex = i;
frame.hasAlpha = YES;
frame.isFullSize = YES;
[frames addObject:frame];
// 获取图像源属性信息
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.
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);
}
// gif 图像时间
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);
}
通过最初的图像解码得到指定Index的frame,
_YYImageDecoderFrame *frame = [(_YYImageDecoderFrame *)_frames[index] copy];
采用画布进行渲染, 最终获得图像 frame.image = image;