做iOS開發也有半年多了,想想自己對一些第三方庫還只是停留在簡單運用的階段,感覺心慌慌的。於是決定用一個月的時間深入了解一些好的第三方庫。
第一個想到了SDWebImage,這個庫很不錯,幾乎每個iOS項目都會有它的影子,因為它很完美地解決了下載圖片並顯示的處理邏輯。那麼深究它之前,筆者准備先了解一下多圖下載的緩存機制,因為它和SDWebImage的方案類似。
有一個多圖緩存機制的教程是來自李明傑小碼哥的,筆者覺得講得挺不錯的,於是就花了2個小時好好學習了一下。
這裡所說的多圖下載,就是要在tableview的每一個cell裡顯示一張圖片,而且這些圖片都需要從網上下載。
如果不知道或不使用異步操作和緩存機制,那麼寫出來的代碼很可能會是這樣:
cell.textLabel.text = app.name; cell.detailTextLabel.text = app.download;NSData *imageData = [NSData dataWithContentsOfURL:app.url]; cell.imageView.image = [UIImage imageWithData:imageData];
這樣寫有什麼後果呢?
dataWithContentsOfURL:是耗時操作,將其放在主線程會造成卡頓。如果圖片很多,圖片很大,而且網絡情況不好的話肯定會卡出翔!
由於沒有緩存機制,即使下載完成並顯示了當前cell的圖片,但是當該cell再一次需要顯示的時候還是會下載它所對應的圖片:耗費了下載流量,而且還導致重復操作。
很顯然,要達到Tableview滾動的如絲滑般的享受必須二者兼得才可以,具體怎麼做呢?
小碼哥將他的解決方案在PPT裡用流程圖畫了出來,筆者覺得很不錯,但是顏值略低(畢竟人家是一心搞技術,沒時間在意這些外在的東西),筆者理了理思路,自己重新畫了一張(好看麼?):
要想快速看懂此圖,需要先了解該流程所需的所有數據源:
1. 圖片的URL:因為每張圖片對應的URL都是唯一的,所以我們可以通過它來建立圖片緩存和下載操作的緩存的鍵,以及拼接沙盒緩存的路徑字符串。
2. 圖片緩存(字典):存放於內存中;鍵為圖片的URL,值為UIImage對象。作用:讀取速度快,直接使用UIImage對象。
3. 下載操作緩存(字典):存放與內存中,鍵為圖片的URL,值為NSBlockOperation對象。作用:用來避免對於同一張圖片還要開啟多個下載線程。
4. 沙盒緩存(文件路徑對應NSData):存放於磁盤中,位於Cache文件夾內,路徑為“Cache/圖片URL的最後的部分”,值為NSData對象(將UIImage轉化為NSData才能寫入磁盤裡)。作用:程序斷網,再次啟動也可以直接在磁盤中拿到圖片。
2.1圖片緩存,下載操作緩存,沙盒緩存路徑
/** * 存放所有下載完的圖片 */@property (nonatomic, strong) NSMutableDictionary *images;/** * 存放所有的下載操作(key是url,value是operation對象) */@property (nonatomic, strong) NSMutableDictionary *operations;/** * 拼接Cache文件夾的路徑與url最後的部分,合並成唯一約定好的緩存路徑 */#define CachedImageFile(url) [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:[url lastPathComponent]]
2.2 圖片下載之前的查詢緩存部分:
// 先從images緩存中取出圖片url對應的UIImage UIImage *image = self.images[app.icon]; if (image) { // 存在:說明圖片已經下載成功,並緩存成功) cell.imageView.image = image; } else { // 不存在:說明圖片並未下載成功過,或者成功下載但是在images裡緩存失敗,需要在沙盒裡尋找對於的圖片 // 獲得url對於的沙盒緩存路徑 NSString *file = CachedImageFile(app.icon); // 先從沙盒中取出圖片 NSData *data = [NSData dataWithContentsOfFile:file]; if (data) { //data不為空,說明沙盒中存在這個文件 cell.imageView.image = [UIImage imageWithData:data]; } else { // 反之沙盒中不存在這個文件 // 在下載之前顯示占位圖片 cell.imageView.image = [UIImage imageNamed:@"placeholder"]; // 下載圖片 [self download:app.icon indexPath:indexPath]; } }
2.3 圖片的下載部分:
/** * 下載圖片 * * @param imageUrl 圖片的url */ - (void)download:(NSString *)imageUrl indexPath:(NSIndexPath *)indexPath { // 取出當前圖片url對應的下載操作(operation對象) NSBlockOperation *operation = self.operations[imageUrl]; if (operation) return; // 創建操作,下載圖片 __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. 先將圖片轉化為NSData NSData *data = UIImagePNGRepresentation(image); //2. 再生成緩存路徑 [data writeToFile:CachedImageFile(imageUrl) atomically:YES]; } // 從字典中移除下載操作 (保證下載失敗後,能重新下載) [appsVc.operations removeObjectForKey:imageUrl]; // 刷新當前表格,減少系統開銷 [appsVc.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; }]; }]; // 添加下載操作到隊列中 [self.queue addOperation:operation]; // 將當前下載操作添加到下載操作緩存中 (為了解決重復下載) self.operations[imageUrl] = operation; }
要說值得注意的地方,還是離不開對於緩存內容的添加和刪除操作。
3.1 關於圖片緩存:
很簡單,成功下載,拿到了圖片,就將圖片添加到圖片緩存中;下載失敗,什麼都不做,反正沒有圖。在這種機制下,就沒有刪除緩存裡某個圖片項的情況,因為圖片緩存永遠不會出現重復添加多個相同圖片的情況,緩存中只要有一張對應的圖,就直接拿去用了,不會去再下載了。
3.2 關於沙盒緩存:
同樣地,對於沙盒緩存也是一個道理:有圖就將其轉化為NSData,寫入磁盤,並對應唯一的路徑,沒有圖就不寫。所以即使是要下載相同的圖片,因為當前url對應的沙盒路徑已經存在文件了,所以直接拿就可以了,不會再下載。
但是!
下載操作緩存是不同的!
3.3 關於下載操作緩存
我們需要在下載回調完成後,立即將當前的下載操作從下載操作緩存中刪去!
因為要避免下載失敗後,無法再次下載的情況的發生!
為什麼呢?
注意一下將下載操作加入到下載操作緩存的時機:
是在下載開始的那一刻而不是下載成功的那一刻!
如果在下載開始的那一刻加入到緩存中的話,這個緩存信息就包括兩個情況:下載成功和下載失敗:
如果未來下載成功了,那麼我們就不會來到判斷當前下載操作是否在下載操作緩存這一步,在這之前直接就可以拿圖去用了,下載操作是否存在下載操作緩存裡並沒有什麼影響。
但是!如果未來下載失敗了,那就肯定不會有對應的圖片緩存和沙盒緩存,也就肯定會來到判斷當前的下載操作是否在下載操作緩存裡這一步。不幸的是,因為沒有被刪去,它是存在的。存在的話就不做任何其他操作,放任自流,導致曾經下載失敗的圖片永遠不會再次下載。
忘了那段代碼了麼?回看一下代碼(看我多好):
NSBlockOperation *operation = self.operations[imageUrl]; if (operation) return;//轉身就走,毫不留情
因此,無論下載成功或是失敗,在圖片下載的回調裡都要將當前的下載操作從下載操作隊列中移走:用來保證如果下載失敗了,就可以重新開啟對應的下載操作進行下載,邏輯上更加嚴謹。
異步+緩存這兩個機制雙劍合璧的話會對程序新能帶來很大的改觀。這應該app開發進階的必經之路。
小碼哥講述的這套流程還算比較完整的了,更重要的還是學習其中的思想:
將緩存分級:內存緩存,沙盒緩存,下載操作緩存。
而且還要經常使用二分法,將我們的邏輯考慮得滴水不漏。
如果我們沒有認識到將下載操作添加到下載操作緩存的時機是包含下載成功和下載失敗兩個情況,那麼就不會考慮到即時要將下載操作從下載操作緩存中刪去的操作,很容易引起bug。所以在以後的開發中,成功和失敗兩個情況都要考慮進去,也就是說有if一定要有else!