探究YYAnimatedImageView為什么可以顯示動(dòng)態(tài)圖片

大家都知道如果想讓UIImageView顯示動(dòng)態(tài)圖片,可以設(shè)置animationImages賦值一個(gè)圖片數(shù)組,然后設(shè)置一下動(dòng)畫時(shí)間,再開啟動(dòng)畫??偢杏X使用很麻煩,而且如果是一個(gè)gif格式或者其他格式的動(dòng)態(tài)圖,直接就無法使用了。后來在github上發(fā)現(xiàn)了YYImage,發(fā)現(xiàn)使用起來很方便,但同時(shí)也很好奇是怎么做到的,現(xiàn)在就以YYAnimatedImageView為主介紹一下這個(gè)庫是怎么實(shí)現(xiàn)顯示動(dòng)態(tài)圖的。廢話不多說,上圖

這個(gè)就是用來測試的gif

先寫兩句代碼,運(yùn)行一下,然后打斷點(diǎn)進(jìn)去看看是怎么加載的

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;

這樣就能顯示動(dòng)態(tài)圖了。

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

作者繼承UIImage類寫了YYImage,他自己的介紹是這是可以高效的展示動(dòng)態(tài)圖的類,我們從imageNamed:方法看起,這個(gè)方法主要是去遍歷工程文件中是否有匹配的文件,如果找到路徑然后直接獲取圖片的二進(jìn)制文件,YYImage重寫了initWithData:scale:方法,在這個(gè)里面使用了YYImageDecoder對(duì)圖片進(jìn)行界面,這就是YYImage為什么比較快的原因了

initWithData:scale:主要代碼
  • YYImageDecoder *decoder = [YYImageDecoder decoderWithData:datascale:scale]進(jìn)入YYImagecoder去看看是怎么處理的

updatedata.png

根據(jù)方法調(diào)用和參數(shù)傳遞,來到如上圖所示方法,是yyImageCoder第一個(gè)做實(shí)事的方法,YYImageDetectType先得到這個(gè)圖片的格式(比如PNG,JPEG,GIF等),具體怎么得出來的可以點(diǎn)進(jìn)去仔細(xì)看看,大概就是獲得這個(gè)二進(jìn)制文件的前16字節(jié),然后進(jìn)行匹配,得到圖片的格式,得到了圖片格式之后進(jìn)入下一步[self _updateSource]

updateSource.png

在這里作者對(duì)webp和apng格式的動(dòng)畫進(jìn)行了專門的解碼優(yōu)化所以進(jìn)入不同方法,總之這個(gè)方法就是對(duì)不同格式的圖片進(jìn)行解碼路由,我們點(diǎn)進(jìn)去_updateSourceImageIO看看怎么做的,看下主要代碼(去除了一些條件判斷)

//使用ImageIO框架去獲得圖片
//使用CGImageSourceCreateWithData獲得圖片類型是CGImageSourceRef
_source=CGImageSourceCreateWithData((__bridgeCFDataRef)_data,NULL);
//這里frameCount代表圖片的數(shù)量,比如GIF其實(shí)就是一組圖片
//下面是一些不同類型的判斷
_frameCount = CGImageSourceGetCount(_source);
if (_type == YYImageTypeGIF) {
            //這字典打印出來是
            //FileSize = 487202;
            //"{GIF}" =     {
            //    HasGlobalColorMap = 1;
            //    LoopCount = 0;
            //};
            //  loopCount = 0 表示會(huì)無線循環(huán)當(dāng)前的gif
            CFDictionaryRef properties = CGImageSourceCopyProperties(_source, NULL);
            if (properties) {
                CFTypeRef loop = CFDictionaryGetValue(properties, kCGImagePropertyGIFLoopCount);
                if (loop) CFNumberGetValue(loop, kCFNumberNSIntegerType, &_loopCount);
                
                CFRelease(properties);
            }
        }
//建立一個(gè)數(shù)組,把每個(gè)圖片的索引,延遲時(shí)間等封裝成_YYImageDecoderFrame類
    //加入到數(shù)組中
    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];
        //得到每一幀圖片的屬性
        //包括圖片的 寬  高   延遲時(shí)間  顏色空間等
        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);
                //此處打斷點(diǎn) 輸出
                // {
                //    DelayTime = "0.07";
                //    UnclampedDelayTime = "0.07";
                // }
                //表示每一幀圖片的延遲時(shí)間,也就是需要顯示的時(shí)間
                if (gif) {
                   
                    value = CFDictionaryGetValue(gif, kCGImagePropertyGIFUnclampedDelayTime);
                    if (!value) {
                        value = CFDictionaryGetValue(gif, kCGImagePropertyGIFDelayTime);
                    }
                    if (value) CFNumberGetValue(value, kCFNumberDoubleType, &duration);
                }
            }
            //對(duì)動(dòng)畫中需要到的關(guān)鍵屬性進(jìn)行賦值
            
            frame.width = width;
            frame.height = height;
            frame.duration = duration;

然后把每一幀圖片加入到frames數(shù)組中,這基本上是YYImageCoder完成的大部分工作了

  • 我們?cè)倩氐結(jié)YImage中YYImageCoder還是之前那個(gè)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;
        //當(dāng)frameCount >1 也就是 當(dāng)圖片是動(dòng)態(tài)圖的時(shí)候
        if (decoder.frameCount > 1) {
            //注意看這里,這里把decoder 當(dāng)做自己成員變量了
            //為什么要這樣做? 因?yàn)楫?dāng)時(shí)上面返回的是第一幀的圖片,但是對(duì)于frameCount > 1的動(dòng)態(tài)圖,
           //當(dāng)然返回一張是不夠的,這里保留decoder,在需要使用YYAnimatedImageView代理方法的時(shí)候可以通過decoder來返回不同索引的圖片
            _decoder = decoder;
            _bytesPerFrame = CGImageGetBytesPerRow(image.CGImage) * CGImageGetHeight(image.CGImage);
            _animatedImageMemorySize = _bytesPerFrame * decoder.frameCount;
        }
  • 好了,YYImage所做的工作已經(jīng)完成了,我們現(xiàn)在來看看YYAnimatedImageView是怎么做最后的舞臺(tái)的
    setImage:withType:方法出發(fā),來到- (void)imageChanged方法,同樣我也添加了注釋
YYAnimatedImageType newType = [self currentImageType];
    id newVisibleImage = [self imageForType:newType];
    NSUInteger newImageFrameCount = 0;
    BOOL hasContentsRect = NO;

    if ([newVisibleImage isKindOfClass:[UIImage class]] &&
        //這句話其實(shí)就是把newVisibleImage當(dāng)做代理對(duì)象使用
        //因?yàn)檫@里用的都是YYImage類型,YYImage已經(jīng)實(shí)現(xiàn)了<YYAnimatedImage>代理方法
        [newVisibleImage conformsToProtocol:@protocol(YYAnimatedImage)]) {
        //得到圖片的個(gè)數(shù)
        newImageFrameCount = ((UIImage<YYAnimatedImage> *) newVisibleImage).animatedImageFrameCount;
        if (newImageFrameCount > 1) {
            hasContentsRect = [((UIImage<YYAnimatedImage> *) newVisibleImage) respondsToSelector:@selector(animatedImageContentsRectAtIndex:)];
        }
    }
    if (!hasContentsRect && _curImageHasContentsRect) {
        //這個(gè)是關(guān)閉默認(rèn)的隱式動(dòng)畫,防止對(duì)自己的動(dòng)畫播放產(chǎn)生影響,
        //有興趣的可以看看 core animation
        if (!CGRectEqualToRect(self.layer.contentsRect, CGRectMake(0, 0, 1, 1)) ) {
            [CATransaction begin];
            [CATransaction setDisableActions:YES];
            //設(shè)置layer的顯示范圍是整個(gè)寄宿圖片
            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) {
        //如果是 圖片數(shù)量>1 針對(duì)動(dòng)態(tài)圖
        //resetAnimated就是創(chuàng)建了一個(gè)CADisplayLink定時(shí)器去刷新圖片顯示
        [self resetAnimated];
        _curAnimatedImage = newVisibleImage;
        _curFrame = newVisibleImage;
        _totalLoop = _curAnimatedImage.animatedImageLoopCount;
        _totalFrameCount = _curAnimatedImage.animatedImageFrameCount;
        [self calcMaxBufferCount];
    }
    [self setNeedsDisplay];
    //開始播放動(dòng)畫
    [self didMoved];

[self resetAnimated]方法中,使用dispatch_once來保證這個(gè)imageView中只起一次定時(shí)器,同時(shí)把這個(gè)定時(shí)器加到mainRunLoop,模式默認(rèn)為NSRunLoopCommonModes,也就是說在你滑動(dòng)的時(shí)候不會(huì)影響到動(dòng)態(tài)圖的播放,同時(shí)添加進(jìn)通知中心,對(duì)于關(guān)于內(nèi)存警告的通知,和后臺(tái)的通知進(jìn)行相應(yīng)的一些處理,定時(shí)器定時(shí)調(diào)起step:方法,這個(gè)方法主要是做什么呢,

_time += link.duration;
        //拿到當(dāng)前索引圖片的延遲時(shí)間,也就是需要顯示的時(shí)間
        delay = [image animatedImageDurationAtIndex:_curIndex];
        //如果當(dāng)前的link.duration還沒到,直接返回等到下一次調(diào)起
        //就拿文章頭部的那個(gè)動(dòng)態(tài)圖來說,每張圖顯示的時(shí)間大約在0.07秒左右
        //而CADisplayLink每次任務(wù)執(zhí)行的時(shí)間大約是0.016秒
        //所以不會(huì)用每次都刷新圖片顯示
        if (_time < delay) return;
        //如果調(diào)用了就用當(dāng)前的時(shí)間減去 當(dāng)前圖片需要顯示的時(shí)間
        _time -= delay;
        if (nextIndex == 0) {
            _curLoop++;
            if (_curLoop >= _totalLoop && _totalLoop != 0) {
                _loopEnd = YES;
                [self stopAnimating];
                //主動(dòng)調(diào)起刷新layer,系統(tǒng)會(huì)調(diào)用displayLayer
                [self.layer setNeedsDisplay];
                return;
            }
        }

_curFrame就是當(dāng)前要顯示的圖片,_curFrame的賦值也在step中,具體就不解釋了,然后就通過下面這句代碼完成了imageview的layer的寄宿圖的設(shè)置
layer.contents = (__bridge id)_curFrame.CGImage;
好了到這,YYAnimatedImageView就開始播放動(dòng)態(tài)圖了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容