YYAnimatedImageView 源码浅析

2019-11-14  本文已影响0人  好有魔力

YYAnimatedImageViewUIImageView的子类,如果imagehighlightedImage属性继承YYAnimatedImage协议, 则可以显示多帧动画图像,可以使startAnimationstopAnimation控制动画.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
}
上一篇下一篇

猜你喜欢

热点阅读