圖片的歷史早於文字,是最原始的信息傳遞方式。六書中的象形文構造思想就是用文字的線條或筆畫,把要表達物體的外形特徵,具體地勾畫出來。html
現代社會的信息傳遞中,圖片仍然是不可或缺的一環,不管是報紙、雜誌、漫畫等實體刊物仍是生活中超市地鐵廣告活動,都會有專門的配圖抓人眼球。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 是一款功能強大的 iOS 圖像框架,支持當前市場主流的靜/動態圖像編/解碼與動態圖像的動畫播放顯示,其具備如下特性:緩存
經過 YYImage 源碼能夠按照其與 UIKit 的對應關係劃分爲三個層級:微信
層級 | UIKit | YYImage |
---|---|---|
圖像層 | UIImage | YYImage, YYFrameImage, YYSpriteSheetImage |
視圖層 | UIImageView | YYAnimatedImageView |
編/解碼層 | ImageIO.framework | YYImageCoder |
Note: ImageIO.framework 是 iOS 底層實現的圖片編/解碼庫,負責管理顏色和訪問圖像元數據。其內部的實現使用了第三方編/解碼庫(如 libpng 等)並對第三方庫進行調整優化。除此以外,iOS 還專門針對 JPEG 的編/解碼開發了 AppleJPEG.framework,實現了性能更高的硬編碼和硬解碼。數據結構
先來介紹 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 中的實現流程以下:
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 是專門用來顯示基於幀的動畫圖像類,其也是 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
UIImage
的 initWithCGImage:scale:orientation:
初始化並返回初始化結果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),其內部實現大致步驟:
animatedImageContentsRectAtIndex:
方法找到對應索引的真實位置 RealRectCGRectIntersection
方法計算邏輯定位 LogicRect 與 CGRectMake(0, 0, 1, 1) 的交集,確保邏輯定位沒有超出畫布的部分返回的 LayerRect 做爲對應索引幀的畫布內相對位置存在,結合畫布就能夠定位到對應幀圖像的具體尺寸和位置。
人眼中呈現的動畫是由一幅幅內容連貫的圖像以較短期按順序替換造成的,因此要顯示動畫只須要知道動畫順序中每一幀圖像以及對應的顯示時間等信息便可。YYImage 中對應於 UIImage 層級的內容(YYImage, YYFrameImage, YYSpriteSheetImage)在上文已經介紹過了,雖然它們之間存在內容和形式上的差別,可是對於人眼動畫呈現的原理倒是不變的。
YYAnimatedImageView 是 YYImage 的重要組成,它是 UIImageView 的子類,負責 YYImage 圖像層中不一樣的圖像類的視圖顯示(包含動態圖像的動畫播放),其內部包含 YYAnimatedImage 協議以及 YYAnimatedImageView 自身兩部分。
上文提到不管是 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 做爲 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 內部實現源碼頗有趣,有不少值得分享的地方。不過爲了避免把文章寫成 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
上鎖- 經過將圖片請求隊列
_requestQueue
的maxConcurrentOperationCount
設置爲 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 方法時,須要關注isExecuting
和isFinished
兩個 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
的一些基本操做包含 hash
和 isEqual
這些通通都轉到 target 上,而且使用 forwardingTargetForSelector:
消息重定向將不能響應的運行時消息也重定向給 target 來響應。
Emmmmm..那麼問題來了,既然都消息重定向給 target 了還要消息轉發幹嗎?由於要避免循環引用問題因此對 target 使用弱引用,期間沒法保證 target 必定存在,因此 forwardingTargetForSelector:
方法可能返回 nil,接着在 Runtime 消息轉發中借用 init 消息返回空以「吞掉」異常。
Note: 消息轉發產生的開銷要比動態方法解析和消息重定向大。
YYImageCoder 做爲 YYImage 的編/解碼器,對應於 iOS 中的 ImageIO.framework 圖片編/解碼庫,正是由於有了 YYImageCoder 的存在,YYImage 才得以支持如此多的圖片格式,因此說 YYImageCoder 是 YYImage 的底層核心。
YYImageCoder 內部定義了許多 YYImage 中用到的核心數據結構:
其中 YYImageFrame 是對一幀圖像數據的封裝,便於在 YYImageCoder 編/解碼過程當中使用。
YYImageCoder 內部圖像編碼器 YYImageEncoder 和圖像解碼器 YYImageDecoder 實際上是分開來的,咱們下面分別對它們作分析。
先來說一下 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 對於圖片的編碼工做,其實做者根據要支持的圖片類型和對應圖片類型的編碼方式作了底層封裝,再根據當前圖片的類型選擇對應的底層編碼方法執行。
關於不一樣圖片類型的圖片編碼格式能夠查閱本文文末的擴展閱讀章節,結合擴展閱讀的內容查閱 YYImage 這部分源碼能夠理解做者對於底層圖片格式信息的結構封裝以及編/解碼操做具體實現。
關於 YYImageEncoder 的一些簡單使用示例能夠查看 YYImageCoder.h 瞭解。
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 內部使用了兩種鎖:
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 內在初始化時會初始化鎖並更新圖像源數據,在更新圖像源時調用 _updateSource
方法根據當前圖像類型以做者對該類型封裝好的底層數據結構和對應圖像類型解碼規則作解碼,解碼以後設置對應屬性。
關於做者對不一樣格式的圖像數據的底層封裝源碼感興趣的讀者能夠參考本文文末的擴展閱讀章節內容自行查閱。
關於 YYImageDecoder 的一些簡單使用示例能夠查看 YYImageCoder.h 瞭解。
文章寫得比較用心(是我我的的原創文章,轉載請註明出處 lision.me/),若是發現錯誤會優先在個人 我的博客 中更新。能力不足,水平有限,若是有任何問題歡迎在個人微博 @Lision 聯繫我,另外個人 GitHub 主頁 裏有不少有趣的小玩意哦~
最後,但願個人文章能夠爲你帶來價值~
補充~ 我建了一個技術交流微信羣,想在裏面認識更多的朋友!若是各位同窗對文章有什麼疑問或者工做之中遇到一些小問題均可以在羣裏找到我或者其餘羣友交流討論,期待你的加入喲~
Emmmmm..因爲微信羣人數過百致使不能夠掃碼入羣,因此請掃描上面的二維碼關注公衆號進羣。