多執行緒非同步載入圖片async_pictures
非同步載入圖片
- 目標:在表格中非同步載入網路圖片
目的:
- 模擬
SDWebImage
基本功能實現 - 理解
SDWebImage
的底層實現機制 SDWebImage
是非常著名的網路圖片處理框架,目前國內超過90%
公司都在使用!
- 模擬
要求:
- 不要求能夠打出來
- 需要掌握思路
- 需要知道開發過程中,每一個細節是怎麼遞進的
- 需要知道每一個隱晦的問題是如何發現的
搭建介面&資料準備
程式碼
資料準備
@interface AppInfo : NSObject
/// App 名稱
@property (nonatomic, copy) NSString *name;
/// 圖示 URL
@property (nonatomic, copy) NSString *icon;
/// 下載數量
@property (nonatomic, copy) NSString *download;
+ (instancetype)appInfoWithDict:(NSDictionary *)dict;
/// 從 Plist 載入 AppInfo
+ (NSArray *)appList;
@end
+ (instancetype)appInfoWithDict:(NSDictionary *)dict {
id obj = [[self alloc] init];
[obj setValuesForKeysWithDictionary:dict];
return obj;
}
/// 從 Plist 載入 AppInfo
+ (NSArray *)appList {
NSURL *url = [[NSBundle mainBundle] URLForResource:@"apps.plist" withExtension:nil];
NSArray *array = [NSArray arrayWithContentsOfURL:url];
NSMutableArray *arrayM = [NSMutableArray arrayWithCapacity:array.count];
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
[arrayM addObject:[self appInfoWithDict:obj]];
}];
return arrayM.copy;
}
檢視控制器資料
/// 應用程式列表
@property (nonatomic, strong) NSArray *appList;
- 懶載入
- (NSArray *)appList {
if (_appList == nil) {
_appList = [AppInfo appList];
}
return _appList;
}
表格資料來源方法
#pragma mark - 資料來源方法
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.appList.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"AppCell"];
// 設定 Cell...
AppInfo *app = self.appList[indexPath.row];
cell.textLabel.text = app.name;
cell.detailTextLabel.text = app.download;
return cell;
}
知識點
- 資料模型應該負責所有資料準備工作,在需要時被呼叫
- 資料模型不需要關心被誰呼叫
- 陣列使用
[NSMutableArray arrayWithCapacity:array.count];
的效率更高- 使用塊程式碼遍歷的效率比 for 要快
@"AppCell"
格式定義的字串是儲存在常量區的- 在 OC 中,懶載入是無處不在的
- 設定
cell
內容時如果沒有指定影象,擇不會建立imageView
# 同步載入影象
- 設定
// 同步載入影象
// 1. 模擬延時
NSLog(@"正在下載 %@", app.name);
[NSThread sleepForTimeInterval:0.5];
// 2. 同步載入網路圖片
NSURL *url = [NSURL URLWithString:app.icon];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
cell.imageView.image = image;
注意:之前沒有設定
imageView
時,imageView
並不會被建立
存在的問題
- 如果網速慢,會卡爆了!影響使用者體驗
- 滾動表格,會重複下載影象,造成使用者經濟上的損失!
解決辦法
- 非同步下載影象
非同步下載影象
全域性操作佇列
/// 全域性佇列,統一管理所有下載操作
@property (nonatomic, strong) NSOperationQueue *downloadQueue;
- 懶載入
- (NSOperationQueue *)downloadQueue {
if (_downloadQueue == nil) {
_downloadQueue = [[NSOperationQueue alloc] init];
}
return _downloadQueue;
}
非同步下載
// 非同步載入影象
// 1. 定義下載操作
// 非同步載入影象
NSBlockOperation *downloadOp = [NSBlockOperation blockOperationWithBlock:^{
// 1. 模擬延時
NSLog(@"正在下載 %@", app.name);
[NSThread sleepForTimeInterval:0.5];
// 2. 非同步載入網路圖片
NSURL *url = [NSURL URLWithString:app.icon];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
// 3. 主執行緒更新 UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
cell.imageView.image = image;
}];
}];
// 2. 將下載操作新增到佇列
[self.downloadQueue addOperation:downloadOp];
執行測試
存在的問題
- 下載完成後不現實圖片
原因分析:
* 使用的是系統提供的 cell
* 非同步方法中只設置了影象,但是沒有設定 frame
* 影象載入後,一旦與 cell 互動,會呼叫 cell 的 layoutSubviews
方法,重新調整 cell 的佈局
解決辦法
- 使用佔位影象
- 自定義 Cell
注意演示不在主執行緒更新影象的效果
佔位影象
// 0. 佔位影象
UIImage *placeholder = [UIImage imageNamed:@"user_default"];
cell.imageView.image = placeholder;
問題
- 因為使用的是系統提供的 cell
- 每次和 cell 互動,
layoutSubviews
方法會根據影象的大小自動調整imageView
的尺寸
解決辦法
- 自定義 Cell
自定義 Cell
cell.nameLabel.text = app.name;
cell.downloadLabel.text = app.download;
// 非同步載入影象
// 0. 佔位影象
UIImage *placeholder = [UIImage imageNamed:@"user_default"];
cell.iconView.image = placeholder;
// 1. 定義下載操作
NSBlockOperation *downloadOp = [NSBlockOperation blockOperationWithBlock:^{
// 1. 模擬延時
NSLog(@"正在下載 %@", app.name);
[NSThread sleepForTimeInterval:0.5];
// 2. 非同步載入網路圖片
NSURL *url = [NSURL URLWithString:app.icon];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
// 3. 主執行緒更新 UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
cell.iconView.image = image;
}];
}];
// 2. 將下載操作新增到佇列
[self.downloadQueue addOperation:downloadOp];
問題
如果網路圖片下載速度不一致,同時使用者滾動圖片,可能會出現圖片顯示”錯行”的問題
修改延時程式碼,檢視錯誤
// 1. 模擬延時
if (indexPath.row > 9) {
[NSThread sleepForTimeInterval:3.0];
}
上下滾動一下表格即可看到 cell 複用的錯誤
解決辦法
- MVC
MVC
在模型中新增 image
屬性
#import <UIKit/UIKit.h>
/// 下載的影象
@property (nonatomic, strong) UIImage *image;
使用 MVC 更新表格影象
- 判斷模型中是否已經存在影象
if (app.image != nil) {
NSLog(@"載入模型影象...");
cell.iconView.image = app.image;
return cell;
}
- 下載完成後設定模型影象
// 3. 主執行緒更新 UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// 設定模型中的影象
app.image = image;
// 重新整理表格
[tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}];
問題
如果影象下載很慢,使用者滾動表格很快,會造成重複建立下載操作
修改延時程式碼
// 1. 模擬延時
if (indexPath.row == 0) {
[NSThread sleepForTimeInterval:10.0];
}
快速滾動表格,將第一行不斷“滾出/滾入”介面可以檢視操作被重複建立的問題
解決辦法
- 操作緩衝池
操作緩衝池
緩衝池的選擇
所謂緩衝池,其實就是一個容器,能夠存放多個物件
- 陣列:按照下標,可以通過
indexPath
可以判斷操作是否已經在進行中
- 無法解決上拉&下拉重新整理
- NSSet -> 無序的
- 無法定位到快取的操作
字典
:按照key
,可以通過下載影象的URL
(唯一定位網路資源的字串)
小結:選擇字典作為操作緩衝池
緩衝池屬性
/// 操作緩衝池
@property (nonatomic, strong) NSMutableDictionary *operationCache;
- 懶載入
- (NSMutableDictionary *)operationCache {
if (_operationCache == nil) {
_operationCache = [NSMutableDictionary dictionary];
}
return _operationCache;
}
修改程式碼
- 判斷下載操作是否被快取——正在下載
// 非同步載入影象
// 0. 佔位影象
UIImage *placeholder = [UIImage imageNamed:@"user_default"];
cell.iconView.image = placeholder;
// 判斷操作是否存在
if (self.operationCache[app.icon] != nil) {
NSLog(@"正在玩命下載中...");
return cell;
}
- 將操作新增到操作緩衝池
// 2. 將操作新增到操作緩衝池
[self.operationCache setObject:downloadOp forKey:app.icon];
// 3. 將下載操作新增到佇列
[self.downloadQueue addOperation:downloadOp];
修改佔位影象的程式碼位置,觀察會出現的問題
- 下載完成後,將操作從緩衝池中刪除
[self.operationCache removeObjectForKey:app.icon];
迴圈引用分析!
- 弱引用
self
的編寫方法:
__weak typeof(self) weakSelf = self;
- 利用
dealloc
輔助分析
- (void)dealloc {
NSLog(@"我去了");
}
- 注意
- 如果使用
self
,檢視控制器會在下載完成後被銷燬 - 而使用
weakSelf
,檢視控制器在第一時間被銷燬
- 如果使用
影象緩衝池
使用模型快取影象的問題
優點
- 不用重複下載,利用MVC重新整理表格,不會造成資料混亂
缺點
- 所有下載後的影象,都會記錄在模型中
- 如果模型資料本身很多(2000),單純影象就會佔用很大的記憶體空間
- 如果影象和模型繫結的很緊,不容易清理記憶體
解決辦法
- 使用影象快取池
影象快取
- 快取屬性
/// 影象緩衝池
@property (nonatomic, strong) NSMutableDictionary *imageCache;
- 懶載入
- (NSMutableDictionary *)imageCache {
if (_imageCache == nil) {
_imageCache = [[NSMutableDictionary alloc] init];
}
return _imageCache;
}
- 刪除模型中的
image
屬性 - 哪裡出錯改哪裡!
斷網測試
問題
image == nil
時會崩潰=>不能向字典中插入 nilimage == nil
時會重複重新整理表格,陷入死迴圈
解決辦法
- 修改主執行緒回撥程式碼
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
if (image != nil) {
// 設定模型中的影象
[weakSelf.imageCache setObject:image forKey:app.icon];
// 重新整理表格
[weakSelf.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
}];
程式碼重構
程式碼重構介紹
重構目的
- 相同的程式碼最好只出現一次
- 主次方法
- 主方法
- 只包含實現完整邏輯的子方法
- 思維清楚,便於閱讀
- 次方法
- 實現具體邏輯功能
- 測試通過後,後續幾乎不用維護
- 主方法
重構的步驟
- 新建一個方法
- 新建方法
- 把要抽取的程式碼,直接複製到新方法中
- 根據需求調整引數
- 調整舊程式碼
- 註釋原始碼,給自己一個後悔的機會
- 呼叫新方法
- 測試
- 優化程式碼
- 在原有位置,因為要照顧更多的邏輯,程式碼有可能是合理的
- 而抽取之後,因為程式碼少了,可以檢查是否能夠優化
- 分支巢狀多,不僅執行效能會差,而且不易於閱讀
- 測試
- 修改註釋
- 在開發中,註釋不是越多越好
- 如果忽視了註釋,有可能過一段時間,自己都看不懂那個註釋
- .m 關鍵的實現邏輯,或者複雜程式碼,需要添加註釋,否則,時間長了自己都看不懂!
- .h 中的所有屬性和方法,都需要有完整的註釋,因為 .h 檔案是給整個團隊看的
重構一定要小步走,要邊改變測試
重構後的程式碼
- (void)downloadImage:(NSIndexPath *)indexPath {
// 1. 根據 indexPath 獲取資料模型
AppInfo *app = self.appList[indexPath.row];
// 2. 判斷操作是否存在
if (self.operationCache[app.icon] != nil) {
NSLog(@"正在玩命下載中...");
return;
}
// 3. 定義下載操作
__weak typeof(self) weakSelf = self;
NSBlockOperation *downloadOp = [NSBlockOperation blockOperationWithBlock:^{
// 1. 模擬延時
NSLog(@"正在下載 %@", app.name);
if (indexPath.row == 0) {
[NSThread sleepForTimeInterval:3.0];
}
// 2. 非同步載入網路圖片
NSURL *url = [NSURL URLWithString:app.icon];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
// 3. 主執行緒更新 UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// 將下載操作從緩衝池中刪除
[weakSelf.operationCache removeObjectForKey:app.icon];
if (image != nil) {
// 設定模型中的影象
[weakSelf.imageCache setObject:image forKey:app.icon];
// 重新整理表格
[weakSelf.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
}];
}];
// 4. 將操作新增到操作緩衝池
[self.operationCache setObject:downloadOp forKey:app.icon];
// 5. 將下載操作新增到佇列
[self.downloadQueue addOperation:downloadOp];
}
記憶體警告
如果接收到記憶體警告,程式一定要做處理,日常上課時,不會特意處理。但是工作中的程式一定要處理,否則後果很嚴重!!!
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// 1. 取消下載操作
[self.downloadQueue cancelAllOperations];
// 2. 清空緩衝池
[self.operationCache removeAllObjects];
[self.imageCache removeAllObjects];
}
黑名單
如果網路正常,但是影象下載失敗後,為了避免再次都從網路上下載該影象,可以使用“黑名單”
- 黑名單屬性
@property (nonatomic, strong) NSMutableArray *blackList;
- 懶載入
- (NSMutableArray *)blackList {
if (_blackList == nil) {
_blackList = [NSMutableArray array];
}
return _blackList;
}
- 下載失敗記錄在黑名單中
if (image != nil) {
// 設定模型中的影象
[weakSelf.imageCache setObject:image forKey:app.icon];
// 重新整理表格
[weakSelf.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
} else {
// 下載失敗記錄在黑名單中
[weakSelf.blackList addObject:app.icon];
}
- 判斷黑名單
// 2.1 判斷黑名單
if ([self.blackList containsObject:app.icon]) {
NSLog(@"已經將 %@ 加入黑名單...", app.icon);
return;
}
沙盒快取實現
沙盒目錄介紹
- Documents
- 儲存由應用程式產生的檔案或者資料,例如:塗鴉程式生成的圖片,遊戲關卡記錄
- iCloud 會自動備份 Document 中的所有檔案
- 如果儲存了從網路下載的檔案,在上架審批的時候,會被拒!
tmp
- 臨時資料夾,儲存臨時檔案
- 儲存在 tmp 資料夾中的檔案,系統會自動回收,譬如磁碟空間緊張或者重新啟動手機
- 程式設計師不需要管 tmp 資料夾中的釋放
Caches
- 快取,儲存從網路下載的檔案,後續仍然需要繼續使用,例如:網路下載的離線資料,圖片,視訊…
- 快取目錄中的檔案系統不會自動刪除,可以做離線訪問!
- 要求程式必需提供一個完善的清除快取目錄的”解決方案”!
Preferences
- 系統偏好,使用者偏好
- 操作是通過
[NSUserDefaults standardDefaults]
來直接操作
iOS 不同版本間沙盒目錄的變化
- iOS 7.0及以前版本
bundle
目錄和沙盒目錄是在一起的 - iOS 8.0之後,
bundle
目錄和沙盒目錄是分開的
NSString+Path
#import "NSString+Path.h"
@implementation NSString (Path)
- (NSString *)appendDocumentPath {
NSString *dir = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
return [dir stringByAppendingPathComponent:self.lastPathComponent];
}
- (NSString *)appendCachePath {
NSString *dir = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject;
return [dir stringByAppendingPathComponent:self.lastPathComponent];
}
- (NSString *)appendTempPath {
return [NSTemporaryDirectory() stringByAppendingPathComponent:self.lastPathComponent];
}
@end
沙盒快取
- 將影象儲存至沙盒
if (data != nil) {
[data writeToFile:app.icon.appendCachePath atomically:true];
}
- 檢查沙盒快取
// 判斷沙盒檔案是否存在
UIImage *image = [UIImage imageWithContentsOfFile:app.icon.appendCachePath];
if (image != nil) {
NSLog(@"從沙盒載入影象 ... %@", app.name);
// 將影象新增至影象快取
[self.imageCache setObject:image forKey:app.icon];
cell.iconView.image = image;
return cell;
}
iOS6 的適配問題
面試題:iOS 6.0 的程式直接執行在 iOS 7.0 的系統中,通常會出現什麼問題
狀態列高度 20 個點是不包含在
view.frame
中的,self.view
的左上角原點的座標位置是從狀態列下方開始計算- iOS 6.0 程式直接在 iOS 7.0 的系統中執行最常見的問題,就是少了20個點
如果包含有
UINavigationController
,self.view
的左上角座標原點從狀態列下方開始計算- 因此,iOS 6.0的系統無法實現表格從導航條下方穿透的效果
如果包含有
UITabBarController
,self.view
的底部不包含 TabBar- 因此,iOS 6.0的系統無法實現表格從 TabBar 下方穿透效果
小結
程式碼實現回顧
- 從
tableView
資料來源方法入手 - 根據
indexPath
非同步載入網路圖片 - 使用
操作緩衝池
避免下載操作重複被建立 - 使用
影象緩衝池
實現記憶體快取
,同時能夠對記憶體警告做出響應 - 使用
沙盒快取
實現再次執行程式時,直接從沙盒載入影象,提高程式響應速度,節約使用者網路流量
遺留問題
- 程式碼耦合度太高,由於下載功能是與資料來源的
indexPath
繫結的,如果想將下載影象抽取到cell
中,難度很大!
SDWebImage初體驗
簡介
- iOS中著名的牛逼的網路圖片處理框架
- 包含的功能:圖片下載、圖片快取、下載進度監聽、gif處理等等
- 用法極其簡單,功能十分強大,大大提高了網路圖片的處理效率
- 國內超過90%的iOS專案都有它的影子
演示 SDWebImage
- 匯入框架
- 新增標頭檔案
#import "UIImageView+WebCache.h"
- 設定影象
[cell.iconView sd_setImageWithURL:[NSURL URLWithString:app.icon]];
思考:SDWebImage 是如何實現的?
- 將網路圖片的非同步載入功能封裝在
UIImageView
的分類中 - 與
UITableView
完全解耦
要實現這一目標,需要解決以下問題:
- 給
UIImageView
下載影象的功能 - 要解決表格滾動時,因為影象下載速度慢造成的圖片錯行問題,可以在給
UIImageView
設定新的URL
時,取消之前未完成的下載操作
目標鎖定:取消正在執行中的操作!