• 2016-03-16 sdwebimage


    SDWebImage框架底层讲解

    一. 异步加载图片

    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 清空磁盘缓存,将所有缓存目录中的文件,全部删除!
        实际工作,将缓存目录直接删除,再次创建一个同名空目录!
  • 相关阅读:
    SpringMVC设置不拦截静态资源css,js
    关于Spring 国际化 No message found under code 的解决方案
    数据库中文乱码问题
    Maven下载清除jar包
    Swift学习笔记7:关闭
    bash构造tmux显示tmux ssh状态
    Mysql入门到精通数据表的操作
    MapReduce源代码分析MapTask分析
    世界目光聚焦美国:埃博拉病患者是否认真是可以治愈的?
    Android Studio如何引用jar包裹(不gradle)
  • 原文地址:https://www.cnblogs.com/gzz2016/p/5284331.html
Copyright © 2020-2023  润新知