本文通過模擬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;
}
知識點(diǎn)
- 數(shù)據(jù)模型應(yīng)該負(fù)責(zé)所有數(shù)據(jù)準(zhǔn)備工作,在需要時(shí)被調(diào)用
- 數(shù)據(jù)模型不需要關(guān)心被誰調(diào)用
- 數(shù)組使用
-
[NSMutableArray arrayWithCapacity:array.count];的效率更高 - 使用塊代碼遍歷的效率比 for 要快
-
-
@"AppCell"格式定義的字符串是保存在常量區(qū)的 - 在 OC 中,懶加載是無處不在的
- 設(shè)置
cell內(nèi)容時(shí)如果沒有指定圖像,則不會創(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;
注意:之前沒有設(shè)置
imageView時(shí),imageView并不會被創(chuàng)建
-
存在的問題
- 如果網(wǎng)速慢,會卡爆了!影響用戶體驗(yàn)
- 滾動表格,會重復(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)行測試,存在的問題--->下載完成后不顯示圖片
原因分析:
- 使用的是系統(tǒng)提供的 cell
- 異步方法中只設(shè)置了圖像,但是沒有設(shè)置 frame
- 圖像加載后,一旦與 cell 交互,會調(diào)用 cell 的
layoutSubviews方法,重新調(diào)整 cell 的布局
解決辦法--->使用占位圖像 or 自定義 Cell
注意演示不在主線程更新圖像的效果
4.占位圖像
// 占位圖像
UIImage *placeholder = [UIImage imageNamed:@"user_default"];
cell.imageView.image = placeholder;
- 問題
- 因?yàn)槭褂玫氖窍到y(tǒng)提供的 cell
- 每次和 cell 交互,
layoutSubviews方法會根據(jù)圖像的大小自動調(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ǎng)絡(luò)圖片下載速度不一致,同時(shí)用戶滾動圖片,可能會出現(xiàn)圖片顯示"錯行"的問題
修改延時(shí)代碼,查看錯誤
// 1. 模擬延時(shí)
if (indexPath.row > 9) {
[NSThread sleepForTimeInterval:3.0];
}
上下滾動一下表格即可看到 cell 復(fù)用的錯誤
解決辦法---> 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];
}];
-
問題
如果圖像下載很慢,用戶滾動表格很快,會造成重復(fù)創(chuàng)建下載操作
修改延時(shí)代碼
// 模擬延時(shí)
if (indexPath.row == 0) {
[NSThread sleepForTimeInterval:10.0];
}
快速滾動表格,將第一行不斷“滾出/滾入”界面可以查看操作被重復(fù)創(chuàng)建的問題
解決辦法 ---> 操作緩沖池
6.操作緩沖池
所謂緩沖池,其實(shí)就是一個容器,能夠存放多個對象
- 數(shù)組:按照下標(biāo),可以通過
indexPath可以判斷操作是否已經(jīng)在進(jìn)行中- 無法解決上拉&下拉刷新
- NSSet -> 無序的
- 無法定位到緩存的操作
-
字典:按照key,可以通過下載圖像的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];
修改占位圖像的代碼位置,觀察會出現(xiàn)的問題
- 下載完成后,將操作從緩沖池中刪除
[self.operationCache removeObjectForKey:app.icon];
循環(huán)引用分析!
- 弱引用
self的編寫方法:
__weak typeof(self) weakSelf = self;
- 利用
dealloc輔助分析
- (void)dealloc {
NSLog(@"我給你最后的疼愛是手放開");
}
- 注意
- 如果使用
self,視圖控制器會在下載完成后被銷毀 - 而使用
weakSelf,視圖控制器在第一時(shí)間被銷毀
- 如果使用
8.代碼重構(gòu)
重構(gòu)目的
- 相同的代碼最好只出現(xiàn)一次
- 主次方法
- 主方法
- 只包含實(shí)現(xiàn)完整邏輯的子方法
- 思維清楚,便于閱讀
- 次方法
- 實(shí)現(xiàn)具體邏輯功能
- 測試通過后,后續(xù)幾乎不用維護(hù)
- 主方法
重構(gòu)的步驟
- 1.新建一個方法
- 新建方法
- 把要抽取的代碼,直接復(fù)制到新方法中
- 根據(jù)需求調(diào)整參數(shù)
- 2.調(diào)整舊代碼
- 注釋原代碼,給自己一個后悔的機(jī)會
- 調(diào)用新方法
- 3.測試
- 4.優(yōu)化代碼
- 在原有位置,因?yàn)橐疹櫢嗟倪壿?,代碼有可能是合理的
- 而抽取之后,因?yàn)榇a少了,可以檢查是否能夠優(yōu)化
- 分支嵌套多,不僅執(zhí)行性能會差,而且不易于閱讀
- 5.測試
- 6.修改注釋
- 在開發(fā)中,注釋不是越多越好
- 如果忽視了注釋,有可能過一段時(shí)間,自己都看不懂那個注釋
- .m 關(guān)鍵的實(shí)現(xiàn)邏輯,或者復(fù)雜代碼,需要添加注釋,否則,時(shí)間長了自己都看不懂!
- .h 中的所有屬性和方法,都需要有完整的注釋,因?yàn)?.h 文件是給整個團(tuán)隊(duì)看的
重構(gòu)一定要小步走,要邊改變測試
重構(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)重?。?!
- (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 會自動備份 Document 中的所有文件
- 如果保存了從網(wǎng)絡(luò)下載的文件,在上架審批的時(shí)候,會被拒!
-
tmp
- 臨時(shí)文件夾,保存臨時(shí)文件
- 保存在 tmp 文件夾中的文件,系統(tǒng)會自動回收,譬如磁盤空間緊張或者重新啟動手機(jī)
- 程序員不需要管 tmp 文件夾中的釋放
-
Caches
- 緩存,保存從網(wǎng)絡(luò)下載的文件,后續(xù)仍然需要繼續(xù)使用,例如:網(wǎng)絡(luò)下載的緩存數(shù)據(jù),圖片
- Caches目錄下面的文件,當(dāng)手機(jī)存儲空間不足的時(shí)候,會自動刪除
- 要求程序必需提供一個完善的清除緩存目錄的"解決方案"!
-
Preferences
- 系統(tǒng)偏好,用戶偏好
- 操作是通過
[NSUserDefaults standardDefaults]來直接操作
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)
簡介
- iOS中著名的牛逼的網(wǎng)絡(luò)圖片處理框架
- 包含的功能:圖片下載、圖片緩存、下載進(jìn)度監(jiān)聽、gif處理等等
- 用法極其簡單,功能十分強(qiáng)大,大大提高了網(wǎng)絡(luò)圖片的處理效率
- 國內(nèi)超過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),需要解決以下問題:
- 給
UIImageView下載圖像的功能 - 要解決表格滾動時(shí),因?yàn)閳D像下載速度慢造成的圖片錯行問題,可以在給
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ò)流量
遺留問題
- 代碼耦合度太高,由于下載功能是與數(shù)據(jù)源的
indexPath綁定的,如果想將下載圖像抽取到cell中,難度很大!
二. 仿SDWebImage
- 目標(biāo):模擬
SDWebImage的實(shí)現(xiàn) - 說明:整體代碼與異步加載圖片基本一致,只是編寫順序會有變化!
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.測試下載操作
- (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)行測試!
緩存管理
- 定義圖像緩存屬性
/// 圖像緩存
@property (nonatomic, strong) NSMutableDictionary *imageCache;
- 懶加載
- (NSMutableDictionary *)imageCache {
if (_imageCache == nil) {
_imageCache = [NSMutableDictionary dictionary];
}
return _imageCache;
}
- 檢測圖像緩存方法準(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)行測試
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)對象
// MARK: - 運(yùn)行時(shí)關(guān)聯(lián)對象
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是蘋果提供的一個專門用來做緩存的類 - 使用和
NSMutableDictionary非常相似 - 是線程安全的
- 當(dāng)內(nèi)存
不足的時(shí)候,會自動清理緩存 - 程序開始時(shí),可以指定緩存的
數(shù)量&成本
方法
-
取值
- (id)objectForKey:(id)key;
-
設(shè)置對象,0成本
- (void)setObject:(id)obj forKey:(id)key;
-
設(shè)置對象并指定
成本- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g;
-
成本示例,以圖片為例:
- 方案一:緩存
100張圖片 - 方案二:總緩存成本設(shè)定為
10M,以圖片的寬 * 高當(dāng)作成本,圖像像素。這樣,無論緩存的多少張照片,只要像素值超過 10M,就會自動清理 - 結(jié)論:在緩存圖像時(shí),使用成本,比單純設(shè)置數(shù)量要科學(xué)!
- 方案一:緩存
-
刪除
- (void)removeObjectForKey:(id)key;
-
刪除全部
- (void)removeAllObjects;
屬性
-
@property NSUInteger totalCostLimit;- 緩存總成本
-
@property NSUInteger countLimit;- 緩存總數(shù)量
-
@property BOOL evictsObjectsWithDiscardedContent;- 是否自動清理緩存,默認(rèn)是
YES
- 是否自動清理緩存,默認(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)]);
}
}
// 代理方法,僅供觀察使用,開發(fā)時(shí)不建議重寫此方法
- (void)cache:(NSCache *)cache willEvictObject:(id)obj {
NSLog(@"remove -> %@", obj);
}
修改網(wǎng)絡(luò)圖片框架
- 修改圖像緩沖池類型,并移動到
.h中,以便后續(xù)測試
/// 圖像緩沖池
@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];測試緩存中的圖片變化
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
for (AppInfo *app in self.appList) {
NSLog(@"%@ %@", [[DownloadImageManager sharedManager].imageCache objectForKey:app.icon], app.name);
}
}
- 注冊通知,監(jiān)聽內(nèi)存警告
- (instancetype)init
{
self = [super init];
if (self) {
// 注冊通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(clearMemory) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
return self;
}
// 提示:雖然執(zhí)行不到,但是寫了也無所謂
- (void)dealloc {
// 刪除通知
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- 清理內(nèi)存
- (void)clearMemory {
NSLog(@"%s", __FUNCTION__);
// 取消所有下載操作
[self.downloadQueue cancelAllOperations];
// 刪除緩沖池
[self.operationChache removeAllObjects];
}
注意:內(nèi)存警告或者超出限制后,緩存中的任何對象,都有可能被清理。
四.一些你應(yīng)該知道的SDWebImage知識點(diǎn)
1> 圖片文件緩存的時(shí)間有多長:1周
_maxCacheAge = kDefaultCacheMaxCacheAge
2> SDWebImage 的內(nèi)存緩存是用什么實(shí)現(xiàn)的?
NSCache
3> SDWebImage 的最大并發(fā)數(shù)是多少?
maxConcurrentDownloads = 6
- 是程序固定死了,可以通過屬性進(jìn)行調(diào)整!
4> SDWebImage 支持動圖嗎?GIF
#import <ImageIO/ImageIO.h>
[UIImage animatedImageWithImages:images duration:duration];
5> SDWebImage是如何區(qū)分不同格式的圖像的
-
根據(jù)圖像數(shù)據(jù)第一個字節(jié)來判斷的!
- PNG:壓縮比沒有JPG高,但是無損壓縮,解壓縮性能高,蘋果推薦的圖像格式!
- JPG:壓縮比最高的一種圖片格式,有損壓縮!最多使用的場景,照相機(jī)!解壓縮的性能不好!
- GIF:序列楨動圖,特點(diǎn):只支持256種顏色!最流行的時(shí)候在1998~1999,有專利的!
6> SDWebImage 緩存圖片的名稱是怎么確定的!
-
md5- 如果單純使用 文件名保存,重名的幾率很高!
- 使用 MD5 的散列函數(shù)!對完整的 URL 進(jìn)行 md5,結(jié)果是一個 32 個字符長度的字符串!
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)入后臺通知- 執(zhí)行
backgroundCleanDisk方法,后臺清理磁盤! - 通過以上通知監(jiān)聽,能夠保證緩存文件的大小始終在控制范圍之內(nèi)!
-
clearDisk清空磁盤緩存,將所有緩存目錄中的文件,全部刪除!
實(shí)際工作,將緩存目錄直接刪除,再次創(chuàng)建一個同名空目錄!
- 執(zhí)行