YYAnimatedImageView
是UIImageView
的子类,如果image
和highlightedImage
属性继承YYAnimatedImage
协议, 则可以显示多帧动画图像,可以使startAnimation
和stopAnimation
控制动画.YYAnimatedImageView
会缓存一些帧,来减小CPU消耗.缓存的大小会根据可用的内存改变.
YYAnimatedImageView接口
@interface YYAnimatedImageView : UIImageView
//如果图片数据有多帧,设置为`YES`时,当view 可见/消失时会自动播放/停止动画
@property (nonatomic) BOOL autoPlayAnimatedImage;
//当前显示的帧( 0 based),设置它会立即显示某一帧,支持KVO
@property (nonatomic) NSUInteger currentAnimatedImageIndex;
//是否是播放状态,支持KVO
@property (nonatomic, readonly) BOOL currentIsPlayingAnimation;
//动画timer的运行模式,默认NSRunLoopCommonModes
@property (nonatomic, copy) NSString *runloopMode;
//缓存的最大限制,设置为0会根据系统可用内存动态计算
@property (nonatomic) NSUInteger maxBufferSize;
@end
YYAnimatedImage协议
@protocol YYAnimatedImage <NSObject>
@required
//动画的总帧数,小于1则其他的方法将被忽略
- (NSUInteger)animatedImageFrameCount;
//动画循环次数,0是无限循环
- (NSUInteger)animatedImageLoopCount;
//每帧图片的大小,用来决定缓存buffer大小
- (NSUInteger)animatedImageBytesPerFrame;
//使用索引获取某一帧,可能会在后台线程调用
- (nullable UIImage *)animatedImageFrameAtIndex:(NSUInteger)index;
//某一帧的动画时长
- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index;
@optional
//图片坐标系的子区域,用来显示 sprite animation
- (CGRect)animatedImageContentsRectAtIndex:(NSUInteger)index;
@end
设置image
- (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];
//清空当前帧数据
_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;
//获取到的是UIImage类型,并且继承了YYAnimatedImage 说明可能是动图
if ([newVisibleImage isKindOfClass:[UIImage class]] &&
[newVisibleImage conformsToProtocol:@protocol(YYAnimatedImage)]) {
//获取图片帧数
newImageFrameCount = ((UIImage<YYAnimatedImage> *) newVisibleImage).animatedImageFrameCount;
if (newImageFrameCount > 1) {
//是否设置了 animatedImageContentsRect
hasContentsRect = [((UIImage<YYAnimatedImage> *) newVisibleImage) respondsToSelector:@selector(animatedImageContentsRectAtIndex:)];
}
}
//没有设置contentRect则恢复默认设置
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;
//设置了contentRect
if (hasContentsRect) {
//获取第一帧的animatedImageContentsRect
CGRect rect = [((UIImage<YYAnimatedImage> *) newVisibleImage) animatedImageContentsRectAtIndex:0];
//设置animatedImageContentsRect
[self setContentsRect:rect forImage:newVisibleImage];
}
//帧数大于1时
if (newImageFrameCount > 1) {
//重新设置动画
[self resetAnimated];
//保存当前动画帧
_curAnimatedImage = newVisibleImage;
//保存当前帧
_curFrame = newVisibleImage;
//保存循环次数
_totalLoop = _curAnimatedImage.animatedImageLoopCount;
//保存帧数
_totalFrameCount = _curAnimatedImage.animatedImageFrameCount;
//计算缓存大小
[self calcMaxBufferCount];
}
//标记重绘
[self setNeedsDisplay];
//更新动画状态
[self didMoved];
}
在修改image
属性后,主要要处理三件事:1.停止当前动画, 2. 解析图片是否是动图,更新与图片相关数据,重新计算缓存大小 3.根据设置开启动画.
动画相关方法
- (void)stopAnimating {
//调用super方法
[super stopAnimating];
//取消队列中的所有任务
[_requestQueue cancelAllOperations];
//暂停CADisplayLink
_link.paused = YES;
//更新状态
self.currentIsPlayingAnimation = NO;
}
// 清空动画参数
- (void)resetAnimated {
dispatch_once(&_onceToken, ^{
//初始化锁
_lock = dispatch_semaphore_create(1);
//初始化缓冲区
_buffer = [NSMutableDictionary new];
//初始化数据请求队列
_requestQueue = [[NSOperationQueue alloc] init];
_requestQueue.maxConcurrentOperationCount = 1;
//初始化timer CADisplayLink
_link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy 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];
});
}
);
//暂停CADisplaylink
_link.paused = YES;
_time = 0;
//currentAnimatedImageIndex对生成KVO通知
if (_curIndex != 0) {
[self willChangeValueForKey:@"currentAnimatedImageIndex"];
_curIndex = 0;
[self didChangeValueForKey:@"currentAnimatedImageIndex"];
}
//清空当前动画的帧
_curAnimatedImage = nil;
//清空当前帧
_curFrame = nil;
//清空帧索引
_curLoop = 0;
//清空循环总数
_totalLoop = 0;
//设置所有帧数为1
_totalFrameCount = 1;
//清空循环结束标记
_loopEnd = NO;
//清空缓存miss标记
_bufferMiss = NO;
//清空buffer中实际的缓存个数
_incrBufferCount = 0;
}
//开始动画方法
- (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;
}
} else {
//不是多帧图片数据
if (_curAnimatedImage && _link.paused) {
//清空循环索引
_curLoop = 0;
//清空循环结束标记
_loopEnd = NO;
//开始CADisplayLink
_link.paused = NO;
//设置动画播放状态
self.currentIsPlayingAnimation = YES;
}
}
}
CADisplay 刷新方法
- (void)step:(CADisplayLink *)link {
//获取当前动画图片
UIImage <YYAnimatedImage> *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;
//如果之前是否缓存命中,第1帧已经展示
if (!_bufferMiss) {
//当前的时间区间
_time += link.duration;
//获取当前帧的持续时长
delay = [image animatedImageDurationAtIndex:_curIndex];
//当前帧没有显示结束 直接return
if (_time < delay) return;
//在本displayLink周期内计算本帧剩余时长
_time -= delay;
if (nextIndex == 0) { //是一次新的循环
_curLoop++;
//播放循环结束时
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];
//本周期内剩余时长大于下一帧的持续时长,_time变为下一帧的时长
if (_time > delay) _time = delay; // do not jump over frame
}
LOCK(
//从缓存中取图片
bufferedImage = buffer[@(nextIndex)];
if (bufferedImage) {
//当缓存区最大个数小于帧的总数时,要移除一个
if ((int)_incrBufferCount < _totalFrameCount) {
[buffer removeObjectForKey:@(nextIndex)];
}
//生成currentAnimatedImageIndexKVO通知
[self willChangeValueForKey:@"currentAnimatedImageIndex"];
//更新当前帧索引
_curIndex = nextIndex;
[self didChangeValueForKey:@"currentAnimatedImageIndex"];
//更新当前帧数据
_curFrame = bufferedImage == (id)[NSNull null] ? nil : bufferedImage;
if (_curImageHasContentsRect) {
_curContentsRect = [image animatedImageContentsRectAtIndex:_curIndex];
//为layer设置contentsRect
[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
}
//buffer没有满并且没有加载任务,则创建加载图片任务放到请求队列中执行
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];
}
}
在每一次CADisplay
的回调方法中,根据CADisplay
迭代周期_display.duration
计算时间区间_time
,当时间区间足够显示当前帧(_time > 当前帧的duration
)时,从缓存获取帧,进行layer
内容的更新,但是通过源代码得知,如果缓存没有图片,需要异步等待缓存中获取到图片之后才能更新layer
,这个时间最少是一个CADisplay.duration
layer更新内容的方法
- (void)displayLayer:(CALayer *)layer {
if (_curFrame) {
//直接用_curFrame显示
layer.contents = (__bridge id)_curFrame.CGImage;
}
}
图片加载任务_YYAnimatedImageViewFetchOperation
_YYAnimatedImageViewFetchOperation 是 NSOpeation的子类,封装了图片子线程加载过程,主要功能时是从当前帧索引开始向后加载_incrBufferCount个帧图片,然后放到缓存YYAnimatedImageView->_buffer
中.
- (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) {
UIImage *img = [_curImage animatedImageFrameAtIndex:idx];
//子线程解码图片
img = img.imageByDecoded;
if ([self isCancelled]) break;
//设置到缓存中
LOCK_VIEW(view->_buffer[@(idx)] = img ? img : [NSNull null]);
//解除强引用
view = nil;
}
}
}
}
收到内存警告
- (void)didReceiveMemoryWarning:(NSNotification *)notification {
//取消所有图片请求任务
[_requestQueue cancelAllOperations];
//后台线程清除缓存
[_requestQueue addOperationWithBlock: ^{
//???
_incrBufferCount = -60 - (int)(arc4random() % 120); // about 1~3 seconds to grow back..
NSNumber *next = @((_curIndex + 1) % _totalFrameCount);
LOCK(
//保留下一帧图片,清除缓存中的其它帧,使重新显示时避免出现跳帧现象
NSArray * keys = _buffer.allKeys;
for (NSNumber * key in keys) {
if (![key isEqualToNumber:next]) { // keep the next frame for smoothly animation
[_buffer removeObjectForKey:key];
}
}
)//LOCK
}];
}
进入后台时
进入后台时与收到内存警告时的操作基本一致
- (void)didEnterBackground:(NSNotification *)notification {
[_requestQueue cancelAllOperations];
NSNumber *next = @((_curIndex + 1) % _totalFrameCount);
LOCK(
NSArray * keys = _buffer.allKeys;
for (NSNumber * key in keys) {
if (![key isEqualToNumber:next]) { // keep the next frame for smoothly animation
[_buffer removeObjectForKey:key];
}
}
)//LOCK
}