YYAnimatedImageView 源码浅析

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
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,084评论 1 32
  • 1.自定义控件 a.继承某个控件 b.重写initWithFrame方法可以设置一些它的属性 c.在layouts...
    圍繞的城阅读 3,342评论 2 4
  • 在iOS实际开发中常用的动画无非是以下四种:UIView动画,核心动画,帧动画,自定义转场动画。 1.UIView...
    请叫我周小帅阅读 3,076评论 1 23
  • 在iOS实际开发中常用的动画无非是以下四种:UIView动画,核心动画,帧动画,自定义转场动画。下面我们逐个介绍。...
    4b5cb36a2ee2阅读 348评论 0 0
  • 现在分析到YYImage 首先看文件 YYImage YYFrameImage YYSpriteSheetImag...
    充满活力的早晨阅读 2,505评论 0 3