#import "ViewController.h" #import "WZYApp.h" @interface ViewController () // 數據模型數組 @property (nonatomic, strong) NSArray *apps; // 保存操作對象的字典 @property (nonatomic, strong) NSMutableDictionary *operations; // 內存緩存 @property (nonatomic, strong) NSMutableDictionary *images; // 操作隊列(防止重復創建) @property (nonatomic, strong) NSOperationQueue *queue; @end @implementation ViewController // 存放操作 -(NSMutableDictionary *)operations { if (_operations == nil) { _operations = [NSMutableDictionary dictionary]; } return _operations; } -(NSOperationQueue *)queue { if (_queue ==nil) { _queue = [[NSOperationQueue alloc]init]; } return _queue; } -(NSMutableDictionary *)images { if (_images == nil) { _images = [NSMutableDictionary dictionary]; } return _images; } -(NSArray *)apps { if (_apps == nil) { // 加載plist文件 NSArray *array = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"apps.plist" ofType:nil]]; // 字典數組 -->模型數組 NSMutableArray *arrayM = [NSMutableArray array]; for (NSDictionary *dict in array) { [arrayM addObject:[WZYApp appWithDict:dict]]; } _apps = arrayM; } return _apps; } -(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.apps.count; } -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSLog(@"---%@", [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]); //01 創建cell static NSString *ID = @"app"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID]; //02 設置cell的數據 //2.1 得到該行cell對應的數據 WZYApp *appM = self.apps[indexPath.row]; //2.2 設置標題 cell.textLabel.text = appM.name; //2.3 設置子標題 cell.detailTextLabel.text = appM.download; //2.4 設置圖片 // 內存緩存(指一個字典屬性)思路 /* 001 當把圖片下載完成之後需要把該圖片保存到內存緩存 002 在需要顯示圖片的時候,先檢查本地的緩存中時候已經下載了該圖片 003 如果緩存中有該圖片,直接設置 004 如果緩存中沒有改圖片,此時需要去下載圖片 */ // 磁盤緩存(沙盒Caches下)思路 /* 001 當圖片下載完成之後除了保存到內存緩存中之外,還需要保存一份到磁盤緩存中 002 當圖片需要顯示的時候,先檢查內存緩存,如果內存緩存中有數據那麼就直接設置 003 如果內存緩存中沒有數據,那麼再去檢查磁盤緩存 004 如果有數據,那麼就直接拿來設置就可以 | 保存一份到內存緩存中 005 如果沒有數據,那麼這個時候再去下載數據 */ // 改善緩存結構(內存緩存--->二級緩存結構[內存緩存-沙盒緩存]) UIImage *image = [self.images objectForKey:appM.icon]; if (image) { // 內存緩存中有數據,就直接設置數據 cell.imageView.image = image; NSLog(@"第%zd行cell對應的圖片已經存在,直接使用內存緩存",indexPath.row); } else { // 內存緩存中沒有數據 // 獲得磁盤緩存路徑(三步) NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; NSString *fileName = [appM.icon lastPathComponent]; // 得到url中最後一個節點(從路徑中獲得完整的文件名,帶後綴) NSString *fullPath = [caches stringByAppendingPathComponent:fileName]; // 拼接沙盒緩存路徑(將上面兩個字符串拼接) // 檢查磁盤緩存 NSData *data = [NSData dataWithContentsOfFile:fullPath]; // data = nil; if (data) { // 磁盤緩存中有數據(模擬二次重啟程序,內存緩存清空了,但是磁盤緩存還在,所以先將數據展示,然後再保存一份數據到內存緩存中) //顯示圖片 UIImage *image = [UIImage imageWithData:data]; cell.imageView.image = image; // 保存一份到內存緩存中 [self.images setObject:image forKey:appM.icon]; NSLog(@"%zd行cell對應的圖片使用了磁盤緩存",indexPath.row); } else { // 磁盤緩存無數據(模擬首次進入程序,內存緩存和磁盤緩存都是空的。那麼就先下載數據,然後再顯示數據,接著保存一份數據到內存緩存,最後再保存一份數據到磁盤緩存) // 解決數據錯亂問題(由於cell的重用原則,會重用cell及其內部數據) // 解決方案001 cell.imageView.image = nil;(但這樣不好,如果網速很卡,用戶會認為沒有圖片存在 // 解決方案002 設置占位圖片,如下行代碼 cell.imageView.image = [UIImage imageNamed:@"Snip20160712_43"]; // 解決圖片重復下載問題(由於用戶可能會不停拖拽界面,當cell重復出現在視野中並且網速較慢的時候,第一次cell進入的時候就已經創建好操作進行下載圖片,但是此時cell若再次進入視野並且首次下載還未執行完,那麼就會進行二次重復下載。) // 解決方案:先檢查圖片的下載操作是否已經存在 // 若 存在 等待就行(攔截二次下載) // 若 不存在 封裝操作並且添加到隊列(進行首次下載) NSBlockOperation *download = [self.operations objectForKey:appM.icon]; if (download == nil) { // 如果操作不存在 //封裝操作 NSBlockOperation *download = [NSBlockOperation blockOperationWithBlock:^{ NSURL *url = [NSURL URLWithString:appM.icon]; for (NSInteger i = 0; i<1000000000; i++) { //模擬下載該圖片需要花費較長的時間|網絡不好的情況 } // 下載操作 NSData *data = [NSData dataWithContentsOfURL:url]; UIImage *image = [UIImage imageWithData:data]; if (image == nil) { // 解決image為空時存到內存緩存報錯問題(如果修改了數據,比如圖片的url修改了,url還在但圖片沒有了,此時如果再執行將image保存到內存緩存(也就是字典中),是會報錯的。因為nil不能往字典中存。) // 解決方案: // 要加一個判定if語句,如果數據不存在,就不要賦值,直接返回 // 解決網絡卡頓下載失敗情況下的再次下載問題 // 解決方案: // 將操作從緩存中移除(如果在下載的過程中網絡中斷,造成了下載失敗,下載操作已經創建,但是下載任務還沒有執行完畢。此時二次聯網,再次執行下載操作,就不會再繼續下載了。為什麼呢?因為防止圖片重復下載,操作已經創建之後就不會再次創建。那麼這個情況下就要在判定image==nil的if中清空操作。也就是如果image沒有成功設置,就清空下載操作,下次下載時再重新添加操作下載。) [self.operations removeObjectForKey:appM.icon]; return ; } // 把圖片保存到內存緩存中 [self.images setObject:image forKey:appM.icon]; NSLog(@"下載%zd行cell對應的圖片",indexPath.row); // 保存一份到磁盤緩存中 [data writeToFile:fullPath atomically:YES]; [[NSOperationQueue mainQueue] addOperationWithBlock:^{ // 解決圖片不顯示問題(拖動tableView才會顯示。為什麼呢?因為是異步執行,所以說沒有等待cell.imageView.image設置成功就返回cell了。) // 解決方案: // 手動刷新一下cell的當前行,這樣不用拖動也會顯示數據了。 //[tableView reloadData]; 刷新整個tableView,耗費內存資源,不推薦 [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; // 刷新當前行 }]; }]; // 把操作緩存起來(用一個字典去接收保存操作對象,避免重復創建消耗內存) [self.operations setObject:download forKey:appM.icon]; // 添加操作到隊列(執行操作中的內容) // 將下載操作保存到子線程中去執行,解決UI卡頓的問題。 [self.queue addOperation:download]; } else { // 如果操作不存在 //等著 NSLog(@"%zd行對應的圖片已經正在下載,請等待....",indexPath.row); } } } //03 返回cell return cell; }
以上操作我們完全沒有必要去寫,因為十分繁瑣,而且考慮到的情況還是有限的。我們可以用第三方框架SDWebImage幫我們實現下載圖片二級緩存的操作。該框架內部還處理了很多我們暫時考慮不到的bug。省去了大量繁瑣的工作。上面設置cell.imageView.image的操作共計100余行,我們可以用下面一行代碼搞定:
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:appM.icon] placeholderImage:[UIImage imageNamed:@"Snip20160712_43"]];
注意一點:
直接用SDWebImage去設置image的時候,如果是在tableView上面設置,那麼會因為imageView的尺寸沒有提前設置而產生一些問題,所以我們需要提前設置好cell.imageView的尺寸。這時就需要自定義cell了。(SDWebImage這個框架是服務很多地方的,並不只是tableView一種,所以說會出現這種bug,而作者也提出了解決方案)