美文网首页
YYAnimatedImageView 源码浅析

YYAnimatedImageView 源码浅析

作者: 好有魔力 | 来源:发表于2019-11-14 17:57 被阅读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
    }
    

    相关文章

      网友评论

          本文标题:YYAnimatedImageView 源码浅析

          本文链接:https://www.haomeiwen.com/subject/grwzbctx.html