实现异步下载网络图片


1数据准备, 建立一个appInfo模型 ,在视图控制器中懒加载数组,实现字典转模型,加载模型数组


    NSMutableArray *data = [NSMutableArray array];
   

    NSMutableArray *list = [NSMutableArray arrayWithCapacity:data.count];


arrayWithCapacity: 容量,指定数组容量,在实例化数组的同时,准备好容量指定的空间,

假如是10,一次性在内存中准备好10个空间,再添加元素的时候就不会再次申请内存,如果增加第十一个元素,会再次开辟十个内存空间,

[NSMutableArray array] 每次添加一个元素临时申请内存空间

显然上面的性能比下面的要高很多


将可变数组变成不可变的—>有助于线程安全,外部不能修改

使用代码块遍历数组要比for快


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;


存在问题:


  1. 如果网速慢,会卡爆了!影响用户体验

  2. 滚动表格,会重复下载图像,造成用户经济上的损失,大量耗费流量

  3. 破法 —>异步加载图片

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交互,会调用layoutsubviews,重新调整布局    




破法 : 0 使用占位图像

       1 使用自定义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 

在模型中添加 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 (nonatomicstrongNSMutableDictionary *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];
    }

}]



重构后的代码

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


你可能感兴趣的:(实现异步下载网络图片)