YYImage 設計思路,實現細節剖析

前言

圖片的歷史早於文字,是最原始的信息傳遞方式。六書中的象形文構造思想就是用文字的線條或筆畫,把要表達物體的外形特徵,具體地勾畫出來。html

許慎說文解字》雲:「象形者,畫成其物,隨體詰詘,日、月是也。」ios

現代社會的信息傳遞中,圖片仍然是不可或缺的一環,不管是報紙、雜誌、漫畫等實體刊物仍是生活中超市地鐵廣告活動,都會有專門的配圖抓人眼球。git

在移動端 App 中,圖片一般佔據着重要的視覺空間,做爲 iOS 開發來說,全部的 App 都有精心設計的 AppIcon 陳列在 SpringBoard 中,打開任意一款主流 App 都少不了琳琅滿目的圖片搭配。github

YYImage 是一款功能強大的 iOS 圖像框架(該項目是 YYKit 組件之一),支持目前市場上全部主流的圖片格式的顯示與編/解碼,而且提供高效的動態內存緩存管理,以保證高性能低內存的動畫播放。web

YYKit 的做者 @ibireme 對於 iOS 圖片處理寫有兩篇很是不錯的文章,推薦各位讀者在閱讀本文以前查閱。api

本文引用代碼均爲 YYImage v1.0.4 版本源碼,文章旨在剖析 YYImage 的架構思想以及設計思路並對筆者在閱讀源碼過程當中發現的有趣實現細節探究分享,不會逐行翻譯源碼,建議對源碼實現感興趣的同窗結合 YYImage v1.0.4 版本源碼食用本文~數組

索引

  • YYImage 簡介
  • YYImage, YYFrameImage, YYSpriteSheetImage
  • YYAnimatedImageView
  • YYImageCoder
  • 總結
  • 擴展閱讀

YYImage 簡介

YYImage 是一款功能強大的 iOS 圖像框架,支持當前市場主流的靜/動態圖像編/解碼與動態圖像的動畫播放顯示,其具備如下特性:緩存

  • 支持如下類型動畫圖像的播放/編碼/解碼: WebP, APNG, GIF。
  • 支持如下類型靜態圖像的顯示/編碼/解碼: WebP, PNG, GIF, JPEG, JP2, TIFF, BMP, ICO, ICNS。
  • 支持如下類型圖片的漸進式/逐行掃描/隔行掃描解碼: PNG, GIF, JPEG, BMP。
  • 支持多張圖片構成的幀動畫播放,支持單張圖片的 sprite sheet 動畫。
  • 高效的動態內存緩存管理,以保證高性能低內存的動畫播放。
  • 徹底兼容 UIImage 和 UIImageView,使用方便。
  • 保留可擴展的接口,以支持自定義動畫。
  • 每一個類和方法都有完善的文檔註釋。

YYImage 架構分析

經過 YYImage 源碼能夠按照其與 UIKit 的對應關係劃分爲三個層級:微信

層級 UIKit YYImage
圖像層 UIImage YYImage, YYFrameImage, YYSpriteSheetImage
視圖層 UIImageView YYAnimatedImageView
編/解碼層 ImageIO.framework YYImageCoder
  • 圖像層,把不一樣類型的圖像信息封裝成類並提供初始化和其餘便捷接口。
  • 視圖層,負責圖像層內容的顯示(包含動態圖像的動畫播放)工做。
  • 編/解碼層,提供圖像底層支持,使整個框架得以支持市場主流的圖片格式。

Note: ImageIO.framework 是 iOS 底層實現的圖片編/解碼庫,負責管理顏色和訪問圖像元數據。其內部的實現使用了第三方編/解碼庫(如 libpng 等)並對第三方庫進行調整優化。除此以外,iOS 還專門針對 JPEG 的編/解碼開發了 AppleJPEG.framework,實現了性能更高的硬編碼和硬解碼。數據結構

YYImage, YYFrameImage, YYSpriteSheetImage

先來介紹 YYImage 庫中圖像層的三個類,它們分別是:

  • YYImage
  • YYFrameImage
  • YYSpriteSheetImage

YYImage

YYImage 是一個顯示動態圖片數據的高級別類,其繼承自 UIImage 並對 UIImage 作了擴展以支持 WebP,APNG 和 GIF 格式的圖片解碼。它還支持 NSCoding 協議能夠對多幀圖像數據進行 archive 和 unarchive 操做。

@interface YYImage : UIImage <YYAnimatedImage>

+ (nullable YYImage *)imageNamed:(NSString *)name; // 不一樣於 UIImage,此方法無緩存
+ (nullable YYImage *)imageWithContentsOfFile:(NSString *)path;
+ (nullable YYImage *)imageWithData:(NSData *)data;
+ (nullable YYImage *)imageWithData:(NSData *)data scale:(CGFloat)scale;

@property (nonatomic, readonly) YYImageType animatedImageType; // 圖像數據類型
@property (nullable, nonatomic, readonly) NSData *animatedImageData; // 動態圖像的元數據
@property (nonatomic, readonly) NSUInteger animatedImageMemorySize; // 多幀圖像內存佔用量
@property (nonatomic) BOOL preloadAllAnimatedImageFrames; // 預加載全部幀(到內存)

@end
複製代碼

YYImage 提供了相似 UIImage 的初始化方法,公開了一些屬性便於咱們檢測和控制其內存使用。

值得一提的是 YYImage 的 imageNamed: 初始化方法並不支持緩存。由於其 imageNamed: 內部實現並不一樣於 UIImage 的 imageNamed: 方法,YYImage 中的實現流程以下:

  • 推測出給定圖像資源路徑
  • 拿到路徑中的圖像數據(NSData)
  • 調用 YYImage 的 initWithData:scale: 方法初始化

YYImage 的私有變量部分也比較簡單,相信你們能夠根據上面暴露出的屬性和接口猜獲得哈。

@implementation YYImage {
    YYImageDecoder *_decoder; // 解碼器
    NSArray *_preloadedFrames; // 預加載的圖像幀
    dispatch_semaphore_t _preloadedLock; // 預加載鎖
    NSUInteger _bytesPerFrame; // 內存佔用量
}
複製代碼

其內部有一把鎖 dispatch_semaphore_t,咱們知道 dispatch_semaphore_t 當信號量爲 1 時能夠當作鎖來使用,在不阻塞時其做爲鎖的效率很是高。這裏使用 _preloadedLock 的主要目的是保證 _preloadedFrames 的讀寫,因爲 _preloadedFrames 的讀寫過程是在內存中完成的,操做耗時不會太多,因此不會長時間阻塞,這種狀況使用 dispatch_semaphore_t 很是合適。

嘛~ _preloadedFrames 對應 preloadAllAnimatedImageFrames 屬性,開啓預加載全部幀到內存的話,_preloadedFrames 做爲一個數組會保存全部幀的圖像。_bytesPerFrame 則對應 animatedImageMemorySize 屬性,在初始化 YYImage 時,若是幀總數超過 1 則會計算 _bytesPerFrame 的大小。

if (decoder.frameCount > 1) {
    _decoder = decoder;
    _bytesPerFrame = CGImageGetBytesPerRow(image.CGImage) * CGImageGetHeight(image.CGImage);
    _animatedImageMemorySize = _bytesPerFrame * decoder.frameCount;
}
複製代碼

其實 YYImage 中還有一些實現也比較有趣,好比 animatedImageDurationAtIndex: 的實現中若是取到 <= 10 ms 的時長會替換爲 100 ms,並在 註釋 中解釋了爲何(必定要點進去看啊,笑~)。

YYFrameImage

YYFrameImage 是專門用來顯示基於幀的動畫圖像類,其也是 UIImage 的子類。YYFrameImage 僅支持系統圖片格式例如 png 和 jpeg。

Note: 使用 YYFrameImage 顯示動畫圖像一樣要基於 YYAnimatedImageView 播放。

@interface YYFrameImage : UIImage <YYAnimatedImage>

- (nullable instancetype)initWithImagePaths:(NSArray<NSString *> *)paths
                           oneFrameDuration:(NSTimeInterval)oneFrameDuration
                                  loopCount:(NSUInteger)loopCount;
- (nullable instancetype)initWithImagePaths:(NSArray<NSString *> *)paths
                             frameDurations:(NSArray<NSNumber *> *)frameDurations
                                  loopCount:(NSUInteger)loopCount;
- (nullable instancetype)initWithImageDataArray:(NSArray<NSData *> *)dataArray
                               oneFrameDuration:(NSTimeInterval)oneFrameDuration
                                      loopCount:(NSUInteger)loopCount;
- (nullable instancetype)initWithImageDataArray:(NSArray<NSData *> *)dataArray
                                 frameDurations:(NSArray *)frameDurations
                                      loopCount:(NSUInteger)loopCount;

@end
複製代碼

YYFrameImage 能夠把靜態圖片類型如 png 和 jpeg 格式的靜態圖像用幀切換的方式以動態圖片的形式顯示,而且提供了 4 個經常使用的初始化方法方便咱們使用。

YYFrameImage 內部有一些基本的變量分別對應於其暴露的 4 個經常使用初始化接口:

@implementation YYFrameImage {
    NSUInteger _loopCount;
    NSUInteger _oneFrameBytes;
    NSArray *_imagePaths;
    NSArray *_imageDatas;
    NSArray *_frameDurations;
}
複製代碼

YYFrameImage 的實現代碼很是簡單,初始化方法大體能夠分爲如下步驟:

  • 入參校驗
  • 根據入參取到首張圖片
  • 用首圖初始化 _oneFrameBytes ,如入參初始化 _imageDatas_frameDurations_loopCount
  • UIImageinitWithCGImage:scale:orientation: 初始化並返回初始化結果

YYSpriteSheetImage

YYSpriteSheetImage 是用來作 Spritesheet 動畫顯示的圖像類,它也是 UIImage 的子類。

關於 Spritesheet 可能作過遊戲開發或者之前鼓搗過簡單網頁遊戲 Demo 的同窗會很熟悉,其動畫原理是把一個動畫過程分解爲多個動畫幀,按照順序將這些動畫幀排布在一張大的畫布中,播放動畫時只須要按照每一幀圖像的尺寸大小以及對應索引去畫布中提取對應的幀替換顯示以達到人眼斷定動畫的效果,點擊 An Introduction to Spritesheet Animation 或者 What is a sprite sheet? 瞭解更多關於 Spritesheet 動畫的信息。

Note: 關於 SpriteSheet 素材的製做有一款工具 SpriteSheetMaker 推薦使用。

@interface YYSpriteSheetImage : UIImage <YYAnimatedImage>

// 初始化方法,這個第一次接觸 Spritesheet 的同窗可能會以爲比較繁瑣
- (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; // 循環數

// 根據索引找到對應幀 CALayer 的位置
- (CGRect)contentsRectForCALayerAtIndex:(NSUInteger)index;

@end
複製代碼

其中初始化方法的入參爲 SpriteSheet 畫布(包含全部動畫幀的大圖)image,每一幀的位置 contentRects,每一幀對應的持續顯示時間 frameDurations,循環次數 loopCount,初始化示例在 YYImage 源文件 YYSpriteSheetImage.h 註釋中有寫。

Note: 下文中要講的 YYAnimatedImageView 中定義了 YYAnimatedImage 協議,這個協議中有一個可選方法 animatedImageContentsRectAtIndex: 就是爲 YYSpriteSheetImage 量身打造的。

這裏須要提一下 contentsRectForCALayerAtIndex: 接口會根據索引找到對應幀的 CALayer 位置,該接口返回一個由 0.0~1.0 之間的數值組成的圖層定位 LayerRect,若是在查找位置過程當中發現異常則返回 CGRectMake(0, 0, 1, 1),其內部實現大致步驟:

  • 校驗入參索引是否超過 SpriteSheet 分割幀總數,超過返回 CGRectMake(0, 0, 1, 1)
  • 沒超過則經過 YYAnimatedImage 協議的 animatedImageContentsRectAtIndex: 方法找到對應索引的真實位置 RealRect
  • 經過真實位置 RealRect 與 SpriteSheet 畫布的比算錯 0.0~1.0 之間的值,獲得指定索引幀的邏輯定位 LogicRect
  • 經過 CGRectIntersection 方法計算邏輯定位 LogicRect 與 CGRectMake(0, 0, 1, 1) 的交集,確保邏輯定位沒有超出畫布的部分
  • 將處理後的邏輯定位 LogicRect 做爲圖層定位 LayerRect 返回

返回的 LayerRect 做爲對應索引幀的畫布內相對位置存在,結合畫布就能夠定位到對應幀圖像的具體尺寸和位置。

YYAnimatedImageView

人眼中呈現的動畫是由一幅幅內容連貫的圖像以較短期按順序替換造成的,因此要顯示動畫只須要知道動畫順序中每一幀圖像以及對應的顯示時間等信息便可。YYImage 中對應於 UIImage 層級的內容(YYImage, YYFrameImage, YYSpriteSheetImage)在上文已經介紹過了,雖然它們之間存在內容和形式上的差別,可是對於人眼動畫呈現的原理倒是不變的。

YYAnimatedImageView 是 YYImage 的重要組成,它是 UIImageView 的子類,負責 YYImage 圖像層中不一樣的圖像類的視圖顯示(包含動態圖像的動畫播放),其內部包含 YYAnimatedImage 協議以及 YYAnimatedImageView 自身兩部分。

YYAnimatedImage 協議

上文提到不管是 YYImage, YYFrameImage, YYSpriteSheetImage 仍是之後可能會擴展的圖像類,雖然它們之間存在內容和形式上的差別,可是對於人眼動畫呈現的原理倒是不變的。

YYAnimatedImage 協議就是在不影響原來圖像類的狀況下把不一樣圖像類之間的共性找出來(求同存異?笑),以統一化的接口將人眼動畫呈現所需的基本信息輸出給 YYAnimatedImageView 使用的協議。

Note: 做爲圖像類須遵循 YYAnimatedImage 協議以即可以使用 YYAnimatedImageView 播放動畫。

@protocol YYAnimatedImage <NSObject>

@required
// 動畫幀總數
- (NSUInteger)animatedImageFrameCount;
// 動畫循環次數,0 表示無限循環
- (NSUInteger)animatedImageLoopCount;
// 每幀字節數(在內存中),可能用於優化內存緩衝區大小
- (NSUInteger)animatedImageBytesPerFrame;
// 返回給定特殊索引對應的幀圖像,這個方法可能在異步線程中調用
- (nullable UIImage *)animatedImageFrameAtIndex:(NSUInteger)index;
// 返回給定特殊索引對應的幀圖像對應的顯示持續時長
- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index;

@optional
// 針對 Spritesheet 動畫的方法,用於顯示某一幀圖像在 Spritesheet 畫布中的位置
- (CGRect)animatedImageContentsRectAtIndex:(NSUInteger)index;

@end
複製代碼

上文提到過可選實現接口 animatedImageContentsRectAtIndex: 是專爲 Spritesheet 動畫設計的。

像這樣規定一個協議,使不相關的類遵循此協議擁有統一的功能接口方便另外一個類調用的設計思想咱們在本身平常項目的開發過程當中不少場景均可以用到,例如能夠封裝一個 TableView,設計一個 TableViewCell 協議,讓全部 TableViewCell 都實現這個協議以擁有統一的功能接口,而後咱們封裝的 TableView 類就能夠統一的使用這些 TableViewCell 顯示數據啦,省去了反覆寫相同功能 UITableView 的勞動力(實際應用場景不少,這裏只是簡單舉例,拋磚引玉)。

YYAnimatedImageView

上文提到過 YYAnimatedImageView 做爲 YYImage 框架中的圖片視圖層,上接圖像層,下啓編/解碼底層,是樞紐通常的存在(承上啓下啊有木有?),咱們須要重點研究其內部實現:

@interface YYAnimatedImageView : UIImageView

// 若是 image 爲多幀組成時,自動賦值爲 YES,能夠在顯示和隱藏時自動播放和中止動畫
@property (nonatomic) BOOL autoPlayAnimatedImage;
// 當前顯示的幀(從 0 起始),設置新值後會當即顯示對應幀,若是新值無效則此方法無效
@property (nonatomic) NSUInteger currentAnimatedImageIndex;
// 當前是否在播放動畫
@property (nonatomic, readonly) BOOL currentIsPlayingAnimation;
// 動畫定時器所在的 runloop mode,默認爲 NSRunLoopCommonModes,關乎動畫定時器的觸發
@property (nonatomic, copy) NSString *runloopMode;
// 內部緩存區的最大值(in bytes),默認爲 0(動態),若是有值將會把緩存區限制爲值大小,當收到內存警告或者 App 進入後臺時,緩存區將會當即釋放而且在適時的時候回覆原狀
@property (nonatomic) NSUInteger maxBufferSize;

@end
複製代碼

額...出乎意料的簡單呢~ 只有一些屬性暴露出來以便咱們在使用過程當中實時查看動畫的播放狀態以及內存使用狀況。筆者看源碼總結出一條經驗,即若是某個組件在庫中佔據重要地位,其 .h 文件中暴露的內容越是簡單,其 .m 內部實現就越是複雜

經過 runloopMode 屬性你們用猜的也應該能夠猜出 YYAnimatedImageView 內部實現動畫的原理離不開 RunLoop,並且極有多是用定時器 NSTimer 或者 CADisplayLink 實現的。下面咱們來對 YYAnimatedImageView 的實現剖析,驗證一下咱們剛纔的猜測。

YYAnimatedImageView 的實現剖析

YYAnimatedImageView 內部實現源碼頗有趣,有不少值得分享的地方。不過爲了避免把文章寫成 MarkDown 編輯器文(笑~)筆者不會逐行翻譯源碼。讀者若是想要知道實現的細節建議結合文章去翻閱源碼。相信有了文章梳理的思路源碼看起來應該不會有太大的困難,文章仍是重在傳播實現思想和一些值得分享的技巧。

咱們先簡單看一下 YYAnimatedImageView 的內部結構,方便後面分析實現思路時你們腦中對 YYAnimatedImageView 的結構提早有一個大概的認識。

@interface YYAnimatedImageView() {
    @package
    UIImage <YYAnimatedImage> *_curAnimatedImage; ///< 當前圖像
    
    dispatch_once_t _onceToken; ///< 用於確保初始化代碼只執行一次
    dispatch_semaphore_t _lock; ///< 信號量鎖(用於 _buffer)
    NSOperationQueue *_requestQueue; ///< 圖片請求隊列,串行
    
    CADisplayLink *_link; ///< 幀轉換
    NSTimeInterval _time; ///< 上一幀以後的時間
    
    UIImage *_curFrame; ///< 當前幀
    NSUInteger _curIndex; ///< 當前幀索引
    NSUInteger _totalFrameCount; ///< 幀總數
    
    BOOL _loopEnd; ///< 是否在循環末尾
    NSUInteger _curLoop; ///< 當前循環次數
    NSUInteger _totalLoop; ///< 總循環次數, 0 表示無窮
    
    NSMutableDictionary *_buffer; ///< 幀緩衝區
    BOOL _bufferMiss; ///< 是否丟幀,在上面 _link 定時執行的 step 函數中從幀緩衝區讀取下一幀圖片時若是沒讀到,則視爲丟幀
    NSUInteger _maxBufferCount; ///< 最大緩衝計數
    NSInteger _incrBufferCount; ///< 當前容許的緩存區計數(將逐步增長)
    
    CGRect _curContentsRect; ///< 針對 YYSpriteSheetImage
    BOOL _curImageHasContentsRect; ///< 圖像類是否實現了 animatedImageContentsRectAtIndex: 接口
}
@property (nonatomic, readwrite) BOOL currentIsPlayingAnimation;
- (void)calcMaxBufferCount; // 動態調節緩衝區最大限制 _maxBufferCount
@end
複製代碼

能夠看到 YYAnimatedImageView 內部結構比 .h 中暴露的屬性要複雜的多,而 CADisplayLink *_link 屬性也證明了咱們以前關於 .h 中 runloopMode 屬性的猜測。

YYAnimatedImageView 內部的初始化沒什麼特別之處,初始化函數中會設置圖片,當斷定圖片有更改時會依照下面 4 步去處理:

  • 改變圖片
  • 重置動畫
  • 初始化動畫參數
  • 重繪

Note: 這樣能夠保證 YYAnimatedImageView 的圖片更改時都會執行上面的步驟爲新的圖片初始化配套的新動畫參數而且重繪,而重置動畫實現中會使用到上面的 dispatch_once_t _onceToken; 以確保某些內部變量的建立以及對 App 內存警告和進入後臺的通知觀察代碼只執行一次。

YYAnimatedImageView 使圖片動起來是依靠 CADisplayLink *_link; 變量切換幀圖像,其內部的實現邏輯能夠簡單理解爲:

  • 根據當前幀索引推出下一幀索引
  • 使用下一幀索引去幀緩衝區嘗試獲取對應幀圖像
  • 若是找到對應幀圖像則使用其重繪
  • 若是沒找到則根據條件向圖片請求隊列加入請求操做(向圖片緩衝區錄入以後的幀圖像數據)

嘛~ 這裏面有一些值得一提的實現細節哈!

  • YYAnimatedImageView 實現中當 _curIndex 即當前幀索引修改時在修改代碼先後加入了 willChangeValueForKey:didChangeValueForKey: 方法以支持 KVO
  • 對幀緩衝區 _buffer 的操做都使用 _lock 上鎖
  • 經過將圖片請求隊列 _requestQueuemaxConcurrentOperationCount 設置爲 1 使圖片請求隊列成爲串行隊列(最大併發數爲 1)
  • 圖片請求隊列中加入的操做均爲 _YYAnimatedImageViewFetchOperation
  • 爲了不使用 CADisplayLink 可能形成的循環引用設計了 _YYImageWeakProxy

先看一下 _YYAnimatedImageViewFetchOperation 的源碼:

@interface _YYAnimatedImageViewFetchOperation : NSOperation
@property (nonatomic, weak) YYAnimatedImageView *view;
@property (nonatomic, assign) NSUInteger nextIndex;
@property (nonatomic, strong) UIImage <YYAnimatedImage> *curImage;
@end

@implementation _YYAnimatedImageViewFetchOperation
- (void)main {//...}
@end
複製代碼

_YYAnimatedImageViewFetchOperation 繼承自 NSOperation 類,是自定義操做類,做者將其操做內容實現寫在了 main 中,代碼太長並且我以爲貼出來不只不會幫助讀者理解反而會由於片面的源碼實現影響讀者對 YYAnimatedImageView 的總體實現思路理解(由於大量貼源碼會使文章生澀不少,並且會把讀者注意力轉移到某一個實現),這裏簡單描述一下 main 函數內部實現邏輯:

  • 判斷幀緩衝區大小
  • 掃描下一幀以及當前容許緩衝範圍內以後的幀圖片
  • 若是發現丟失的幀則嘗試從新獲取幀圖像並加入到幀緩衝

嘛~ 不貼源碼歸不貼源碼,該注意的細節仍是須要列出來的(笑)。

  • 操做中對於 view 緩衝區的操做也都上了鎖
  • 操做因爲是放入圖片請求隊列中進行的,內部有對 isCancelled 作判斷,若是操做已經被取消(發生在更改圖片、中止動畫、手動更改當前幀、收到內存警告或 App 進入後臺等)則須要及時跳出
  • 對於新的線程優先級只在 main 方法範圍內有效,因此推薦把操做的實現放在 main 中而非 start(如需覆蓋 start 方法時,須要關注 isExecutingisFinished 兩個 key paths)

YYAnimatedImageView 內部設計了 _YYImageWeakProxy 來避免使用 NSTimer 或者 CADisplayLink 可能形成的循環引用問題,_YYImageWeakProxy 內部實現也比較簡單,繼承自 NSProxy,關於 NSProxy 能夠查看官方文檔以瞭解更多。

@interface _YYImageWeakProxy : NSProxy
@property (nonatomic, weak, readonly) id target;
- (instancetype)initWithTarget:(id)target;
+ (instancetype)proxyWithTarget:(id)target;
@end

@implementation _YYImageWeakProxy
// ...
- (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)];
}
// ...
@end
複製代碼

上面貼出的源碼省略了比較基礎的實現部分,_YYImageWeakProxy 內部弱引用一個對象 target,對於 _YYImageWeakProxy 的一些基本操做包含 hashisEqual 這些通通都轉到 target 上,而且使用 forwardingTargetForSelector: 消息重定向將不能響應的運行時消息也重定向給 target 來響應。

Emmmmm..那麼問題來了,既然都消息重定向給 target 了還要消息轉發幹嗎?由於要避免循環引用問題因此對 target 使用弱引用,期間沒法保證 target 必定存在,因此 forwardingTargetForSelector: 方法可能返回 nil,接着在 Runtime 消息轉發中借用 init 消息返回空以「吞掉」異常。

Note: 消息轉發產生的開銷要比動態方法解析和消息重定向大。

YYImageCoder

YYImageCoder 做爲 YYImage 的編/解碼器,對應於 iOS 中的 ImageIO.framework 圖片編/解碼庫,正是由於有了 YYImageCoder 的存在,YYImage 才得以支持如此多的圖片格式,因此說 YYImageCoder 是 YYImage 的底層核心。

YYImageCoder 內部定義了許多 YYImage 中用到的核心數據結構:

  • YYImageType,全部的支持的圖片格式作了枚舉定義
  • YYImageDisposeMethod,指定在畫布上渲染下一個幀以前如何處理當前幀所使用的區域方法
  • YYImageBlendOperation,指定當前幀的透明像素如何與前一個畫布的透明像素混合操做
  • YYImageFrame,一幀圖像數據
  • YYImageEncoder,圖像編碼器
  • YYImageDecoder,圖像解碼器
  • UIImage+YYImageCoder,UIImage 的分類,裏面提供了一些方便使用的方法

其中 YYImageFrame 是對一幀圖像數據的封裝,便於在 YYImageCoder 編/解碼過程當中使用。

YYImageCoder 內部圖像編碼器 YYImageEncoder 和圖像解碼器 YYImageDecoder 實際上是分開來的,咱們下面分別對它們作分析。

YYImageEncoder

先來說一下 YYImageEncoder,其在 YYImageCoder 中擔任編碼器的角色。

@interface YYImageEncoder : NSObject

@property (nonatomic, readonly) YYImageType type; ///< 圖像類型
@property (nonatomic) NSUInteger loopCount;       ///< 循環次數,0 無限循環,僅適用於 GIF/APNG/WebP 格式
@property (nonatomic) BOOL lossless;              ///< 無損標記,僅適用於 WebP.
@property (nonatomic) CGFloat quality;            ///< 壓縮質量,0.0~1.0,僅適用於 JPG/JP2/WebP.

// 禁止適用 init、new 初始化編碼器(我沒忘記我說過這些編碼技巧會在以後統一寫一篇文章彙總)
- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)new UNAVAILABLE_ATTRIBUTE;

// 根據給定圖片類型建立編碼器
- (nullable instancetype)initWithType:(YYImageType)type NS_DESIGNATED_INITIALIZER;
// 添加圖像
- (void)addImage:(UIImage *)image duration:(NSTimeInterval)duration;
// 添加圖像數據
- (void)addImageWithData:(NSData *)data duration:(NSTimeInterval)duration;
// 添加文件路徑
- (void)addImageWithFile:(NSString *)path duration:(NSTimeInterval)duration;
// 開始圖像編碼並嘗試返回編碼後的數據
- (nullable NSData *)encode;
// 編碼並將獲得的數據保存到給定路徑文件中
- (BOOL)encodeToFile:(NSString *)path;
// 便捷方法,對一個單幀圖像編碼
+ (nullable NSData *)encodeImage:(UIImage *)image type:(YYImageType)type quality:(CGFloat)quality;
// 便捷方法,從解碼器中編碼圖像數據
+ (nullable NSData *)encodeImageWithDecoder:(YYImageDecoder *)decoder type:(YYImageType)type quality:(CGFloat)quality;

@end
複製代碼

能夠看到 YYImageEncoder 內部的一些屬性和接口都比較基本,關於其內部實現咱們須要先看一下私有變量:

@implementation YYImageEncoder {
    NSMutableArray *_images; // 已添加到編碼器的圖片(數組)
    NSMutableArray *_durations; // 對應的圖片幀顯示持續時長(數組)
}
複製代碼

YYImageEncoder 的實現思路

YYImageEncoder 的初始化部分沒有多複雜,根據圖片的類型按照編碼最優的參數作初始化而已。關於 YYImageEncoder 對於圖片的編碼工做,其實做者根據要支持的圖片類型和對應圖片類型的編碼方式作了底層封裝,再根據當前圖片的類型選擇對應的底層編碼方法執行。

關於不一樣圖片類型的圖片編碼格式能夠查閱本文文末的擴展閱讀章節,結合擴展閱讀的內容查閱 YYImage 這部分源碼能夠理解做者對於底層圖片格式信息的結構封裝以及編/解碼操做具體實現。

關於 YYImageEncoder 的一些簡單使用示例能夠查看 YYImageCoder.h 瞭解。

YYImageDecoder

YYImageDecoder 在 YYImageCoder 中擔任解碼器的角色,其與上述 YYImageEncoder 對應,一個負責圖像編碼一個負責圖像解碼,不過 YYImageDecoder 的實現比 YYImageEncoder 更爲複雜。

@interface YYImageDecoder : NSObject

@property (nullable, nonatomic, readonly) NSData *data;    ///< 圖像數據
@property (nonatomic, readonly) YYImageType type;          ///< 圖像數據類型
@property (nonatomic, readonly) CGFloat scale;             ///< 圖像大小
@property (nonatomic, readonly) NSUInteger frameCount;     ///< 圖像幀數量
@property (nonatomic, readonly) NSUInteger loopCount;      ///< 圖像循環次數,0 無限循環
@property (nonatomic, readonly) NSUInteger width;          ///< 圖像畫布寬度
@property (nonatomic, readonly) NSUInteger height;         ///< 圖像畫布高度
@property (nonatomic, readonly, getter=isFinalized) BOOL finalized;

// 建立一個圖像解碼器
- (instancetype)initWithScale:(CGFloat)scale NS_DESIGNATED_INITIALIZER;
// 用新數據增量更新圖像
- (BOOL)updateData:(nullable NSData *)data final:(BOOL)final;
// 方便用一個特殊的數據建立對應的解碼器
+ (nullable instancetype)decoderWithData:(NSData *)data scale:(CGFloat)scale;
// 解碼並返回給定索引對應的幀數據
- (nullable YYImageFrame *)frameAtIndex:(NSUInteger)index decodeForDisplay:(BOOL)decodeForDisplay;
// 返回給定索引對應的幀持續顯示時長
- (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index;
// 返回給定索引對應幀的屬性信息,去 ImageIO.framework 的 "CGImageProperties.h" 文件中瞭解更多
- (nullable NSDictionary *)framePropertiesAtIndex:(NSUInteger)index;
// 返回圖片的屬性信息,去 ImageIO.framework 的 "CGImageProperties.h" 文件中瞭解更多
- (nullable NSDictionary *)imageProperties;

@end
複製代碼

能夠看到 YYImageDecoder 暴露了一些關於解碼圖像的屬性並提供了初始化解碼器方法、圖像解碼方法以及訪問圖像幀信息的方法。不過上文也說過 YYImageDecoder 的實現比較複雜,咱們接着看一下其內部變量結構:

@implementation YYImageDecoder {
    pthread_mutex_t _lock; // 遞歸鎖
    
    BOOL _sourceTypeDetected; // 是否推測圖像源類型
    CGImageSourceRef _source; // 圖像源
    yy_png_info *_apngSource; // 若是斷定圖像爲 YYImageTypePNG 則會以 APNG 更新圖像源
#if YYIMAGE_WEBP_ENABLED
    WebPDemuxer *_webpSource; // 若是斷定圖像爲 YYImageTypeWebP 則會議 WebP 更新圖像源
#endif
    
    UIImageOrientation _orientation; // 繪製方向
    dispatch_semaphore_t _framesLock; // 針對於圖像幀的鎖
    NSArray *_frames; ///< Array<_YYImageDecoderFrame *>, without image
    BOOL _needBlend; // 是否須要混合
    NSUInteger _blendFrameIndex; // 從幀索引混合到當前幀
    CGContextRef _blendCanvas; // 混合畫布
}
複製代碼

_YYImageDecoderFrame 繼承自 YYImageFrame 類做爲 YYImageCoder 圖像解碼器 YYImageDecoder 使用的內部框架類存在,是對於一幀圖像的數據封裝提供了便於編/解碼時須要訪問的數據。

YYImageDecoder 內鎖的選擇

能夠看到做者在 YYImageDecoder 內部使用了兩種鎖:

  • pthread_mutex_t _lock;
  • dispatch_semaphore_t _framesLock;

pthread_mutex_t 在解碼器初始化過程當中被以 PTHREAD_MUTEX_RECURSIVE 類型設置爲了遞歸鎖。

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

Note: 通常狀況下一個線程只能申請一次鎖,也只能在得到鎖的狀況下才能釋放鎖,屢次申請鎖或釋放未得到的鎖都會致使崩潰。假設在已經得到鎖的狀況下再次申請鎖,線程會由於等待鎖的釋放而進入睡眠狀態,所以就不可能再釋放鎖,從而致使死鎖。

然而這種狀況常常會發生,好比某個函數申請了鎖,在臨界區內又遞歸調用了本身。辛運的是 pthread_mutex 支持遞歸鎖,也就是容許一個線程遞歸的申請鎖,只要把 attr 的類型改爲 PTHREAD_MUTEX_RECURSIVE 便可。

做者使用 dispatch_semaphore_t 做爲圖像幀數組的鎖是由於 dispatch_semaphore_t 更加輕量且對於圖像幀數組的臨界操做比較快,不會形成長時間的阻塞,這種狀況下 dispatch_semaphore_t 具備性能優點(Emmmmmm..老生常談了,熟悉的同窗不要抱怨,照顧一下後面的同窗)。

YYImageDecoder 內的實現思路

YYImageDecoder 內在初始化時會初始化鎖並更新圖像源數據,在更新圖像源時調用 _updateSource 方法根據當前圖像類型以做者對該類型封裝好的底層數據結構和對應圖像類型解碼規則作解碼,解碼以後設置對應屬性。

關於做者對不一樣格式的圖像數據的底層封裝源碼感興趣的讀者能夠參考本文文末的擴展閱讀章節內容自行查閱。

關於 YYImageDecoder 的一些簡單使用示例能夠查看 YYImageCoder.h 瞭解。

總結

  • 文章系統的分析了 YYImage 源碼,但願各位讀者在閱讀本文以後能夠對 YYImage 總體架構和設計思路有清晰的認識。
  • 文章對 YYImage 的 Image 層級的三類圖像(YYImage, YYFrameImage, YYSpriteSheetImage)分別解讀,但願能夠對各位讀者關於這三類圖像的組成原理和呈現動畫的方式的理解有所幫助。
  • 文章深刻剖析了 YYAnimatedImageView 的內部實現,提煉出其設計思路以供讀者探究。
  • 筆者把本身在閱讀源碼中發現的值得分享的實現細節結合源碼單獨拎出來分析,但願各位讀者能夠在本身平時工做中遇到類似狀況時可以多一些思路,封裝項目組件時能夠用到這些技巧。

文章寫得比較用心(是我我的的原創文章,轉載請註明出處 lision.me/),若是發現錯誤會優先在個人 我的博客 中更新。能力不足,水平有限,若是有任何問題歡迎在個人微博 @Lision 聯繫我,另外個人 GitHub 主頁 裏有不少有趣的小玩意哦~

最後,但願個人文章能夠爲你帶來價值~

擴展閱讀


補充~ 我建了一個技術交流微信羣,想在裏面認識更多的朋友!若是各位同窗對文章有什麼疑問或者工做之中遇到一些小問題均可以在羣裏找到我或者其餘羣友交流討論,期待你的加入喲~

Emmmmm..因爲微信羣人數過百致使不能夠掃碼入羣,因此請掃描上面的二維碼關注公衆號進羣。

相關文章
相關標籤/搜索