YYKit 系列源碼剖析文章:ios
首先問一個問題:你會用圖片麼?git
圖片是現代化 APP 界面設計裏應用普遍的東西,精美的圖片能夠帶來視覺上的享受,提升用戶體驗。由此給技術上帶來了一些挑戰,好比動圖的處理、圖片顯示流暢程度的優化、圖片包大小的優化、超大圖片的處理等。github
本文主要是結合 YYImage 源碼對圖片處理技巧進行講解。而筆者不會逐字逐句的翻譯源碼,主要是提取源碼中有思惟價值的東西。因此最好是打開源碼,本文做爲思想引導。web
源碼基於 1.0.4 版本。算法
首先來談一談圖片處理的一些注意事項和技巧,如下結論參考其餘博文、官方文檔、實際測試得出,歡迎指出錯誤😁。數組
一張圖片從磁盤中顯示到屏幕上過程大體以下:從磁盤加載圖片信息、解碼二進制圖片數據爲位圖、經過 CoreAnimation 框架處理最終繪製到屏幕上。瀏覽器
實際上圖片的繪製過程每每不是性能瓶頸,最耗時的操做是解碼過程,若圖片文件過大,從磁盤讀取的過程也有可觀的耗時。緩存
通常使用imageNamed:
或者imageWithData:
從內存中加載圖片生成UIImage
的實例,此刻圖片並不會解壓,當 RunLoop 準備處理圖片顯示的事務(CATransaction)時,才進行解壓,而這個解壓過程是在主線程中的,這是致使卡頓的重要因素。安全
使用imageNamed:
方法加載圖片信息的同時(生成UIImage
實例),還會將圖片信息緩存起來,因此當使用該方法第一次加載某張圖片時,會消耗較多的時間,而以後再次加載該圖片速度就會很是快(注意此時該圖片是未繪製到屏幕上的,也就是說還未解壓)。bash
在繪製到屏幕以前,第一次解壓成功後,系統會將解壓信息緩存到內存。
值得注意的是,這些緩存都是全局的,並不會由於當前UIImage
實例的釋放而清除,在收到內存警告或者 APP 第一次進入後臺纔有可能會清除,而這個清除的時機和內容是系統決定的,咱們沒法干涉。
使用imageWithData:
方式加載圖片時,無論是加載過程仍是解壓過程,都不會像imageNamed:
緩存到全局,當該UIImage
實例釋放時,相關的圖片信息和解壓信息就會銷燬。
從上面的分析可知,imageNamed:
使用時會產生全局的內存佔用,可是第二次使用同一張圖片時性能很好;imageWithData:
不會有全局的內存佔用,但對於同一張圖片每次加載和解壓都會「從頭開始」。
因而可知,imageNamed:
適合「小」且「使用頻繁」的圖片,imageWithData:
適合「大」且「低頻使用」的圖片。
這裏說的優化並非解壓算法的優化,只是基於用戶體驗的優化。
對於加載過程,若文件過大或加載頻繁影響了幀率(好比列表展現大圖),可使用異步方式加載圖片,減小主線程的壓力,代碼大體以下:
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(...)
系列方法就能實現。
該處理方式在衆多圖片處理框架下都有體現。
值得注意的是,可能業務中須要載入一張很大的圖片。這時,若還使用常規的方式加載會佔用過多的內存;何況,若圖片的像素過大(目前主流 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);
複製代碼
或者直接使用CALayer
的contentsRect
屬性來達到相同的效果。
筆者有寫過一個小東西,裏面就使用了異步壓縮和異步裁剪來處理超大圖片:打造開源第一 iOS 圖片瀏覽器 (支持視頻)
上文中談了一下圖片處理的一些原理和核心思想,作爲背景知識,下面從一個宏觀的角度觀察一下 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 寫的特性:
該類對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
的讀寫安全,耗時短,使用信號量性能很好。
該類是幀動畫圖片類,能夠配置每一幀的圖片信息和顯示時長,圖片支持 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
載體操做和顯示。
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
表示範圍的數組,一個是對應時長的數組。
而後利用CALayer
的contentsRect
屬性,動態的讀取這張大圖某個範圍的內容。固然,這個過程的邏輯一樣在YYAnimatedImageView
類中。
YYAnimatedImage 協議是YYAnimatedImageView
和YYImage、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
類經過YYImage、YYFrameImage、YYSpriteSheetImage
實現的<YYAnimatedImage>
協議方法拿到幀圖片數據和相關信息進行動畫展現。
它的原理就是如此,下面主要分析技術細節,含金量蠻高。
@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 類型來源
/*一、若上一次是 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;
/*二、SpriteSheet 類型時,經過`setContentsRect:forImage:`方法
配置self.layer.contentsRect */
if (hasContentsRect) {
CGRect rect = [((UIImage<YYAnimatedImage> *) newVisibleImage) animatedImageContentsRectAtIndex:0];
[self setContentsRect:rect forImage:newVisibleImage];
}
/*三、如果多幀的圖片,經過`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.contentsRect
爲CGRectMake(0, 0, 1, 1)
使用了CATransaction
事務來取消隱式動畫。(因爲此處徹底不須要那 0.25 秒的隱式動畫)
- (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
週期方法中嘗試啓動或結束動畫,不須要在組件內部特地的去調用就能實現自動的播放和中止。而didMoved
方法中判斷是否開啓動畫寫了個self.superview && self.window
,意味着YYAnimatedImageView
光有父視圖還不能開啓動畫,還須要展現在window
上才行。
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
串行隊列執行的,不會影響主線程。
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
}
複製代碼
在進入後臺時,清除全部的異步解壓任務,而後計算下一幀的下標,最後移除不是下一幀的全部緩存,保證進入前臺時下一幀的及時顯示。
在收到內存警告時處理方式大同小異,很少贅述。
該類使用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 {...}
就是調用動畫的核心代碼,實際上代碼比較容易看懂,主要是顯示當前幀圖像、發起下一幀的解壓任務等。
該文件中主要包含了YYImageFrame
圖片幀信息的類、YYImageDecoder
解碼器、YYImageEncoder
編碼器。
注意,本文對 WebP / APNG 等的圖片解壓縮算法不會討論,主要是說明一些基於 ImageIO 的使用。
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()
經過上下文生成圖片。在_updateSourceImageIO
私有方法中能夠看到漸進式的解壓邏輯,因爲代碼過多不貼出來,主要邏輯大體以下:
CGImageSourceCreateIncremental(NULL)
建立空圖片源。CGImageSourceUpdateData()
更新圖片源CGImageSourceCreateImageAtIndex()
建立圖片漸進式解壓能夠在下載圖片的過程當中進行解壓、顯示,達到網頁上顯示圖片的效果,體驗不錯。
確實筆者疲於繼續查看 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 移動端圖片格式調研