FLAnimatedImage源碼剖析

FLAnimatedImage iOS平臺上播放GIF動畫的一個優(yōu)秀解決方案价淌,支持可變幀間延時秒咐、內(nèi)存內(nèi)存表現(xiàn)良好漾抬、播放流暢等特點。

FLAnimatedImage有兩個類:

  • FLAnimatedImage 用來解析碑诉、封裝GIF圖像信息 (GIF幀數(shù)彪腔、GIF size、播放循環(huán)次數(shù)进栽、posterImage德挣、幀間延時)
  • FLAnimatedImageView 用來控制GIF的播放

FLAnimatedImage

GIF圖像信息的解析,關(guān)鍵代碼:
關(guān)鍵是獲取循環(huán)次數(shù)快毛、幀間延時delayTimesForIndexesMutable格嗅, 用到了底層的CGImageSourceRef

_imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data,
                                                   (__bridge CFDictionaryRef)@{(NSString *)kCGImageSourceShouldCache: @NO});
        // Early return on failure!
        if (!_imageSource) {
            FLLog(FLLogLevelError, @"Failed to `CGImageSourceCreateWithData` for animated GIF data %@", data);
            return nil;
        }
        
        // Early return if not GIF!
        CFStringRef imageSourceContainerType = CGImageSourceGetType(_imageSource);
        BOOL isGIFData = UTTypeConformsTo(imageSourceContainerType, kUTTypeGIF);
        if (!isGIFData) {
            FLLog(FLLogLevelError, @"Supplied data is of type %@ and doesn't seem to be GIF data %@", imageSourceContainerType, data);
            return nil;
        }
        
        NSDictionary *imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(_imageSource, NULL);
        _loopCount = [[[imageProperties objectForKey:(id)kCGImagePropertyGIFDictionary] objectForKey:(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue];
        
        // Iterate through frame images
        size_t imageCount = CGImageSourceGetCount(_imageSource);
        NSUInteger skippedFrameCount = 0;
        NSMutableDictionary *delayTimesForIndexesMutable = [NSMutableDictionary dictionaryWithCapacity:imageCount];
        for (size_t i = 0; i < imageCount; i++) {
            @autoreleasepool {
                CGImageRef frameImageRef = CGImageSourceCreateImageAtIndex(_imageSource, i, NULL);
                if (frameImageRef) {
                    UIImage *frameImage = [UIImage imageWithCGImage:frameImageRef];
                    // Check for valid `frameImage` before parsing its properties as frames can be corrupted (and `frameImage` even `nil` when `frameImageRef` was valid).
                    if (frameImage) {
                        // Set poster image
                        if (!self.posterImage) {
                            _posterImage = frameImage;
                            // Set its size to proxy our size.
                            _size = _posterImage.size;
                            // Remember index of poster image so we never purge it; also add it to the cache.
                            _posterImageFrameIndex = i;
                            [self.cachedFramesForIndexes setObject:self.posterImage forKey:@(self.posterImageFrameIndex)];
                            [self.cachedFrameIndexes addIndex:self.posterImageFrameIndex];
                        }
                        
                        NSDictionary *frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(_imageSource, i, NULL);
                        NSDictionary *framePropertiesGIF = [frameProperties objectForKey:(id)kCGImagePropertyGIFDictionary];
                        
                        // Try to use the unclamped delay time; fall back to the normal delay time.
                        NSNumber *delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFUnclampedDelayTime];
                        if (!delayTime) {
                            delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFDelayTime];
                        }
                        
                        delayTimesForIndexesMutable[@(i)] = delayTime;
                    } else {
                        skippedFrameCount++;
                    }
                    CFRelease(frameImageRef);
                } else {
                    skippedFrameCount++;
                }
            }
        }

FLAnimatedImage有一個關(guān)鍵接口imageLazilyCachedAtIndex 用于獲取某一幀對應(yīng)的Image。
關(guān)鍵思想是:內(nèi)存管理唠帝、內(nèi)存警告處理屯掖、緩存幀管理、子線程異步加載襟衰。
imageLazilyCachedAtIndex 獲取某一幀的時候贴铜,會進行前面幾幀的預(yù)加載,如果獲取的一幀還沒加載完成瀑晒,那么會返回 nil 值绍坝,避免卡頓的情況。

FLAnimatedImageView

FLAnimatedImageView的職責(zé)是繪制GIF動畫苔悦。
那么如何繪制動畫轩褐?如何驅(qū)動動畫的繪制?怎么繪制间坐?

驅(qū)動的關(guān)鍵是CADisplayLink

- (void)startAnimating
{
    if (self.animatedImage) {
        // Lazily create the display link.
        if (!self.displayLink) {
            // It is important to note the use of a weak proxy here to avoid a retain cycle. `-displayLinkWithTarget:selector:`
            // will retain its target until it is invalidated. We use a weak proxy so that the image view will get deallocated
            // independent of the display link's lifetime. Upon image view deallocation, we invalidate the display
            // link which will lead to the deallocation of both the display link and the weak proxy.
            FLWeakProxy *weakProxy = [FLWeakProxy weakProxyForObject:self];
            self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)];
            
            [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode];
        }

        // Note: The display link's `.frameInterval` value of 1 (default) means getting callbacks at the refresh rate of the display (~60Hz).
        // Setting it to 2 divides the frame rate by 2 and hence calls back at every other display refresh.
        const NSTimeInterval kDisplayRefreshRate = 60.0; // 60Hz
        self.displayLink.frameInterval = MAX([self frameDelayGreatestCommonDivisor] * kDisplayRefreshRate, 1);

        self.displayLink.paused = NO;
    } else {
        [super startAnimating];
    }
}

注意:其中NSRunLoop的mode設(shè)置灾挨, NSDefaultRunLoopMode 時邑退,滑動scrollview時竹宋,GIF會暫停播放劳澄,NSRunLoopCommonModes模式是不會暫停。

其中蜈七,有一個問題:使用CADisplayLink如何避免循環(huán)引用秒拔?
CADisplayLink的target是retain這個target, 而displayLink會add到主線程的Runloop中,就會形成 Runloop -> CADisplayLink -> self 的引用關(guān)系飒硅。
解決辦法是使用FLWeakProxy 弱引用self, 這樣引用關(guān)系變成了 Runloop -> CADisplayLink -> WeakProxy砂缩, WeakProxy再弱引用self。當self釋放時移除CADisplayLink三娩,這樣就避免了循環(huán)引用庵芭。

- (void)dealloc
{
    [_displayLink invalidate];
}

繪制

有了驅(qū)動,如何繪制雀监?
CADisplayLink的回調(diào)中:
- (void)displayDidRefresh:(CADisplayLink *)displayLink
實現(xiàn)了loopCount控制双吆、幀Index計數(shù)、延時管理(不能播放太快会前,也不能太慢:美帧)
看源碼:

- (void)displayDidRefresh:(CADisplayLink *)displayLink
{   
    NSNumber *delayTimeNumber = [self.animatedImage.delayTimesForIndexes objectForKey:@(self.currentFrameIndex)];
    // If we don't have a frame delay (e.g. corrupt frame), don't update the view but skip the playhead to the next frame (in else-block).
    if (delayTimeNumber) {
        NSTimeInterval delayTime = [delayTimeNumber floatValue];
        // If we have a nil image (e.g. waiting for frame), don't update the view nor playhead.
        UIImage *image = [self.animatedImage imageLazilyCachedAtIndex:self.currentFrameIndex];
        if (image) {
            FLLog(FLLogLevelVerbose, @"Showing frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
            self.currentFrame = image;          //更新當前currentFrame,在繪制的時候使用
            if (self.needsDisplayWhenImageBecomesAvailable) {
                [self.layer setNeedsDisplay];
                self.needsDisplayWhenImageBecomesAvailable = NO;
            }
            
            self.accumulator += displayLink.duration * displayLink.frameInterval;
            
            // While-loop first inspired by & good Karma to: https://github.com/ondalabs/OLImageView/blob/master/OLImageView.m
            while (self.accumulator >= delayTime) { 
                self.accumulator -= delayTime;
                self.currentFrameIndex++;
                if (self.currentFrameIndex >= self.animatedImage.frameCount) {
                    // If we've looped the number of times that this animated image describes, stop looping.
                    self.loopCountdown--;
                    if (self.loopCompletionBlock) {
                        self.loopCompletionBlock(self.loopCountdown);
                    }
                    
                    if (self.loopCountdown == 0) {
                        [self stopAnimating];
                        return;
                    }
                    self.currentFrameIndex = 0;
                }
                // Calling `-setNeedsDisplay` will just paint the current frame, not the new frame that we may have moved to.
                // Instead, set `needsDisplayWhenImageBecomesAvailable` to `YES` -- this will paint the new image once loaded.
                self.needsDisplayWhenImageBecomesAvailable = YES;
            }
        } else {
        }
    } else {
        self.currentFrameIndex++;
    }

特別注意的是其中while的設(shè)計瓦宜,是為了在本次DisplayLink中拿到正確的currentFrameIndex蔚万。

繪制

非常簡單,拿到GIF幀的圖片后临庇,直接顯示:

- (void)displayLayer:(CALayer *)layer
{
    layer.contents = (__bridge id)self.image.CGImage;
}

- (UIImage *)image
{
    UIImage *image = nil;
    if (self.animatedImage) {
        // Initially set to the poster image.
        image = self.currentFrame;
    } else {
        image = super.image;
    }
    return image;
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末反璃,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子假夺,更是在濱河造成了極大的恐慌版扩,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件侄泽,死亡現(xiàn)場離奇詭異礁芦,居然都是意外死亡,警方通過查閱死者的電腦和手機悼尾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進店門柿扣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人闺魏,你說我怎么就攤上這事未状。” “怎么了析桥?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵司草,是天一觀的道長艰垂。 經(jīng)常有香客問我,道長埋虹,這世上最難降的妖魔是什么猜憎? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任,我火速辦了婚禮搔课,結(jié)果婚禮上胰柑,老公的妹妹穿的比我還像新娘。我一直安慰自己爬泥,他們只是感情好柬讨,可當我...
    茶點故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著袍啡,像睡著了一般踩官。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上境输,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天蔗牡,我揣著相機與錄音,去河邊找鬼畴嘶。 笑死蛋逾,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的窗悯。 我是一名探鬼主播区匣,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蒋院!你這毒婦竟也來了亏钩?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤欺旧,失蹤者是張志新(化名)和其女友劉穎姑丑,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體辞友,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡栅哀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了称龙。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片留拾。...
    茶點故事閱讀 38,809評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖鲫尊,靈堂內(nèi)的尸體忽然破棺而出痴柔,到底是詐尸還是另有隱情,我是刑警寧澤疫向,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布咳蔚,位于F島的核電站豪嚎,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏谈火。R本人自食惡果不足惜侈询,卻給世界環(huán)境...
    茶點故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望堆巧。 院中可真熱鬧妄荔,春花似錦泼菌、人聲如沸谍肤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽荒揣。三九已至,卻和暖如春焊刹,著一層夾襖步出監(jiān)牢的瞬間系任,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工虐块, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留俩滥,地道東北人。 一個月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓贺奠,卻偏偏與公主長得像霜旧,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子儡率,可洞房花燭夜當晚...
    茶點故事閱讀 43,724評論 2 351

推薦閱讀更多精彩內(nèi)容