大量小圖顯示指的也是多圖下載旗们。我們?cè)诤芏囗?xiàng)目中會(huì)遇到UITableView需要去顯示一些標(biāo)題廷痘、詳情蔓涧、圖片等內(nèi)容。我們需要去加載圖片顯示笋额,有時(shí)候我們會(huì)為書寫簡(jiǎn)單選擇這種思路元暴。
cell.textLabel.text = app.name;
cell.detailTextLabel.text = app.download;NSData *imageData = [NSData dataWithContentsOfURL:app.url];
cell.imageView.image = [UIImage imageWithData:imageData];
這樣寫有什么后果呢?
- 1.不可避免的卡頓(因?yàn)闆]有異步下載操作)
- 2.dataWithContentsOfURL:是耗時(shí)操作,將其放在主線程會(huì)造成卡頓兄猩。如果圖片很多茉盏、很大,并且網(wǎng)絡(luò)情況不好的話肯定會(huì)卡.
- 3.同一圖片重復(fù)下載枢冤,耗費(fèi)流量和系統(tǒng)開銷(因?yàn)闆]有建立緩存機(jī)制)由于沒有緩存機(jī)制鸠姨,即使下載完成并顯示了當(dāng)前cell的圖片,但是當(dāng)該cell再一次需要顯示的時(shí)候還是會(huì)下載它所對(duì)應(yīng)的圖片:耗費(fèi)了下載流量淹真,而且還導(dǎo)致重復(fù)操作讶迁。
怎么可以避免這些問(wèn)題呢?
1.解決方案流程圖
注:該流程圖所需要的數(shù)據(jù)源:
- 圖片的URL:因?yàn)槊繌垐D片對(duì)應(yīng)的URL都是唯一的核蘸,所以我們可以通過(guò)它來(lái)建立圖片緩存和下載操作的緩存的鍵巍糯,以及拼接沙盒緩存的路徑字符串。
- 圖片緩存(字典):存放于內(nèi)存中;鍵為圖片的URL客扎,值為UIImage對(duì)象祟峦。作用:讀取速度快,直接使用UIImage對(duì)象徙鱼。
- 下載操作緩存(字典):存放與內(nèi)存中宅楞,鍵為圖片的URL,值為NSBlockOperation對(duì)象。作用:用來(lái)避免對(duì)于同一張圖片還要開啟多個(gè)下載線程咱筛。
- 沙盒緩存(文件路徑對(duì)應(yīng)NSData):存放于磁盤中搓幌,位于Cache文件夾內(nèi)。值為NSData對(duì)象(將UIImage轉(zhuǎn)化為NSData才能寫入磁盤里)迅箩。作用:程序斷網(wǎng)溉愁,再次啟動(dòng)也可以直接在磁盤中拿到圖片。
異步+緩存管理圖片的相關(guān)實(shí)現(xiàn)代碼
//圖片緩存饲趋,下載操作緩存拐揭,沙盒緩存路徑
/**
* 存放所有下載完的圖片
*/@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]]
//圖片下載之前的查詢緩存部分:
//先從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)開銷
[appsVc.tableView reloadRowsAtIndexPaths:@ [indexPath] withRowAnimation:UITableViewRowAnimationNone];
}];
}]; // 添加下載操作到隊(duì)列中
[self.queue addOperation:operation]; // 將當(dāng)前下載操作添加到下載操作緩存中 (為了解決重復(fù)下載)
self.operations[imageUrl] = operation;
}
UITableView滑動(dòng)過(guò)程圖片的處理
關(guān)于UITableView我們知道固蚤,tableViewCell是有重用機(jī)制的娘汞,也就是說(shuō),內(nèi)存中只有當(dāng)前可見的cell數(shù)目的實(shí)例夕玩,滑動(dòng)的時(shí)候你弦,新顯示cell會(huì)重用被滑出的cell對(duì)象。這樣就存在一個(gè)問(wèn)題:
一般情況下在我們會(huì)在cellForRow方法里面設(shè)置cell的圖片數(shù)據(jù)源燎孟,也就是說(shuō)如果一個(gè)cell的imageview對(duì)象開啟了一個(gè)下載任務(wù)禽作,這個(gè)時(shí)候該cell對(duì)象發(fā)生了重用,新的image數(shù)據(jù)源會(huì)開啟另外的一個(gè)下載任務(wù)缤弦,由于他們關(guān)聯(lián)的imageview對(duì)象實(shí)際上是同一個(gè)cell實(shí)例的imageview對(duì)象领迈,就會(huì)發(fā)生2個(gè)下載任務(wù)回調(diào)給同一個(gè)imageview對(duì)象。這個(gè)時(shí)候就有必要做一些處理碍沐,避免回調(diào)發(fā)生時(shí)狸捅,錯(cuò)誤的image數(shù)據(jù)源刷新了UI。如第三方的SDWebImage提供的UIImageView擴(kuò)展的解決方案就很實(shí)用:imageView對(duì)象會(huì)關(guān)聯(lián)一個(gè)下載列表(列表是給AnimationImages用的累提,這個(gè)時(shí)候會(huì)下載多張圖片)尘喝,當(dāng)tableview滑動(dòng),imageView重設(shè)數(shù)據(jù)源(url)時(shí)斋陪,會(huì)cancel掉下載列表中所有的任務(wù)朽褪,然后開啟一個(gè)新的下載任務(wù)置吓。這樣子就保證了只有當(dāng)前可見的cell對(duì)象的imageView對(duì)象關(guān)聯(lián)的下載任務(wù)能夠回調(diào),不會(huì)發(fā)生image錯(cuò)亂缔赠。同時(shí)衍锚,SDWebImage管理了一個(gè)全局下載隊(duì)列(在DownloadManager中),并發(fā)量設(shè)置為6.也就是說(shuō)如果可見cell的數(shù)目是大于6的,就會(huì)有部分下載隊(duì)列處于等待狀態(tài)嗤堰。而且戴质,在添加下載任務(wù)到全局的下載隊(duì)列中去的時(shí)候,SDWebImage默認(rèn)是采取LIFO策略的踢匣,具體是在添加下載任務(wù)的時(shí)候告匠,將上次添加的下載任務(wù)添加依賴為新添加的下載任務(wù)。這樣可以優(yōu)化性能不至于使其出現(xiàn)卡頓的情況(滑動(dòng)時(shí)候不進(jìn)行加載)离唬。
[wself.downloadQueue addOperation:operation];
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
需要注意的部分
1.關(guān)于圖片緩存:
下載成功后专,拿到了圖片,就將圖片添加到圖片緩存中;下載失敗输莺,什么都不做(反正此時(shí)也沒有圖片)戚哎。在這種機(jī)制下,就沒有刪除緩存里某個(gè)圖片項(xiàng)的情況嫂用,因?yàn)閳D片緩存永遠(yuǎn)不會(huì)出現(xiàn)重復(fù)添加多個(gè)相同圖片的情況建瘫,緩存中只要有一張對(duì)應(yīng)的圖,就直接拿去用了尸折,不會(huì)再去加載。
2.關(guān)于沙盒緩存:
同樣地殷蛇,對(duì)于沙盒緩存也是一個(gè)道理:有圖就將其轉(zhuǎn)化為NSData实夹,寫入磁盤,并對(duì)應(yīng)唯一的路徑粒梦,沒有圖就不寫亮航。所以即使是要下載相同的圖片,因?yàn)楫?dāng)前url對(duì)應(yīng)的沙盒路徑已經(jīng)存在文件了匀们,所以直接拿就可以了缴淋,不會(huì)再下載。
3.關(guān)于下載操作緩存
我們需要在下載回調(diào)完成后泄朴,立即將當(dāng)前的下載操作從下載操作緩存中刪去(為了要避免下載失敗后重抖,無(wú)法再次下載的情況的發(fā)生)。由于將下載操作加入到下載操作緩存的時(shí)機(jī)是在下載開始的那一刻而不是下載成功的那一刻祖灰。如果在下載開始的那一刻加入到緩存中的話钟沛,這個(gè)緩存信息就包括兩個(gè)情況:下載成功和下載失敗:下載成功也就有相應(yīng)的圖片緩存和沙盒緩存局扶,假如下載失敗了恨统,那就肯定不會(huì)有對(duì)應(yīng)的圖片緩存和沙盒緩存叁扫,也就肯定會(huì)來(lái)到判斷當(dāng)前的下載操作是否在下載操作緩存里這一步。因?yàn)闆]有被刪去畜埋,它是存在的莫绣。導(dǎo)致曾經(jīng)下載失敗的圖片永遠(yuǎn)不會(huì)再次下載。
因此悠鞍,無(wú)論下載成功或是失敗对室,在圖片下載的回調(diào)里都要將當(dāng)前的下載操作從下載操作隊(duì)列中移走:用來(lái)保證如果下載失敗了,就可以重新開啟對(duì)應(yīng)的下載