系列文章:
YYText 源碼剖析:CoreText 與異步繪制
YYAsyncLayer 源碼剖析:異步繪制
YYCache 源碼剖析:一覽亮點(diǎn)
YYModel 源碼剖析:關(guān)注性能
YYImage 源碼剖析:圖片處理技巧
YYWebImage 源碼剖析:線程處理與緩存策略
引言
在 iOS 開發(fā)中,異步網(wǎng)絡(luò)圖片下載框架可以說是很大的解放了生產(chǎn)力穿香,通常情況下開發(fā)者只需要簡單的代碼就能將網(wǎng)絡(luò)圖片異步下載并顯示到手機(jī)屏幕上亭引,并且還帶有緩存優(yōu)化。
業(yè)界名氣最高的異步圖片下載框架是 SDWebImage扔水,而后 ibireme 前輩開源了 YYWebImage痛侍,對(duì)性能有所優(yōu)化。之前有粗略的瀏覽過 SDWebImage 的源碼,對(duì)比 YYWebImage 源碼過后主届,實(shí)際上筆者更喜歡 YYWebImage赵哲,因?yàn)槠浯a風(fēng)格很簡潔、代碼結(jié)構(gòu)更清晰君丁。
技術(shù)層面來看枫夺,兩者對(duì)線程處理的處理方式有所不同,緩存策略也有細(xì)節(jié)上的差異绘闷,雖然筆者的理解來看 YYWebImage 性能更為優(yōu)越橡庞,但是并沒有充分的測試用例來驗(yàn)證。有些遺憾的是印蔗,YYWebImage 似乎挺久沒有維護(hù)了扒最,作者在 這條 issues 說過計(jì)劃會(huì)將NSURLConnection
替換為NSURLSession
,到現(xiàn)在都沒有動(dòng)作??华嘹。
所以實(shí)際開發(fā)中為了穩(wěn)定性可能還是會(huì)首選 SDWebImage吧趣,但是這絲毫不影響我們學(xué)習(xí) YYWebImage 的優(yōu)秀源碼,本文主要是分析 YYWebImage 的核心思路和亮點(diǎn)耙厚。
源碼版本:1.0.5
一强挫、框架總覽
//包含所有文件的頭文件
YYWebImage.h
//緩存相關(guān)
YYImageCache.h (.m)
//請(qǐng)求任務(wù)預(yù)處理類
_YYWebImageSetter.h (.m)
//請(qǐng)求任務(wù)管理類
YYWebImageManager.h (.m)
//自定義請(qǐng)求類(繼承自NSOperation)
YYWebImageOperation.h (.m)
//方便業(yè)務(wù)調(diào)用的分類
CALayer+YYWebImage.h (.m)
MKAnnotationView+YYWebImage.h (.m)
UIButton+YYWebImage.h (.m)
UIImage+YYWebImage.h (.m)
UIImageView+YYWebImage.h (.m)
上面這些方便業(yè)務(wù)調(diào)用的分類,它們的實(shí)現(xiàn)大同小異薛躬,使用最多的是UIImageView+YYWebImage.h
俯渤,完全可以以其為入口探究框架的原理。
正如作者框架的簡短說明:
Asynchronous image loading framework.
該框架的核心就是異步下載網(wǎng)絡(luò)圖片型宝。
- 既然是異步下載八匠,就涉及到線程的高效調(diào)度問題,由于在業(yè)務(wù)場景中下載圖片的任務(wù)可能是繁重的诡曙,所以線程處理的性能至關(guān)重要臀叙。
- 圖片下載成功過后,為了避免顯示圖片時(shí)在主線程解壓价卤,框架做了異步解壓,對(duì)于
gif渊涝、APNG慎璧、WebP
等都有支持,這部分功能是基于作者的另一個(gè)框架 YYImage跨释,筆者之前寫過源碼分析:YYImage 源碼剖析:圖片處理技巧胸私。 - 為了不重復(fù)下載和重復(fù)解壓,框架做了緩存優(yōu)化鳖谈,至于是否緩存解壓過后的圖片岁疼,可以由開發(fā)者選擇,當(dāng)然,緩存分內(nèi)存緩存和磁盤緩存捷绒,讀寫速度一般也是內(nèi)存大于磁盤瑰排,這部分功能是基于作者的另一個(gè)框架 YYCache,筆者之前也寫過源碼分析:YYCache 源碼剖析:一覽亮點(diǎn)暖侨。
二椭住、重復(fù)下載請(qǐng)求處理
該處理主要是基于_YYWebImageSetter.h
下的一個(gè)屬性:
@property (nonatomic, readonly) int32_t sentinel;
從UIImageView+YYWebImage.h
的一個(gè)方法看起:
- (void)yy_setImageWithURL:(NSURL *)imageURL
placeholder:(UIImage *)placeholder
options:(YYWebImageOptions)options
manager:(YYWebImageManager *)manager
progress:(YYWebImageProgressBlock)progress
transform:(YYWebImageTransformBlock)transform
completion:(YYWebImageCompletionBlock)completion {
...
//第一步:為 UIImageView 綁定一個(gè) _YYWebImageSetter 對(duì)象
_YYWebImageSetter *setter = objc_getAssociatedObject(self, &_YYWebImageSetterKey);
if (!setter) {
setter = [_YYWebImageSetter new];
objc_setAssociatedObject(self, &_YYWebImageSetterKey, setter, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
int32_t sentinel = [setter cancelWithNewURL:imageURL];
_yy_dispatch_sync_on_main_queue(^{
...
__weak typeof(self) _self = self;
dispatch_async([_YYWebImageSetter setterQueue], ^{
...
//第二步:開始下載任務(wù)
newSentinel = [setter setOperationWithSentinel:sentinel url:imageURL options:options manager:manager progress:_progress transform:transform completion:_completion];
weakSetter = setter;
});
});
}
筆者省略了大部分代碼,不用在意這些線程操作字逗,現(xiàn)在只關(guān)注重復(fù)請(qǐng)求的處理京郑。
在 第一步 中,利用 runtime 為UIImageView
綁定一個(gè)_YYWebImageSetter
對(duì)象葫掉,然后調(diào)用了一個(gè)方法cancelWithNewURL:
些举,該方法實(shí)現(xiàn)如下:
- (int32_t)cancelWithNewURL:(NSURL *)imageURL {
int32_t sentinel;
dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
if (_operation) {
[_operation cancel];
_operation = nil;
}
_imageURL = imageURL;
sentinel = OSAtomicIncrement32(&_sentinel);
dispatch_semaphore_signal(_lock);
return sentinel;
}
可以看到作者取消了_operation
任務(wù),對(duì)于同一個(gè)UIImageView
的重復(fù)請(qǐng)求時(shí)俭厚,取消_operation
任務(wù)也就是取消上一次請(qǐng)求的任務(wù)户魏。
然后有一句至關(guān)重要的代碼:sentinel = OSAtomicIncrement32(&_sentinel);
,使用原子自增保證全局變量_sentinel
的線程安全和讀取性能套腹。也就是說绪抛,對(duì)于同一個(gè)UIImageView
每次調(diào)用yy_setImageWithURL: ...
方法都會(huì)取消上次的請(qǐng)求并且將其_sentinel
加一。
這么做的意義电禀,往下面看幢码。
在 第二步 中,調(diào)用了_YYWebImageSetter
的setOperationWithSentinel: ...
方法:
- (int32_t)setOperationWithSentinel:(int32_t)sentinel
url:(NSURL *)imageURL
options:(YYWebImageOptions)options
manager:(YYWebImageManager *)manager
progress:(YYWebImageProgressBlock)progress
transform:(YYWebImageTransformBlock)transform
completion:(YYWebImageCompletionBlock)completion {
//1尖飞、判斷當(dāng)前請(qǐng)求是否是最新請(qǐng)求
if (sentinel != _sentinel) {
if (completion) completion(nil, imageURL, YYWebImageFromNone, YYWebImageStageCancelled, nil);
return _sentinel;
}
NSOperation *operation = ... //省略實(shí)際網(wǎng)絡(luò)請(qǐng)求邏輯
//2症副、判斷當(dāng)前請(qǐng)求是否是最新請(qǐng)求
dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
if (sentinel == _sentinel) {
if (_operation) [_operation cancel];
_operation = operation;
sentinel = OSAtomicIncrement32(&_sentinel);
} else {
[operation cancel];
}
dispatch_semaphore_signal(_lock);
return sentinel;
}
可以看到兩個(gè)地方都有 判斷當(dāng)前請(qǐng)求是否是最新請(qǐng)求 的邏輯。對(duì)于第 1 個(gè)地方政基,因?yàn)樵谠摲椒ㄈ霔5臅r(shí)候贞铣,可能該UIImageView
的下一次yy_setImageWithURL: ...
又一次入棧,也就是說_sentinel
可能已經(jīng)加一了沮明,那么這里就沒有必要繼續(xù)下面的網(wǎng)絡(luò)請(qǐng)求邏輯了(代碼已省略)辕坝;對(duì)于第 2 個(gè)地方,也是同樣的考慮荐健,若此刻_sentinel
已經(jīng)加一了酱畅,就取消掉當(dāng)前已經(jīng)創(chuàng)建好的NSOperation
,若此刻_sentinel
沒變江场,就取消掉上一次的_operation
纺酸,然后_sentinel
自增。
值得注意的是址否,這里的信號(hào)量使用是為了保證_operation
讀寫安全餐蔬,而不是為了保護(hù)_sentinel
(因?yàn)樵幼栽霰旧砭褪蔷€程安全的)。
大致重復(fù)請(qǐng)求的處理就是如此,若看得有些費(fèi)解建議多看幾遍源碼里面完整的代碼樊诺。
三仗考、線程的處理
1、下載任務(wù)的預(yù)處理
同樣是在UIImageView+YYWebImage.h
下的入口方法:
- (void)yy_setImageWithURL:(NSURL *)imageURL
placeholder:(UIImage *)placeholder
options:(YYWebImageOptions)options
manager:(YYWebImageManager *)manager
progress:(YYWebImageProgressBlock)progress
transform:(YYWebImageTransformBlock)transform
completion:(YYWebImageCompletionBlock)completion {
...
_yy_dispatch_sync_on_main_queue(^{
...
//第一步:在主線程讀取內(nèi)存緩存
// get the image from memory as quickly as possible
UIImage *imageFromMemory = nil;
if (manager.cache &&
!(options & YYWebImageOptionUseNSURLCache) &&
!(options & YYWebImageOptionRefreshImageCache)) {
imageFromMemory = [manager.cache getImageForKey:[manager cacheKeyForURL:imageURL] withType:YYImageCacheTypeMemory];
}
if (imageFromMemory) {
if (!(options & YYWebImageOptionAvoidSetImage)) {
self.image = imageFromMemory;
}
if(completion) completion(imageFromMemory, imageURL, YYWebImageFromMemoryCacheFast, YYWebImageStageFinished, nil);
return;
}
...
__weak typeof(self) _self = self;
//第二步:在異步線程做下載任務(wù)的預(yù)處理
dispatch_async([_YYWebImageSetter setterQueue], ^{
...
newSentinel = [setter setOperationWithSentinel:sentinel url:imageURL options:options manager:manager progress:_progress transform:transform completion:_completion];
weakSetter = setter;
});
});
}
第一步
可以看到作者的一句英文注釋啄骇,也就是盡可能快的從內(nèi)存讀取緩存 (如果有)痴鳄,這里是一個(gè)很有意思的優(yōu)化點(diǎn)。了解 YYCache 框架的讀者應(yīng)該知道缸夹,作者是使用 雙向鏈表+hash 的方式實(shí)現(xiàn)的內(nèi)存緩存痪寻,直接查找的開銷比切換后臺(tái)線程查找而后返回主線程的開銷要小。
第二步
下載任務(wù)的預(yù)處理是在一個(gè)[_YYWebImageSetter setterQueue]
隊(duì)列虽惭,代碼如下:
+ (dispatch_queue_t)setterQueue {
static dispatch_queue_t queue;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
queue = dispatch_queue_create("com.ibireme.webimage.setter", DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(queue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
});
return queue;
}
可以看到這是一個(gè)串行的隊(duì)列橡类,優(yōu)先級(jí)為DISPATCH_QUEUE_PRIORITY_DEFAULT
,小于主隊(duì)列芽唇。
可能有朋友會(huì)疑問顾画,下載任務(wù)在異步隊(duì)列?那豈不是同一時(shí)刻只有一個(gè)下載任務(wù)執(zhí)行匆笤?
哈哈研侣,注意看清筆者的描述:下載任務(wù)的預(yù)處理。這里面包含了任務(wù)的創(chuàng)建炮捧、重復(fù)請(qǐng)求處理等邏輯庶诡,并沒有耗時(shí)過多的操作,使用一個(gè)異步的線程來處理也是為了減輕主線程的壓力咆课。下載任務(wù)的線程處理后面會(huì)講到末誓,并不是此處的串行隊(duì)列。
2书蚪、下載任務(wù)的處理
該框架使用了NSURLConnection
處理下載任務(wù)喇澡,姑且不談它的用法,畢竟已經(jīng)淘汰了殊校。它的代理線程是如此創(chuàng)建的:
/// Network thread entry point.
+ (void)_networkThreadMain:(id)object {
@autoreleasepool {
[[NSThread currentThread] setName:@"com.ibireme.webimage.request"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
/// Global image request network thread, used by NSURLConnection delegate.
+ (NSThread *)_networkThread {
static NSThread *thread = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
thread = [[NSThread alloc] initWithTarget:self selector:@selector(_networkThreadMain:) object:nil];
if ([thread respondsToSelector:@selector(setQualityOfService:)]) {
thread.qualityOfService = NSQualityOfServiceBackground;
}
[thread start];
});
return thread;
}
這段代碼在老版本的 AFNetwork 和 SDWebImage 里面都出現(xiàn)過晴玖,創(chuàng)建一個(gè)常駐線程來處理下載任務(wù)的回調(diào),通過添加一個(gè) NSMachPort 端口保證該線程的 runloop 的正常運(yùn)行不退出为流,由于手動(dòng)創(chuàng)建的線程不包含自動(dòng)釋放池窜醉,所以作者加了一個(gè)。
這里的亮點(diǎn)其實(shí)是這么一句方法:thread.qualityOfService = NSQualityOfServiceBackground;
艺谆。
作者很細(xì)心的將線程的優(yōu)先級(jí)設(shè)置為NSQualityOfServiceBackground
,這是一個(gè)比較低的優(yōu)先級(jí)拜英,作者希望圖片的下載回調(diào)相關(guān)處理不會(huì)和其他線程競爭 CPU 的資源(比如操作 UI 的主線程等)静汤。
3、圖片讀取和解壓處理
圖片從磁盤中讀取、寫入虫给、解壓等操作都是在下面這個(gè)隊(duì)列處理的(圖片處理具體原理可看YYImage 源碼剖析:圖片處理技巧):
+ (dispatch_queue_t)_imageQueue {
#define MAX_QUEUE_COUNT 16
static int queueCount;
static dispatch_queue_t queues[MAX_QUEUE_COUNT];
static dispatch_once_t onceToken;
static int32_t counter = 0;
dispatch_once(&onceToken, ^{
queueCount = (int)[NSProcessInfo processInfo].activeProcessorCount;
queueCount = queueCount < 1 ? 1 : queueCount > MAX_QUEUE_COUNT ? MAX_QUEUE_COUNT : queueCount;
if ([UIDevice currentDevice].systemVersion.floatValue >= 8.0) {
for (NSUInteger i = 0; i < queueCount; i++) {
dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0);
queues[i] = dispatch_queue_create("com.ibireme.image.decode", attr);
}
} else {
for (NSUInteger i = 0; i < queueCount; i++) {
queues[i] = dispatch_queue_create("com.ibireme.image.decode", DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(queues[i], dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0));
}
}
});
int32_t cur = OSAtomicIncrement32(&counter);
if (cur < 0) cur = -cur;
return queues[(cur) % queueCount];
#undef MAX_QUEUE_COUNT
}
創(chuàng)建與處理器相同的串行隊(duì)列模擬并發(fā)控制藤抡,具體的原理分析可以看筆者的一篇文章:YYAsyncLayer 源碼剖析:異步繪制 中對(duì)線程的討論,這種并發(fā)線程的處理是作者的一個(gè)常規(guī)思路抹估,不多說缠黍。
四、緩存策略
在該框架中的體現(xiàn)药蜻,上層的業(yè)務(wù)邏輯是這樣的:
- 優(yōu)先查找內(nèi)存緩存瓷式,若找到則返回
- 若內(nèi)存緩存未找到,會(huì)異步從磁盤查找緩存语泽,若找到則返回贸典,并且寫入內(nèi)存緩存方便下次查找
- 若磁盤緩存仍然未找到,發(fā)起網(wǎng)絡(luò)請(qǐng)求
- 網(wǎng)絡(luò)請(qǐng)求成功踱卵,同時(shí)寫入磁盤緩存和內(nèi)存緩存
實(shí)際上這個(gè)邏輯和 SDWebImage 基本一致廊驼。值得注意的是,是否查找內(nèi)存或磁盤緩存惋砂、是否需要緩存妒挎、緩存的大小限制等都有自定義的方法。
上層的核心邏輯就是如此西饵,關(guān)于內(nèi)存緩存和磁盤緩存的底層實(shí)現(xiàn)酝掩,可以查看YYCache 源碼剖析:一覽亮點(diǎn)。
五罗标、加載指示器的處理
加載指示器是在YYWebImageManager.m
中處理的庸队,其他代碼就不貼出來了
@interface _YYWebImageApplicationNetworkIndicatorInfo : NSObject
@property (nonatomic, assign) NSInteger count;
@property (nonatomic, strong) NSTimer *timer;
@end
+ (_YYWebImageApplicationNetworkIndicatorInfo *)_networkIndicatorInfo {
return objc_getAssociatedObject(self, @selector(_networkIndicatorInfo));
}
+ (void)_setNetworkIndicatorInfo:(_YYWebImageApplicationNetworkIndicatorInfo *)info {
objc_setAssociatedObject(self, @selector(_networkIndicatorInfo), info, OBJC_ASSOCIATION_RETAIN);
}
...
綁定到YYWebImageManager
的一個(gè)類變量_YYWebImageApplicationNetworkIndicatorInfo
,也就是說變量的timer
和count
都是全局的闯割。
彻消。處理指示器本質(zhì)是容易的,但是作者的思路挺有意思宙拉。
一是作者通過一個(gè)NSTimer
來延時(shí) 1/30 秒開啟或者關(guān)閉加載指示器宾尚。
二是作者通過“計(jì)數(shù)”來控制指示器是否顯示,也就是上面的count
谢澈,當(dāng)有網(wǎng)絡(luò)任務(wù)開始的時(shí)候計(jì)數(shù)加一煌贴,當(dāng)有網(wǎng)絡(luò)任務(wù)結(jié)束或者異常取消時(shí)計(jì)數(shù)減一,那么锥忿,只要count
大于零就顯示指示器牛郑,否則就隱藏。
這思路確實(shí)挺巧妙敬鬓。
六淹朋、框架的性能瓶頸
在YYWebImageOperation.m
下的-connectionDidFinishLoading:
代理方法中可以看到圖片的解壓邏輯笙各,它是在_imageQueue
中執(zhí)行的,解壓完成就緩存起來方便顯示础芍。
雖然解壓的過程是在異步線程杈抢,通常情況下不會(huì)影響到主線程,但是當(dāng)解壓的圖片過多或者圖片分辨率過大時(shí)仑性,解壓和緩存會(huì)占用大量的內(nèi)存惶楼,導(dǎo)致內(nèi)存峰值飆升。
所以诊杆,需要開發(fā)者做一些性能上的優(yōu)化歼捐,不過可喜的是可以通過YYWebImageOptions
的YYWebImageOptionIgnoreImageDecoding
值禁止下載成功后的解壓和緩存邏輯,以此降低內(nèi)存峰值刽辙。
七窥岩、框架中的一些小 tips
1、自動(dòng)釋放池
可以看到框架中使用了大量的自動(dòng)釋放池來避免內(nèi)存峰值宰缤,可能有開發(fā)者感覺如此頻繁的使用自動(dòng)釋放池是否會(huì)造成性能問題颂翼,實(shí)際上影響不大。了解自動(dòng)釋放池的底層原理的朋友都知道慨灭,添加一個(gè)自動(dòng)釋放池不過是添加一個(gè)標(biāo)識(shí)(哨兵)朦乏,需要管理對(duì)象加入自動(dòng)釋放池可以看做是入棧操作,當(dāng)棧頂?shù)倪@個(gè)自動(dòng)釋放池結(jié)束氧骤,會(huì)自動(dòng)給池內(nèi)對(duì)象發(fā)送release
消息(這里池內(nèi)就是棧頂?shù)健吧诒钡姆秶?/p>
2呻疹、鎖的使用
在YYWebImageOperation.m
中使用了遞歸鎖NSRecursiveLock
避免多次獲取鎖而導(dǎo)致死鎖,當(dāng)然筹陵,筆者認(rèn)為這里使用pthread_mutex_t
互斥鎖的遞歸實(shí)現(xiàn)處理性能應(yīng)該更好刽锤。
在操作少量的、耗時(shí)少的代碼時(shí)朦佩,使用dispatch_semaphore_t
信號(hào)量保證線程安全并思,有性能優(yōu)勢。
在對(duì)int32_t
類型變量進(jìn)行安全保護(hù)時(shí)语稠,使用OSAtomicIncrement32()
原子方法無疑是很好的選擇宋彼。
3、避免循環(huán)引用
框架中通過一個(gè)中間類的消息轉(zhuǎn)發(fā)來達(dá)到避免循環(huán)引用的目的:
@interface _YYWebImageWeakProxy : NSProxy
@property (nonatomic, weak, readonly) id target;
- (instancetype)initWithTarget:(id)target;
+ (instancetype)proxyWithTarget:(id)target;
@end
@implementation _YYWebImageWeakProxy
- (instancetype)initWithTarget:(id)target {
_target = target;
return self;
}
+ (instancetype)proxyWithTarget:(id)target {
return [[_YYWebImageWeakProxy alloc] initWithTarget:target];
}
- (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)];
}
關(guān)于具體的分析可以看筆者的文章YYImage 源碼剖析:圖片處理技巧有相應(yīng)的解析仙畦。
結(jié)語
不得不說输涕,框架都是有套路的。在閱讀 YYKit 系列的代碼中慨畸,也懂了作者的套路莱坎,所以筆者在閱讀 YYWebImage 源碼時(shí)非常快寸士,幾乎沒有卡殼型奥,可能這就是“厚積薄發(fā)”的小小體現(xiàn)吧瞳收。
考慮到篇幅和碼字太累,筆者的分析文章都是剝繭抽絲的厢汹,若讀者朋友閱讀有障礙,請(qǐng)沉下心來谐宙,多結(jié)合源碼烫葬,多思考??。