在做iOS開發(fā)中加載圖片是經(jīng)常性工作,一種是使用UIImage加載本地圖片皇钞,使用[UIImage imageNamed:@""]酱畅,[UIImage imageWithContentsOfFile:@""]等方法,各有側(cè)重優(yōu)劣孕索,不是本篇重點(diǎn)不必贅述夯巷。另一種實(shí)時(shí)從網(wǎng)絡(luò)加載赛惩,其中一種方法是從服務(wù)端獲取圖片的二進(jìn)制數(shù)據(jù),客戶端將其轉(zhuǎn)化為NSData *類型趁餐,再通過UIImage加載喷兼,這種方式適合小批量的圖片加載,安全性好實(shí)用簡便后雷,另一種方法就是服務(wù)端先提供一個(gè)圖片的URL季惯,客戶端再通過URL加載圖片,這個(gè)適合大批量獲取圖片的場景臀突,在iOS工程中也是廣泛實(shí)用的場景勉抓,也是本篇討論的重點(diǎn)。
如果不使用第三方框架候学,最簡單的方法便是先調(diào)用[NSData dataWithContentsOfURL:url]藕筋,再使用[UIImage imageWithData:data]加載圖片,通常這個(gè)過程可以使用GCD等異步加載方式防止阻塞UI主線程梳码。但通常情況下為了使用方便和提高性能隐圾,通常要使用一些封裝的框架,這其中就有大名鼎鼎的SDWebImage掰茶,這也是本篇文章要介紹的暇藏。
首先,還是來大概瀏覽一下其結(jié)構(gòu):
大概可以看出大概分為loader下載器濒蒋,cache管理盐碱,圖片加載等部分,下面沿襲之前風(fēng)格沪伙,還是通過一個(gè)在工程中的簡單調(diào)用來分析其工作原理瓮顽。
下面就從一個(gè)UIImageView加載一個(gè)圖片的URL開始:
以上就是一個(gè)一個(gè)在UITableView的cell中一個(gè)普通調(diào)用,可以看出這個(gè)框架主要使用了類別來實(shí)現(xiàn)焰坪。
下面進(jìn)入API查看:
發(fā)現(xiàn)其API在UIImageView+WebCache的類別中:
最終調(diào)用方法中參數(shù):url為圖片網(wǎng)絡(luò)地址,placeholder為占位圖聘惦,options為下載處理選擇項(xiàng)默認(rèn)為SDWebImageRetryFailed(也就是說加載失敗這個(gè)URL就會被拉入黑名單不會重復(fù)加載)某饰,operationKey為網(wǎng)絡(luò)操作標(biāo)識符儒恋,setImageBlock、progressBlock和completedBlock這三個(gè)block會在不同時(shí)間調(diào)用黔漂,這些在后續(xù)分析中都會著重解說诫尽。
再繼續(xù)點(diǎn)進(jìn)去就到了UIView+WebCache這個(gè)類別:
這一步多了一個(gè)context參數(shù),這個(gè)會死一個(gè)字典類型炬守,主要管理一些dispatch_group_t牧嫉,后面會分析。
再繼續(xù)就開始脫去層層外衣見識真相了:
下面開始逐行代碼分析:
NSString *validOperationKey = operationKey ?:NSStringFromClass([selfclass]);如果上邊的參數(shù)operationKey為空的話减途,就創(chuàng)建這個(gè)key值酣藻,但是用來做什么呢?向下看:
[self sd_cancelImageLoadOperationWithKey:validOperationKey];從字面意思上看是要通過這個(gè)key值來撤銷一些操作鳍置×删纾可以進(jìn)去詳細(xì)看:
由上可以看出,UIView+WebCacheOperation這個(gè)類別維護(hù)了一個(gè)NSMapTable *類型的屬性税产,NSMapTable類似于字典吧怕轿,這個(gè)字典就是以operationKey為key值,遵守協(xié)議<SDWebImageOperation>的對象為value值辟拷,這行代碼就是通過key找到這個(gè)operation撞羽,將其撤銷,并從字典中刪除衫冻,書中代言operation就是用來做圖片依次IO的操作诀紊,后邊還會詳細(xì)介紹。
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);這一步是將url保存為這個(gè)UIView+WebCache的一個(gè)關(guān)聯(lián)類羽杰,類似于屬性渡紫。
dispatch_group_t group = context[SDWebImageInternalSetImageGroupKey];這個(gè)是用來做一個(gè)任務(wù)隊(duì)列管理。
這一步是根據(jù)options值決定是否立即顯示占位圖考赛,
這一步就知道setImageBlock這個(gè)參數(shù)是用來做什么的惕澎,
這里可以看出這個(gè)block其實(shí)目的是用來將image給對應(yīng)的控件一般是UIImageView賦值的,如果為空就構(gòu)造一個(gè)finalSetImageBlock颜骤,最終在這一步
調(diào)用這個(gè)block給相應(yīng)圖片顯示控件賦值唧喉。
繼續(xù)向下走:
如果url存在,會通過url去IO圖片忍抽,如果不存在也會有個(gè)錯(cuò)誤處理八孝。
現(xiàn)在開始具體分析url存在下的圖片加載和緩存機(jī)制,這個(gè)是重點(diǎn):
self.sd_imageProgress.totalUnitCount = 0;
self.sd_imageProgress.completedUnitCount = 0;
這兩行從字面就可以看出是重置加載進(jìn)度鸠项。
到這一步干跛,SDWebImageManager浮出水面,SDWebImageManager也是一個(gè)很重要的部分祟绊,相當(dāng)于這個(gè)框架的一個(gè)管理類楼入。
進(jìn)去可以看到哥捕,這個(gè)類的全局單例對象負(fù)責(zé)管理緩存、加載嘉熊、失敗處理和運(yùn)行operation(上文有提到過)遥赚。
這一步可看出是對progressBlock的處理,從字面可看出是對加載進(jìn)度的一個(gè)處理阐肤,如果工程中需要對加載進(jìn)度進(jìn)行處理凫佛,可實(shí)現(xiàn)這個(gè)block。
這一步就是具體的operation操作孕惜。也是即將需要大量筆墨分析的部分愧薛。
展開這部分代碼:
可看出在completed的block中是operation結(jié)束后的處理,也就是說在調(diào)用這個(gè)block時(shí)诊赊,image已經(jīng)完成加載和緩存了厚满,是最后一步,既然是最后一步碧磅,暫且不談押后處理碘箍。
下面先重點(diǎn)分析一下operation的構(gòu)造方法:
可以看到operation是遵守<SDWebImageOperation>協(xié)議的SDWebImageCombinedOperation類的對象,這個(gè)方法的調(diào)用者便是SDWebImageManager類的單例對象鲸郊。
這部分是對url的一個(gè)處理以及和operation的創(chuàng)建丰榴。
manager維護(hù)了一個(gè)failedURLs的數(shù)組,從字面可看出failedURLs是一個(gè)加載失敗的url的數(shù)組秆撮,這里會判斷四濒,如果url為空,或者符合花括號中的條件职辨,就會走這個(gè)方法[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];展開這個(gè)方法:
可以看出這個(gè)方法就是處理上文提到過的構(gòu)造方法中最后一個(gè)block參數(shù)的盗蟆,在這里調(diào),說明整個(gè)operation也就結(jié)束了舒裤,當(dāng)然是以加載失敗告終喳资。
這一步將operation加入到manager維護(hù)的runningOperations數(shù)組中,LOCK鎖是用信號量dispatch_semaphore_t實(shí)現(xiàn)的腾供。然后通過url得到key值仆邓,在
中,可以看出我們可以對url加自定義的過濾器來獲取key值伴鳖。
這一步是緩存一個(gè)SDImageCacheOptions的緩存加載查詢策略類型的cacheOptions节值。
緊接著是構(gòu)建一個(gè)緩存的operation,
可以看出SDWebImageCombinedOperation實(shí)際上是一個(gè)對operation的管理類榜聂,cacheOperation才是具體圖片IO的operation搞疗。
從構(gòu)造方法中,可看出done后邊的block應(yīng)該是最后一步執(zhí)行的block须肆,暫且不論匿乃,先進(jìn)去看cacheOperation的具體構(gòu)造方法脐往。
這個(gè)構(gòu)造方法是由manager維護(hù)的SDImageCache *類型的imageCache來完成的。imageCache是框架的緩存管理類扳埂。
先對key做判斷處理,若為空直接執(zhí)行doneBlock瘤礁。
首先從內(nèi)存緩存中獲取阳懂,如果內(nèi)存中存在且滿足花括號中的條件直接返回。
接著柜思,從硬盤緩存去獲取岩调。由于磁盤IO比較緩慢,所以提供了一個(gè)異步任務(wù)隊(duì)列赡盘,可以選擇異步磁盤獲取号枕,可見作者思慮之周詳。
展開queryDiskBlock:
先從磁盤中獲取到diskData數(shù)據(jù)陨享,若內(nèi)存中已經(jīng)獲取到image便賦值diskImage = image;否則再將diskData解碼后的數(shù)據(jù)賦值給diskImage葱淳,然后調(diào)用[self.memCache setObject:diskImage forKey:key cost:cost];將其存入內(nèi)存緩存,最后再執(zhí)行doneBlock抛姑。
由上邊方法可看出赞厕,從磁盤中獲取數(shù)據(jù)時(shí)解碼是一個(gè)很大的耗時(shí)耗性能操作,恰好SDWebImage能很巧妙的解決這個(gè)問題定硝,其解決方法就是先將其繪制成bitmap數(shù)據(jù)到畫布上皿桑,這些在進(jìn)行網(wǎng)絡(luò)獲取時(shí)還會用到,之后再詳細(xì)講解蔬啡,這也是SDWebImage很經(jīng)典的一部分诲侮。
現(xiàn)在就回來再看doneBlock中部分:
由于block會強(qiáng)持有對象,所以這里要對operation做weak處理箱蟆。
如果operation被撤銷沟绪,則會被從runningOperations數(shù)組中安全移除。
根據(jù)加載策略顽腾,是否從緩存中取得圖片等條件確定是否從網(wǎng)絡(luò)下載近零。
如果需要不需要從網(wǎng)絡(luò)下載,也會回調(diào)執(zhí)行最后的completed的block抄肖,并從數(shù)組中安全移除operation久信。
如果需要從網(wǎng)絡(luò)加載,走下面的選擇支:
如果從緩存中加載到圖片漓摩,切加載策略為從網(wǎng)絡(luò)刷新裙士,則先返回執(zhí)行completedBlock,但事沒完管毙,還要繼續(xù)從網(wǎng)絡(luò)加載刷新腿椎,這個(gè)適合可能在后臺同一個(gè)url換另一張圖片的情形桌硫。
設(shè)置一個(gè)網(wǎng)絡(luò)下載策略。
接著構(gòu)造了一個(gè)downloadToken作為屬性賦給了operation啃炸∶可能我們會好奇,downloadToken是啥南用,點(diǎn)進(jìn)去看:
可以看得出膀钠,這個(gè)token的作用是作為一個(gè)下載的獨(dú)立標(biāo)示,這樣一來圖片加載處理的operation可以通過持有的這個(gè)token來進(jìn)行撤銷下載等操作裹虫。
從上邊這整塊代碼來看肿嘲,可以發(fā)現(xiàn)是又manager維護(hù)的imageDownloader下載器負(fù)責(zé)下載圖片的,并返回這個(gè)token筑公。
在這個(gè)方法中傳入了url雳窟、downloaderOptions下載策略、progressBlock匣屡、completedBlock等參數(shù)封救,progressBlock就是前文API過程調(diào)用的block,completedBlock從字面上看應(yīng)該也是結(jié)束后調(diào)用的block捣作,暫且不論兴泥,首先來分析這個(gè)下載器的工作:
可以看出SDWebImageDownloader其實(shí)是一個(gè)下載管理類,imageDownloader就是這個(gè)管理類的一個(gè)單例對象虾宇,負(fù)責(zé)在運(yùn)行期間管理圖片下載搓彻,維護(hù)者下載operation的URLOperations字典,downloadQueue下載任務(wù)隊(duì)列等嘱朽。
逐步展開來看:
如果url為空旭贬,直接返回空,并接受操作搪泳。
先根據(jù)以url為key值從self.URLOperations中查找具體的下載operation稀轨。如果不存在或已結(jié)束或已撤銷就創(chuàng)建一個(gè)新的并加入self.URLOperations字典和self.downloadQueue隊(duì)列。如果存在岸军,但并不在執(zhí)行中奋刽,可根據(jù)operation下載策略來調(diào)整下載operation的優(yōu)先級。整個(gè)過程都是在線程安全中進(jìn)行的艰赞。
緊接著構(gòu)造SDWebImageDownloadToken *token返回賦給圖片加載處理的operation的downloadToken佣谐。
下面繼續(xù)深入分析具體的下載operation:
可以看到下載operation其實(shí)是NSOperation的一個(gè)實(shí)例,遵守<SDWebImageDownloaderOperationInterface>協(xié)議方妖。
展開分析:
設(shè)置超時(shí)時(shí)間狭魂,網(wǎng)絡(luò)請求安全策略,構(gòu)造網(wǎng)絡(luò)請求request。
根據(jù)request構(gòu)造相應(yīng)的operation雌澄,并根據(jù)下載策略options做優(yōu)先級處理等斋泄。
進(jìn)入operation的構(gòu)造方法:
可以看出operation的類其實(shí)是框架自定義的繼承于NSOperation的類。其任務(wù)主體主要是通過重寫的start函數(shù)實(shí)現(xiàn)的:
首先也是用@synchronized 鎖來鎖住相關(guān)代碼镐牺,這部分代碼就是初始化session炫掐、dataTask部分。
展開這個(gè)鎖:
如果任務(wù)已經(jīng)被撤銷睬涧,重置卒废。
設(shè)置應(yīng)用退入后臺的處理。
初始化session宙地,不過這里的session一般使用的是初始化方法傳進(jìn)來的session,一般是由imageDownloader維護(hù)的一個(gè)框架全局的session逆皮。
根據(jù)下載策略option設(shè)置網(wǎng)絡(luò)請求緩存策略宅粥。
通過session和request構(gòu)造請求任務(wù)dataTask,并將任務(wù)operation的executing狀態(tài)設(shè)置為執(zhí)行电谣。
根據(jù)下載策略option設(shè)置dataTask的優(yōu)先級秽梅,并運(yùn)行dataTask。
回頭發(fā)現(xiàn)imageDownloader的初始化函數(shù)中
session的delegate給了imageDownloader剿牺,但同樣SDWebImageDownloader也繼承了<NSURLSessionTaskDelegate, NSURLSessionDataDelegate>協(xié)議企垦,并實(shí)現(xiàn)了具體協(xié)議方法。
而在協(xié)議方法中:
又將回調(diào)方法在下載dataOperation中執(zhí)行晒来,所以再來到dataOperation中實(shí)現(xiàn)的代理方法:
首先在線程安全下降dataTask置空钞诡,緊接著就開始處理返回的圖片的二進(jìn)制數(shù)據(jù):
除去錯(cuò)誤處理等,核心代碼是這塊:
這塊主要就是在self.coderQueue異步隊(duì)列中完成圖片解碼湃崩,下面逐步分析:
在這里SDWebImageCodersManager這個(gè)解碼管理類就粉墨登場了荧降,同樣也是實(shí)現(xiàn)了一個(gè)全局的單例實(shí)例來做解碼工作。
首先看一下SDWebImageCodersManager的初始化方法:
初始化中除了實(shí)現(xiàn)安全鎖外攒读,更重要的初始化這個(gè)數(shù)組_coders很重要朵诫,這個(gè)數(shù)組目前只有一個(gè)元素SDWebImageImageIOCoder*類型的實(shí)例。
然后進(jìn)入第一個(gè)解碼方法:
根據(jù)初始化方法會走到SDWebImageImageIOCoder*類型的實(shí)例方法:
第一行代碼是普通的解碼方法薄扁,第二行代碼是框架通過NSData的類別實(shí)現(xiàn)的一個(gè)查看圖片類型的方法不再詳述剪返。
然后獲取圖片的緩存key值从橘,并矯正圖片方向定踱。
然而就此結(jié)束了嗎蹦锋?向下走:
這里有又了一個(gè)解碼方法霍衫,這點(diǎn)可能就會引起大家的困惑董栽,為什么要這樣呢椅野?原來[[UIImage alloc] initWithData:data]這個(gè)方法并沒有實(shí)際解碼鸣峭,只有將image第一次顯示的時(shí)候才會解碼盆犁,并長久滯留內(nèi)存,這并不是一個(gè)很好的處理方法诈铛。這一點(diǎn)也就是SDWebImage在那個(gè)時(shí)代很精華的一部分乙各,通過調(diào)用
這個(gè)方法將圖片繪制到CG畫布上,控件直接加載畫布上的image幢竹,這在當(dāng)時(shí)是一個(gè)巨大的創(chuàng)新耳峦,下面就具體分析這個(gè)方法:
如果下載策略不包含大圖片等比例縮小的話將走到這個(gè)方法:
展開分析:
做個(gè)判斷,如果image為空或是動畫直接返回焕毫。
緊接著就是CG的天地了:
主要思路竟是通過創(chuàng)建一個(gè)bitmap context蹲坷,將圖片繪制到畫布上,才從畫布上獲取image邑飒。
就此解碼工作完成循签,也會產(chǎn)生長久滯留內(nèi)存的圖片信息數(shù)據(jù)。順便提到上文在從硬盤中獲取緩存時(shí)賣的關(guān)子疙咸,從硬盤IO獲取到圖片的二進(jìn)制數(shù)據(jù)也走的是這條線县匠。
就此image加載也就完成了,先回到下載operation的回調(diào)方法中撒轮,還在那個(gè)圖片解碼的隊(duì)列coderQueue的代碼塊中:
進(jìn)去這個(gè)方法:
可以看出是通過下載operation維護(hù)的callbackBlock數(shù)組找到回調(diào)的completedBlock執(zhí)行回調(diào)乞旦,到這里同學(xué)們是否有迷路的,能否找到回家的路呢题山?
回到這個(gè)下載operation的構(gòu)造方法:
可以看出就是在
這個(gè)方法中將過程progressBlock和完成completedBlock傳入下載operation中的兰粉。
到此為止,整個(gè)網(wǎng)絡(luò)下載過程也就分析完了顶瞳,回到SDWebImageManager勒種圖片加載operaion的構(gòu)造方法中玖姑,
現(xiàn)在就到這completedBlock的執(zhí)行部分。
正常的話就會走到這個(gè)選擇支:
先做緩存慨菱,由manger持有的緩存管理imageCache來執(zhí)行:
展開分析:
如果image或key不存在客峭,直接返回。
將解碼后的image放入內(nèi)存緩存抡柿,下次從內(nèi)存中加載就不需要解碼舔琅。
異步磁盤IO,將圖片壓縮后二進(jìn)制數(shù)據(jù)存入硬盤洲劣。
結(jié)束緩存后:
終于到了激動人心的時(shí)候了备蚓,就像一個(gè)走了很多路的孩子終于要回家了,終于回調(diào)框架API中的completionBlock了囱稽。
展開分析郊尝,除去過程處理和重繪處理,主干代碼走到這里:
賦值要處理的image和data战惊,再走到之前提到到的
方法流昏,給相應(yīng)控件賦值,就此一個(gè)調(diào)用結(jié)束,可以長吁一口氣了况凉。
說了這么多谚鄙,感覺是時(shí)候總結(jié)一下了,
從網(wǎng)路上借張圖:
按照本篇的分析刁绒,整個(gè)路徑是這樣的:首先調(diào)用加載圖片的UIImageVIew*類型的imageView的分類UIView+WebCache的API闷营,在API方法中,通過id<SDWebImageOperation>的對象知市,也就是SDWebImageManager*類型的manager傻盟,通過manager構(gòu)造加載圖片的id <SDWebImageOperation> operation,也就是SDWebImageCombinedOperation *類型的operation嫂丙,并由分類UIView+WebCache持有的sd_operationDictionary字典來管理娘赴。然后通過id <SDWebImageOperation> operation來構(gòu)造并持有cacheOperation來獲取緩存,同時(shí)如果需要從網(wǎng)絡(luò)下載跟啤,便構(gòu)造持有網(wǎng)絡(luò)下載的dataOperation來完成下載以及圖片解碼诽表。這就是大致流程,當(dāng)然在這個(gè)過程中緩存管理腥光,圖片解碼等都有很多可圈可點(diǎn)的地方,再次不再贅述糊秆,可以單獨(dú)開篇講解武福。