YYImage源碼剖析與學(xué)習(xí)

建議查看原文:http://www.reibang.com/p/83edaeeb5851(不定時(shí)更新)

源碼剖析學(xué)習(xí)系列:(不斷更新)

1金踪、FBKVOController源碼剖析與學(xué)習(xí)
2、MJRefresh源碼剖析與學(xué)習(xí)
3竞帽、YYImage源碼剖析與學(xué)習(xí)


前言:

要看懂YYImage框架虽界,最好先了解熱身部分(具體的自行百度)汽烦,如果懶得看,直接跨過該部分莉御,等到下面部分有疑問撇吞,再回過頭看這部分的知識(shí),也是可以礁叔。

熱身部分

移動(dòng)端圖片格式調(diào)研

1牍颈、Image I/O

Image I/O 學(xué)習(xí)筆記
Image I/O官方文檔
GIF圖添加文字Demo

使用 CGBitmapContextCreate 函數(shù)創(chuàng)建一個(gè)位圖上下文;
使用 CGContextDrawImage 函數(shù)將原始位圖繪制到上下文中琅关;
使用 CGBitmapContextCreateImage 函數(shù)創(chuàng)建一張新的解壓縮后的位圖煮岁。

2、 CGBitmapContextCreate 中的參數(shù)

談?wù)?iOS 中圖片的解壓縮

  • data :如果不為 NULL ,那么它應(yīng)該指向一塊大小至少為 bytesPerRow * height 字節(jié)的內(nèi)存画机;如果 為 NULL 冶伞,那么系統(tǒng)就會(huì)為我們自動(dòng)分配和釋放所需的內(nèi)存,所以一般指定 NULL 即可步氏;
  • widthheight :位圖的寬度和高度响禽,分別賦值為圖片的像素寬度和像素高度即可;
  • bitsPerComponent :像素的每個(gè)顏色分量使用的 bit 數(shù)戳护,在 RGB 顏色空間下指定 8 即可金抡;
  • bytesPerRow :位圖的每一行使用的字節(jié)數(shù),大小至少為 width * bytes per pixel 字節(jié)腌且。有意思的是,當(dāng)我們指定 0 時(shí)榛瓮,系統(tǒng)不僅會(huì)為我們自動(dòng)計(jì)算铺董,而且還會(huì)進(jìn)行 cache line alignment 的優(yōu)化
  • space顏色空間,一般使用 RGB 即可禀晓;
  • bitmapInfo :位圖的布局信息精续。當(dāng)圖片不包含 alpha 的時(shí)候使用 kCGImageAlphaNoneSkipFirst ,否則使用 kCGImageAlphaPremultipliedFirst
3粹懒、信號(hào)量

信號(hào)量的講解

/* 注意重付,正常的使用順序是先降低然后再提高,這兩個(gè)函數(shù)通常成對(duì)使用凫乖。 */
    dispatch_semaphore_wait(_framesLock, DISPATCH_TIME_FOREVER); //等待降低信號(hào)量
    // to do
    dispatch_semaphore_signal(_framesLock); //提高信號(hào)量

所用到的知識(shí):
復(fù)合賦值運(yùn)算符确垫、Image I/O、CADisplayLink帽芽、willChangeValueForKey:删掀、

一、YYImage總體介紹

YImage 結(jié)構(gòu)

1导街、YYImage 源碼
2披泪、YYImage 源碼的文字解析版本

1、YYImage 功能

  • 顯示動(dòng)畫類型的圖片
  • 播放幀動(dòng)畫
  • 播放 sprite sheet 動(dòng)畫
  • 圖片類型探測
  • 圖片解碼搬瑰、編碼(最核心功能)

2款票、YYImage 主要類介紹

YYImage 類

它是一個(gè)完全兼容的“UIImage”子類。它擴(kuò)展了UIImage
支持動(dòng)畫WebP, APNG和GIF格式的圖像數(shù)據(jù)解碼泽论。它還
支持NSCoding協(xié)議艾少,以存檔和反存檔多幀圖像數(shù)據(jù)。

a佩厚、animatedImageMemorySize

如果所有幀圖像都被加載到內(nèi)存中姆钉,那么總內(nèi)存使用(以字節(jié)為單位)。
如果圖像不是從多幀圖像數(shù)據(jù)創(chuàng)建的,則該值為0潮瓶。

b陶冷、preloadAllAnimatedImageFrames

將此屬性設(shè)置為“YES”將阻塞要解碼的調(diào)用線程
所有動(dòng)畫幀圖像到內(nèi)存,設(shè)置為“NO”將釋放預(yù)裝幀毯辅。
如果圖像被許多圖像視圖(如emoticon)共享埂伦,則預(yù)加載所有視圖
幀將降低CPU成本。

YYAnimatedImageView 類

用于顯示動(dòng)畫圖像的圖像視圖思恐。
可以用來播放多幀動(dòng)畫以及普通動(dòng)畫沾谜,可以控制、暫停動(dòng)畫
當(dāng)設(shè)備有足夠的空閑內(nèi)存時(shí)胀莹,這個(gè)視圖及時(shí)請(qǐng)求幀數(shù)據(jù)基跑。
這個(gè)視圖可以在內(nèi)部緩沖區(qū)中緩存一些或所有未來的幀,以降低CPU成本描焰。

3媳否、YYImage 的意義(圖片解碼的原因)

從磁盤中加載一張圖片,并將它顯示到屏幕上荆秦,這個(gè)過程其實(shí)經(jīng)歷很多篱竭,非常耗性能。隨著顯示的圖片增加步绸,性能下降尤其明顯掺逼。不管是 JPEG 還是 PNG 等圖片,都是一種編碼后(壓縮)的位圖圖形格式瓤介。我們先看下顯示到屏幕這個(gè)過程的工作流:

1吕喘、我們使用+[UIImage imageWithContentsOfFile:]方法從磁盤中加載一張圖片。此時(shí)惑朦,圖片還沒有被解碼兽泄,仍舊是編碼狀態(tài)下。
2漾月、返回的圖片被分配給UIImageView
3病梢、接著一個(gè)隱式的 CATransaction 捕獲到了圖層樹的變化;
4梁肿、在主線程的下一個(gè) run loop到來時(shí)蜓陌,Core Animation 提交了這個(gè)隱式的事務(wù),可能會(huì)涉及copy這些圖片(已經(jīng)成為圖層樹中的圖層內(nèi)容的圖片)吩蔑。這個(gè) copy 操作可能會(huì)涉及以下部分或全部步驟:

a.分配緩沖區(qū)來管理文件IO和解壓縮操作钮热。
b.文件數(shù)據(jù)從磁盤讀取到內(nèi)存。
c.將壓縮的圖片數(shù)據(jù)解碼成未壓縮的位圖形式烛芬,這是一個(gè)非常耗時(shí)的 CPU 操作隧期;
d.最后 Core Animation 使用未壓縮的位圖數(shù)據(jù)渲染 UIImageView 的圖層

圖層樹:(個(gè)人理解)洋蔥看過去有很多層飒责,這就是洋蔥的圖層,而屏幕上顯示的文字仆潮、圖片啊宏蛉,都可以理解成為圖層,很多圖層就形成了一個(gè)結(jié)構(gòu)性置,這個(gè)很多圖層的結(jié)構(gòu)就叫做圖層樹拾并。

因此,在將磁盤中的圖片渲染到屏幕之前鹏浅,必須先要得到圖片的原始像素?cái)?shù)據(jù)嗅义,才能執(zhí)行后續(xù)的繪制操作,這就是為什么需要對(duì)圖片解碼的原因隐砸。

二之碗、YYImage主要類調(diào)用邏輯

A、渲染GIF/WebP/PNG(APNG)方法調(diào)用順序

1季希、YYImage *image = [YYImage imageNamed:name]; //傳入圖片名創(chuàng)建YYImage對(duì)象

2继控、[[self alloc] initWithData:data scale:scale];//用重寫的方法初始化圖像數(shù)據(jù)

3、YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];//創(chuàng)建解碼類 YYImageDecoder 對(duì)象胖眷,緊接著更新數(shù)據(jù)

4、result = [self _updateData:data final:final];//根據(jù)圖像的data算出圖片的type以及其他信息霹崎,再根據(jù)不同type 的圖像去分別更新數(shù)據(jù)

5珊搀、[self _updateSourceImageIO];// 計(jì)算出PNG、GIF等圖片信息(圖片的每一幀的屬性尾菇,包括寬境析、高、方向派诬、動(dòng)畫重復(fù)次數(shù)(gif類型)劳淆、持續(xù)時(shí)間(gif類型))

6、 YYAnimatedImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image]; //把圖片添加到 UIImageView 的子類默赂,這個(gè)子類后面講(第7點(diǎn)后都是它的核心)沛鸵,這里暫且當(dāng)它為普通 ImageView 那樣看。

7缆八、[self setImage:image withType:YYAnimatedImageTypeImage];// 設(shè)置圖片曲掰,類似Setter方法

8、[self imageChanged];//判斷當(dāng)前圖片類型以及幀數(shù)奈辰,由CATransaction支持的顯示事務(wù)去更新圖層的 contentsRect栏妖,以及重置動(dòng)畫的參數(shù),后面詳解該方法奖恰。

9吊趾、[self resetAnimated];//重置動(dòng)畫多種參數(shù)宛裕;[self calcMaxBufferCount]; // 動(dòng)態(tài)調(diào)整當(dāng)前內(nèi)存的緩沖區(qū)大小。

10论泛、[self didMoved];// 窗口對(duì)象或者父視圖對(duì)象改變揩尸,則開始控制動(dòng)畫的啟動(dòng)(停止),這是動(dòng)畫得以顯示的關(guān)鍵

B孵奶、渲染幀動(dòng)畫方法調(diào)用順序

1疲酌、UIImage *image = [[YYFrameImage alloc] initWithImagePaths:paths oneFrameDuration:0.1 loopCount:0]; //傳入圖片組的路徑、每一個(gè)幀(每一個(gè)圖片)的時(shí)間以及循環(huán)多少次了袁,計(jì)算出總的durations
2朗恳、[self initWithImagePaths:paths frameDurations:durations loopCount:loopCount];// 把第一張圖片解碼后返回,并求出第一幀的大小载绿,作為每一幀的大小
3粥诫、YYAnimatedImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image];
后面步驟跟 渲染GIF/WebP/PNG(APNG)方法調(diào)用順序 第7點(diǎn)開始幾乎一樣

注意:由于代碼過多,不可能面面俱到崭庸,所以下面只會(huì)摘取核心進(jìn)行講解怀浆。這樣,讀者看完此文以及看完我標(biāo)注過的源碼()怕享,执赡,去讀源代碼,也更容易理解函筋。

三沙合、核心代碼

// 它接受一個(gè)原始的位圖參數(shù) imageRef 款侵,最終返回一個(gè)新的解壓縮后的位圖 newImage
CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) {
    if (!imageRef) return NULL;
    size_t width = CGImageGetWidth(imageRef);
    size_t height = CGImageGetHeight(imageRef);
    if (width == 0 || height == 0) return NULL;
    
    // 重新繪制解碼(可能會(huì)失去一些精度)
    if (decodeForDisplay) { //decode with redraw (may lose some precision)
        CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask;
        BOOL hasAlpha = NO;
        if (alphaInfo == kCGImageAlphaPremultipliedLast ||
            alphaInfo == kCGImageAlphaPremultipliedFirst ||
            alphaInfo == kCGImageAlphaLast ||
            alphaInfo == kCGImageAlphaFirst) {
            hasAlpha = YES;
        }
        // BGRA8888 (premultiplied) or BGRX8888
        // same as UIGraphicsBeginImageContext() and -[UIView drawRect:]
        CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
        bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
        CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, YYCGColorSpaceGetDeviceRGB(), bitmapInfo);
        if (!context) return NULL;
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); // decode
        CGImageRef newImage = CGBitmapContextCreateImage(context);
        CFRelease(context);
        return newImage; // 返回一個(gè)新的解壓縮后的位圖 newImage
        
    } else {
    
    }
}

YYCGImageCreateDecodedCopy 是解壓縮的核心刀荒,也就是渲染圖片性能顯著的原因。該方法首先求出圖片的寬高垂睬,注意谨敛,這里的圖片是指編碼前的圖片的每一幀圖片究履。

- (void)imageChanged {
    YYAnimatedImageType newType = [self currentImageType];
    id newVisibleImage = [self imageForType:newType];
    NSUInteger newImageFrameCount = 0;
    BOOL hasContentsRect = NO;
    if ([newVisibleImage isKindOfClass:[UIImage class]] &&
        [newVisibleImage conformsToProtocol:@protocol(YYAnimatedImage)]) {
        // 求出有多少幀(如果是幀動(dòng)畫(由多張圖組合的),相當(dāng)于有多少張圖)
        newImageFrameCount = ((UIImage<YYAnimatedImage> *) newVisibleImage).animatedImageFrameCount;
        if (newImageFrameCount > 1) { // 動(dòng)態(tài)圖
            hasContentsRect = [((UIImage<YYAnimatedImage> *) newVisibleImage) respondsToSelector:@selector(animatedImageContentsRectAtIndex:)];
        }
    }
    // 由CATransaction支持的顯示事務(wù)去更新圖層的 contentsRect, 但一般不用走這段代碼脸狸。大都走的是 CATransaction 的隱式事務(wù)自己更新
    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;

    // YYSpriteSheetImage 類用到最仑,先不理
    if (hasContentsRect) {
        CGRect rect = [((UIImage<YYAnimatedImage> *) newVisibleImage) animatedImageContentsRectAtIndex:0];
        [self setContentsRect:rect forImage:newVisibleImage];
    }
    
    if (newImageFrameCount > 1) {
        [self resetAnimated]; // 重置動(dòng)畫多種參數(shù),包括在后臺(tái)釋放圖像肥惭,下面再賦值已經(jīng)被重置過的動(dòng)畫參數(shù)
        _curAnimatedImage = newVisibleImage; // 當(dāng)前動(dòng)畫圖片
        _curFrame = newVisibleImage; // 當(dāng)前幀
        _totalLoop = _curAnimatedImage.animatedImageLoopCount; // 總循環(huán)次數(shù)
        _totalFrameCount = _curAnimatedImage.animatedImageFrameCount; // 總幀數(shù)
        [self calcMaxBufferCount]; // 動(dòng)態(tài)調(diào)整當(dāng)前內(nèi)存的緩沖區(qū)大小盯仪。
    }
    [self setNeedsDisplay]; // 標(biāo)志需要重繪,會(huì)在下一個(gè)循環(huán)到來時(shí)刷新
    [self didMoved]; // 窗口對(duì)象或者父視圖對(duì)象改變蜜葱,則開始控制動(dòng)畫的啟動(dòng)(停止)全景,這是動(dòng)畫得以顯示的關(guān)鍵
}

圖片改變的處理核心

主要做了以下幾點(diǎn):

  • 初始化動(dòng)畫參數(shù) resetAniamted
  • 初始化或者重置后求出動(dòng)畫播放循環(huán)次數(shù)、當(dāng)前幀牵囤、總幀數(shù)
  • 調(diào)用動(dòng)態(tài)調(diào)整緩沖區(qū)方法 calcMaxBufferCount 爸黄、調(diào)用控制動(dòng)畫方法 didMoved
// init the animated params.
- (void)resetAnimated {
    if (!_link) {
        _lock = dispatch_semaphore_create(1);
        _buffer = [NSMutableDictionary new];
        
        // 添加到這種隊(duì)列中的操作滞伟,就會(huì)自動(dòng)放到子線程中執(zhí)行。
        _requestQueue = [[NSOperationQueue alloc] init];
        /* maxConcurrentOperationCount 默認(rèn)情況下為-1炕贵,表示不進(jìn)行限制梆奈,可進(jìn)行并發(fā)執(zhí)行。
        為1時(shí)称开,隊(duì)列為串行隊(duì)列亩钟。只能串行執(zhí)行。大于1時(shí)鳖轰,隊(duì)列為并發(fā)隊(duì)列 */
        _requestQueue.maxConcurrentOperationCount = 1;
        /* 初始化一個(gè)新的 CADisplayLink 對(duì)象清酥,在屏幕更新時(shí)調(diào)用。為了使顯示循環(huán)與顯示同步蕴侣,應(yīng)用程序使用addToRunLoop:forMode:方法將其添加到運(yùn)行循環(huán)中
            一個(gè)計(jì)時(shí)器對(duì)象焰轻,允許應(yīng)用程序?qū)⑵淅L圖同步到顯示的刷新率。
         */
        _link = [CADisplayLink displayLinkWithTarget:[_YYImageWeakProxy proxyWithTarget:self] selector:@selector(step:)];
        if (_runloopMode) {
            [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:_runloopMode];
        }
        // 禁用通知
        _link.paused = YES;
        
        // 接受內(nèi)存警告的通知
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
        // 接受返回后臺(tái)的通知昆雀,返回后臺(tái)時(shí)辱志,記錄即將顯示的下一幀
        [[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];  // 捕獲字典到全局隊(duì)列,在后臺(tái)釋放這些圖像以避免阻塞UI線程狞膘。
                 
             });
         }
    );
    _link.paused = YES;
    _time = 0;
    if (_curIndex != 0) {
        [self willChangeValueForKey:@"currentAnimatedImageIndex"];
        _curIndex = 0; // 把索引值重置為0
        [self didChangeValueForKey:@"currentAnimatedImageIndex"];
    }
    _curAnimatedImage = nil; // 當(dāng)前圖像為空
    _curFrame = nil; // 當(dāng)前幀
    _curLoop = 0; //當(dāng)前循環(huán)次數(shù)
    _totalLoop = 0; // 總循環(huán)次數(shù)
    _totalFrameCount = 1; // 總幀數(shù)
    _loopEnd = NO; // 是否循環(huán)結(jié)尾
    _bufferMiss = NO; // 是否丟幀
    _incrBufferCount = 0; // 當(dāng)前允許的緩存
}

重置圖片的參數(shù)揩懒;
內(nèi)存警告時(shí)釋放內(nèi)存;
初始化一個(gè)新的 CADisplayLink 對(duì)象挽封,在屏幕更新時(shí)調(diào)用旭从。

// 只有屏幕刷新累加時(shí)間不小于當(dāng)前幀的動(dòng)畫播放時(shí)間才顯示圖片,播放下一幀场仲。
// 播放 GIF 的關(guān)鍵
- (void)step:(CADisplayLink *)link {
    UIImage <YYAnimatedImage> *image = _curAnimatedImage;
    NSMutableDictionary *buffer = _buffer;
    // 下一張的圖片
    UIImage *bufferedImage = nil;
    // 下一張要顯示的索引
    NSUInteger nextIndex = (_curIndex + 1) % _totalFrameCount;
    BOOL bufferIsFull = NO;
    
    // // 當(dāng)前無圖像顯示 返回
    if (!image) return;
    if (_loopEnd) { // view will keep in last frame // 結(jié)束循環(huán) 停留在最后幀
        [self stopAnimating]; // 如果動(dòng)畫播放循環(huán)結(jié)束了,就停止動(dòng)畫
        return;
    }
    
    NSTimeInterval delay = 0;
    if (!_bufferMiss) {
        // 屏幕刷新時(shí)間的累加
        _time += link.duration; // link.duration 屏幕刷新的時(shí)間退疫,默認(rèn)1/60 s
        delay = [image animatedImageDurationAtIndex:_curIndex]; // 返回當(dāng)前幀的持續(xù)時(shí)間
        if (_time < delay) return;
        _time -= delay; // 減去上一幀播放的時(shí)間
        if (nextIndex == 0) {
            _curLoop++; // 增加一輪循環(huán)次數(shù)
            if (_curLoop >= _totalLoop && _totalLoop != 0) { // 已經(jīng)到了循環(huán)次數(shù)渠缕,停止播放
                _loopEnd = YES;
                [self stopAnimating];
                [self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep
                return; // stop at last frame
            }
        }
        delay = [image animatedImageDurationAtIndex:nextIndex]; // 返回下一幀的的持續(xù)時(shí)間
        
        /**  */
        if (_time > delay) _time = delay; // do not jump over frame
    }
    LOCK(
         bufferedImage = buffer[@(nextIndex)];
         if (bufferedImage) {
             if ((int)_incrBufferCount < _totalFrameCount) {
                 [buffer removeObjectForKey:@(nextIndex)];
             }
             [self willChangeValueForKey:@"currentAnimatedImageIndex"];
             _curIndex = nextIndex; // 用KVO改變 當(dāng)前索引值
             [self didChangeValueForKey:@"currentAnimatedImageIndex"];
             _curFrame = bufferedImage == (id)[NSNull null] ? nil : bufferedImage;
             
             // 實(shí)現(xiàn)YYSpriteSheetImage 的協(xié)議方法,才會(huì)進(jìn)入該 if 語句
             if (_curImageHasContentsRect) {
                 _curContentsRect = [image animatedImageContentsRectAtIndex:_curIndex];
                 [self setContentsRect:_curContentsRect forImage:_curFrame];
             }
             nextIndex = (_curIndex + 1) % _totalFrameCount;
             _bufferMiss = NO;
             if (buffer.count == _totalFrameCount) {
                 bufferIsFull = YES; // 緩沖區(qū)已經(jīng)滿
             }
         } else {
             // 丟幀褒繁,某一幀沒有辦法找到顯示
             _bufferMiss = YES;
         }
    )//LOCK
    
    if (!_bufferMiss) {
        // 刷新顯示圖像
        [self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep
    }
    
    /* _YYAnimatedImageViewFetchOperation 為 NSOperation 的子類
        還未獲取完所有圖像亦鳞,交給它獲取下一張圖像 */
    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]; //
    }
}

這是動(dòng)畫播放的關(guān)鍵,是 CADisplayLink對(duì)象 的方法棒坏,每 1/60s 也就是屏幕刷新一次就調(diào)用一次

- (void)calcMaxBufferCount {
    int64_t bytes = (int64_t)_curAnimatedImage.animatedImageBytesPerFrame; // 求出每一幀的字節(jié)數(shù)
    if (bytes == 0) bytes = 1024; // 如果為0燕差,則給定1024
    
    int64_t total = _YYDeviceMemoryTotal(); // 獲取設(shè)備的CPU物理內(nèi)存
    int64_t free = _YYDeviceMemoryFree(); // 獲取設(shè)備的容量
    int64_t max = MIN(total * 0.2, free * 0.6); // 比較內(nèi)存的0.2倍以及容量的0.6倍最小值
    max = MAX(max, BUFFER_SIZE); // 如果不夠 10 M,則以 10 M 作為最大緩沖區(qū)大小
    
    /** _maxBufferSize 內(nèi)部幀緩沖區(qū)大小
     * 當(dāng)設(shè)備有足夠的空閑內(nèi)存時(shí),這個(gè)視圖將請(qǐng)求并解碼一些或所有未來的幀圖像進(jìn)入一個(gè)內(nèi)部緩沖區(qū)坝冕。
     * 默認(rèn)值為0 如果這個(gè)屬性的值是0徒探,那么最大緩沖區(qū)大小將根據(jù)當(dāng)前的狀態(tài)進(jìn)行動(dòng)態(tài)調(diào)整設(shè)備釋放內(nèi)存。否則喂窟,緩沖區(qū)大小將受到此值的限制测暗。
     * 當(dāng)收到內(nèi)存警告或應(yīng)用程序進(jìn)入后臺(tái)時(shí)央串,緩沖區(qū)將被立即釋放
     */
    if (_maxBufferSize) max = max > _maxBufferSize ? _maxBufferSize : max; //得出緩沖區(qū)的最大值
    
    double maxBufferCount = (double)max / (double)bytes;
    if (maxBufferCount < 1) maxBufferCount = 1;
    else if (maxBufferCount > 512) maxBufferCount = 512;
    _maxBufferCount = maxBufferCount; // 最大緩沖數(shù)
}

動(dòng)態(tài)求出最大緩沖數(shù)--->參考

/* 從自定義的 start 方法中調(diào)用 main 方法
 調(diào)用[self didMoved]; 從而調(diào)用此方法
*/
- (void)main {
    __strong YYAnimatedImageView *view = _view;
    if (!view) return;
    if ([self isCancelled]) return;
    view->_incrBufferCount++;
    
    //動(dòng)態(tài)調(diào)整當(dāng)前內(nèi)存的緩沖區(qū)大小。
    if (view->_incrBufferCount == 0) [view calcMaxBufferCount];
    
    if (view->_incrBufferCount > (NSInteger)view->_maxBufferCount) {
        view->_incrBufferCount = view->_maxBufferCount;
    }
    NSUInteger idx = _nextIndex; // 獲取 Operation 中傳過來的 下一個(gè)索引值
    NSUInteger max = view->_incrBufferCount < 1 ? 1 : view->_incrBufferCount; // 當(dāng)前的緩沖區(qū)計(jì)數(shù)
    NSUInteger total = view->_totalFrameCount; // 總圖片幀數(shù)
    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)); //  拿索引值去當(dāng)前緩沖區(qū)取圖片
            
            // 如果沒有取到圖片碗啄,則在子線程重新解碼质和,得到解碼后的圖片
            if (miss) {
                // 等到當(dāng)前還未解碼的圖片
                UIImage *img = [_curImage animatedImageFrameAtIndex:idx];
                NSLog(@"當(dāng)前線程---%@", [NSThread currentThread]); // 打印當(dāng)前線程,每次打印都是 name = (null)稚字,說明在異步線程
                // 在異步線程再次調(diào)用解碼圖片饲宿,如果無法解碼或已經(jīng)解碼就返回self
                img = img.yy_imageByDecoded;
                if ([self isCancelled]) break;
                LOCK_VIEW(view->_buffer[@(idx)] = img ? img : [NSNull null]); // 每次添加一張圖片到 _buffer 數(shù)組
                view = nil;
            }
        }
    }
}

該方法負(fù)責(zé)把圖片存入緩沖區(qū)中。(過程:取未解碼圖片-->解碼存入緩沖區(qū))

在此胆描,對(duì)YYImage框架完畢了瘫想,希望大家都能從大神源碼學(xué)到知識(shí)。




其他額外收獲:

1袄友、是否模擬器

- (BOOL)isSimulator {
    size_t size;
    sysctlbyname("hw.machine", NULL, &size, NULL, 0);
    char *machine = malloc(size);
    sysctlbyname("hw.machine", machine, &size, NULL, 0);
    NSString *model = [NSString stringWithUTF8String:machine];
    free(machine);
    return [model isEqualToString:@"x86_64"] || [model isEqualToString:@"i386"];
}

2殿托、根據(jù)不同的系統(tǒng) scale 選擇圖片

/** 一個(gè)NSNumber對(duì)象數(shù)組,根據(jù)不同的系統(tǒng)scale返回?cái)?shù)組內(nèi)部不同順序的數(shù)字
e.g. iPhone3GS:@[@1,@2,@3] iPhone5:@[@2,@3,@1]  iPhone6 Plus:@[@3,@2,@1]
*/
static NSArray *_NSBundlePreferredScales() {
    static NSArray *scales;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        CGFloat screenScale = [UIScreen mainScreen].scale;
        if (screenScale <= 1) {
            scales = @[@1,@2,@3];
        } else if (screenScale <= 2) {
            scales = @[@2,@3,@1];
        } else {
            scales = @[@3,@2,@1];
        }
    });
    return scales;
}

咋一看剧蚣,這不是單例嗎支竹?保證初始化代碼只執(zhí)行一次,可移步單例相關(guān)文章

3鸠按、判斷圖片后綴

    NSArray *exts = ext.length > 0 ? @[ext] : @[@"", @"png", @"jpeg", @"jpg", @"gif", @"webp", @"apng"];
    NSArray *scales = _NSBundlePreferredScales();
    for (int s = 0; s < scales.count; s++) {
        scale = ((NSNumber *)scales[s]).floatValue;
        NSString *scaledName = _NSStringByAppendingNameScale(res, scale);
        for (NSString *e in exts) {
            path = [[NSBundle mainBundle] pathForResource:scaledName ofType:e];
            if (path) break;
        }
        if (path) break;
    }

如果圖片沒標(biāo)明后綴礼搁,則遍歷后綴數(shù)組,并添加后綴到傳進(jìn)來的圖片名目尖,最后到 mainBundle 里面取圖片路徑馒吴,取到地址則停止

CF_RETURNS_RETAINED 標(biāo)記返回CF類型的函數(shù),該類型需要調(diào)用方釋放
NSDefaultRunLoopMode 保持gif 圖在scrollView 拉動(dòng)時(shí)不停止
|= 為按位或運(yùn)算符 eg: a|=b; 相當(dāng)于 a=a|b;

參考:
快速解決GIF圖的鋸齒問題

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末瑟曲,一起剝皮案震驚了整個(gè)濱河市饮戳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌洞拨,老刑警劉巖扯罐,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異烦衣,居然都是意外死亡歹河,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門花吟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來秸歧,“玉大人,你說我怎么就攤上這事衅澈〖猓” “怎么了?”我有些...
    開封第一講書人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵今布,是天一觀的道長纱耻。 經(jīng)常有香客問我芭梯,道長,這世上最難降的妖魔是什么弄喘? 我笑而不...
    開封第一講書人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任玖喘,我火速辦了婚禮,結(jié)果婚禮上蘑志,老公的妹妹穿的比我還像新娘累奈。我一直安慰自己,他們只是感情好急但,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開白布澎媒。 她就那樣靜靜地躺著,像睡著了一般波桩。 火紅的嫁衣襯著肌膚如雪戒努。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評(píng)論 1 302
  • 那天镐躲,我揣著相機(jī)與錄音储玫,去河邊找鬼。 笑死萤皂,一個(gè)胖子當(dāng)著我的面吹牛撒穷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播裆熙,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼端礼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了入录?” 一聲冷哼從身側(cè)響起蛤奥,我...
    開封第一講書人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎僚稿,沒想到半個(gè)月后喻括,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡贫奠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了望蜡。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片唤崭。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖脖律,靈堂內(nèi)的尸體忽然破棺而出谢肾,到底是詐尸還是另有隱情,我是刑警寧澤小泉,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布芦疏,位于F島的核電站冕杠,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏酸茴。R本人自食惡果不足惜分预,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望薪捍。 院中可真熱鬧笼痹,春花似錦、人聲如沸酪穿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽被济。三九已至救赐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間只磷,已是汗流浹背经磅。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留喳瓣,地道東北人馋贤。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像畏陕,于是被迫代替她去往敵國和親配乓。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

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

  • 在iOS中隨處都可以看到絢麗的動(dòng)畫效果惠毁,實(shí)現(xiàn)這些動(dòng)畫的過程并不復(fù)雜犹芹,今天將帶大家一窺iOS動(dòng)畫全貌。在這里你可以看...
    F麥子閱讀 5,110評(píng)論 5 13
  • 在iOS中隨處都可以看到絢麗的動(dòng)畫效果鞠绰,實(shí)現(xiàn)這些動(dòng)畫的過程并不復(fù)雜腰埂,今天將帶大家一窺ios動(dòng)畫全貌。在這里你可以看...
    每天刷兩次牙閱讀 8,488評(píng)論 6 30
  • 前幾天蜈膨,我聽一個(gè)情感類節(jié)目屿笼,其中有位男士,暫且稱他為A先生吧翁巍,打進(jìn)電話和主持人談心:“隔著電臺(tái)驴一,彼此不認(rèn)識(shí),我想講...
    雋虹閱讀 508評(píng)論 0 0
  • 拋開滴滴和監(jiān)管之外灶壶,談?wù)剛€(gè)人的一些建議: 對(duì)于女士: 在經(jīng)歷了上一次事件之后肝断,對(duì)于個(gè)人,或者任何時(shí)候出門在外,都需...
    zh_way閱讀 239評(píng)論 0 0
  • 姚建新 其實(shí),你我都清楚 白就是白趣钱,黑就是黑 可是為什么會(huì)接受 那個(gè)黑白攪拌出來的灰 其實(shí)涌献,你我都認(rèn)得 鹿就是鹿,...
    姚建新閱讀 231評(píng)論 0 0