本文通過(guò)模擬SDWebImage基本功能實(shí)現(xiàn),從而幫助讀者理解
SDWebImage
的底層實(shí)現(xiàn)機(jī)制
首先看一下官方的架構(gòu)圖:
一. 異步加載圖片
1.搭建界面&數(shù)據(jù)準(zhǔn)備
- 數(shù)據(jù)準(zhǔn)備
@interface AppInfo : NSObject
/// App 名稱
@property (nonatomic, copy) NSString *name;
/// 圖標(biāo) URL
@property (nonatomic, copy) NSString *icon;
/// 下載數(shù)量
@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;
}
- 視圖控制器數(shù)據(jù)
/// 應(yīng)用程序列表
@property (nonatomic, strong) NSArray *appList;
- 懶加載
- (NSArray *)appList {
if (_appList == nil) {
_appList = [AppInfo appList];
}
return _appList;
}
- 表格數(shù)據(jù)源方法
#pragma mark - 數(shù)據(jù)源方法
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.appList.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"AppCell"];
// 設(shè)置 Cell...
AppInfo *app = self.appList[indexPath.row];
cell.textLabel.text = app.name;
cell.detailTextLabel.text = app.download;
return cell;
}
知識(shí)點(diǎn)
- 數(shù)據(jù)模型應(yīng)該負(fù)責(zé)所有數(shù)據(jù)準(zhǔn)備工作直秆,在需要時(shí)被調(diào)用
- 數(shù)據(jù)模型不需要關(guān)心被誰(shuí)調(diào)用
- 數(shù)組使用
-
[NSMutableArray arrayWithCapacity:array.count];
的效率更高 - 使用塊代碼遍歷的效率比 for 要快
-
-
@"AppCell"
格式定義的字符串是保存在常量區(qū)的 - 在 OC 中咧叭,懶加載是無(wú)處不在的
- 設(shè)置
cell
內(nèi)容時(shí)如果沒(méi)有指定圖像,則不會(huì)創(chuàng)建imageView
- 設(shè)置
2.同步加載圖像
// 同步加載圖像
// 1. 模擬延時(shí)
NSLog(@"正在下載 %@", app.name);
[NSThread sleepForTimeInterval:0.5];
// 2. 同步加載網(wǎng)絡(luò)圖片
NSURL *url = [NSURL URLWithString:app.icon];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
cell.imageView.image = image;
注意:之前沒(méi)有設(shè)置
imageView
時(shí)减细,imageView
并不會(huì)被創(chuàng)建
-
存在的問(wèn)題
- 如果網(wǎng)速慢毛俏,會(huì)卡爆了砂豌!影響用戶體驗(yàn)
- 滾動(dòng)表格,會(huì)重復(fù)下載圖像轧苫,造成用戶經(jīng)濟(jì)上的損失楚堤!
解決辦法--->異步下載圖像
3.異步下載圖像
- 全局操作隊(duì)列
/// 全局隊(duì)列,統(tǒng)一管理所有下載操作
@property (nonatomic, strong) NSOperationQueue *downloadQueue;
- 懶加載
- (NSOperationQueue *)downloadQueue {
if (_downloadQueue == nil) {
_downloadQueue = [[NSOperationQueue alloc] init];
}
return _downloadQueue;
}
- 異步下載
// 異步加載圖像
// 1. 定義下載操作
// 異步加載圖像
NSBlockOperation *downloadOp = [NSBlockOperation blockOperationWithBlock:^{
// 1. 模擬延時(shí)
NSLog(@"正在下載 %@", app.name);
[NSThread sleepForTimeInterval:0.5];
// 2. 異步加載網(wǎng)絡(luò)圖片
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. 將下載操作添加到隊(duì)列
[self.downloadQueue addOperation:downloadOp];
運(yùn)行測(cè)試,存在的問(wèn)題--->下載完成后不顯示圖片
原因分析:
- 使用的是系統(tǒng)提供的 cell
- 異步方法中只設(shè)置了圖像浸剩,但是沒(méi)有設(shè)置 frame
- 圖像加載后钾军,一旦與 cell 交互,會(huì)調(diào)用 cell 的
layoutSubviews
方法绢要,重新調(diào)整 cell 的布局
解決辦法--->使用占位圖像 or 自定義 Cell
注意演示不在主線程更新圖像的效果
4.占位圖像
// 占位圖像
UIImage *placeholder = [UIImage imageNamed:@"user_default"];
cell.imageView.image = placeholder;
- 問(wèn)題
- 因?yàn)槭褂玫氖窍到y(tǒng)提供的 cell
- 每次和 cell 交互吏恭,
layoutSubviews
方法會(huì)根據(jù)圖像的大小自動(dòng)調(diào)整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. 模擬延時(shí)
NSLog(@"正在下載 %@", app.name);
[NSThread sleepForTimeInterval:0.5];
// 2. 異步加載網(wǎng)絡(luò)圖片
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. 將下載操作添加到隊(duì)列
[self.downloadQueue addOperation:downloadOp];
- 問(wèn)題
如果網(wǎng)絡(luò)圖片下載速度不一致,同時(shí)用戶滾動(dòng)圖片重罪,可能會(huì)出現(xiàn)圖片顯示"錯(cuò)行"的問(wèn)題
修改延時(shí)代碼樱哼,查看錯(cuò)誤
// 1. 模擬延時(shí)
if (indexPath.row > 9) {
[NSThread sleepForTimeInterval:3.0];
}
上下滾動(dòng)一下表格即可看到 cell 復(fù)用的錯(cuò)誤
解決辦法---> MVC
5.MVC
- 在模型中添加
image
屬性
#import <UIKit/UIKit.h>
/// 下載的圖像
@property (nonatomic, strong) UIImage *image;
使用 MVC 更新表格圖像
- 判斷模型中是否已經(jīng)存在圖像
if (app.image != nil) {
NSLog(@"加載模型圖像...");
cell.iconView.image = app.image;
return cell;
}
- 下載完成后設(shè)置模型圖像
// 3. 主線程更新 UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// 設(shè)置模型中的圖像
app.image = image;
// 刷新表格
[tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}];
-
問(wèn)題
如果圖像下載很慢,用戶滾動(dòng)表格很快剿配,會(huì)造成重復(fù)創(chuàng)建下載操作
修改延時(shí)代碼
// 模擬延時(shí)
if (indexPath.row == 0) {
[NSThread sleepForTimeInterval:10.0];
}
快速滾動(dòng)表格搅幅,將第一行不斷“滾出/滾入”界面可以查看操作被重復(fù)創(chuàng)建的問(wèn)題
解決辦法 ---> 操作緩沖池
6.操作緩沖池
所謂緩沖池,其實(shí)就是一個(gè)容器呼胚,能夠存放多個(gè)對(duì)象
- 數(shù)組:按照下標(biāo)茄唐,可以通過(guò)
indexPath
可以判斷操作是否已經(jīng)在進(jìn)行中- 無(wú)法解決上拉&下拉刷新
- NSSet -> 無(wú)序的
- 無(wú)法定位到緩存的操作
-
字典
:按照key
,可以通過(guò)下載圖像的URL
(唯一定位網(wǎng)絡(luò)資源的字符串)
小結(jié):選擇字典作為操作緩沖池
緩沖池屬性
/// 操作緩沖池
@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. 將下載操作添加到隊(duì)列
[self.downloadQueue addOperation:downloadOp];
修改占位圖像的代碼位置,觀察會(huì)出現(xiàn)的問(wèn)題
- 下載完成后沪编,將操作從緩沖池中刪除
[self.operationCache removeObjectForKey:app.icon];
循環(huán)引用分析呼盆!
- 弱引用
self
的編寫方法:
__weak typeof(self) weakSelf = self;
- 利用
dealloc
輔助分析
- (void)dealloc {
NSLog(@"我給你最后的疼愛(ài)是手放開(kāi)");
}
- 注意
- 如果使用
self
,視圖控制器會(huì)在下載完成后被銷毀 - 而使用
weakSelf
蚁廓,視圖控制器在第一時(shí)間被銷毀
- 如果使用
8.代碼重構(gòu)
重構(gòu)目的
- 相同的代碼最好只出現(xiàn)一次
- 主次方法
- 主方法
- 只包含實(shí)現(xiàn)完整邏輯的子方法
- 思維清楚访圃,便于閱讀
- 次方法
- 實(shí)現(xiàn)具體邏輯功能
- 測(cè)試通過(guò)后,后續(xù)幾乎不用維護(hù)
- 主方法
重構(gòu)的步驟
- 1.新建一個(gè)方法
- 新建方法
- 把要抽取的代碼相嵌,直接復(fù)制到新方法中
- 根據(jù)需求調(diào)整參數(shù)
- 2.調(diào)整舊代碼
- 注釋原代碼腿时,給自己一個(gè)后悔的機(jī)會(huì)
- 調(diào)用新方法
- 3.測(cè)試
- 4.優(yōu)化代碼
- 在原有位置,因?yàn)橐疹櫢嗟倪壿嫹贡觯a有可能是合理的
- 而抽取之后批糟,因?yàn)榇a少了,可以檢查是否能夠優(yōu)化
- 分支嵌套多捏雌,不僅執(zhí)行性能會(huì)差跃赚,而且不易于閱讀
- 5.測(cè)試
- 6.修改注釋
- 在開(kāi)發(fā)中,注釋不是越多越好
- 如果忽視了注釋性湿,有可能過(guò)一段時(shí)間纬傲,自己都看不懂那個(gè)注釋
- .m 關(guān)鍵的實(shí)現(xiàn)邏輯,或者復(fù)雜代碼肤频,需要添加注釋叹括,否則,時(shí)間長(zhǎng)了自己都看不懂宵荒!
- .h 中的所有屬性和方法汁雷,都需要有完整的注釋,因?yàn)?.h 文件是給整個(gè)團(tuán)隊(duì)看的
重構(gòu)一定要小步走报咳,要邊改變測(cè)試
重構(gòu)后的代碼
- (void)downloadImage:(NSIndexPath *)indexPath {
// 1. 根據(jù) indexPath 獲取數(shù)據(jù)模型
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. 模擬延時(shí)
NSLog(@"正在下載 %@", app.name);
if (indexPath.row == 0) {
[NSThread sleepForTimeInterval:3.0];
}
// 2. 異步加載網(wǎng)絡(luò)圖片
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) {
// 設(shè)置模型中的圖像
[weakSelf.imageCache setObject:image forKey:app.icon];
// 刷新表格
[weakSelf.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
}];
}];
// 4. 將操作添加到操作緩沖池
[self.operationCache setObject:downloadOp forKey:app.icon];
// 5. 將下載操作添加到隊(duì)列
[self.downloadQueue addOperation:downloadOp];
}
9.內(nèi)存警告
如果接收到內(nèi)存警告,程序一定要做處理暑刃,否則后果很嚴(yán)重!A锸取架谎!
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// 1. 取消下載操作
[self.downloadQueue cancelAllOperations];
// 2. 清空緩沖池
[self.operationCache removeAllObjects];
[self.imageCache removeAllObjects];
}
10.沙盒緩存實(shí)現(xiàn)
沙盒目錄介紹
-
Documents
- 保存由應(yīng)用程序產(chǎn)生的文件或者數(shù)據(jù)谷扣,例如:涂鴉程序生成的圖片,游戲關(guān)卡記錄
- iCloud 會(huì)自動(dòng)備份 Document 中的所有文件
- 如果保存了從網(wǎng)絡(luò)下載的文件裹匙,在上架審批的時(shí)候野哭,會(huì)被拒!
-
tmp
- 臨時(shí)文件夾幻件,保存臨時(shí)文件
- 保存在 tmp 文件夾中的文件,系統(tǒng)會(huì)自動(dòng)回收蛔溃,譬如磁盤空間緊張或者重新啟動(dòng)手機(jī)
- 程序員不需要管 tmp 文件夾中的釋放
-
Caches
- 緩存绰沥,保存從網(wǎng)絡(luò)下載的文件,后續(xù)仍然需要繼續(xù)使用贺待,例如:網(wǎng)絡(luò)下載的緩存數(shù)據(jù)徽曲,圖片
- Caches目錄下面的文件,當(dāng)手機(jī)存儲(chǔ)空間不足的時(shí)候,會(huì)自動(dòng)刪除
- 要求程序必需提供一個(gè)完善的清除緩存目錄的"解決方案"!
-
Preferences
- 系統(tǒng)偏好麸塞,用戶偏好
- 操作是通過(guò)
[NSUserDefaults standardDefaults]
來(lái)直接操作
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;
}
11.SDWebImage初體驗(yàn)
簡(jiǎn)介
- iOS中著名的牛逼的網(wǎng)絡(luò)圖片處理框架
- 包含的功能:圖片下載秃臣、圖片緩存、下載進(jìn)度監(jiān)聽(tīng)哪工、gif處理等等
- 用法極其簡(jiǎn)單奥此,功能十分強(qiáng)大,大大提高了網(wǎng)絡(luò)圖片的處理效率
- 國(guó)內(nèi)超過(guò)90%的iOS項(xiàng)目都有它的影子
- 框架地址:https://github.com/rs/SDWebImage
演示 SDWebImage
- 導(dǎo)入框架
- 添加頭文件
#import "UIImageView+WebCache.h"
- 設(shè)置圖像
[cell.iconView sd_setImageWithURL:[NSURL URLWithString:app.icon]];
思考:SDWebImage 是如何實(shí)現(xiàn)的雁比?
- 將網(wǎng)絡(luò)圖片的異步加載功能封裝在
UIImageView
的分類中 - 與
UITableView
完全解耦
要實(shí)現(xiàn)這一目標(biāo)稚虎,需要解決以下問(wèn)題:
- 給
UIImageView
下載圖像的功能 - 要解決表格滾動(dòng)時(shí),因?yàn)閳D像下載速度慢造成的圖片錯(cuò)行問(wèn)題偎捎,可以在給
UIImageView
設(shè)置新的URL
時(shí)蠢终,取消之前未完成的下載操作
目標(biāo)鎖定:取消正在執(zhí)行中的操作!
12.小結(jié)
代碼實(shí)現(xiàn)回顧
- 從
tableView
數(shù)據(jù)源方法入手 - 根據(jù)
indexPath
異步加載網(wǎng)絡(luò)圖片 - 使用
操作緩沖池
避免下載操作重復(fù)被創(chuàng)建 - 使用
圖像緩沖池
實(shí)現(xiàn)內(nèi)存緩存
茴她,同時(shí)能夠?qū)?nèi)存警告做出響應(yīng) - 使用
沙盒緩存
實(shí)現(xiàn)再次運(yùn)行程序時(shí)寻拂,直接從沙盒加載圖像祭钉,提高程序響應(yīng)速度,節(jié)約用戶網(wǎng)絡(luò)流量
遺留問(wèn)題
- 代碼耦合度太高遂铡,由于下載功能是與數(shù)據(jù)源的
indexPath
綁定的,如果想將下載圖像抽取到cell
中钾怔,難度很大愚臀!
二. 仿SDWebImage
- 目標(biāo):模擬
SDWebImage
的實(shí)現(xiàn) - 說(shuō)明:整體代碼與異步加載圖片基本一致,只是編寫順序會(huì)有變化舶斧!
1.下載操作實(shí)現(xiàn)
#import "NSString+Path.h"
@interface DownloadImageOperation()
/// 要下載圖像的 URL 字符串
@property (nonatomic, copy) NSString *URLString;
/// 完成回調(diào) Block
@property (nonatomic, copy) void (^finishedBlock)(UIImage *image);
@end
@implementation DownloadImageOperation
+ (instancetype)downloadImageOperationWithURLString:(NSString *)URLString finished:(void (^)(UIImage *))finished {
DownloadImageOperation *op = [[DownloadImageOperation alloc] init];
op.URLString = URLString;
op.finishedBlock = finished;
return op;
}
- (void)main {
@autoreleasepool {
// 1. NSURL
NSURL *url = [NSURL URLWithString:self.URLString];
// 2. 獲取二進(jìn)制數(shù)據(jù)
NSData *data = [NSData dataWithContentsOfURL:url];
// 3. 保存至沙盒
if (data != nil) {
[data writeToFile:self.URLString.appendCachePath atomically:YES];
}
if (self.isCancelled) {
NSLog(@"下載操作被取消");
return;
}
// 4. 主線程回調(diào)
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.finishedBlock([UIImage imageWithData:data]);
}];
}
}
2.測(cè)試下載操作
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
int seed = arc4random_uniform((UInt32)self.appList.count);
AppInfo *app = self.appList[seed];
// 取消之前的下載操作
if (![app.icon isEqualToString:self.currentURLString]) {
// 取消之前操作
[self.operationCache[self.currentURLString] cancel];
}
// 記錄當(dāng)前操作
self.currentURLString = app.icon;
// 創(chuàng)建下載操作
DownloadImageOperation *op = [DownloadImageOperation downloadImageOperationWithURLString:app.icon finished:^(UIImage *image) {
self.iconView.image = image;
// 從緩沖池刪除操作
[self.operationCache removeObjectForKey:app.icon];
}];
// 將操作添加到緩沖池
[self.operationCache setObject:op forKey:app.icon];
// 將操作添加到隊(duì)列
[self.downloadQueue addOperation:op];
}
框架結(jié)構(gòu)設(shè)計(jì)
3.下載管理器
- 單例實(shí)現(xiàn)
+ (instancetype)sharedManager {
static id instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
之所以設(shè)計(jì)成單例矾缓,是為了實(shí)現(xiàn)全局的圖像下載管理
- 移植屬性和懶加載代碼
/// 下載隊(duì)列
@property (nonatomic, strong) NSOperationQueue *downloadQueue;
/// 下載操作緩存
@property (nonatomic, strong) NSMutableDictionary *operationCache;
// MARK: - 懶加載
- (NSMutableDictionary *)operationCache {
if (_operationCache == nil) {
_operationCache = [NSMutableDictionary dictionary];
}
return _operationCache;
}
- (NSOperationQueue *)downloadQueue {
if (_downloadQueue == nil) {
_downloadQueue = [[NSOperationQueue alloc] init];
}
return _downloadQueue;
}
- 定義方法
/// 下載指定 URL 的圖像
///
/// @param URLString 圖像 URL 字符串
/// @param finished 下載完成回調(diào)
- (void)downloadImageOperationWithURLString:(NSString *)URLString finished:(void (^)(UIImage *image))finished;
- 方法實(shí)現(xiàn)
- (void)downloadImageOperationWithURLString:(NSString *)URLString finished:(void (^)(UIImage *))finished {
// 檢查操作緩沖池
if (self.operationCache[URLString] != nil) {
NSLog(@"正在玩命下載中,稍安勿躁");
return;
}
// 創(chuàng)建下載操作
DownloadImageOperation *op = [DownloadImageOperation downloadImageOperationWithURLString:URLString finished:^(UIImage *image) {
// 從緩沖池刪除操作
[self.operationCache removeObjectForKey:URLString];
// 執(zhí)行回調(diào)
finished(image);
}];
// 將操作添加到緩沖池
[self.operationCache setObject:op forKey:URLString];
// 將操作添加到隊(duì)列
[self.downloadQueue addOperation:op];
}
修改 ViewController
中的代碼
- 刪除相關(guān)屬性和懶加載方法
- 用下載管理器接管之前的下載方法
// 創(chuàng)建下載操作
[[DownloadImageManager sharedManager] downloadImageOperationWithURLString:self.currentURLString finished:^(UIImage *image) {
self.iconView.image = image;
}];
- 增加取消下載功能
/// 取消指定 URL 的下載操作
- (void)cancelDownloadWithURLString:(NSString *)URLString {
// 1. 從緩沖池中取出下載操作
DownloadImageOperation *op = self.operationCache[URLString];
if (op == nil) {
return;
}
// 2. 如果有取消
[op cancel];
// 3. 從緩沖池中刪除下載操作
[self.operationCache removeObjectForKey:URLString];
}
運(yùn)行測(cè)試!
緩存管理
- 定義圖像緩存屬性
/// 圖像緩存
@property (nonatomic, strong) NSMutableDictionary *imageCache;
- 懶加載
- (NSMutableDictionary *)imageCache {
if (_imageCache == nil) {
_imageCache = [NSMutableDictionary dictionary];
}
return _imageCache;
}
- 檢測(cè)圖像緩存方法準(zhǔn)備
/// 檢查圖像緩存
///
/// @return 是否存在圖像緩存
- (BOOL)chechImageCache {
return NO;
}
- 方法調(diào)用
// 如果存在圖像緩存,直接回調(diào)
if ([self chechImageCache]) {
finished(self.imageCache[URLString]);
return;
}
- 緩存方法實(shí)現(xiàn)
- (BOOL)chechImageCache:(NSString *)URLString {
// 1. 如果存在內(nèi)存緩存锯茄,直接返回
if (self.imageCache[URLString]) {
NSLog(@"內(nèi)存緩存");
return YES;
}
// 2. 如果存在磁盤緩存
UIImage *image = [UIImage imageWithContentsOfFile:URLString.appendCachePath];
if (image != nil) {
// 2.1 加載圖像并設(shè)置內(nèi)存緩存
NSLog(@"從沙盒緩存");
[self.imageCache setObject:image forKey:URLString];
// 2.2 返回
return YES;
}
return NO;
}
運(yùn)行測(cè)試
4.自定義 UIImageView
-
目標(biāo):
- 利用下載管理器獲取指定
URLString
的圖像,完成后設(shè)置image
- 如果之前存在未完成的下載喂急,判斷是否與給定的
URLString
一致 - 如果一致糕簿,等待下載結(jié)束
- 如果不一致懂诗,取消之前的下載操作
- 利用下載管理器獲取指定
定義方法
/// 設(shè)置指定 URL 字符串的網(wǎng)絡(luò)圖像
///
/// @param URLString 網(wǎng)絡(luò)圖像 URL 字符串
- (void)setImageWithURLString:(NSString *)URLString;
- 方法實(shí)現(xiàn)
@interface WebImageView()
/// 當(dāng)前正在下載的 URL 字符串
@property (nonatomic, copy) NSString *currentURLString;
@end
@implementation WebImageView
- (void)setImageWithURLString:(NSString *)URLString {
// 取消之前的下載操作
if (![URLString isEqualToString:self.currentURLString]) {
// 取消之前操作
[[DownloadImageManager sharedManager] cancelDownloadWithURLString:self.currentURLString];
}
// 記錄當(dāng)前操作
self.currentURLString = URLString;
// 創(chuàng)建下載操作
__weak typeof(self) weakSelf = self;
[[DownloadImageManager sharedManager] downloadImageOperationWithURLString:URLString finished:^(UIImage *image) {
weakSelf.image = image;
}];
}
@end
- 修改
ViewController
中的調(diào)用代碼
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
int seed = arc4random_uniform((UInt32)self.appList.count);
AppInfo *app = self.appList[seed];
[self.iconView setImageWithURLString:app.icon];
}
- 運(yùn)行時(shí)機(jī)制 —— 關(guān)聯(lián)對(duì)象
// MARK: - 運(yùn)行時(shí)關(guān)聯(lián)對(duì)象
const void *HMCurrentURLStringKey = "HMCurrentURLStringKey";
- (void)setCurrentURLString:(NSString *)currentURLString {
objc_setAssociatedObject(self, HMCurrentURLStringKey, currentURLString, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)currentURLString {
return objc_getAssociatedObject(self, HMCurrentURLStringKey);
}
- 為了防止
Cell
重用芋类,取消之前下載操作的同時(shí),清空 image
self.image = nil;
三.關(guān)于NSCache緩存
介紹
-
NSCache
是蘋果提供的一個(gè)專門用來(lái)做緩存的類 - 使用和
NSMutableDictionary
非常相似 - 是線程安全的
- 當(dāng)內(nèi)存
不足
的時(shí)候泡躯,會(huì)自動(dòng)清理緩存 - 程序開(kāi)始時(shí),可以指定緩存的
數(shù)量
&成本
方法
-
取值
- (id)objectForKey:(id)key;
-
設(shè)置對(duì)象写穴,0成本
- (void)setObject:(id)obj forKey:(id)key;
-
設(shè)置對(duì)象并指定
成本
- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g;
-
成本示例,以圖片為例:
- 方案一:緩存
100
張圖片 - 方案二:總緩存成本設(shè)定為
10M
馋没,以圖片的寬 * 高
當(dāng)作成本篷朵,圖像像素
。這樣腮猖,無(wú)論緩存的多少?gòu)堈掌抗唬灰袼刂党^(guò) 10M误堡,就會(huì)自動(dòng)清理 - 結(jié)論:在緩存圖像時(shí)锁施,使用成本,比單純?cè)O(shè)置數(shù)量要科學(xué)姥饰!
- 方案一:緩存
-
刪除
- (void)removeObjectForKey:(id)key;
-
刪除全部
- (void)removeAllObjects;
屬性
-
@property NSUInteger totalCostLimit;
- 緩存總成本
-
@property NSUInteger countLimit;
- 緩存總數(shù)量
-
@property BOOL evictsObjectsWithDiscardedContent;
- 是否自動(dòng)清理緩存列粪,默認(rèn)是
YES
- 是否自動(dòng)清理緩存列粪,默認(rèn)是
代碼演練
- 定義緩存屬性
@property (nonatomic, strong) NSCache *cache;
- 懶加載并設(shè)置限制
- (NSCache *)cache {
if (_cache == nil) {
_cache = [[NSCache alloc] init];
_cache.delegate = self;
_cache.countLimit = 10;
}
return _cache;
}
- 觸摸事件添加緩存
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
for (int i = 0; i < 20; ++i) {
NSString *str = [NSString stringWithFormat:@"%d", i];
NSLog(@"set -> %@", str);
[self.cache setObject:str forKey:@(i)];
NSLog(@"set -> %@ over", str);
}
// 遍歷緩存
NSLog(@"------");
for (int i = 0; i < 20; ++i) {
NSLog(@"%@", [self.cache objectForKey:@(i)]);
}
}
// 代理方法,僅供觀察使用费什,開(kāi)發(fā)時(shí)不建議重寫此方法
- (void)cache:(NSCache *)cache willEvictObject:(id)obj {
NSLog(@"remove -> %@", obj);
}
修改網(wǎng)絡(luò)圖片框架
- 修改圖像緩沖池類型,并移動(dòng)到
.h
中氯质,以便后續(xù)測(cè)試
/// 圖像緩沖池
@property (nonatomic, strong) NSCache *imageCache;
- 修改懶加載闻察,并設(shè)置數(shù)量限制
- (NSCache *)imageCache {
if (_imageCache == nil) {
_imageCache = [[NSCache alloc] init];
_imageCache.countLimit = 15;
}
return _imageCache;
}
修改其他幾處代碼,將
self.imageCache[URLString]
替換為[self.imageCache setObject:image forKey:URLString];
測(cè)試緩存中的圖片變化
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
for (AppInfo *app in self.appList) {
NSLog(@"%@ %@", [[DownloadImageManager sharedManager].imageCache objectForKey:app.icon], app.name);
}
}
- 注冊(cè)通知吴超,監(jiān)聽(tīng)內(nèi)存警告
- (instancetype)init
{
self = [super init];
if (self) {
// 注冊(cè)通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(clearMemory) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
return self;
}
// 提示:雖然執(zhí)行不到跋涣,但是寫了也無(wú)所謂
- (void)dealloc {
// 刪除通知
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- 清理內(nèi)存
- (void)clearMemory {
NSLog(@"%s", __FUNCTION__);
// 取消所有下載操作
[self.downloadQueue cancelAllOperations];
// 刪除緩沖池
[self.operationChache removeAllObjects];
}
注意:內(nèi)存警告或者超出限制后奖年,緩存中的任何對(duì)象陋守,都有可能被清理。
四.一些你應(yīng)該知道的SDWebImage知識(shí)點(diǎn)
1> 圖片文件緩存的時(shí)間有多長(zhǎng):1周
_maxCacheAge = kDefaultCacheMaxCacheAge
2> SDWebImage 的內(nèi)存緩存是用什么實(shí)現(xiàn)的中燥?
NSCache
3> SDWebImage 的最大并發(fā)數(shù)是多少疗涉?
maxConcurrentDownloads = 6
- 是程序固定死了峰尝,可以通過(guò)屬性進(jìn)行調(diào)整武学!
4> SDWebImage 支持動(dòng)圖嗎火窒?GIF
#import <ImageIO/ImageIO.h>
[UIImage animatedImageWithImages:images duration:duration];
5> SDWebImage是如何區(qū)分不同格式的圖像的
-
根據(jù)圖像數(shù)據(jù)第一個(gè)字節(jié)來(lái)判斷的!
- PNG:壓縮比沒(méi)有JPG高票编,但是無(wú)損壓縮卵渴,解壓縮性能高昔榴,蘋果推薦的圖像格式互订!
- JPG:壓縮比最高的一種圖片格式岩榆,有損壓縮勇边!最多使用的場(chǎng)景,照相機(jī)奕坟!解壓縮的性能不好!
- GIF:序列楨動(dòng)圖苛萎,特點(diǎn):只支持256種顏色腌歉!最流行的時(shí)候在1998~1999,有專利的馍驯!
6> SDWebImage 緩存圖片的名稱是怎么確定的!
-
md5
- 如果單純使用 文件名保存吟吝,重名的幾率很高剑逃!
- 使用 MD5 的散列函數(shù)蛹磺!對(duì)完整的 URL 進(jìn)行 md5裙品,結(jié)果是一個(gè) 32 個(gè)字符長(zhǎng)度的字符串!
7> SDWebImage 的內(nèi)存警告是如何處理的!
- 利用通知中心觀察
-
- UIApplicationDidReceiveMemoryWarningNotification
接收到內(nèi)存警告的通知- 執(zhí)行
clearMemory
方法驰弄,清理內(nèi)存緩存!
- 執(zhí)行
-
- UIApplicationWillTerminateNotification
接收到應(yīng)用程序?qū)⒁K止通知- 執(zhí)行
cleanDisk
方法岔擂,清理磁盤緩存忆某!
- 執(zhí)行
-
- UIApplicationDidEnterBackgroundNotification
接收到應(yīng)用程序進(jìn)入后臺(tái)通知- 執(zhí)行
backgroundCleanDisk
方法点待,后臺(tái)清理磁盤! - 通過(guò)以上通知監(jiān)聽(tīng)弃舒,能夠保證緩存文件的大小始終在控制范圍之內(nèi)癞埠!
-
clearDisk
清空磁盤緩存,將所有緩存目錄中的文件聋呢,全部刪除苗踪!
實(shí)際工作,將緩存目錄直接刪除削锰,再次創(chuàng)建一個(gè)同名空目錄颅夺!
- 執(zhí)行