簡(jiǎn)書(shū)博客已經(jīng)暫停更新,想看更多技術(shù)博客請(qǐng)到:
做iOS開(kāi)發(fā)也有半年多了金赦,想想自己對(duì)一些第三方庫(kù)還只是停留在簡(jiǎn)單運(yùn)用的階段音瓷,感覺(jué)心慌慌的。于是決定用一個(gè)月的時(shí)間深入了解一些好的第三方庫(kù)夹抗。
第一個(gè)想到了SDWebImage绳慎,這個(gè)庫(kù)很不錯(cuò),幾乎每個(gè)iOS項(xiàng)目都會(huì)有它的影子漠烧,因?yàn)樗芡昝赖亟鉀Q了下載圖片并顯示的處理邏輯杏愤。那么深究它之前,筆者準(zhǔn)備先了解一下多圖下載的緩存機(jī)制已脓,因?yàn)樗蚐DWebImage的方案類(lèi)似珊楼。
有一個(gè)多圖緩存機(jī)制的教程是來(lái)自李明杰小碼哥的,筆者覺(jué)得講得挺不錯(cuò)的度液,于是就花了2個(gè)小時(shí)好好學(xué)習(xí)了一下厕宗。
Demo地址:multi_image_cache_and_download
1. 需求點(diǎn)是什么?
這里所說(shuō)的多圖下載堕担,就是要在tableview的每一個(gè)cell里顯示一張圖片,而且這些圖片都需要從網(wǎng)上下載已慢。
2. 容易遇到的問(wèn)題
如果不知道或不使用異步操作和緩存機(jī)制,那么寫(xiě)出來(lái)的代碼很可能會(huì)是這樣:
cell.textLabel.text = app.name;
cell.detailTextLabel.text = app.download;
NSData *imageData = [NSData dataWithContentsOfURL:app.url];
cell.imageView.image = [UIImage imageWithData:imageData];
這樣寫(xiě)有什么后果呢霹购?
后果1:不可避免的卡頓(因?yàn)闆](méi)有異步下載操作)
dataWithContentsOfURL:是耗時(shí)操作佑惠,將其放在主線程會(huì)造成卡頓。如果圖片很多,圖片很大膜楷,而且網(wǎng)絡(luò)情況不好的話(huà)肯定會(huì)卡出翔旭咽!
后果2:同一圖片重復(fù)下載,耗費(fèi)流量和系統(tǒng)開(kāi)銷(xiāo)(因?yàn)闆](méi)有建立緩存機(jī)制)
由于沒(méi)有緩存機(jī)制赌厅,即使下載完成并顯示了當(dāng)前cell的圖片穷绵,但是當(dāng)該cell再一次需要顯示的時(shí)候還是會(huì)下載它所對(duì)應(yīng)的圖片:耗費(fèi)了下載流量,而且還導(dǎo)致重復(fù)操作察蹲。
很顯然请垛,要達(dá)到Tableview滾動(dòng)的如絲滑般的享受必須二者兼得才可以催训,具體怎么做呢洽议?
3. 解決方案
1. 先看一下解決方案的流程圖
小碼哥將他的解決方案在PPT里用流程圖畫(huà)了出來(lái),筆者覺(jué)得很不錯(cuò)漫拭,但是顏值略低(畢竟人家是一心搞技術(shù)亚兄,沒(méi)時(shí)間在意這些外在的東西),筆者理了理思路采驻,自己重新畫(huà)了一張(好看么审胚?):
要想快速看懂此圖,需要先了解該流程所需的所有數(shù)據(jù)源:
1. 圖片的URL:因?yàn)槊繌垐D片對(duì)應(yīng)的URL都是唯一的礼旅,所以我們可以通過(guò)它來(lái)建立圖片緩存和下載操作的緩存的鍵膳叨,以及拼接沙盒緩存的路徑字符串。
2. 圖片緩存(字典):存放于內(nèi)存中痘系;鍵為圖片的URL菲嘴,值為UIImage對(duì)象。作用:讀取速度快汰翠,直接使用UIImage對(duì)象龄坪。
3. 下載操作緩存(字典):存放與內(nèi)存中,鍵為圖片的URL复唤,值為NSBlockOperation對(duì)象健田。作用:用來(lái)避免對(duì)于同一張圖片還要開(kāi)啟多個(gè)下載線程。
4. 沙盒緩存(文件路徑對(duì)應(yīng)NSData):存放于磁盤(pán)中佛纫,位于Cache文件夾內(nèi)妓局,路徑為“Cache/圖片URL的最后的部分”,值為NSData對(duì)象(將UIImage轉(zhuǎn)化為NSData才能寫(xiě)入磁盤(pán)里)呈宇。作用:程序斷網(wǎng)好爬,再次啟動(dòng)也可以直接在磁盤(pán)中拿到圖片。
2. 再看一下解決方案的代碼
2.1圖片緩存攒盈,下載操作緩存抵拘,沙盒緩存路徑
/**
* 存放所有下載完的圖片
*/
@property (nonatomic, strong) NSMutableDictionary *images;
/**
* 存放所有的下載操作(key是url,value是operation對(duì)象)
*/
@property (nonatomic, strong) NSMutableDictionary *operations;
/**
* 拼接Cache文件夾的路徑與url最后的部分型豁,合并成唯一約定好的緩存路徑
*/
#define CachedImageFile(url) [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:[url lastPathComponent]]
2.2 圖片下載之前的查詢(xún)緩存部分:
// 先從images緩存中取出圖片url對(duì)應(yīng)的UIImage
UIImage *image = self.images[app.icon];
if (image) {
// 存在:說(shuō)明圖片已經(jīng)下載成功僵蛛,并緩存成功)
cell.imageView.image = image;
} else {
// 不存在:說(shuō)明圖片并未下載成功過(guò)尚蝌,或者成功下載但是在images里緩存失敗,需要在沙盒里尋找對(duì)于的圖片
// 獲得url對(duì)于的沙盒緩存路徑
NSString *file = CachedImageFile(app.icon);
// 先從沙盒中取出圖片
NSData *data = [NSData dataWithContentsOfFile:file];
if (data) {
//data不為空充尉,說(shuō)明沙盒中存在這個(gè)文件
cell.imageView.image = [UIImage imageWithData:data];
} else {
// 反之沙盒中不存在這個(gè)文件
// 在下載之前顯示占位圖片
cell.imageView.image = [UIImage imageNamed:@"placeholder"];
// 下載圖片
[self download:app.icon indexPath:indexPath];
}
}
2.3 圖片的下載部分:
/**
* 下載圖片
*
* @param imageUrl 圖片的url
*/
- (void)download:(NSString *)imageUrl indexPath:(NSIndexPath *)indexPath
{
// 取出當(dāng)前圖片url對(duì)應(yīng)的下載操作(operation對(duì)象)
NSBlockOperation *operation = self.operations[imageUrl];
if (operation) return;
// 創(chuàng)建操作飘言,下載圖片
__weak typeof(self) appsVc = self;
operation = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:imageUrl];
NSData *data = [NSData dataWithContentsOfURL:url]; // 下載
UIImage *image = [UIImage imageWithData:data]; // NSData -> UIImage
// 回到主線程
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
if (image) {
// 如果存在圖片(下載完成),存放圖片到圖片緩存字典中
appsVc.images[imageUrl] = image;
//將圖片存入沙盒中
//1. 先將圖片轉(zhuǎn)化為NSData
NSData *data = UIImagePNGRepresentation(image);
//2. 再生成緩存路徑
[data writeToFile:CachedImageFile(imageUrl) atomically:YES];
}
// 從字典中移除下載操作 (保證下載失敗后驼侠,能重新下載)
[appsVc.operations removeObjectForKey:imageUrl];
// 刷新當(dāng)前表格姿鸿,減少系統(tǒng)開(kāi)銷(xiāo)
[appsVc.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}];
}];
// 添加下載操作到隊(duì)列中
[self.queue addOperation:operation];
// 將當(dāng)前下載操作添加到下載操作緩存中 (為了解決重復(fù)下載)
self.operations[imageUrl] = operation;
}
3. 有哪些點(diǎn)是值得注意的?
要說(shuō)值得注意的地方倒源,還是離不開(kāi)對(duì)于緩存內(nèi)容的添加和刪除操作苛预。
3.1 關(guān)于圖片緩存:
很簡(jiǎn)單,成功下載笋熬,拿到了圖片热某,就將圖片添加到圖片緩存中;下載失敗胳螟,什么都不做昔馋,反正沒(méi)有圖。在這種機(jī)制下糖耸,就沒(méi)有刪除緩存里某個(gè)圖片項(xiàng)的情況秘遏,因?yàn)閳D片緩存永遠(yuǎn)不會(huì)出現(xiàn)重復(fù)添加多個(gè)相同圖片的情況,緩存中只要有一張對(duì)應(yīng)的圖嘉竟,就直接拿去用了邦危,不會(huì)去再下載了。
3.2 關(guān)于沙盒緩存:
同樣地周拐,對(duì)于沙盒緩存也是一個(gè)道理:有圖就將其轉(zhuǎn)化為NSData铡俐,寫(xiě)入磁盤(pán),并對(duì)應(yīng)唯一的路徑妥粟,沒(méi)有圖就不寫(xiě)审丘。所以即使是要下載相同的圖片,因?yàn)楫?dāng)前url對(duì)應(yīng)的沙盒路徑已經(jīng)存在文件了勾给,所以直接拿就可以了滩报,不會(huì)再下載。
但是播急!
下載操作緩存是不同的脓钾!
3.3 關(guān)于下載操作緩存
我們需要在下載回調(diào)完成后,立即將當(dāng)前的下載操作從下載操作緩存中刪去桩警!
因?yàn)橐苊庀螺d失敗后可训,無(wú)法再次下載的情況的發(fā)生!
為什么呢?
注意一下將下載操作加入到下載操作緩存的時(shí)機(jī):
是在下載開(kāi)始的那一刻而不是下載成功的那一刻握截!
如果在下載開(kāi)始的那一刻加入到緩存中的話(huà)飞崖,這個(gè)緩存信息就包括兩個(gè)情況:下載成功和下載失敗:
如果未來(lái)下載成功了谨胞,那么我們就不會(huì)來(lái)到判斷當(dāng)前下載操作是否在下載操作緩存這一步固歪,在這之前直接就可以拿圖去用了,下載操作是否存在下載操作緩存里并沒(méi)有什么影響胯努。
但是牢裳!如果未來(lái)下載失敗了,那就肯定不會(huì)有對(duì)應(yīng)的圖片緩存和沙盒緩存叶沛,也就肯定會(huì)來(lái)到判斷當(dāng)前的下載操作是否在下載操作緩存里這一步蒲讯。不幸的是,因?yàn)闆](méi)有被刪去恬汁,它是存在的伶椿。存在的話(huà)就不做任何其他操作,放任自流,導(dǎo)致曾經(jīng)下載失敗的圖片永遠(yuǎn)不會(huì)再次下載。
忘了那段代碼了么塘娶?回看一下代碼(看我多好):
NSBlockOperation *operation = self.operations[imageUrl];
if (operation) return;//轉(zhuǎn)身就走记某,毫不留情
因此,無(wú)論下載成功或是失敗旱捧,在圖片下載的回調(diào)里都要將當(dāng)前的下載操作從下載操作隊(duì)列中移走:用來(lái)保證如果下載失敗了独郎,就可以重新開(kāi)啟對(duì)應(yīng)的下載操作進(jìn)行下載,邏輯上更加嚴(yán)謹(jǐn)枚赡。
4. 最后的話(huà)
異步+緩存這兩個(gè)機(jī)制雙劍合璧的話(huà)會(huì)對(duì)程序新能帶來(lái)很大的改觀氓癌。這應(yīng)該app開(kāi)發(fā)進(jìn)階的必經(jīng)之路。
小碼哥講述的這套流程還算比較完整的了贫橙,更重要的還是學(xué)習(xí)其中的思想:
- 將緩存分級(jí):內(nèi)存緩存贪婉,沙盒緩存,下載操作緩存卢肃。
- 而且還要經(jīng)常使用二分法疲迂,將我們的邏輯考慮得滴水不漏。
如果我們沒(méi)有認(rèn)識(shí)到將下載操作添加到下載操作緩存的時(shí)機(jī)是包含下載成功和下載失敗兩個(gè)情況莫湘,那么就不會(huì)考慮到即時(shí)要將下載操作從下載操作緩存中刪去的操作尤蒿,很容易引起bug。所以在以后的開(kāi)發(fā)中幅垮,成功和失敗兩個(gè)情況都要考慮進(jìn)去腰池,也就是說(shuō)有if一定要有else!
-------------------------------- 2018年7月16日更新 --------------------------------
注意注意!J竟演怎!
筆者在近期開(kāi)通了個(gè)人公眾號(hào),主要分享編程避乏,讀書(shū)筆記爷耀,思考類(lèi)的文章。
- 編程類(lèi)文章:包括筆者以前發(fā)布的精選技術(shù)文章拍皮,以及后續(xù)發(fā)布的技術(shù)文章(以原創(chuàng)為主)歹叮,并且逐漸脫離 iOS 的內(nèi)容,將側(cè)重點(diǎn)會(huì)轉(zhuǎn)移到提高編程能力的方向上铆帽。
- 讀書(shū)筆記類(lèi)文章:分享編程類(lèi)咆耿,思考類(lèi),心理類(lèi)爹橱,職場(chǎng)類(lèi)書(shū)籍的讀書(shū)筆記萨螺。
- 思考類(lèi)文章:分享筆者平時(shí)在技術(shù)上,生活上的思考愧驱。
因?yàn)楣娞?hào)每天發(fā)布的消息數(shù)有限制慰技,所以到目前為止還沒(méi)有將所有過(guò)去的精選文章都發(fā)布在公眾號(hào)上,后續(xù)會(huì)逐步發(fā)布的组砚。
而且因?yàn)楦鞔蟛┛推脚_(tái)的各種限制吻商,后面還會(huì)在公眾號(hào)上發(fā)布一些短小精干,以小見(jiàn)大的干貨文章哦~
掃下方的公眾號(hào)二維碼并點(diǎn)擊關(guān)注糟红,期待與您的共同成長(zhǎng)~