YYImage 原始碼剖析:圖片處理技巧

波兒菜發表於2019-07-30

YYKit 系列原始碼剖析文章:

引言

首先問一個問題:你會用圖片麼?

圖片是現代化 APP 介面設計裡應用廣泛的東西,精美的圖片可以帶來視覺上的享受,提高使用者體驗。由此給技術上帶來了一些挑戰,比如動圖的處理、圖片顯示流暢程度的優化、圖片包大小的優化、超大圖片的處理等。

本文主要是結合 YYImage 原始碼對圖片處理技巧進行講解。而筆者不會逐字逐句的翻譯原始碼,主要是提取原始碼中有思維價值的東西。所以最好是開啟原始碼,本文作為思想引導。

原始碼基於 1.0.4 版本。

一、圖片處理技巧

首先來談一談圖片處理的一些注意事項和技巧,以下結論參考其他博文、官方文件、實際測試得出,歡迎指出錯誤?。

一張圖片從磁碟中顯示到螢幕上過程大致如下:從磁碟載入圖片資訊、解碼二進位制圖片資料為點陣圖、通過 CoreAnimation 框架處理最終繪製到螢幕上。

實際上圖片的繪製過程往往不是效能瓶頸,最耗時的操作是解碼過程,若圖片檔案過大,從磁碟讀取的過程也有可觀的耗時。

1、載入和解壓

一般使用imageNamed:或者imageWithData:從記憶體中載入圖片生成UIImage的例項,此刻圖片並不會解壓,當 RunLoop 準備處理圖片顯示的事務(CATransaction)時,才進行解壓,而這個解壓過程是在主執行緒中的,這是導致卡頓的重要因素。

imageNamed: 方法

使用imageNamed:方法載入圖片資訊的同時(生成UIImage例項),還會將圖片資訊快取起來,所以當使用該方法第一次載入某張圖片時,會消耗較多的時間,而之後再次載入該圖片速度就會非常快(注意此時該圖片是未繪製到螢幕上的,也就是說還未解壓)。

在繪製到螢幕之前,第一次解壓成功後,系統會將解壓資訊快取到記憶體。

值得注意的是,這些快取都是全域性的,並不會因為當前UIImage例項的釋放而清除,在收到記憶體警告或者 APP 第一次進入後臺才有可能會清除,而這個清除的時機和內容是系統決定的,我們無法干涉。

imageWithData: 方法

使用imageWithData:方式載入圖片時,不管是載入過程還是解壓過程,都不會像imageNamed:快取到全域性,當該UIImage例項釋放時,相關的圖片資訊和解壓資訊就會銷燬。

兩種載入方式的區別

從上面的分析可知,imageNamed:使用時會產生全域性的記憶體佔用,但是第二次使用同一張圖片時效能很好;imageWithData:不會有全域性的記憶體佔用,但對於同一張圖片每次載入和解壓都會“從頭開始”。

由此可見,imageNamed:適合“小”且“使用頻繁”的圖片,imageWithData:適合“大”且“低頻使用”的圖片。

2、載入和解壓的優化

這裡說的優化並不是解壓演算法的優化,只是基於使用者體驗的優化。

載入優化

對於載入過程,若檔案過大或載入頻繁影響了幀率(比如列表展示大圖),可以使用非同步方式載入圖片,減少主執行緒的壓力,程式碼大致如下:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      UIImage *image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"testImage" ofType:@"jpeg"]];
      dispatch_async(dispatch_get_main_queue(), ^{
          //業務
      });
});
複製程式碼
解壓優化

解壓是耗時的,而系統預設是在主執行緒執行,所以業界通常有一種做法是,非同步強制解壓,也就是在非同步執行緒主動將二進位制圖片資料解壓成點陣圖資料,使用CGBitmapContextCreate(...)系列方法就能實現。

該處理方式在眾多圖片處理框架下都有體現。

3、超大圖的處理

值得注意的是,可能業務中需要載入一張很大的圖片。這時,若還使用常規的方式載入會佔用過多的記憶體;況且,若圖片的畫素過大(目前主流 iOS 裝置最高支援 4096 x 4096 紋理尺寸),在顯示的時候 CPU 和 GPU 都會消耗額外的資源來處理圖片。

所以,在處理超大圖時,需要一些特別的手段。

比如想要顯示完整的圖片,就可以使用如下方法壓縮到目標大小 (targetSize):

UIGraphicsBeginImageContext(targetSize);
[originalImage drawInRect:CGRectMake(0, 0, targetSize.width, targetSize.height)];
UIImage *targetImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
複製程式碼

若想要顯示超大圖的區域性,可以這麼做:

CGImageRef tmpImage = CGImageCreateWithImageInRect(originalImage, rect);
UIImage *targetImage = [UIImage imageWithCGImage: tmpImage];
CGImageRelease(tmpImage);
複製程式碼

或者直接使用CALayercontentsRect屬性來達到相同的效果。

二、YYImage 框架整體概覽

上文中談了一下圖片處理的一些原理和核心思想,做為背景知識,下面從一個巨集觀的角度觀察一下 YYImage 框架的設計,目錄結構如下:

YYImage.h (.m)  
YYFrameImage.h (.m)
YYSpriteSheetImage.h (.m)
YYAnimatedImageView.h (.m)
YYImageCoder.h (.m)
複製程式碼

從命名大致就可以猜測出來它們的功能,YYImage、YYFrameImage、YYSpriteSheetImage都是繼承自UIImage的圖片類,YYAnimatedImageView繼承自UIImageView用於處理框架自定義的圖片類,YYImageCoder是編碼和解碼器。

以下是該框架 github 上 README 寫的特性:

  • 支援以下型別動畫影像的播放/編碼/解碼: WebP, APNG, GIF。
  • 支援以下型別靜態影像的顯示/編碼/解碼: WebP, PNG, GIF, JPEG, JP2, TIFF, BMP, ICO, ICNS。
  • 支援以下型別圖片的漸進式/逐行掃描/隔行掃描解碼: PNG, GIF, JPEG, BMP。
  • 支援多張圖片構成的幀動畫播放,支援單張圖片的 sprite sheet 動畫。
  • 高效的動態記憶體快取管理,以保證高效能低記憶體的動畫播放。
  • 完全相容 UIImage 和 UIImageView,使用方便。
  • 保留可擴充套件的介面,以支援自定義動畫。
  • 每個類和方法都有完善的文件註釋。

三、YYImage 類

該類對UIImage進行擴充,支援 WebP、APNG、GIF 格式的圖片解碼,為了避免產生全域性快取,過載了imageNamed:方法:

+ (YYImage *)imageNamed:(NSString *)name {
    ...
    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;
    }
    ...
    return [[self alloc] initWithData:data scale:scale];
}
複製程式碼
  • 可以看到,若未指定圖片的擴充名,這裡會遍歷查詢所有支援的型別。
  • scales為形為@[@1,@2,@3];的陣列,不同螢幕 物理解析度/邏輯解析度 不同,查詢的優先順序也不同。
  • 找到第一個有效的path就會呼叫initWithData:scale:方法初始化。

這裡雖然比以往使用UIImage更方便,除png外的圖片型別也可以不寫擴充名,但是為了極致的效能考慮,還是指定擴充名比較好。

眾多初始化方法的落腳點都是initWithData:scale:,在該方法中初始化了訊號量 (作為鎖)、圖片解碼器 (YYImageDecoder),以及通過解碼器獲取第一幀解壓過後的影像等。最終呼叫initWithCGImage:scale:orientation獲取例項。

可以看到這樣一個屬性:@property (nonatomic) BOOL preloadAllAnimatedImageFrames;,它的作用是預載入,快取解壓過後的所有幀圖片,是一個優化選項,但是需要注意記憶體的佔用,看看它的setter方法實現:

- (void)setPreloadAllAnimatedImageFrames:(BOOL)preloadAllAnimatedImageFrames {
    if (_preloadAllAnimatedImageFrames != preloadAllAnimatedImageFrames) {
        if (preloadAllAnimatedImageFrames && _decoder.frameCount > 0) {
            NSMutableArray *frames = [NSMutableArray new];
            //拿到所有幀的圖片
            for (NSUInteger i = 0, max = _decoder.frameCount; i < max; i++) {
                UIImage *img = [self animatedImageFrameAtIndex:i];
                [frames addObject:img ?: [NSNull null]];
            }
            dispatch_semaphore_wait(_preloadedLock, DISPATCH_TIME_FOREVER);
            _preloadedFrames = frames;
            dispatch_semaphore_signal(_preloadedLock);
        } else {
            dispatch_semaphore_wait(_preloadedLock, DISPATCH_TIME_FOREVER);
            _preloadedFrames = nil;
            dispatch_semaphore_signal(_preloadedLock);
        }
    }
}
複製程式碼

主要是在for迴圈中,拿到每一幀解壓後的圖片(筆者改動了一下程式碼,至於animatedImageFrameAtIndex後面解釋)。由於是解壓後的,所以該方法實際上會消耗一定的 CPU 資源,所以在實際使用中可以在非同步執行緒呼叫。

值得一提的是,此處使用訊號量dispatch_semaphore_t作為執行緒鎖來用非常適合,因為該鎖主要是保證_preloadedFrames的讀寫安全,耗時短,使用訊號量效能很好。

四、YYFrameImage 類

該類是幀動畫圖片類,可以配置每一幀的圖片資訊和顯示時長,圖片支援 png 和 jpeg:

- (nullable instancetype)initWithImagePaths:(NSArray<NSString *> *)paths
                             frameDurations:(NSArray<NSNumber *> *)frameDurations
                                  loopCount:(NSUInteger)loopCount;
- (nullable instancetype)initWithImageDataArray:(NSArray<NSData *> *)dataArray
                                 frameDurations:(NSArray *)frameDurations
                                      loopCount:(NSUInteger)loopCount;
複製程式碼

主要是這兩個初始化方法,很簡單,然後配置好每一幀的圖片後,通過YYAnimatedImageView載體操作和顯示。

五、YYSpriteSheetImage 類

SpriteSheet 動畫,原理可以理解為一張大圖上分佈有很多完整的小圖,然後不同時刻顯示不同位置的小圖。

這麼做的目的是將多張圖片的載入、解壓合併為一張大圖的載入、解壓,可以減少圖片佔用的記憶體,提高整體的解壓縮效能。

其實該框架的做法很簡單,YYSpriteSheetImage.h方法如下:

- (nullable instancetype)initWithSpriteSheetImage:(UIImage *)image
                                     contentRects:(NSArray<NSValue *> *)contentRects
                                   frameDurations:(NSArray<NSNumber *> *)frameDurations
                                        loopCount:(NSUInteger)loopCount;

@property (nonatomic, readonly) NSArray<NSValue *> *contentRects;
@property (nonatomic, readonly) NSArray<NSValue *> *frameDurations;
@property (nonatomic, readonly) NSUInteger loopCount;
複製程式碼

初始化方法中,需要傳入兩個陣列,一個是CGRect表示範圍的陣列,一個是對應時長的陣列。

然後利用CALayercontentsRect屬性,動態的讀取這張大圖某個範圍的內容。當然,這個過程的邏輯同樣在YYAnimatedImageView類中。

六、YYAnimatedImage 協議

YYAnimatedImage 協議是YYAnimatedImageViewYYImage、YYFrameImage、YYSpriteSheetImage互動的橋樑。

@protocol YYAnimatedImage <NSObject>
@required
//幀數量
- (NSUInteger)animatedImageFrameCount;
//動畫迴圈次數
- (NSUInteger)animatedImageLoopCount;
//每幀在記憶體中的大小
- (NSUInteger)animatedImageBytesPerFrame;
//index 下標的幀圖片
- (nullable UIImage *)animatedImageFrameAtIndex:(NSUInteger)index;
//index 下標幀圖片持續時間
- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index;
@optional
//index 下標幀圖片的範圍(CGRect)
- (CGRect)animatedImageContentsRectAtIndex:(NSUInteger)index;
@end
複製程式碼

不管是.gif還是幀圖片陣列還是 SpriteSheet,當我們需要利用動畫來顯示它們的時候實際上並不關心它們是何種來源,該協議是一個共有邏輯提取。任何型別的UIImage子類的動畫圖片的資料都能通過這個協議體現,YYImage、YYFrameImage、YYSpriteSheetImage都分別實現了該協議,具體操作可以看原始碼,沒有難度。

其中,- (CGRect)animatedImageContentsRectAtIndex:(NSUInteger)index;是可選方法,是YYSpriteSheetImage做 SpriteSheet 動畫需要的資料,這算是一個共有邏輯之外的特例。

利用協議來規範共有邏輯,是一個值得學習的技巧,它能讓邏輯更清晰,程式碼更有條理。

七、YYAnimatedImageView 類

一句話理解:YYAnimatedImageView類通過YYImage、YYFrameImage、YYSpriteSheetImage實現的<YYAnimatedImage>協議方法拿到幀圖片資料和相關資訊進行動畫展示。

它的原理就是如此,下面主要分析技術細節,含金量蠻高。

1、初始化流程

@property (nonatomic, copy) NSString *runloopMode;屬性預設為NSRunLoopCommonModes保證在拖動滾動檢視時動畫還能繼續。

該類重寫了一系列方法讓它們都走自定義配置:

- (void)setImage:(UIImage *)image {
    if (self.image == image) return;
    [self setImage:image withType:YYAnimatedImageTypeImage];
}
- (void)setHighlightedImage:(UIImage *)highlightedImage {
    if (self.highlightedImage == highlightedImage) return;
    [self setImage:highlightedImage withType:YYAnimatedImageTypeHighlightedImage];
}
...
複製程式碼

setImage:withType:方法就是將這些圖片資料賦值給super.image等,該方法最後會走imageChanged方法,這才是主要的初始化配置:

- (void)imageChanged {
    YYAnimatedImageType newType = [self currentImageType];
    id newVisibleImage = [self imageForType:newType];
    NSUInteger newImageFrameCount = 0;
    BOOL hasContentsRect = NO;
    ... //省略判斷是否是 SpriteSheet 型別來源

    /*1、若上一次是 SpriteSheet 型別而當前顯示的圖片不是,
    歸位 self.layer.contentsRect */
    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;

    /*2、SpriteSheet 型別時,通過`setContentsRect:forImage:`方法
    配置self.layer.contentsRect */
    if (hasContentsRect) {
        CGRect rect = [((UIImage<YYAnimatedImage> *) newVisibleImage) animatedImageContentsRectAtIndex:0];
        [self setContentsRect:rect forImage:newVisibleImage];
    }
    
    /*3、若是多幀的圖片,通過`resetAnimated`方法初始化顯示多幀動畫需要的配置;
    然後拿到第一幀圖片呼叫`setNeedsDisplay `繪製出來 */
    if (newImageFrameCount > 1) {
        [self resetAnimated];
        _curAnimatedImage = newVisibleImage;
        _curFrame = newVisibleImage;
        _totalLoop = _curAnimatedImage.animatedImageLoopCount;
        _totalFrameCount = _curAnimatedImage.animatedImageFrameCount;
        [self calcMaxBufferCount];
    }
    [self setNeedsDisplay];
    [self didMoved];
}
複製程式碼

值得提出的是,1 中歸位self.layer.contentsRectCGRectMake(0, 0, 1, 1)使用了CATransaction事務來取消隱式動畫。(由於此處完全不需要那 0.25 秒的隱式動畫)

2、動畫啟動和結束的時機

- (void)didMoved {
    if (self.autoPlayAnimatedImage) {
        if(self.superview && self.window) {
            [self startAnimating];
        } else {
            [self stopAnimating];
        }
    }
}
- (void)didMoveToWindow {
    [super didMoveToWindow];
    [self didMoved];
}
- (void)didMoveToSuperview {
    [super didMoveToSuperview];
    [self didMoved];
}
複製程式碼

didMoveToWindowdidMoveToSuperview週期方法中嘗試啟動或結束動畫,不需要在元件內部特意的去呼叫就能實現自動的播放和停止。而didMoved方法中判斷是否開啟動畫寫了個self.superview && self.window,意味著YYAnimatedImageView光有父檢視還不能開啟動畫,還需要展示在window上才行。

3、非同步解壓

YYAnimatedImageView有個佇列變數NSOperationQueue *_requestQueue;

_requestQueue = [[NSOperationQueue alloc] init];
_requestQueue.maxConcurrentOperationCount = 1;
複製程式碼

可以看出_requestQueue是一個序列的佇列,用於處理解壓任務。

_YYAnimatedImageViewFetchOperation繼承自NSOperation,重寫了main方法自定義解壓任務。它是結合變數_requestQueue;來使用的:

- (void)main {
    ...
    for (int i = 0; i < max; i++, idx++) {
        @autoreleasepool {
            ...
            if (miss) {
                UIImage *img = [_curImage animatedImageFrameAtIndex:idx];
                img = img.yy_imageByDecoded;
                if ([self isCancelled]) break;
                LOCK_VIEW(view->_buffer[@(idx)] = img ? img : [NSNull null]);
                view = nil;
            }
        }
    }
}
複製程式碼

關鍵程式碼中,animatedImageFrameAtIndex方法便會呼叫解碼,後面yy_imageByDecoded屬性是對解碼成功的第二重保證,view->_buffer[@(idx)] = img是做快取。

可以看到作者經常使用if ([self isCancelled]) break(return);判斷返回,因為在執行NSOperation任務的過程中該任務可能會被取消。

for迴圈中使用@autoreleasepool避免同一 RunLoop 迴圈中堆積過多的區域性變數。

由此,基本可以保證解壓過程是在_requestQueue序列佇列執行的,不會影響主執行緒。

4、快取機制

YYAnimatedImageView有如下幾個變數:

NSMutableDictionary *_buffer; ///< frame buffer
BOOL _bufferMiss; ///< whether miss frame on last opportunity
NSUInteger _maxBufferCount; ///< maximum buffer count
NSInteger _incrBufferCount; ///< current allowed buffer count (will increase by step)
複製程式碼

_buffter就是快取池,在_YYAnimatedImageViewFetchOperation私有類的main函式中有給_buffer賦值,作者還限制了最大快取數量。

快取限制計算

- (void)calcMaxBufferCount {
    int64_t bytes = (int64_t)_curAnimatedImage.animatedImageBytesPerFrame;
    if (bytes == 0) bytes = 1024;
    
    int64_t total = _YYDeviceMemoryTotal();
    int64_t free = _YYDeviceMemoryFree();
    int64_t max = MIN(total * 0.2, free * 0.6);
    max = MAX(max, BUFFER_SIZE);
    if (_maxBufferSize) max = max > _maxBufferSize ? _maxBufferSize : max;
    double maxBufferCount = (double)max / (double)bytes;
    if (maxBufferCount < 1) maxBufferCount = 1;
    else if (maxBufferCount > 512) maxBufferCount = 512;
    _maxBufferCount = maxBufferCount;
}
複製程式碼

該方法並不複雜,通過_YYDeviceMemoryTotal()拿到記憶體總數乘以 0.2,通過_YYDeviceMemoryFree()拿到剩餘的記憶體乘以 0.6,然後取它們最小值;之後通過最小的快取值BUFFER_SIZE和使用者自定義的_maxBufferSize屬性綜合判斷。

快取清理時機

resetAnimated方法中註冊了兩個監聽:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
複製程式碼

在收到記憶體警告或者 APP 進入後臺時,作者修剪了快取:

- (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
}
複製程式碼

在進入後臺時,清除所有的非同步解壓任務,然後計算下一幀的下標,最後移除不是下一幀的所有快取,保證進入前臺時下一幀的及時顯示。

在收到記憶體警告時處理方式大同小異,不多贅述。

5、計時器

該類使用CADisplayLink做計時任務,顯示系統每幀回撥都會觸發,所以預設大致是 60 次/秒。CADisplayLink的特性決定了它非常適合做和幀率相關的 UI 邏輯。

防止迴圈引用

_link = [CADisplayLink displayLinkWithTarget:[_YYImageWeakProxy proxyWithTarget:self] selector:@selector(step:)];
複製程式碼

這裡使用了一個_YYImageWeakProxy私有類進行訊息轉發防止迴圈引用,看看_YYImageWeakProxy核心程式碼:

@interface _YYImageWeakProxy : NSProxy
@property (nonatomic, weak, readonly) id target;
...
@end
...
- (id)forwardingTargetForSelector:(SEL)selector {
    return _target;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
    void *null = NULL;
    [invocation setReturnValue:&null];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
    return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
,,,
複製程式碼

target存在時,傳送給_YYImageWeakProxy例項的方法能正常的轉發給target

target釋放時,forwardingTargetForSelector:重定向失敗,會呼叫methodSignatureForSelector:嘗試獲取有效的方法,而若獲取的方法無效,將會丟擲異常,所以這裡隨便返回了一個init方法。

methodSignatureForSelector:獲取到一個有效的方法過後,會呼叫forwardInvocation:方法開始訊息轉發。而這裡作者給[invocation setReturnValue:&null];一個空的返回值,讓最外層的方法呼叫者不會得到不可控的返回值。雖然這裡不呼叫方法預設會返回 null ,但是為了保險起見,能儘量人為控制預設值就不要用系統控制。

計時任務

計時器回撥方法- (void)step:(CADisplayLink *)link {...}就是呼叫動畫的核心程式碼,實際上程式碼比較容易看懂,主要是顯示當前幀影像、發起下一幀的解壓任務等。

八、YYImageCoder 編解碼

該檔案中主要包含了YYImageFrame圖片幀資訊的類、YYImageDecoder解碼器、YYImageEncoder編碼器。

注意,本文對 WebP / APNG 等的圖片解壓縮演算法不會討論,主要是說明一些基於 ImageIO 的使用。

1、解碼核心程式碼

CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) {
    ...
        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;
    ...
}
複製程式碼

解碼核心程式碼不難找到,實際上就是將CGImageRef資料轉化為點陣圖資料:

  • 使用CGBitmapContextCreate()建立圖片上下文。
  • 使用CGContextDrawImage()將圖片繪製到上下文中。
  • 使用CGBitmapContextCreateImage()通過上下文生成圖片。

2、漸進式解碼

_updateSourceImageIO私有方法中可以看到漸進式的解壓邏輯,由於程式碼過多不貼出來,主要邏輯大致如下:

  • 使用CGImageSourceCreateIncremental(NULL)建立空圖片源。
  • 使用CGImageSourceUpdateData()更新圖片源
  • 使用CGImageSourceCreateImageAtIndex()建立圖片

漸進式解壓可以在下載圖片的過程中進行解壓、顯示,達到網頁上顯示圖片的效果,體驗不錯。

3、YYImageDecoder 類使用的鎖

確實筆者疲於繼續檢視 ImageIO 或 CoreGraphics 下晦澀的 C 程式碼,個人認為這些東西瞭解一些就好,如果業務有需要在深入探究,想要一次性吃透確實過於困難?。

有意思的是,在YYImageDecoder中使用了兩個鎖。

一個是dispatch_semaphore_t _framesLock;訊號量,從它的命名就可以看出,_framesLock鎖是用來保護NSArray *_frames; ///< Array<GGImageDecoderFrame>, without image變數的執行緒安全,由於受保護的程式碼塊執行速度快,可以體現訊號量的效能優勢。

另一個是pthread_mutex_t _lock; // recursive lock互斥鎖,當筆者看到作者的註釋// recursive lock時,趕緊去檢視了一下使用過程:

pthread_mutexattr_t attr;
pthread_mutexattr_init (&attr);
pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init (&_lock, &attr);
pthread_mutexattr_destroy (&attr);
複製程式碼

果不其然,互斥鎖pthread_mutex_t還支援遞迴鎖,確實學了一手,完全可以替代效能更差的NSRecursiveLock

那麼,這裡為什麼要使用遞迴鎖呢?

互斥鎖有個特性,當同一個執行緒多次獲取鎖時(鎖還未解開),會導致死鎖,而遞迴鎖允許同一執行緒多次獲取鎖,或者說“遞迴”獲取鎖。也就是說,對於同一執行緒,遞迴鎖是可重入的,對於多執行緒仍然和互斥鎖無異。

但是,筆者檢視了一下原始碼,貌似也沒發現重入鎖的情況發生,估計也是作者長遠的考慮,降低編碼死鎖的可能性。

後語

對於這種比較大一點的開源庫,切勿陷入逐字逐句看明白的誤區,因為一個成熟的專案是經過很多次維護的,重要的是看明白作者的思路,理解一些核心的東西,本文拋磚引玉,不喜勿噴。

那麼現在,讀者朋友可以說自己會用圖片了麼?

參考文獻: iOS 處理圖片的一些小 Tip 移動端圖片格式調研

相關文章