1.前言
先了解一下作者提供的相关资料:
1)github地址
2)iOS处理图片的一些小tips
3)移动端图片格式调研
2.代码学习
本文基于作者提供的YYKitExample。文中涉及一些知识点,本人找了一些链接提供参考,更深入的了解还希望自己深入学习。
这部分的代码示例集中于YYImageDisplayExample.m
中。在这之前,你需要得到一个动态图的YYImage实例
,demo中的动图如下:
2.1 简单使用
代码中很直观,github上也给出了基本示例:
YYAnimatedImageView *imageView = ...;
// pause:
[imageView stopAnimating];
// play:
[imageView startAnimating];
// set frame index:
imageView.currentAnimatedImageIndex = 12;
// get current status
image.currentIsPlayingAnimation;
2.2 YYAnimatedImageView
翻译一下作者的注释:
YYAnimatedImageView
用于展示动态图。它是UIImageView
的子类,如果图片实现了YYAnimatedImage
协议,它可以用于展示多帧动画。我们可以使用-startAnimating
,-stopAnimating
and-isAnimating
等方法控制动画。
当设备拥有足够的空闲内存时,它会缓存部分或所有将要展示的帧,并且这只消耗少量的CPU。缓存的尺寸根据当前内存的状态可以动态调整。
根据作者的描述,我们从初始化方法开始了解。
part 1
- (instancetype)initWithImage:(UIImage *)image {
self = [super init];
_runloopMode = NSRunLoopCommonModes;
_autoPlayAnimatedImage = YES;
self.frame = (CGRect) {CGPointZero, image.size };
self.image = image;
return self;
}
explain 1
①可以了解一下NSRunLoopMode,动态图的刷新基于CADisplayLink,YYImage将其注册在了NSRunLoopCommonModes
模式下(通常都是这样)。
②setImage
方法实际调用如下,其中有2个重要的方法:resetAnimated
和imageChanged
:
- (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];
}
part 2 resetAnimated
故名思议,重置动画。当然,首次调用setImage
时,_link
还为空,所以不会调用这个方法。我们先看一下它内部做了什么。
//摘录部分代码
- (void)resetAnimated {
if (!_link) {
_lock = dispatch_semaphore_create(1);
_buffer = [NSMutableDictionary new];
_requestQueue = [[NSOperationQueue alloc] init];
_requestQueue.maxConcurrentOperationCount = 1;
_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];
});
}
);
_link.paused = YES;
_time = 0;
if (_curIndex != 0) {
[self willChangeValueForKey:@"currentAnimatedImageIndex"];
_curIndex = 0;
[self didChangeValueForKey:@"currentAnimatedImageIndex"];
}
//...部分属性初始化赋值
}
explain 2
①_link
:动态图的刷新基于CADisplayLink
,注册了@selector(step:)
(后文详细介绍)处理刷新操作。
YYWeakProxy
一个弱引用代理类,主要为了防止循环引用。这里类似NSTimer的一个循环引用陷阱,self
持有_link
,displayLinkWithTarget:
方法又将self
传入,让_link
持有self
,如果不阻止其中的一项持有关系就会造成循环引用。
②resetAnimated
操作需要将动态图复位,所以判断_curIndex
是否为0,如果不是,手动调用KVO方法更新属性。
NSKeyValueObservingOptions
这里实现了手动发送通知。需要重写+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;
方法,设置需要手动发送通知的属性。
part 3
我们再看看setImage
方法中的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) {
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) {
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];
}
explain 3
①根据imageType
拿到newVisibleImage
,hasContentsRect
是用于SpriteSheetImage
的(裁剪图片,本文先不深入介绍了)。
②如果动图数量大于1,则复位动画,准备开始显示动图。
③calcMaxBufferCount
方法可根据当前内存情况动态适配buffer size
。我们看下它是怎么做到动态的?
- (void)calcMaxBufferCount {
int64_t bytes = (int64_t)_curAnimatedImage.animatedImageBytesPerFrame;
if (bytes == 0) bytes = 1024;
int64_t total = [UIDevice currentDevice].memoryTotal;
int64_t free = [UIDevice currentDevice].memoryFree;
int64_t max = MIN(total * 0.2, free * 0.6);
max = MAX(max, BUFFER_SIZE);
if (_maxBufferSize) max = max > _maxBufferSize ? _maxBufferSize : max;
double maxBufferCount = (double)max / (double)bytes;
maxBufferCount = YY_CLAMP(maxBufferCount, 1, 512);
_maxBufferCount = maxBufferCount;
}
从代码可看出,max取总内存的20%或者空闲内存的60%的较大值。单帧的默认bytes
为1024字节。最终得到的maxBufferCount
为一个[1, 512]区间内的值。
part 4
imageChanged
方法最后调用的了didMoved
方法。内部很简单,如果图片的autoPlayAnimatedImage
属性为真,就调用startAnimating
开始动画。我们看一下代码:
- (void)stopAnimating {
[super stopAnimating];
[_requestQueue cancelAllOperations];
_link.paused = YES;
self.currentIsPlayingAnimation = NO;
}
- (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;
_link.paused = NO;
self.currentIsPlayingAnimation = YES;
}
}
}
explain 4
额,也没什么好解释的。_link.paused = NO
,我们之前注册的step
方法开始执行。最后,我们看一下里面到底做了什么事情。
part 5
只摘录部分代码,如下:
- (void)step:(CADisplayLink *)link {
//...略
NSTimeInterval delay = 0;
if (!_bufferMiss) {
_time += link.duration;
delay = [image animatedImageDurationAtIndex:_curIndex];
if (_time < delay) return;
_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];
if (_time > delay) _time = delay; // do not jump over frame
}
LOCK(//...略);
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];
}
}
explain 5
①_time
用于记录屏幕刷新的累计时间,delay
是当前帧要停留的时间。可以看到最后有一句if (_time > delay) _time = delay; // do not jump over frame
,怎么理解这里保护动图不会跳帧的?
因为_time
每次都会多出delay
一些时间,有可能累计一定时间后,_time
直接大于下一帧的delay
要求,相当于_time
认为已经为该帧动画停留了足够多的时间,然后就继续执行下一帧了。结果会导致这一帧被跳过了。不过CADisplayLink刷新的时间间隔是1/60 s
,对于动图来说,出现这种补帧的情况是很低概率的。
②当动图展示完了之后,看到调用了[self.layer setNeedsDisplay];
主要是为了防止上一句调用stopAnimating
后runloop休眠,导致最后一帧没有展示。
③创建缓存管理线程_YYAnimatedImageViewFetchOperation
。我们单独讲一下,缓存这块是如何处理的?
2.3 缓存处理
前文提过:当设备拥有足够的空闲内存时,它会缓存部分或所有将要展示的帧,并且这只消耗少量的CPU。缓存的尺寸根据当前内存的状态可以动态调整。
我们知道通过- (void)calcMaxBufferCount;
方法拿到了_maxBufferSize
和_maxBufferCount
2个变量。还知道有一个_requestQueue
队列和_YYAnimatedImageViewFetchOperation
线程管理缓存。看一下线程的main
方法中做了什么?
- (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;
}
}
}
}
_incrBufferCount
是记录了当前试图的缓存数量,它取值自_maxBufferCount
。可看出,main
方法主要做的事就是,遍历并填充缓存区。
当收到内存警告或程序退到后台时,会触发之前监听的2个方法:didReceiveMemoryWarning:
和didEnterBackground:
。内部主要做的事情就是清除_buffer
的数据。
3 总结
全文把YYAnimatedImageView
中的方法基本都提及了一遍,由于篇幅较长,有些内容可能没有讲清楚。
总的来说,展示动态图主要包含以下几部分:
①动态图,需将其分割为多帧显示。
②显示时间用CADisplayLink控制,涉及到了Runloop的知识。
③性能上用缓存优化,缓存的操作使用了队列和线程相关的知识。
在后续的YYImage源码学习中,我相信会有新的收获的~