YYAnimatedImageView
是UIImageView
的子類,如果image
和highlightedImage
屬性繼承YYAnimatedImage
協(xié)議, 則可以顯示多幀動(dòng)畫圖像,可以使startAnimation
和stopAnimation
控制動(dòng)畫.YYAnimatedImageView
會(huì)緩存一些幀,來減小CPU消耗.緩存的大小會(huì)根據(jù)可用的內(nèi)存改變.
YYAnimatedImageView接口
@interface YYAnimatedImageView : UIImageView
//如果圖片數(shù)據(jù)有多幀,設(shè)置為`YES`時(shí),當(dāng)view 可見/消失時(shí)會(huì)自動(dòng)播放/停止動(dòng)畫
@property (nonatomic) BOOL autoPlayAnimatedImage;
//當(dāng)前顯示的幀( 0 based),設(shè)置它會(huì)立即顯示某一幀,支持KVO
@property (nonatomic) NSUInteger currentAnimatedImageIndex;
//是否是播放狀態(tài),支持KVO
@property (nonatomic, readonly) BOOL currentIsPlayingAnimation;
//動(dòng)畫timer的運(yùn)行模式,默認(rèn)NSRunLoopCommonModes
@property (nonatomic, copy) NSString *runloopMode;
//緩存的最大限制,設(shè)置為0會(huì)根據(jù)系統(tǒng)可用內(nèi)存動(dòng)態(tài)計(jì)算
@property (nonatomic) NSUInteger maxBufferSize;
@end
YYAnimatedImage協(xié)議
@protocol YYAnimatedImage <NSObject>
@required
//動(dòng)畫的總幀數(shù),小于1則其他的方法將被忽略
- (NSUInteger)animatedImageFrameCount;
//動(dòng)畫循環(huán)次數(shù),0是無限循環(huán)
- (NSUInteger)animatedImageLoopCount;
//每幀圖片的大小,用來決定緩存buffer大小
- (NSUInteger)animatedImageBytesPerFrame;
//使用索引獲取某一幀,可能會(huì)在后臺(tái)線程調(diào)用
- (nullable UIImage *)animatedImageFrameAtIndex:(NSUInteger)index;
//某一幀的動(dòng)畫時(shí)長
- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index;
@optional
//圖片坐標(biāo)系的子區(qū)域,用來顯示 sprite animation
- (CGRect)animatedImageContentsRectAtIndex:(NSUInteger)index;
@end
設(shè)置image
- (void)setImage:(UIImage *)image {
//與當(dāng)前圖片相同,不做任何操作直接返回
if (self.image == image) return;
//調(diào)用私有方法
[self setImage:image withType:YYAnimatedImageTypeImage];
}
//私有設(shè)置圖片方法
- (void)setImage:(id)image withType:(YYAnimatedImageType)type {
//停止動(dòng)畫
[self stopAnimating];
//重新設(shè)置動(dòng)畫
if (_link) [self resetAnimated];
//清空當(dāng)前幀數(shù)據(jù)
_curFrame = nil;
//解析圖片類型調(diào)用不同的父類方法
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;
}
//調(diào)用圖片發(fā)生改變
[self imageChanged];
}
- (void)imageChanged {
//重新獲取真正的類型
YYAnimatedImageType newType = [self currentImageType];
//取出對(duì)應(yīng)類型下的圖片
id newVisibleImage = [self imageForType:newType];
NSUInteger newImageFrameCount = 0;
BOOL hasContentsRect = NO;
//獲取到的是UIImage類型,并且繼承了YYAnimatedImage 說明可能是動(dòng)圖
if ([newVisibleImage isKindOfClass:[UIImage class]] &&
[newVisibleImage conformsToProtocol:@protocol(YYAnimatedImage)]) {
//獲取圖片幀數(shù)
newImageFrameCount = ((UIImage<YYAnimatedImage> *) newVisibleImage).animatedImageFrameCount;
if (newImageFrameCount > 1) {
//是否設(shè)置了 animatedImageContentsRect
hasContentsRect = [((UIImage<YYAnimatedImage> *) newVisibleImage) respondsToSelector:@selector(animatedImageContentsRectAtIndex:)];
}
}
//沒有設(shè)置contentRect則恢復(fù)默認(rèn)設(shè)置
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;
//設(shè)置了contentRect
if (hasContentsRect) {
//獲取第一幀的animatedImageContentsRect
CGRect rect = [((UIImage<YYAnimatedImage> *) newVisibleImage) animatedImageContentsRectAtIndex:0];
//設(shè)置animatedImageContentsRect
[self setContentsRect:rect forImage:newVisibleImage];
}
//幀數(shù)大于1時(shí)
if (newImageFrameCount > 1) {
//重新設(shè)置動(dòng)畫
[self resetAnimated];
//保存當(dāng)前動(dòng)畫幀
_curAnimatedImage = newVisibleImage;
//保存當(dāng)前幀
_curFrame = newVisibleImage;
//保存循環(huán)次數(shù)
_totalLoop = _curAnimatedImage.animatedImageLoopCount;
//保存幀數(shù)
_totalFrameCount = _curAnimatedImage.animatedImageFrameCount;
//計(jì)算緩存大小
[self calcMaxBufferCount];
}
//標(biāo)記重繪
[self setNeedsDisplay];
//更新動(dòng)畫狀態(tài)
[self didMoved];
}
在修改image
屬性后,主要要處理三件事:1.停止當(dāng)前動(dòng)畫, 2. 解析圖片是否是動(dòng)圖,更新與圖片相關(guān)數(shù)據(jù),重新計(jì)算緩存大小 3.根據(jù)設(shè)置開啟動(dòng)畫.
動(dòng)畫相關(guān)方法
- (void)stopAnimating {
//調(diào)用super方法
[super stopAnimating];
//取消隊(duì)列中的所有任務(wù)
[_requestQueue cancelAllOperations];
//暫停CADisplayLink
_link.paused = YES;
//更新狀態(tài)
self.currentIsPlayingAnimation = NO;
}
// 清空動(dòng)畫參數(shù)
- (void)resetAnimated {
dispatch_once(&_onceToken, ^{
//初始化鎖
_lock = dispatch_semaphore_create(1);
//初始化緩沖區(qū)
_buffer = [NSMutableDictionary new];
//初始化數(shù)據(jù)請(qǐng)求隊(duì)列
_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;
//監(jiān)聽內(nèi)存警告
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
//監(jiān)聽程序進(jìn)入后臺(tái)
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
});
//數(shù)據(jù)請(qǐng)求隊(duì)列取消所有任務(wù)
[_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對(duì)生成KVO通知
if (_curIndex != 0) {
[self willChangeValueForKey:@"currentAnimatedImageIndex"];
_curIndex = 0;
[self didChangeValueForKey:@"currentAnimatedImageIndex"];
}
//清空當(dāng)前動(dòng)畫的幀
_curAnimatedImage = nil;
//清空當(dāng)前幀
_curFrame = nil;
//清空幀索引
_curLoop = 0;
//清空循環(huán)總數(shù)
_totalLoop = 0;
//設(shè)置所有幀數(shù)為1
_totalFrameCount = 1;
//清空循環(huán)結(jié)束標(biāo)記
_loopEnd = NO;
//清空緩存miss標(biāo)記
_bufferMiss = NO;
//清空buffer中實(shí)際的緩存?zhèn)€數(shù)
_incrBufferCount = 0;
}
//開始動(dòng)畫方法
- (void)startAnimating {
//獲取當(dāng)前的圖片類型
YYAnimatedImageType type = [self currentImageType];
//如果是多幀圖片的類型
if (type == YYAnimatedImageTypeImages || type == YYAnimatedImageTypeHighlightedImages) {
//獲取多幀圖片數(shù)據(jù)
NSArray *images = [self imageForType:type];
if (images.count > 0) {
//調(diào)用父類開始方法
[super startAnimating];
//更新動(dòng)畫狀態(tài)
self.currentIsPlayingAnimation = YES;
}
} else {
//不是多幀圖片數(shù)據(jù)
if (_curAnimatedImage && _link.paused) {
//清空循環(huán)索引
_curLoop = 0;
//清空循環(huán)結(jié)束標(biāo)記
_loopEnd = NO;
//開始CADisplayLink
_link.paused = NO;
//設(shè)置動(dòng)畫播放狀態(tài)
self.currentIsPlayingAnimation = YES;
}
}
}
CADisplay 刷新方法
- (void)step:(CADisplayLink *)link {
//獲取當(dāng)前動(dòng)畫圖片
UIImage <YYAnimatedImage> *image = _curAnimatedImage;
NSMutableDictionary *buffer = _buffer;
UIImage *bufferedImage = nil;
//計(jì)算索引值
NSUInteger nextIndex = (_curIndex + 1) % _totalFrameCount;
BOOL bufferIsFull = NO;
//動(dòng)畫幀為空,直接返回
if (!image) return;
if (_loopEnd) { // view will keep in last frame
//循環(huán)結(jié)束則停止動(dòng)畫
[self stopAnimating];
return;
}
NSTimeInterval delay = 0;
//如果之前是否緩存命中,第1幀已經(jīng)展示
if (!_bufferMiss) {
//當(dāng)前的時(shí)間區(qū)間
_time += link.duration;
//獲取當(dāng)前幀的持續(xù)時(shí)長
delay = [image animatedImageDurationAtIndex:_curIndex];
//當(dāng)前幀沒有顯示結(jié)束 直接return
if (_time < delay) return;
//在本displayLink周期內(nèi)計(jì)算本幀剩余時(shí)長
_time -= delay;
if (nextIndex == 0) { //是一次新的循環(huán)
_curLoop++;
//播放循環(huán)結(jié)束時(shí)
if (_curLoop >= _totalLoop && _totalLoop != 0) {
_loopEnd = YES;
[self stopAnimating];
[self.layer setNeedsDisplay]; //let system call `displayLayer:` before runloop sleep
return; // stop at last frame
}
}
//獲取下幀動(dòng)畫時(shí)長
delay = [image animatedImageDurationAtIndex:nextIndex];
//本周期內(nèi)剩余時(shí)長大于下一幀的持續(xù)時(shí)長,_time變?yōu)橄乱粠臅r(shí)長
if (_time > delay) _time = delay; // do not jump over frame
}
LOCK(
//從緩存中取圖片
bufferedImage = buffer[@(nextIndex)];
if (bufferedImage) {
//當(dāng)緩存區(qū)最大個(gè)數(shù)小于幀的總數(shù)時(shí),要移除一個(gè)
if ((int)_incrBufferCount < _totalFrameCount) {
[buffer removeObjectForKey:@(nextIndex)];
}
//生成currentAnimatedImageIndexKVO通知
[self willChangeValueForKey:@"currentAnimatedImageIndex"];
//更新當(dāng)前幀索引
_curIndex = nextIndex;
[self didChangeValueForKey:@"currentAnimatedImageIndex"];
//更新當(dāng)前幀數(shù)據(jù)
_curFrame = bufferedImage == (id)[NSNull null] ? nil : bufferedImage;
if (_curImageHasContentsRect) {
_curContentsRect = [image animatedImageContentsRectAtIndex:_curIndex];
//為layer設(shè)置contentsRect
[self setContentsRect:_curContentsRect forImage:_curFrame];
}
//移動(dòng)到下一幀幀
nextIndex = (_curIndex + 1) % _totalFrameCount;
//標(biāo)記緩存命中
_bufferMiss = NO;
//緩存中是否已經(jīng)包含了所有幀
if (buffer.count == _totalFrameCount) {
bufferIsFull = YES;
}
} else { //標(biāo)記緩存未命中
_bufferMiss = YES;
}
)//LOCK
//緩存命中則更新layer
if (!_bufferMiss) {
[self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep
}
//buffer沒有滿并且沒有加載任務(wù),則創(chuàng)建加載圖片任務(wù)放到請(qǐng)求隊(duì)列中執(zhí)行
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
的回調(diào)方法中,根據(jù)CADisplay
迭代周期_display.duration
計(jì)算時(shí)間區(qū)間_time
,當(dāng)時(shí)間區(qū)間足夠顯示當(dāng)前幀(_time > 當(dāng)前幀的duration
)時(shí),從緩存獲取幀,進(jìn)行layer
內(nèi)容的更新,但是通過源代碼得知,如果緩存沒有圖片,需要異步等待緩存中獲取到圖片之后才能更新layer
,這個(gè)時(shí)間最少是一個(gè)CADisplay.duration
layer更新內(nèi)容的方法
- (void)displayLayer:(CALayer *)layer {
if (_curFrame) {
//直接用_curFrame顯示
layer.contents = (__bridge id)_curFrame.CGImage;
}
}
圖片加載任務(wù)_YYAnimatedImageViewFetchOperation
_YYAnimatedImageViewFetchOperation 是 NSOpeation的子類,封裝了圖片子線程加載過程,主要功能時(shí)是從當(dāng)前幀索引開始向后加載_incrBufferCount個(gè)幀圖片,然后放到緩存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;
//設(shè)置到緩存中
LOCK_VIEW(view->_buffer[@(idx)] = img ? img : [NSNull null]);
//解除強(qiáng)引用
view = nil;
}
}
}
}
收到內(nèi)存警告
- (void)didReceiveMemoryWarning:(NSNotification *)notification {
//取消所有圖片請(qǐng)求任務(wù)
[_requestQueue cancelAllOperations];
//后臺(tái)線程清除緩存
[_requestQueue addOperationWithBlock: ^{
//???
_incrBufferCount = -60 - (int)(arc4random() % 120); // about 1~3 seconds to grow back..
NSNumber *next = @((_curIndex + 1) % _totalFrameCount);
LOCK(
//保留下一幀圖片,清除緩存中的其它幀,使重新顯示時(shí)避免出現(xiàn)跳幀現(xiàn)象
NSArray * keys = _buffer.allKeys;
for (NSNumber * key in keys) {
if (![key isEqualToNumber:next]) { // keep the next frame for smoothly animation
[_buffer removeObjectForKey:key];
}
}
)//LOCK
}];
}
進(jìn)入后臺(tái)時(shí)
進(jìn)入后臺(tái)時(shí)與收到內(nèi)存警告時(shí)的操作基本一致
- (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
}