SDWebImage框架底层讲解
一张图讲清楚二级缓存!本文通过模拟SDWebImage基本功能实现,从而帮助读者理解
SDWebImage
的底层实现机制
首先看一下官方的架构图:
SDWebImageSequenceDiagram.png SDWebImageClassDiagram.png一. 异步加载图片
1.搭建界面&数据准备
- 数据准备
@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
- 设置
2.同步加载图像
// 同步加载图像
// 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
并不会被创建
-
存在的问题
- 如果网速慢,会卡爆了!影响用户体验
- 滚动表格,会重复下载图像,造成用户经济上的损失!
解决办法--->异步下载图像
3.异步下载图像
- 全局操作队列
/// 全局队列,统一管理所有下载操作
@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 的布局
解决办法--->使用占位图像 or 自定义 Cell
注意演示不在主线程更新图像的效果
4.占位图像
// 占位图像
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
5.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];
}];
-
问题
-
如果图像下载很慢,用户滚动表格很快,会造成重复创建下载操作
-
修改延时代码
-
// 模拟延时
if (indexPath.row == 0) {
[NSThread sleepForTimeInterval:10.0];
}
快速滚动表格,将第一行不断“滚出/滚入”界面可以查看操作被重复创建的问题
解决办法 ---> 操作缓冲池
6.操作缓冲池
所谓缓冲池,其实就是一个容器,能够存放多个对象
- 数组:按照下标,可以通过
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
,视图控制器在第一时间被销毁
- 如果使用
8.代码重构
重构目的
- 相同的代码最好只出现一次
- 主次方法
- 主方法
- 只包含实现完整逻辑的子方法
- 思维清楚,便于阅读
- 次方法
- 实现具体逻辑功能
- 测试通过后,后续几乎不用维护
- 主方法
重构的步骤
- 1.新建一个方法
- 新建方法
- 把要抽取的代码,直接复制到新方法中
- 根据需求调整参数
- 2.调整旧代码
- 注释原代码,给自己一个后悔的机会
- 调用新方法
- 3.测试
- 4.优化代码
- 在原有位置,因为要照顾更多的逻辑,代码有可能是合理的
- 而抽取之后,因为代码少了,可以检查是否能够优化
- 分支嵌套多,不仅执行性能会差,而且不易于阅读
- 5.测试
- 6.修改注释
- 在开发中,注释不是越多越好
- 如果忽视了注释,有可能过一段时间,自己都看不懂那个注释
- .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];
}
9.内存警告
如果接收到内存警告,程序一定要做处理,否则后果很严重!!!
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// 1. 取消下载操作
[self.downloadQueue cancelAllOperations];
// 2. 清空缓冲池
[self.operationCache removeAllObjects];
[self.imageCache removeAllObjects];
}
10.沙盒缓存实现
沙盒目录介绍
-
Documents
- 保存由应用程序产生的文件或者数据,例如:涂鸦程序生成的图片,游戏关卡记录
- iCloud 会自动备份 Document 中的所有文件
- 如果保存了从网络下载的文件,在上架审批的时候,会被拒!
-
tmp
- 临时文件夹,保存临时文件
- 保存在 tmp 文件夹中的文件,系统会自动回收,譬如磁盘空间紧张或者重新启动手机
- 程序员不需要管 tmp 文件夹中的释放
-
Caches
- 缓存,保存从网络下载的文件,后续仍然需要继续使用,例如:网络下载的缓存数据,图片
- Caches目录下面的文件,当手机存储空间不足的时候,会自动删除
- 要求程序必需提供一个完善的清除缓存目录的"解决方案"!
-
Preferences
- 系统偏好,用户偏好
- 操作是通过
[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初体验
简介
- iOS中著名的牛逼的网络图片处理框架
- 包含的功能:图片下载、图片缓存、下载进度监听、gif处理等等
- 用法极其简单,功能十分强大,大大提高了网络图片的处理效率
- 国内超过90%的iOS项目都有它的影子
- 框架地址:https://github.com/rs/SDWebImage
演示 SDWebImage
- 导入框架
- 添加头文件
#import "UIImageView+WebCache.h"
- 设置图像
[cell.iconView sd_setImageWithURL:[NSURL URLWithString:app.icon]];
思考:SDWebImage 是如何实现的?
- 将网络图片的异步加载功能封装在
UIImageView
的分类中 - 与
UITableView
完全解耦
要实现这一目标,需要解决以下问题:
- 给
UIImageView
下载图像的功能 - 要解决表格滚动时,因为图像下载速度慢造成的图片错行问题,可以在给
UIImageView
设置新的URL
时,取消之前未完成的下载操作
目标锁定:取消正在执行中的操作!
12.小结
代码实现回顾
- 从
tableView
数据源方法入手 - 根据
indexPath
异步加载网络图片 - 使用
操作缓冲池
避免下载操作重复被创建 - 使用
图像缓冲池
实现内存缓存
,同时能够对内存警告做出响应 - 使用
沙盒缓存
实现再次运行程序时,直接从沙盒加载图像,提高程序响应速度,节约用户网络流量
遗留问题
- 代码耦合度太高,由于下载功能是与数据源的
indexPath
绑定的,如果想将下载图像抽取到cell
中,难度很大!
二. 仿SDWebImage
- 目标:模拟
SDWebImage
的实现 - 说明:整体代码与异步加载图片基本一致,只是编写顺序会有变化!
1.下载操作实现
#import "NSString+Path.h"
@interface DownloadImageOperation()
/// 要下载图像的 URL 字符串
@property (nonatomic, copy) NSString *URLString;
/// 完成回调 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. 获取二进制数据
NSData *data = [NSData dataWithContentsOfURL:url];
// 3. 保存至沙盒
if (data != nil) {
[data writeToFile:self.URLString.appendCachePath atomically:YES];
}
if (self.isCancelled) {
NSLog(@"下载操作被取消");
return;
}
// 4. 主线程回调
[[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];
}
// 记录当前操作
self.currentURLString = app.icon;
// 创建下载操作
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];
// 将操作添加到队列
[self.downloadQueue addOperation:op];
}
框架结构设计
3.下载管理器
- 单例实现
+ (instancetype)sharedManager {
static id instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
之所以设计成单例,是为了实现全局的图像下载管理
- 移植属性和懒加载代码
/// 下载队列
@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 下载完成回调
- (void)downloadImageOperationWithURLString:(NSString *)URLString finished:(void (^)(UIImage *image))finished;
- 方法实现
- (void)downloadImageOperationWithURLString:(NSString *)URLString finished:(void (^)(UIImage *))finished {
// 检查操作缓冲池
if (self.operationCache[URLString] != nil) {
NSLog(@"正在玩命下载中,稍安勿躁");
return;
}
// 创建下载操作
DownloadImageOperation *op = [DownloadImageOperation downloadImageOperationWithURLString:URLString finished:^(UIImage *image) {
// 从缓冲池删除操作
[self.operationCache removeObjectForKey:URLString];
// 执行回调
finished(image);
}];
// 将操作添加到缓冲池
[self.operationCache setObject:op forKey:URLString];
// 将操作添加到队列
[self.downloadQueue addOperation:op];
}
修改 ViewController
中的代码
- 删除相关属性和懒加载方法
- 用下载管理器接管之前的下载方法
// 创建下载操作
[[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];
}
运行测试!
缓存管理
- 定义图像缓存属性
/// 图像缓存
@property (nonatomic, strong) NSMutableDictionary *imageCache;
- 懒加载
- (NSMutableDictionary *)imageCache {
if (_imageCache == nil) {
_imageCache = [NSMutableDictionary dictionary];
}
return _imageCache;
}
- 检测图像缓存方法准备
/// 检查图像缓存
///
/// @return 是否存在图像缓存
- (BOOL)chechImageCache {
return NO;
}
- 方法调用
// 如果存在图像缓存,直接回调
if ([self chechImageCache]) {
finished(self.imageCache[URLString]);
return;
}
- 缓存方法实现
- (BOOL)chechImageCache:(NSString *)URLString {
// 1. 如果存在内存缓存,直接返回
if (self.imageCache[URLString]) {
NSLog(@"内存缓存");
return YES;
}
// 2. 如果存在磁盘缓存
UIImage *image = [UIImage imageWithContentsOfFile:URLString.appendCachePath];
if (image != nil) {
// 2.1 加载图像并设置内存缓存
NSLog(@"从沙盒缓存");
[self.imageCache setObject:image forKey:URLString];
// 2.2 返回
return YES;
}
return NO;
}
运行测试
4.自定义 UIImageView
-
目标:
- 利用下载管理器获取指定
URLString
的图像,完成后设置image
- 如果之前存在未完成的下载,判断是否与给定的
URLString
一致 - 如果一致,等待下载结束
- 如果不一致,取消之前的下载操作
- 利用下载管理器获取指定
-
定义方法
/// 设置指定 URL 字符串的网络图像
///
/// @param URLString 网络图像 URL 字符串
- (void)setImageWithURLString:(NSString *)URLString;
- 方法实现
@interface WebImageView()
/// 当前正在下载的 URL 字符串
@property (nonatomic, copy) NSString *currentURLString;
@end
@implementation WebImageView
- (void)setImageWithURLString:(NSString *)URLString {
// 取消之前的下载操作
if (![URLString isEqualToString:self.currentURLString]) {
// 取消之前操作
[[DownloadImageManager sharedManager] cancelDownloadWithURLString:self.currentURLString];
}
// 记录当前操作
self.currentURLString = URLString;
// 创建下载操作
__weak typeof(self) weakSelf = self;
[[DownloadImageManager sharedManager] downloadImageOperationWithURLString:URLString finished:^(UIImage *image) {
weakSelf.image = image;
}];
}
@end
- 修改
ViewController
中的调用代码
- (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];
}
- 运行时机制 —— 关联对象
// MARK: - 运行时关联对象
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
重用,取消之前下载操作的同时,清空 image
self.image = nil;
三.关于NSCache缓存
介绍
-
NSCache
是苹果提供的一个专门用来做缓存的类 - 使用和
NSMutableDictionary
非常相似 - 是线程安全的
- 当内存
不足
的时候,会自动清理缓存 - 程序开始时,可以指定缓存的
数量
&成本
方法
-
取值
- (id)objectForKey:(id)key;
-
设置对象,0成本
- (void)setObject:(id)obj forKey:(id)key;
-
设置对象并指定
成本
- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g;
-
成本示例,以图片为例:
- 方案一:缓存
100
张图片 - 方案二:总缓存成本设定为
10M
,以图片的宽 * 高
当作成本,图像像素
。这样,无论缓存的多少张照片,只要像素值超过 10M,就会自动清理 - 结论:在缓存图像时,使用成本,比单纯设置数量要科学!
- 方案一:缓存
-
删除
- (void)removeObjectForKey:(id)key;
-
删除全部
- (void)removeAllObjects;
属性
-
@property NSUInteger totalCostLimit;
- 缓存总成本
-
@property NSUInteger countLimit;
- 缓存总数量
-
@property BOOL evictsObjectsWithDiscardedContent;
- 是否自动清理缓存,默认是
YES
- 是否自动清理缓存,默认是
代码演练
- 定义缓存属性
@property (nonatomic, strong) NSCache *cache;
- 懒加载并设置限制
- (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)]);
}
}
// 代理方法,仅供观察使用,开发时不建议重写此方法
- (void)cache:(NSCache *)cache willEvictObject:(id)obj {
NSLog(@"remove -> %@", obj);
}
修改网络图片框架
- 修改图像缓冲池类型,并移动到
.h
中,以便后续测试
/// 图像缓冲池
@property (nonatomic, strong) NSCache *imageCache;
- 修改懒加载,并设置数量限制
- (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);
}
}
- 注册通知,监听内存警告
- (instancetype)init
{
self = [super init];
if (self) {
// 注册通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(clearMemory) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
return self;
}
// 提示:虽然执行不到,但是写了也无所谓
- (void)dealloc {
// 删除通知
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- 清理内存
- (void)clearMemory {
NSLog(@"%s", __FUNCTION__);
// 取消所有下载操作
[self.downloadQueue cancelAllOperations];
// 删除缓冲池
[self.operationChache removeAllObjects];
}
注意:内存警告或者超出限制后,缓存中的任何对象,都有可能被清理。
四.一些你应该知道的SDWebImage知识点
1> 图片文件缓存的时间有多长:1周
_maxCacheAge = kDefaultCacheMaxCacheAge
2> SDWebImage 的内存缓存是用什么实现的?
NSCache
3> SDWebImage 的最大并发数是多少?
maxConcurrentDownloads = 6
- 是程序固定死了,可以通过属性进行调整!
4> SDWebImage 支持动图吗?GIF
#import <ImageIO/ImageIO.h>
[UIImage animatedImageWithImages:images duration:duration];
5> SDWebImage是如何区分不同格式的图像的
-
根据图像数据第一个字节来判断的!
- PNG:压缩比没有JPG高,但是无损压缩,解压缩性能高,苹果推荐的图像格式!
- JPG:压缩比最高的一种图片格式,有损压缩!最多使用的场景,照相机!解压缩的性能不好!
- GIF:序列桢动图,特点:只支持256种颜色!最流行的时候在1998~1999,有专利的!
6> SDWebImage 缓存图片的名称是怎么确定的!
-
md5
- 如果单纯使用 文件名保存,重名的几率很高!
- 使用 MD5 的散列函数!对完整的 URL 进行 md5,结果是一个 32 个字符长度的字符串!
7> SDWebImage 的内存警告是如何处理的!
- 利用通知中心观察
-
- UIApplicationDidReceiveMemoryWarningNotification
接收到内存警告的通知- 执行
clearMemory
方法,清理内存缓存!
- 执行
-
- UIApplicationWillTerminateNotification
接收到应用程序将要终止通知- 执行
cleanDisk
方法,清理磁盘缓存!
- 执行
-
- UIApplicationDidEnterBackgroundNotification
接收到应用程序进入后台通知- 执行
backgroundCleanDisk
方法,后台清理磁盘! - 通过以上通知监听,能够保证缓存文件的大小始终在控制范围之内!
-
clearDisk
清空磁盘缓存,将所有缓存目录中的文件,全部删除!
实际工作,将缓存目录直接删除,再次创建一个同名空目录!
- 执行