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;