#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,而作者也提出了解決方案)
