探究YYAnimatedImageView为什么可以显示动态图片

大家都知道如果想让UIImageView显示动态图片,可以设置animationImages赋值一个图片数组,然后设置一下动画时间,再开启动画。总感觉使用很麻烦,而且如果是一个gif格式或者其他格式的动态图,直接就无法使用了。后来在github上发现了YYImage,发现使用起来很方便,但同时也很好奇是怎么做到的,现在就以YYAnimatedImageView为主介绍一下这个库是怎么实现显示动态图的。废话不多说,上图

这个就是用来测试的gif

先写两句代码,运行一下,然后打断点进去看看是怎么加载的

YYAnimatedImageView* animatedView = [[YYAnimatedImageView alloc] init];

animatedView.frame=CGRectMake(0,0,200,150);

animatedView.center=self.view.center;

[self.view  addSubview:animatedView];

YYImage* image = [YYImage imageNamed:@"test.gif"];

animatedView.image= image;

这样就能显示动态图了。

  • 首先从YYImage说起
  A YYImage object is a high-level way to display    animated image data

作者继承UIImage类写了YYImage,他自己的介绍是这是可以高效的展示动态图的类,我们从imageNamed:方法看起,这个方法主要是去遍历工程文件中是否有匹配的文件,如果找到路径然后直接获取图片的二进制文件,YYImage重写了initWithData:scale:方法,在这个里面使用了YYImageDecoder对图片进行界面,这就是YYImage为什么比较快的原因了

initWithData:scale:主要代码
  • YYImageDecoder *decoder = [YYImageDecoder decoderWithData:datascale:scale]进入YYImagecoder去看看是怎么处理的

updatedata.png

根据方法调用和参数传递,来到如上图所示方法,是yyImageCoder第一个做实事的方法,YYImageDetectType先得到这个图片的格式(比如PNG,JPEG,GIF等),具体怎么得出来的可以点进去仔细看看,大概就是获得这个二进制文件的前16字节,然后进行匹配,得到图片的格式,得到了图片格式之后进入下一步[self _updateSource]

updateSource.png

在这里作者对webp和apng格式的动画进行了专门的解码优化所以进入不同方法,总之这个方法就是对不同格式的图片进行解码路由,我们点进去_updateSourceImageIO看看怎么做的,看下主要代码(去除了一些条件判断)

//使用ImageIO框架去获得图片
//使用CGImageSourceCreateWithData获得图片类型是CGImageSourceRef
_source=CGImageSourceCreateWithData((__bridgeCFDataRef)_data,NULL);
//这里frameCount代表图片的数量,比如GIF其实就是一组图片
//下面是一些不同类型的判断
_frameCount = CGImageSourceGetCount(_source);
if (_type == YYImageTypeGIF) {
            //这字典打印出来是
            //FileSize = 487202;
            //"{GIF}" =     {
            //    HasGlobalColorMap = 1;
            //    LoopCount = 0;
            //};
            //  loopCount = 0 表示会无线循环当前的gif
            CFDictionaryRef properties = CGImageSourceCopyProperties(_source, NULL);
            if (properties) {
                CFTypeRef loop = CFDictionaryGetValue(properties, kCGImagePropertyGIFLoopCount);
                if (loop) CFNumberGetValue(loop, kCFNumberNSIntegerType, &_loopCount);
                
                CFRelease(properties);
            }
        }
//建立一个数组,把每个图片的索引,延迟时间等封装成_YYImageDecoderFrame类
    //加入到数组中
    NSMutableArray *frames = [NSMutableArray new];
    for (NSUInteger i = 0; i < _frameCount; i++) {
        _YYImageDecoderFrame *frame = [_YYImageDecoderFrame new];
        frame.index = i;
        frame.blendFromIndex = i;
        frame.hasAlpha = YES;
        frame.isFullSize = YES;
        [frames addObject:frame];
        //得到每一帧图片的属性
        //包括图片的 宽  高   延迟时间  颜色空间等
        CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_source, i, NULL);
        if (properties) {
            NSTimeInterval duration = 0;
            NSInteger orientationValue = 0, width = 0, height = 0;
            CFTypeRef value = NULL;
            
            value = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
            if (value) CFNumberGetValue(value, kCFNumberNSIntegerType, &width);
            value = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
            if (value) CFNumberGetValue(value, kCFNumberNSIntegerType, &height);
            if (_type == YYImageTypeGIF) {
                CFDictionaryRef gif = CFDictionaryGetValue(properties, kCGImagePropertyGIFDictionary);
                //此处打断点 输出
                // {
                //    DelayTime = "0.07";
                //    UnclampedDelayTime = "0.07";
                // }
                //表示每一帧图片的延迟时间,也就是需要显示的时间
                if (gif) {
                   
                    value = CFDictionaryGetValue(gif, kCGImagePropertyGIFUnclampedDelayTime);
                    if (!value) {
                        value = CFDictionaryGetValue(gif, kCGImagePropertyGIFDelayTime);
                    }
                    if (value) CFNumberGetValue(value, kCFNumberDoubleType, &duration);
                }
            }
            //对动画中需要到的关键属性进行赋值
            
            frame.width = width;
            frame.height = height;
            frame.duration = duration;

然后把每一帧图片加入到frames数组中,这基本上是YYImageCoder完成的大部分工作了

  • 我们再回到YYImage中YYImageCoder还是之前那个initWithData:scale方法,我加了注释,看一下
YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];
        YYImageFrame *frame = [decoder frameAtIndex:0 decodeForDisplay:YES];
        UIImage *image = frame.image;
        if (!image) return nil;
        //这里返回的是第一帧的图片
        self = [self initWithCGImage:image.CGImage scale:decoder.scale orientation:image.imageOrientation];
        if (!self) return nil;
        _animatedImageType = decoder.type;
        //当frameCount >1 也就是 当图片是动态图的时候
        if (decoder.frameCount > 1) {
            //注意看这里,这里把decoder 当做自己成员变量了
            //为什么要这样做? 因为当时上面返回的是第一帧的图片,但是对于frameCount > 1的动态图,
           //当然返回一张是不够的,这里保留decoder,在需要使用YYAnimatedImageView代理方法的时候可以通过decoder来返回不同索引的图片
            _decoder = decoder;
            _bytesPerFrame = CGImageGetBytesPerRow(image.CGImage) * CGImageGetHeight(image.CGImage);
            _animatedImageMemorySize = _bytesPerFrame * decoder.frameCount;
        }
  • 好了,YYImage所做的工作已经完成了,我们现在来看看YYAnimatedImageView是怎么做最后的舞台的
    setImage:withType:方法出发,来到- (void)imageChanged方法,同样我也添加了注释
YYAnimatedImageType newType = [self currentImageType];
    id newVisibleImage = [self imageForType:newType];
    NSUInteger newImageFrameCount = 0;
    BOOL hasContentsRect = NO;

    if ([newVisibleImage isKindOfClass:[UIImage class]] &&
        //这句话其实就是把newVisibleImage当做代理对象使用
        //因为这里用的都是YYImage类型,YYImage已经实现了<YYAnimatedImage>代理方法
        [newVisibleImage conformsToProtocol:@protocol(YYAnimatedImage)]) {
        //得到图片的个数
        newImageFrameCount = ((UIImage<YYAnimatedImage> *) newVisibleImage).animatedImageFrameCount;
        if (newImageFrameCount > 1) {
            hasContentsRect = [((UIImage<YYAnimatedImage> *) newVisibleImage) respondsToSelector:@selector(animatedImageContentsRectAtIndex:)];
        }
    }
    if (!hasContentsRect && _curImageHasContentsRect) {
        //这个是关闭默认的隐式动画,防止对自己的动画播放产生影响,
        //有兴趣的可以看看 core animation
        if (!CGRectEqualToRect(self.layer.contentsRect, CGRectMake(0, 0, 1, 1)) ) {
            [CATransaction begin];
            [CATransaction setDisableActions:YES];
            //设置layer的显示范围是整个寄宿图片
            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) {
        //如果是 图片数量>1 针对动态图
        //resetAnimated就是创建了一个CADisplayLink定时器去刷新图片显示
        [self resetAnimated];
        _curAnimatedImage = newVisibleImage;
        _curFrame = newVisibleImage;
        _totalLoop = _curAnimatedImage.animatedImageLoopCount;
        _totalFrameCount = _curAnimatedImage.animatedImageFrameCount;
        [self calcMaxBufferCount];
    }
    [self setNeedsDisplay];
    //开始播放动画
    [self didMoved];

[self resetAnimated]方法中,使用dispatch_once来保证这个imageView中只起一次定时器,同时把这个定时器加到mainRunLoop,模式默认为NSRunLoopCommonModes,也就是说在你滑动的时候不会影响到动态图的播放,同时添加进通知中心,对于关于内存警告的通知,和后台的通知进行相应的一些处理,定时器定时调起step:方法,这个方法主要是做什么呢,

_time += link.duration;
        //拿到当前索引图片的延迟时间,也就是需要显示的时间
        delay = [image animatedImageDurationAtIndex:_curIndex];
        //如果当前的link.duration还没到,直接返回等到下一次调起
        //就拿文章头部的那个动态图来说,每张图显示的时间大约在0.07秒左右
        //而CADisplayLink每次任务执行的时间大约是0.016秒
        //所以不会用每次都刷新图片显示
        if (_time < delay) return;
        //如果调用了就用当前的时间减去 当前图片需要显示的时间
        _time -= delay;
        if (nextIndex == 0) {
            _curLoop++;
            if (_curLoop >= _totalLoop && _totalLoop != 0) {
                _loopEnd = YES;
                [self stopAnimating];
                //主动调起刷新layer,系统会调用displayLayer
                [self.layer setNeedsDisplay];
                return;
            }
        }

_curFrame就是当前要显示的图片,_curFrame的赋值也在step中,具体就不解释了,然后就通过下面这句代码完成了imageview的layer的寄宿图的设置
layer.contents = (__bridge id)_curFrame.CGImage;
好了到这,YYAnimatedImageView就开始播放动态图了。

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

推荐阅读更多精彩内容