iOS圖片載入策略的簡單實現
今天和大家一起來討論如何進行iOS圖片載入策略的簡單實現,有疏忽的地方,還望各位不吝賜教。
一、不自量力的說明
對於iOS圖片載入策略的實現,相信大家和我一樣更多的還是藉助於第三方,我在此班門弄斧的意義是應付一些公司的面試,嘗試以一種簡單的方式去實現圖片載入策略,藉此也說明一些其他方面的知識。在此我將使用一個TableView的例子進行說明,採用的是MVC的設計模式。如果採用的是Swift編寫,還請給位大神自行轉換。
二、邏輯敘述
這個邏輯的流程是我隨手畫的,只做參考。
邏輯實現.png
三、實現過程 -- 以多圖下載為例
1、先上一個簡單粗暴的實現。
// cellForRowAtIndexPath:方法中的實現過程
// 1、設置cell的重用標識
static NSString *cellID = @"app";
// 2、創建cell
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID];
// 3、設置cell的數據 AppItem是模型
AppItem *item = self.apps[indexPath.row];
// 4、設置標題
cell.textLabel.text = item.name;
// 5、設置子標題
cell.detailTextLabel.text = item.download;
// 6、設置圖標
NSURL *url = [NSURL URLWithString:item.icon];
NSData *imageData = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:imageData];
cell.imageView.image = image;
NSLog(@"%zd----",indexPath.row);
2、以上的方式實現的問題
圖片重複下載,通過添加的列印可以很明顯的看出來,解決方式是把之前下載好的圖片保存起來,因為圖片和文字要對應,所以採用字典的方式進行存儲。
1、添加內存緩存解決圖片重複下載問題
// 針對圖片重複下載的問題
// 1、創建一個NSMutableDictionary屬性來保存圖片
/** 內存緩存 */
@property (nonatomic, strong) NSMutableDictionary *images;
// 2、懶載入實現
- (NSMutableDictionary *)images{
if (!_images) {
_images = [NSMutableDictionary dictionary];
}
return _images;
}
// 3、設置圖標改造
// 先去查看內存緩存中該圖片有沒有被下載過,如果有直接拿來用,如果沒有再下載
// 設置圖片,否則直接下載
UIImage *image = [self.images objectForKey:item.icon];
// 如果有值直接拿來用
if(image){
cell.imageView.image = image;
NSLog(@"使用了內存緩存中的圖片%zd----",indexPath.row);
}else{
NSURL *url = [NSURL URLWithString:item.icon];
NSData *imageData = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:imageData];
cell.imageView.image = image;
// 保存圖片到內存緩存 用圖片的URL作為圖片的key 保證key唯一
[self.images setObject:image forKey:item.icon];
NSLog(@"下載了圖片%zd----",indexPath.row);
}
2、添加內存緩存存在問題,需要用磁碟緩存(沙盒緩存)來補充
/*
* 兩種情況 圖片沒有下載和應用關閉都要考慮
* 只是添加內存緩存在程序退出的時候內存緩存會被釋放,接著優化
*/
/* 沙盒緩存的相關概念
document :會備份,蘋果官方不允許將緩存放到這裡,上架被拒絕
library:
preference:偏好設置,存放賬號密碼等數據
cache: 保存緩存文件,不會被備份
temp:臨時路徑(隨時有可能被刪除)
*/
// 先去查看內存緩存中該圖片有沒有被下載過,如果有直接拿來用,如果沒有就去檢查磁碟緩存,如果沒有磁碟緩存,就保存一份到內存,設置圖片,否則就直接下載
UIImage *image = [self.images objectForKey:item.icon];
// 如果有值直接拿來用
if(image){
cell.imageView.image = image;
NSLog(@"使用了內存緩存中的圖片----%zd",indexPath.row);
}else{
// 沙盒緩存路徑獲取(磁碟緩存)
NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
// 獲得圖片的名稱
NSString *imageName = [item.icon lastPathComponent];
// 拼接全路徑
NSString *fullPath = [cachePath stringByAppendingPathComponent:imageName];
// 檢查磁碟緩存
NSData *imageData = [NSData dataWithContentsOfFile:fullPath];
if (imageData) {
// 設置圖標
UIImage *image = [UIImage imageWithData:imageData];
cell.imageView.image = image;
NSLog(@"沙盒存儲----%zd",indexPath.row);
// 保存圖片到內存緩存
[self.images setObject:image forKey:item.icon];
}else{
NSURL *url = [NSURL URLWithString:item.icon];
NSData *imageData = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:imageData];
cell.imageView.image = image;
// 保存圖片到內存緩存 用圖片的URL作為圖片的key 保證key唯一
[self.images setObject:image forKey:item.icon];
// 保存圖片到沙盒緩存(磁碟緩存)
[imageData writeToFile:fullPath atomically:YES];
NSLog(@"下載了圖片----%zd",indexPath.row);
}
}
UI不流暢,因為下載圖片操作和刷新UI的操作都是在主線程中操作的,解決方法把下載操作放到子線程中進行操作。
/*
* 改造下載圖片的部分,下載圖片放到子線程中去做,刷新UI放在主線程中做。
*/
/** 並發隊列 使用NSOperation為了防止重複創建隊列,設置全局屬性*/
@property (nonatomic, strong) NSOperationQueue *queue;
// 並發隊列懶載入
- (NSOperationQueue *)queue{
if (!_queue) {
_queue = [[NSOperationQueue alloc] init];
// 設置最大並發數
_queue.maxConcurrentOperationCount = 5;
}
return _queue;
}
// 設置圖標的位置修改如下:
// 先去查看內存緩存中該圖片有沒有被下載過,如果有直接拿來用,如果沒有就去檢查磁碟緩存,如果沒有磁碟緩存,就保存一份到內存,設置圖片,否則就直接下載。
UIImage *image = [self.images objectForKey:item.icon];
// 如果有值直接拿來直接使用
if(image){
cell.imageView.image = image;
NSLog(@"使用了內存緩存中的圖片----%zd",indexPath.row);
}else{
// 沙盒緩存路徑獲取(磁碟緩存)
NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
// 獲得圖片的名稱
NSString *imageName = [item.icon lastPathComponent];
// 拼接全路徑
NSString *fullPath = [cachePath stringByAppendingPathComponent:imageName];
// 檢查磁碟緩存
NSData *imageData = [NSData dataWithContentsOfFile:fullPath];
if (imageData) {
// 設置圖標
UIImage *image = [UIImage imageWithData:imageData];
cell.imageView.image = image;
NSLog(@"沙盒存儲----%zd",indexPath.row);
// 保存圖片到內存緩存
[self.images setObject:image forKey:item.icon];
}else{
// 創建操作
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:item.icon];
NSData *imageData = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:imageData];
NSLog(@"下載------%@",[NSThread currentThread]);
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
cell.imageView.image = image;
NSLog(@"UI------%@",[NSThread currentThread]);
}];
// 保存圖片到內存緩存 用圖片的URL作為圖片的key 保證key唯一
[self.images setObject:image forKey:item.icon];
// 保存圖片到沙盒緩存(磁碟緩存)
[imageData writeToFile:fullPath atomically:YES];
NSLog(@"下載了圖片%zd----",indexPath.row);
}];
// 添加下載操作到並發隊列中
[self.queue addOperation:blockOperation];
}
}
3、進行了2以後產生的新問題
UI不會自動刷新,當我拖動的時候才會刷新頁面,解決方式要使用代碼手動刷新。因為下載操作是非同步的,會先把沒有圖標的cell返回,此時因為圖標的frame為0,所以之後即使圖片下載下來,frame變為其他額尺寸,frame還是會一直為0,所以不會顯示。如果我手動刷新,系統會從新走一遍cellForRowAtIndexPath:方法,到設置圖標這一步時,會直接把內存中存在的圖片(此時圖片是有frame的)直接設置到cell中會顯示。
/*
* 改造下載圖片的部分,下載圖片放到子線程中去做,刷新UI放在主線程中做。
*/
// 設置圖標的位置修改如下:
// 先去查看內存緩存中該圖片有沒有被下載過,如果有直接拿來用,如果沒有就去檢查磁碟緩存,如果沒有磁碟緩存,就保存一份到內存,設置圖片,否則就直接下載。
UIImage *image = [self.images objectForKey:item.icon];
// 如果有值直接拿來直接使用
if(image){
cell.imageView.image = image;
NSLog(@"使用了內存緩存中的圖片----%zd",indexPath.row);
}else{
// 沙盒緩存路徑獲取(磁碟緩存)
NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
// 獲得圖片的名稱
NSString *imageName = [item.icon lastPathComponent];
// 拼接全路徑
NSString *fullPath = [cachePath stringByAppendingPathComponent:imageName];
// 檢查磁碟緩存
NSData *imageData = [NSData dataWithContentsOfFile:fullPath];
if (imageData) {
// 設置圖標
UIImage *image = [UIImage imageWithData:imageData];
cell.imageView.image = image;
NSLog(@"沙盒存儲----%zd",indexPath.row);
// 保存圖片到內存緩存
[self.images setObject:image forKey:item.icon];
}else{
// 創建下載操作
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:item.icon];
NSData *imageData = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:imageData];
NSLog(@"下載------%@",[NSThread currentThread]);
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
cell.imageView.image = image;
NSLog(@"UI------%@",[NSThread currentThread]);
// 手動刷新 刷新UITableView指定的行
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationRight];
}];
// 保存圖片到內存緩存 用圖片的URL作為圖片的key 保證key唯一
[self.images setObject:image forKey:item.icon];
// 保存圖片到沙盒緩存(磁碟緩存)
[imageData writeToFile:fullPath atomically:YES];
NSLog(@"下載了圖片%zd----",indexPath.row);
}];
// 添加下載操作到並發隊列中
[self.queue addOperation:blockOperation];
}
}
由於拖動太快,導致的重複下載的問題。解決方式是定義一個操作緩存(字典),把之前的操作都保存起來,因為上面出現的原因本質就是因為blockOperation重複添加到queue中了。這個問題是這樣的,需要顯示的圖片會進行下載操作,但是一張圖片下載需要時間,如果圖片還沒下載下來,我就拖動了,將它移出屏幕,結果就是這張圖片沒有下載完,如果這時候我又拖動了,又要顯示這張圖片,因為上一次還沒下載完,所以內存和磁碟里都沒有,所以會重複下載。解決方式是定義一個操作緩存(字典),把之前的操作都保存起來,因為上面出現的原因本質就是因為blockOperation重複添加到queue中了。
/** 定義操作緩存屬性 */
@property (nonatomic, strong) NSMutableDictionary *operations;
// 操作緩存屬性懶載入實現
- (NSMutableDictionary *)operations{
if (!_operations) {
_operations = [NSMutableDictionary dictionary];
}
return _operations;
}
// 設置圖標的位置修改如下:
// 先去查看內存緩存中該圖片有沒有被下載過,如果有直接拿來用,如果沒有就去檢查磁碟緩存,如果沒有磁碟緩存,就保存一份到內存,設置圖片,否則就直接下載。
UIImage *image = [self.images objectForKey:item.icon];
// 如果有值直接拿來直接使用
if(image){
cell.imageView.image = image;
NSLog(@"使用了內存緩存中的圖片----%zd",indexPath.row);
}else{
// 沙盒緩存路徑獲取(磁碟緩存)
NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
// 獲得圖片的名稱
NSString *imageName = [item.icon lastPathComponent];
// 拼接全路徑
NSString *fullPath = [cachePath stringByAppendingPathComponent:imageName];
// 檢查磁碟緩存
NSData *imageData = [NSData dataWithContentsOfFile:fullPath];
if (imageData) {
// 設置圖標
UIImage *image = [UIImage imageWithData:imageData];
cell.imageView.image = image;
NSLog(@"沙盒存儲----%zd",indexPath.row);
// 保存圖片到內存緩存
[self.images setObject:image forKey:item.icon];
}else{
// 檢查圖片是否在操作緩存中進行下載,如果在下載就什麼也不做,如果不在就添加下載任務
NSBlockOperation *blockOperation = [self.operations objectForKey:item.icon];
if (blockOperation) {
}else{
// 創建下載操作
blockOperation = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:item.icon];
NSData *imageData = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:imageData];
NSLog(@"下載------%@",[NSThread currentThread]);
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
cell.imageView.image = image;
NSLog(@"UI------%@",[NSThread currentThread]);
// 手動刷新 刷新UITableView指定的行
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationRight];
}];
// 保存圖片到內存緩存 用圖片的URL作為圖片的key 保證key唯一
[self.images setObject:image forKey:item.icon];
// 保存圖片到沙盒緩存(磁碟緩存)
[imageData writeToFile:fullPath atomically:YES];
// 下載操作完成後進行移除操作
[self.operations removeObjectForKey:item.icon];
}];
// 添加下載操作到操作緩存中
[self.operations setObject:blockOperation forKey:item.icon];
// 添加下載操作到並發隊列中
[self.queue addOperation:blockOperation];
}
}
}
cell的重用機制導致的cell圖標展示數據錯亂的問題,解決方案,如果要進行下載圖片,先清空原來cell上的圖片,但是一般不會直接設置為nil,會採用占點陣圖片的方式來解決。
/** 定義操作緩存屬性 */
@property (nonatomic, strong) NSMutableDictionary *operations;
// 操作緩存屬性懶載入實現
- (NSMutableDictionary *)operations{
if (!_operations) {
_operations = [NSMutableDictionary dictionary];
}
return _operations;
}
// 設置圖標的位置修改如下:
// 先去查看內存緩存中該圖片有沒有被下載過,如果有直接拿來用,如果沒有就去檢查磁碟緩存,如果沒有磁碟緩存,就保存一份到內存,設置圖片,否則就直接下載。
UIImage *image = [self.images objectForKey:item.icon];
// 如果有值直接拿來直接使用
if(image){
cell.imageView.image = image;
NSLog(@"使用了內存緩存中的圖片----%zd",indexPath.row);
}else{
// 沙盒緩存路徑獲取(磁碟緩存)
NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
// 獲得圖片的名稱
NSString *imageName = [item.icon lastPathComponent];
// 拼接全路徑
NSString *fullPath = [cachePath stringByAppendingPathComponent:imageName];
// 檢查磁碟緩存
NSData *imageData = [NSData dataWithContentsOfFile:fullPath];
if (imageData) {
// 設置圖標
UIImage *image = [UIImage imageWithData:imageData];
cell.imageView.image = image;
NSLog(@"沙盒存儲----%zd",indexPath.row);
// 保存圖片到內存緩存
[self.images setObject:image forKey:item.icon];
}else{
// 檢查圖片是否在操作緩存中進行下載,如果在下載就什麼也不做,如果不在就添加下載任務
NSBlockOperation *blockOperation = [self.operations objectForKey:item.icon];
if (blockOperation) {
}else{
// 防止cell重用導致的數據錯亂 先設置cell 的 image為空
cell.imageView.image = [UIImage imageNamed:@"placeHolder.png"];
// 創建下載操作
blockOperation = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:item.icon];
NSData *imageData = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:imageData];
NSLog(@"下載------%@",[NSThread currentThread]);
// 當url地址不正確 image為空 容錯處理
if (!image) {
// 為了下一次進來的時候再次嘗試進行圖片下載
[self.operations removeObjectForKey:item.icon];
return ;
}
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
cell.imageView.image = image;
NSLog(@"UI------%@",[NSThread currentThread]);
// 手動刷新 刷新UITableView指定的行
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationRight];
}];
// 保存圖片到內存緩存 用圖片的URL作為圖片的key 保證key唯一
[self.images setObject:image forKey:item.icon];
// 保存圖片到沙盒緩存(磁碟緩存)
[imageData writeToFile:fullPath atomically:YES];
// 下載操作完成後進行移除操作
[self.operations removeObjectForKey:item.icon];
}];
// 添加下載操作到操作緩存中
[self.operations setObject:blockOperation forKey:item.icon];
// 添加下載操作到並發隊列中
[self.queue addOperation:blockOperation];
}
}
}
內存問題:將圖片保存在內存中是很方便的事,圖片少的情況下肯定沒問題,但是圖片多了就會內存警告,要做一下處理。
- (void)didReceiveMemoryWarning{
// 移除內存緩存,這裡不會影響界面顯示,因為有強引用的關係。
[self.images removeAllObjects];
// 移除隊列中所有操作
[self.queue cancelAllOperations];
}
寫在最後的話:關於iOS圖片載入策略的知識今天就分享到這裡,關於iOS圖片載入策略實現方面的問題歡迎大家和我交流,共同進步,謝謝各位。
TAG:Cocoa開發者社區 |