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;
}

知识点

  1. 数据模型应该负责所有数据准备工作,在需要时被调用
  2. 数据模型不需要关心被谁调用
  3. 数组使用
    • [NSMutableArray arrayWithCapacity:array.count]; 的效率更高
    • 使用块代码遍历的效率比 for 要快
  4. @"AppCell" 格式定义的字符串是保存在常量区的
  5. 在 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 并不会被创建

  • 存在的问题
    1. 如果网速慢,会卡爆了!影响用户体验
    2. 滚动表格,会重复下载图像,造成用户经济上的损失!

解决办法--->异步下载图像

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;
  • 问题
    1. 因为使用的是系统提供的 cell
    2. 每次和 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. 如果网络图片下载速度不一致,同时用户滚动图片,可能会出现图片显示"错行"的问题

    2. 修改延时代码,查看错误

// 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];
}];
  • 问题

    1. 如果图像下载很慢,用户滚动表格很快,会造成重复创建下载操作

    2. 修改延时代码

// 模拟延时
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 清空磁盘缓存,将所有缓存目录中的文件,全部删除!
      实际工作,将缓存目录直接删除,再次创建一个同名空目录!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,066评论 4 62
  • 前不久做了一个生成快照的需求,其中用到 SDWebImage 来下载图片,在使用该框架的过程中也遇到了一些问题,索...
    ShannonChenCHN阅读 14,058评论 12 241
  • 数据结构整理篇。 概念: 查找表(Search Table)是同一类型的数据元素(或记录)的集合。 查找表分类静态...
    小学生的博客阅读 152评论 0 0
  • 毫无疑问,处理离婚,是一个人一生所面临的最困难的挑战。没有一个单一的策略能减轻离婚带来的痛苦和损失。既然选择了走这...
    8794e43eaf12阅读 154评论 0 0
  • 十一月末,十二月初。空气中迷漫了离别的气息。那些曾经流血不流泪的铮铮男儿,他们低头不语,第一次红了眼圈,湿了眼眶。...
    l七卡l阅读 508评论 1 6