系列文章:
YYText 源碼剖析:CoreText 與異步繪制
YYAsyncLayer 源碼剖析:異步繪制
YYCache 源碼剖析:一覽亮點
YYModel 源碼剖析:關(guān)注性能
YYImage 源碼剖析:圖片處理技巧
YYWebImage 源碼剖析:線程處理與緩存策略
引言
首先問一個問題:你會用圖片么?
圖片是現(xiàn)代化 APP 界面設(shè)計里應(yīng)用廣泛的東西,精美的圖片可以帶來視覺上的享受,提高用戶體驗。由此給技術(shù)上帶來了一些挑戰(zhàn),比如動圖的處理、圖片顯示流暢程度的優(yōu)化、圖片包大小的優(yōu)化、超大圖片的處理等。
本文主要是結(jié)合 YYImage 源碼對圖片處理技巧進(jìn)行講解。而筆者不會逐字逐句的翻譯源碼,主要是提取源碼中有思維價值的東西。所以最好是打開源碼,本文作為思想引導(dǎo)。
源碼基于 1.0.4 版本。
一、圖片處理技巧
首先來談一談圖片處理的一些注意事項和技巧,以下結(jié)論參考其他博文、官方文檔、實際測試得出,歡迎指出錯誤??。
一張圖片從磁盤中顯示到屏幕上過程大致如下:從磁盤加載圖片信息、解碼二進(jìn)制圖片數(shù)據(jù)為位圖、通過 CoreAnimation 框架處理最終繪制到屏幕上。
實際上圖片的繪制過程往往不是性能瓶頸,最耗時的操作是解碼過程,若圖片文件過大,從磁盤讀取的過程也有可觀的耗時。
1、加載和解壓
一般使用imageNamed:或者imageWithData:從內(nèi)存中加載圖片生成UIImage的實例,此刻圖片并不會解壓,當(dāng) RunLoop 準(zhǔn)備處理圖片顯示的事務(wù)(CATransaction)時,才進(jìn)行解壓,而這個解壓過程是在主線程中的,這是導(dǎo)致卡頓的重要因素。
imageNamed: 方法
使用imageNamed:方法加載圖片信息的同時(生成UIImage實例),還會將圖片信息緩存起來,所以當(dāng)使用該方法第一次加載某張圖片時,會消耗較多的時間,而之后再次加載該圖片速度就會非常快(注意此時該圖片是未繪制到屏幕上的,也就是說還未解壓)。
在繪制到屏幕之前,第一次解壓成功后,系統(tǒng)會將解壓信息緩存到內(nèi)存。
值得注意的是,這些緩存都是全局的,并不會因為當(dāng)前UIImage實例的釋放而清除,在收到內(nèi)存警告或者 APP 第一次進(jìn)入后臺才有可能會清除,而這個清除的時機和內(nèi)容是系統(tǒng)決定的,我們無法干涉。
imageWithData: 方法
使用imageWithData:方式加載圖片時,不管是加載過程還是解壓過程,都不會像imageNamed:緩存到全局,當(dāng)該UIImage實例釋放時,相關(guān)的圖片信息和解壓信息就會銷毀。
兩種加載方式的區(qū)別
從上面的分析可知,imageNamed:使用時會產(chǎn)生全局的內(nèi)存占用,但是第二次使用同一張圖片時性能很好;imageWithData:不會有全局的內(nèi)存占用,但對于同一張圖片每次加載和解壓都會“從頭開始”。
由此可見,imageNamed:適合“小”且“使用頻繁”的圖片,imageWithData:適合“大”且“低頻使用”的圖片。
2、加載和解壓的優(yōu)化
這里說的優(yōu)化并不是解壓算法的優(yōu)化,只是基于用戶體驗的優(yōu)化。
加載優(yōu)化
對于加載過程,若文件過大或加載頻繁影響了幀率(比如列表展示大圖),可以使用異步方式加載圖片,減少主線程的壓力,代碼大致如下:
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(), ^{
//業(yè)務(wù)
});
});
解壓優(yōu)化
解壓是耗時的,而系統(tǒng)默認(rèn)是在主線程執(zhí)行,所以業(yè)界通常有一種做法是,異步強制解壓,也就是在異步線程主動將二進(jìn)制圖片數(shù)據(jù)解壓成位圖數(shù)據(jù),使用CGBitmapContextCreate(...)系列方法就能實現(xiàn)。
該處理方式在眾多圖片處理框架下都有體現(xiàn)。
3、超大圖的處理
值得注意的是,可能業(yè)務(wù)中需要載入一張很大的圖片。這時,若還使用常規(guī)的方式加載會占用過多的內(nèi)存;況且,若圖片的像素過大(目前主流 iOS 設(shè)備最高支持 4096 x 4096 紋理尺寸),在顯示的時候 CPU 和 GPU 都會消耗額外的資源來處理圖片。
所以,在處理超大圖時,需要一些特別的手段。
比如想要顯示完整的圖片,就可以使用如下方法壓縮到目標(biāo)大小 (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);
或者直接使用CALayer的contentsRect屬性來達(dá)到相同的效果。
二、YYImage 框架整體概覽
上文中談了一下圖片處理的一些原理和核心思想,做為背景知識,下面從一個宏觀的角度觀察一下 YYImage 框架的設(shè)計,目錄結(jié)構(gòu)如下:
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。 - 支持以下類型靜態(tài)圖像的顯示/編碼/解碼:
WebP, PNG, GIF, JPEG, JP2, TIFF, BMP, ICO, ICNS。 - 支持以下類型圖片的漸進(jìn)式/逐行掃描/隔行掃描解碼:
PNG, GIF, JPEG, BMP。 - 支持多張圖片構(gòu)成的幀動畫播放,支持單張圖片的 sprite sheet 動畫。
- 高效的動態(tài)內(nèi)存緩存管理,以保證高性能低內(nèi)存的動畫播放。
- 完全兼容 UIImage 和 UIImageView,使用方便。
- 保留可擴(kuò)展的接口,以支持自定義動畫。
- 每個類和方法都有完善的文檔注釋。
三、YYImage 類
該類對UIImage進(jìn)行拓展,支持 WebP、APNG、GIF 格式的圖片解碼,為了避免產(chǎn)生全局緩存,重載了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];的數(shù)組,不同屏幕 物理分辨率/邏輯分辨率 不同,查詢的優(yōu)先級也不同。 - 找到第一個有效的
path就會調(diào)用initWithData:scale:方法初始化。
這里雖然比以往使用UIImage更方便,除png外的圖片類型也可以不寫拓展名,但是為了極致的性能考慮,還是指定拓展名比較好。
眾多初始化方法的落腳點都是initWithData:scale:,在該方法中初始化了信號量 (作為鎖)、圖片解碼器 (YYImageDecoder),以及通過解碼器獲取第一幀解壓過后的圖像等。最終調(diào)用initWithCGImage:scale:orientation獲取實例。
可以看到這樣一個屬性:@property (nonatomic) BOOL preloadAllAnimatedImageFrames;,它的作用是預(yù)加載,緩存解壓過后的所有幀圖片,是一個優(yōu)化選項,但是需要注意內(nèi)存的占用,看看它的setter方法實現(xiàn):
- (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循環(huán)中,拿到每一幀解壓后的圖片(筆者改動了一下代碼,至于animatedImageFrameAtIndex后面解釋)。由于是解壓后的,所以該方法實際上會消耗一定的 CPU 資源,所以在實際使用中可以在異步線程調(diào)用。
值得一提的是,此處使用信號量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 動畫,原理可以理解為一張大圖上分布有很多完整的小圖,然后不同時刻顯示不同位置的小圖。
這么做的目的是將多張圖片的加載、解壓合并為一張大圖的加載、解壓,可以減少圖片占用的內(nèi)存,提高整體的解壓縮性能。
其實該框架的做法很簡單,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;
初始化方法中,需要傳入兩個數(shù)組,一個是CGRect表示范圍的數(shù)組,一個是對應(yīng)時長的數(shù)組。
然后利用CALayer的contentsRect屬性,動態(tài)的讀取這張大圖某個范圍的內(nèi)容。當(dāng)然,這個過程的邏輯同樣在YYAnimatedImageView類中。
六、YYAnimatedImage 協(xié)議
YYAnimatedImage 協(xié)議是YYAnimatedImageView和YYImage、YYFrameImage、YYSpriteSheetImage交互的橋梁。
@protocol YYAnimatedImage <NSObject>
@required
//幀數(shù)量
- (NSUInteger)animatedImageFrameCount;
//動畫循環(huán)次數(shù)
- (NSUInteger)animatedImageLoopCount;
//每幀在內(nèi)存中的大小
- (NSUInteger)animatedImageBytesPerFrame;
//index 下標(biāo)的幀圖片
- (nullable UIImage *)animatedImageFrameAtIndex:(NSUInteger)index;
//index 下標(biāo)幀圖片持續(xù)時間
- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index;
@optional
//index 下標(biāo)幀圖片的范圍(CGRect)
- (CGRect)animatedImageContentsRectAtIndex:(NSUInteger)index;
@end
不管是.gif還是幀圖片數(shù)組還是 SpriteSheet,當(dāng)我們需要利用動畫來顯示它們的時候?qū)嶋H上并不關(guān)心它們是何種來源,該協(xié)議是一個共有邏輯提取。任何類型的UIImage子類的動畫圖片的數(shù)據(jù)都能通過這個協(xié)議體現(xiàn),YYImage、YYFrameImage、YYSpriteSheetImage都分別實現(xiàn)了該協(xié)議,具體操作可以看源碼,沒有難度。
其中,- (CGRect)animatedImageContentsRectAtIndex:(NSUInteger)index;是可選方法,是YYSpriteSheetImage做 SpriteSheet 動畫需要的數(shù)據(jù),這算是一個共有邏輯之外的特例。
利用協(xié)議來規(guī)范共有邏輯,是一個值得學(xué)習(xí)的技巧,它能讓邏輯更清晰,代碼更有條理。
七、YYAnimatedImageView 類
一句話理解:YYAnimatedImageView類通過YYImage、YYFrameImage、YYSpriteSheetImage實現(xiàn)的<YYAnimatedImage>協(xié)議方法拿到幀圖片數(shù)據(jù)和相關(guān)信息進(jìn)行動畫展示。
它的原理就是如此,下面主要分析技術(shù)細(xì)節(jié),含金量蠻高。
1、初始化流程
@property (nonatomic, copy) NSString *runloopMode;屬性默認(rèn)為NSRunLoopCommonModes保證在拖動滾動視圖時動畫還能繼續(xù)。
該類重寫了一系列方法讓它們都走自定義配置:
- (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:方法就是將這些圖片數(shù)據(jù)賦值給super.image等,該方法最后會走imageChanged方法,這才是主要的初始化配置:
- (void)imageChanged {
YYAnimatedImageType newType = [self currentImageType];
id newVisibleImage = [self imageForType:newType];
NSUInteger newImageFrameCount = 0;
BOOL hasContentsRect = NO;
... //省略判斷是否是 SpriteSheet 類型來源
/*1、若上一次是 SpriteSheet 類型而當(dāng)前顯示的圖片不是,
歸位 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`方法初始化顯示多幀動畫需要的配置;
然后拿到第一幀圖片調(diào)用`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.contentsRect為CGRectMake(0, 0, 1, 1)使用了CATransaction事務(wù)來取消隱式動畫。(由于此處完全不需要那 0.25 秒的隱式動畫)
2、動畫啟動和結(jié)束的時機
- (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];
}
在didMoveToWindow和didMoveToSuperview周期方法中嘗試啟動或結(jié)束動畫,不需要在組件內(nèi)部特意的去調(diào)用就能實現(xiàn)自動的播放和停止。而didMoved方法中判斷是否開啟動畫寫了個self.superview && self.window,意味著YYAnimatedImageView光有父視圖還不能開啟動畫,還需要展示在window上才行。
3、異步解壓
YYAnimatedImageView有個隊列變量NSOperationQueue *_requestQueue;
_requestQueue = [[NSOperationQueue alloc] init];
_requestQueue.maxConcurrentOperationCount = 1;
可以看出_requestQueue是一個串行的隊列,用于處理解壓任務(wù)。
_YYAnimatedImageViewFetchOperation繼承自NSOperation,重寫了main方法自定義解壓任務(wù)。它是結(jié)合變量_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;
}
}
}
}
關(guān)鍵代碼中,animatedImageFrameAtIndex方法便會調(diào)用解碼,后面yy_imageByDecoded屬性是對解碼成功的第二重保證,view->_buffer[@(idx)] = img是做緩存。
可以看到作者經(jīng)常使用if ([self isCancelled]) break(return);判斷返回,因為在執(zhí)行NSOperation任務(wù)的過程中該任務(wù)可能會被取消。
for循環(huán)中使用@autoreleasepool避免同一 RunLoop 循環(huán)中堆積過多的局部變量。
由此,基本可以保證解壓過程是在_requestQueue串行隊列執(zhí)行的,不會影響主線程。
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函數(shù)中有給_buffer賦值,作者還限制了最大緩存數(shù)量。
緩存限制計算
- (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;
}
該方法并不復(fù)雜,通過_YYDeviceMemoryTotal()拿到內(nèi)存總數(shù)乘以 0.2,通過_YYDeviceMemoryFree()拿到剩余的內(nèi)存乘以 0.6,然后取它們最小值;之后通過最小的緩存值BUFFER_SIZE和用戶自定義的_maxBufferSize屬性綜合判斷。
緩存清理時機
在resetAnimated方法中注冊了兩個監(jiān)聽:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
在收到內(nèi)存警告或者 APP 進(jìn)入后臺時,作者修剪了緩存:
- (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
}
在進(jìn)入后臺時,清除所有的異步解壓任務(wù),然后計算下一幀的下標(biāo),最后移除不是下一幀的所有緩存,保證進(jìn)入前臺時下一幀的及時顯示。
在收到內(nèi)存警告時處理方式大同小異,不多贅述。
5、計時器
該類使用CADisplayLink做計時任務(wù),顯示系統(tǒng)每幀回調(diào)都會觸發(fā),所以默認(rèn)大致是 60 次/秒。CADisplayLink的特性決定了它非常適合做和幀率相關(guān)的 UI 邏輯。
防止循環(huán)引用
_link = [CADisplayLink displayLinkWithTarget:[_YYImageWeakProxy proxyWithTarget:self] selector:@selector(step:)];
這里使用了一個_YYImageWeakProxy私有類進(jìn)行消息轉(zhuǎn)發(fā)防止循環(huán)引用,看看_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)];
}
,,,
當(dāng)target存在時,發(fā)送給_YYImageWeakProxy實例的方法能正常的轉(zhuǎn)發(fā)給target。
當(dāng)target釋放時,forwardingTargetForSelector:重定向失敗,會調(diào)用methodSignatureForSelector:嘗試獲取有效的方法,而若獲取的方法無效,將會拋出異常,所以這里隨便返回了一個init方法。
當(dāng)methodSignatureForSelector:獲取到一個有效的方法過后,會調(diào)用forwardInvocation:方法開始消息轉(zhuǎn)發(fā)。而這里作者給[invocation setReturnValue:&null];一個空的返回值,讓最外層的方法調(diào)用者不會得到不可控的返回值。雖然這里不調(diào)用方法默認(rèn)會返回 null ,但是為了保險起見,能盡量人為控制默認(rèn)值就不要用系統(tǒng)控制。
計時任務(wù)
計時器回調(diào)方法- (void)step:(CADisplayLink *)link {...}就是調(diào)用動畫的核心代碼,實際上代碼比較容易看懂,主要是顯示當(dāng)前幀圖像、發(fā)起下一幀的解壓任務(wù)等。
八、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數(shù)據(jù)轉(zhuǎn)化為位圖數(shù)據(jù):
- 使用
CGBitmapContextCreate()創(chuàng)建圖片上下文。 - 使用
CGContextDrawImage()將圖片繪制到上下文中。 - 使用
CGBitmapContextCreateImage()通過上下文生成圖片。
2、漸進(jìn)式解碼
在_updateSourceImageIO私有方法中可以看到漸進(jìn)式的解壓邏輯,由于代碼過多不貼出來,主要邏輯大致如下:
- 使用
CGImageSourceCreateIncremental(NULL)創(chuàng)建空圖片源。 - 使用
CGImageSourceUpdateData()更新圖片源 - 使用
CGImageSourceCreateImageAtIndex()創(chuàng)建圖片
漸進(jìn)式解壓可以在下載圖片的過程中進(jìn)行解壓、顯示,達(dá)到網(wǎng)頁上顯示圖片的效果,體驗不錯。
3、YYImageDecoder 類使用的鎖
確實筆者疲于繼續(xù)查看 ImageIO 或 CoreGraphics 下晦澀的 C 代碼,個人認(rèn)為這些東西了解一些就好,如果業(yè)務(wù)有需要在深入探究,想要一次性吃透確實過于困難??。
有意思的是,在YYImageDecoder中使用了兩個鎖。
一個是dispatch_semaphore_t _framesLock;信號量,從它的命名就可以看出,_framesLock鎖是用來保護(hù)NSArray *_frames; ///< Array<GGImageDecoderFrame>, without image變量的線程安全,由于受保護(hù)的代碼塊執(zhí)行速度快,可以體現(xiàn)信號量的性能優(yōu)勢。
另一個是pthread_mutex_t _lock; // recursive lock互斥鎖,當(dāng)筆者看到作者的注釋// 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還支持遞歸鎖,確實學(xué)了一手,完全可以替代性能更差的NSRecursiveLock。
那么,這里為什么要使用遞歸鎖呢?
互斥鎖有個特性,當(dāng)同一個線程多次獲取鎖時(鎖還未解開),會導(dǎo)致死鎖,而遞歸鎖允許同一線程多次獲取鎖,或者說“遞歸”獲取鎖。也就是說,對于同一線程,遞歸鎖是可重入的,對于多線程仍然和互斥鎖無異。
但是,筆者查看了一下源碼,貌似也沒發(fā)現(xiàn)重入鎖的情況發(fā)生,估計也是作者長遠(yuǎn)的考慮,降低編碼死鎖的可能性。
后語
對于這種比較大一點的開源庫,切勿陷入逐字逐句看明白的誤區(qū),因為一個成熟的項目是經(jīng)過很多次維護(hù)的,重要的是看明白作者的思路,理解一些核心的東西,本文拋磚引玉,不喜勿噴。
那么現(xiàn)在,讀者朋友可以說自己會用圖片了么?
參考文獻(xiàn):
iOS 處理圖片的一些小 Tip
移動端圖片格式調(diào)研