前言
昨天公司有新人過來面試,統(tǒng)一被問到了這個多圖下載的相關(guān)問題,感覺有一個小伙子回答的挺完善,同時感覺有不少朋友在廣泛使用SDWebImage的同時忽略了內(nèi)部的實現(xiàn)細節(jié),所以感覺有必要再次說上一說,希望對大家有所幫助
在實現(xiàn)多圖下載時我們需要考慮的問題
在我們平時下載圖片的時候都是使用的SDWebImage這個非常牛X的框架,其實它內(nèi)部已經(jīng)幫我們做了很多的細節(jié)處理,才保證了我們的圖片下載以及顯示包括程序順暢運行,下面我們就來分析下自己實現(xiàn)完美的圖片下載會遇到什么問題呢
這里采用tabbleView每一個cell都需要下載顯示圖片為例來說明
1)首先我們需要考慮到圖片的緩存問題,不然每次用戶下載完的圖片離開當前頁面再回來就需要重復下載
- 圖片緩存第一步,就需要先弄一個內(nèi)存緩存,也就是自己創(chuàng)建一個數(shù)組,用來存放已經(jīng)下載下來的圖片
- 這樣每次用戶上拉、下拉或者從其他頁面回來再次顯示圖片的時候不至于重復下載
- 而且直接從內(nèi)存緩存里面取的話速度是非??斓?,這樣不會造成卡頓也不會給用戶帶來不好的體驗
- 圖片緩存第二步,一個完美的圖片緩存,不可能需要用戶退出程序再次進去后再去重新下載之前已經(jīng)下載過的圖片吧,所以我們需要給用戶已經(jīng)下載過的圖片保存到沙盒里面
- 這里啰嗦的普及一個沙盒文件夾知識,就知道我們需要把緩存的圖片放到哪一個文件夾下面了
1)Documents:
1-1 該目錄下面的數(shù)據(jù)在連接手機時會備份
1-2 蘋果官方不允許把下載的數(shù)據(jù)存放于該目錄下,否則審核直接被拒
2)Libriary:下面有2個子文件夾
2-1 caches:存放緩存文件
2-2 perference:該目錄用來存放偏好設(shè)置如登錄名密碼等等
3)Tmp
該文件夾下的文件會被隨時刪除,所以這個里面最好不要存放用戶的東西
*** 所以很明顯,我們需要把圖片存放于caches文件夾下
- 緊接著就是圖片如何存如何取的問題了,這里先不說圖片的下載,這是一個麻煩的過程,所以這里先說存取問題,也就是我們常說的二級緩存
- 每次圖片需要展示的時候,首先判斷圖片是否存在于當前的內(nèi)存緩存中,也就是我們創(chuàng)建的存放圖片的數(shù)組,有的話就拿出來顯示
- 如果數(shù)組里面沒有我們需要顯示的圖片,那么我們就去沙盒緩存里面去取
- 如果沙盒緩存有,從沙盒緩存里面取出顯示
- 然后將其存放于內(nèi)存緩存中,以便在程序不退出的時候用戶再次需要這張圖片的時候快速獲取并顯示
- 如果沙盒緩存也沒有的話,這個時候就真的需要去網(wǎng)絡(luò)進行下載,下載之后,不僅需要把圖片緩存到沙盒,還需要把圖片保存到內(nèi)存緩存中
2)OK,解決了圖片緩存之后,那么就需要進入最復雜的情況了,就是如何處理下載圖片的時候產(chǎn)生的各種奇葩的問題
- 奇葩問題列表
- 下載圖片實質(zhì)上屬于耗時操作,耗時操作我們是需要放在子線程中去處理的
- 我們需要弄一個字典去存放圖片的下載操作,字典的key就使用圖片的url,本質(zhì)上這個是唯一的,value自然就是圖片本身了
- 之所以要使用字典存放圖片的下載操作是因為,萬一用戶網(wǎng)速慢,這個時候圖片是不會馬上下載完成的,這個時候用戶萬一上下拖動頁面或者進入其他頁面再次回到這個頁面,我們是需要判斷之前的下載操作是否存在的,如果存在就不需要再去創(chuàng)建了,等待下載即可,如果不存在才需要創(chuàng)建的
- 還要考慮的就是存在下載操作但是下載失敗的情況,也就是image是空的,這個時候我們需要去移除下載操作,以便用戶上拉下拉的時候重新創(chuàng)建下載任務(wù)重新下載圖片,不然下載失敗操作還在,這樣永遠都無法重新下載了
- 下載創(chuàng)建回到主線程刷新UI的時候我們這里不能用tableView的reloadata方法,原因是你如果使用了這個方法,那么用戶在圖片還沒有下載完成的時候上下來回拖動的話,cell會循環(huán)利用到其他地方,這樣圖片如果在這個時候下載好,就會把圖片顯示到錯誤的位置,比如本來是第2行的圖片顯示到第5行去了
- 我們需要采用刷新指定行的方式,這樣無論你什么時間下載好圖片,你cell再循環(huán)利用,無論當前cell是否顯示在屏幕,我只要在下載好的時候去找到之前的indexPath去刷新,去給image賦值就行了,都會按照正確的位置去顯示
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationMiddle];
- 我們需要采用刷新指定行的方式,這樣無論你什么時間下載好圖片,你cell再循環(huán)利用,無論當前cell是否顯示在屏幕,我只要在下載好的時候去找到之前的indexPath去刷新,去給image賦值就行了,都會按照正確的位置去顯示
- 完善的下載還需要監(jiān)聽內(nèi)存警告,在收到內(nèi)存警告的時候移除內(nèi)存緩存,并將這個數(shù)組賦值nil,并且移除下載隊列中的的操作
核心代碼
- 首先創(chuàng)建一個存放已下載圖片的內(nèi)存緩存字典-- iconsDict
- 再創(chuàng)建一個存放已創(chuàng)建下載操作的操作字典 -- operationsDict
- 創(chuàng)建一個非主隊列,可供全局操作,目的是將下載操作添加到隊列中,讓下載可以異步執(zhí)行 -- downloadQueue
- iconsDict、operationsDict、downloadQueue需要多次被訪問到,需要使用到懶加載最好哦
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID ];
//這里我自己弄了一個模型,來給cell內(nèi)部子控件賦值,這里就給賦值操作簡單化了,沒有使用MVC
LBApp *model = self.appMarray[indexPath.row];
cell.textLabel.text = model.name;
NSString *downloadStr = [NSString stringWithFormat:@"已有%@下載",model.download];
cell.detailTextLabel.text =downloadStr;
// 先從內(nèi)存緩存中取出圖片
UIImage *image = [self.iconsDict objectForKey:model.icon];
if (image) {// 內(nèi)存中有圖片
cell.imageView.image = image;
}else {// 內(nèi)存中沒有圖片
// 獲得Caches文件夾
NSString *cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
// 獲得文件名
NSString *filename = [app.icon lastPathComponent];
// 獲取文件全路徑
NSString *file = [cachesPath stringByAppendingPathComponent:filename];
// 加載沙盒的文件數(shù)據(jù)
NSData *data = [NSData dataWithContentsOfFile:fullPath];
if (data) {// 直接利用沙盒中圖片
UIImage *image = [UIImage imageWithData:data];
cell.imageView.image = image;
// 存到字典中
[self.iconsDict setObject:image forKey:model.icon];
}else {// 沒有的話就下載圖片
NSBlockOperation *download = nil;
//顯示占位圖片
cell.imageView.image = [UIImage imageNamed:@"placeholder"];
download = self.operationsDict[model.icon];
if (download) {//如果存在下載操作,什么也不做等待下載
}else {//創(chuàng)建下載操作
download = [NSBlockOperation blockOperationWithBlock:^{
//加載圖片
NSURL *url = [NSURL URLWithString:model.icon];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
if (!image) {//下載失敗
//下載失敗移除下載操作,以便用戶上拉下拉的時候重新創(chuàng)建下載任務(wù)重新下載圖片
[self.operationsDict removeObjectForKey:model.icon];
return ;
}
//下載成功
//image存進內(nèi)存緩存,需要先存進內(nèi)存然后再去刷新ui,因為刷新ui會重新調(diào)用cellForRow方法,這個時候如果內(nèi)存有圖片,就會直接從內(nèi)存加載然后顯示上去
[self.iconsDict setObject:image forKey:model.icon];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{//回到主線程刷新UI
//刷新指定行
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationMiddle];
}];
//image數(shù)據(jù)寫入磁盤緩存
[data writeToFile:fullPath atomically:YES];
//下載完成的時候移除下載操作
[self.operationsDict removeObjectForKey:model.icon];
}];
//將下載操作添加到隊列中,讓下載可以異步執(zhí)行
[self.downloadQueue addOperation:download];
//將下載操作添加到操作字典中,防止沒有下載完成上下拖拽的時候重復創(chuàng)建下載操作
self.operationsDict[model.icon] = download;
}
}
}
return cell;
}
結(jié)束語
這里這個大致的思路就說完畢了,大家如果有興趣可以多去研究研究SDWebImage內(nèi)部的實現(xiàn),對大家的幫助會很大哦,當然存在不完善的地方希望大家可以共同討論學習哦,感謝您花時間閱讀,文字描述較多,但是這些理論性的東西難免如此,非常抱歉