• 多线程异步加载图片async_pictures


    异步加载图片

    • 目标:在表格中异步加载网络图片
    • 目的:

      • 模拟 SDWebImage 基本功能实现
      • 理解 SDWebImage 的底层实现机制
      • SDWebImage 是非常著名的网络图片处理框架,目前国内超过 90% 公司都在使用!
    • 要求:

      • 不要求能够打出来
      • 需要掌握思路
      • 需要知道开发过程中,每一个细节是怎么递进的
      • 需要知道每一个隐晦的问题是如何发现的

    搭建界面&数据准备

    代码

    数据准备

    @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
        # 同步加载图像
    // 同步加载图像
    // 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. 滚动表格,会重复下载图像,造成用户经济上的损失!

    解决办法

    • 异步下载图像

    异步下载图像

    全局操作队列

    ///  全局队列,统一管理所有下载操作
    @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 的布局

    解决办法

    • 使用占位图像
    • 自定义 Cell

    注意演示不在主线程更新图像的效果

    占位图像

    // 0. 占位图像
    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

    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. 模拟延时
    if (indexPath.row == 0) {
        [NSThread sleepForTimeInterval:10.0];
    }

    快速滚动表格,将第一行不断“滚出/滚入”界面可以查看操作被重复创建的问题

    解决办法

    • 操作缓冲池

    操作缓冲池

    缓冲池的选择

    所谓缓冲池,其实就是一个容器,能够存放多个对象

    • 数组:按照下标,可以通过 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,视图控制器在第一时间被销毁

    图像缓冲池

    使用模型缓存图像的问题

    优点

    • 不用重复下载,利用MVC刷新表格,不会造成数据混乱

    缺点

    • 所有下载后的图像,都会记录在模型中
    • 如果模型数据本身很多(2000),单纯图像就会占用很大的内存空间
    • 如果图像和模型绑定的很紧,不容易清理内存

    解决办法

    • 使用图像缓存池

    图像缓存

    • 缓存属性
    ///  图像缓冲池
    @property (nonatomic, strong) NSMutableDictionary *imageCache;
    • 懒加载
    - (NSMutableDictionary *)imageCache {
        if (_imageCache == nil) {
            _imageCache = [[NSMutableDictionary alloc] init];
        }
        return _imageCache;
    }
    • 删除模型中的 image 属性
    • 哪里出错改哪里!

    断网测试

    问题

    • image == nil 时会崩溃=>不能向字典中插入 nil
    • image == nil 时会重复刷新表格,陷入死循环

    解决办法

    • 修改主线程回调代码
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        if (image != nil) {
            // 设置模型中的图像
            [weakSelf.imageCache setObject:image forKey:app.icon];
            // 刷新表格
            [weakSelf.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
        }
    }];

    代码重构

    代码重构介绍

    重构目的

    • 相同的代码最好只出现一次
    • 主次方法
      • 主方法
        • 只包含实现完整逻辑的子方法
        • 思维清楚,便于阅读
      • 次方法
        • 实现具体逻辑功能
        • 测试通过后,后续几乎不用维护

    重构的步骤

    • 新建一个方法
      • 新建方法
      • 把要抽取的代码,直接复制到新方法中
      • 根据需求调整参数
    • 调整旧代码
      • 注释原代码,给自己一个后悔的机会
      • 调用新方法
    • 测试
    • 优化代码
      • 在原有位置,因为要照顾更多的逻辑,代码有可能是合理的
      • 而抽取之后,因为代码少了,可以检查是否能够优化
      • 分支嵌套多,不仅执行性能会差,而且不易于阅读
    • 测试
    • 修改注释
      • 在开发中,注释不是越多越好
      • 如果忽视了注释,有可能过一段时间,自己都看不懂那个注释
      • .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];
    }

    内存警告

    如果接收到内存警告,程序一定要做处理,日常上课时,不会特意处理。但是工作中的程序一定要处理,否则后果很严重!!!

    - (void)didReceiveMemoryWarning {
        [super didReceiveMemoryWarning];
    
        // 1. 取消下载操作
        [self.downloadQueue cancelAllOperations];
    
        // 2. 清空缓冲池
        [self.operationCache removeAllObjects];
        [self.imageCache removeAllObjects];
    }

    黑名单

    如果网络正常,但是图像下载失败后,为了避免再次都从网络上下载该图像,可以使用“黑名单”

    • 黑名单属性
    @property (nonatomic, strong) NSMutableArray *blackList;
    • 懒加载
    - (NSMutableArray *)blackList {
        if (_blackList == nil) {
            _blackList = [NSMutableArray array];
        }
        return _blackList;
    }
    • 下载失败记录在黑名单中
    if (image != nil) {
        // 设置模型中的图像
        [weakSelf.imageCache setObject:image forKey:app.icon];
        // 刷新表格
        [weakSelf.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
    } else {
        // 下载失败记录在黑名单中
        [weakSelf.blackList addObject:app.icon];
    }
    • 判断黑名单
    // 2.1 判断黑名单
    if ([self.blackList containsObject:app.icon]) {
        NSLog(@"已经将 %@ 加入黑名单...", app.icon);
        return;
    }

    沙盒缓存实现

    沙盒目录介绍

    • Documents
      • 保存由应用程序产生的文件或者数据,例如:涂鸦程序生成的图片,游戏关卡记录
      • iCloud 会自动备份 Document 中的所有文件
      • 如果保存了从网络下载的文件,在上架审批的时候,会被拒!
    • tmp

      • 临时文件夹,保存临时文件
      • 保存在 tmp 文件夹中的文件,系统会自动回收,譬如磁盘空间紧张或者重新启动手机
      • 程序员不需要管 tmp 文件夹中的释放
    • Caches

      • 缓存,保存从网络下载的文件,后续仍然需要继续使用,例如:网络下载的离线数据,图片,视频…
      • 缓存目录中的文件系统不会自动删除,可以做离线访问!
      • 要求程序必需提供一个完善的清除缓存目录的”解决方案”!
    • Preferences

      • 系统偏好,用户偏好
      • 操作是通过 [NSUserDefaults standardDefaults] 来直接操作

    iOS 不同版本间沙盒目录的变化

    • iOS 7.0及以前版本 bundle 目录和沙盒目录是在一起的
    • iOS 8.0之后,bundle 目录和沙盒目录是分开的

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

    iOS6 的适配问题

    面试题:iOS 6.0 的程序直接运行在 iOS 7.0 的系统中,通常会出现什么问题

    • 状态栏高度 20 个点是不包含在 view.frame 中的,self.view 的左上角原点的坐标位置是从状态栏下方开始计算

      • iOS 6.0 程序直接在 iOS 7.0 的系统中运行最常见的问题,就是少了20个点
    • 如果包含有 UINavigationControllerself.view的左上角坐标原点从状态栏下方开始计算

      • 因此,iOS 6.0的系统无法实现表格从导航条下方穿透的效果
    • 如果包含有 UITabBarControllerself.view的底部不包含 TabBar

      • 因此,iOS 6.0的系统无法实现表格从 TabBar 下方穿透效果

    小结

    代码实现回顾

    • tableView 数据源方法入手
    • 根据 indexPath 异步加载网络图片
    • 使用操作缓冲池避免下载操作重复被创建
    • 使用图像缓冲池实现内存缓存,同时能够对内存警告做出响应
    • 使用沙盒缓存实现再次运行程序时,直接从沙盒加载图像,提高程序响应速度,节约用户网络流量

    遗留问题

    • 代码耦合度太高,由于下载功能是与数据源的 indexPath 绑定的,如果想将下载图像抽取到 cell 中,难度很大!

    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 时,取消之前未完成的下载操作

    目标锁定:取消正在执行中的操作!

  • 相关阅读:
    git之clone
    gulp之sass 监听文件,自动编译
    cat命令
    centos安装yum源
    centos下wget: command not found的解决方法
    centos安装
    为什么很多公司招聘前端开发要求有 Linux / Unix 下的开发经验?
    ASP.NET MVC HtmlHelper用法集锦
    Html.Listbox的用法(实例)
    JSON入门实例
  • 原文地址:https://www.cnblogs.com/jiahao89/p/5118278.html
Copyright © 2020-2023  润新知