tableView加载网络图片

tableView加载网络图片

  • 需求的效果图
列表加载网络图片效果图.png
  • 数据结构
数据结构.png

获取模型数组

准备模型

.h文件

@interface AppInfo : NSObject

/// app名称
@property (nonatomic,copy) NSString *name;
/// app图像
@property (nonatomic,copy) NSString *icon;
/// app下载量
@property (nonatomic,copy) NSString *download;

/// 字典转模型
+ (instancetype)appInfoWithDict:(NSDictionary *)dict;

@end

.m文件

+ (instancetype)appInfoWithDict:(NSDictionary *)dict
{
    AppInfo *appInfo = [[AppInfo alloc] init];

    // 利用kvc将字典转换成模型 : 取出字典中key,对应的value,赋值给模型对应的属性
    [appInfo setValuesForKeysWithDictionary:dict];

    return appInfo;
}
  • KVC字典转模型 : 取出字典中key对应的value,赋值给对应的模型属性.
  • 模型属性一定要跟字典的key一样.
  • 模型属性只能比字典的key多,不能少,否则在KVC赋值的时候会崩溃.

控制器中获取模型数据

定义数据源数组

@interface ViewController ()

/// 数据源数组
@property (nonatomic,strong) NSArray *dataSourceArr;

@end

懒加载数据源数组

- (NSArray *)dataSourceArr
{
    if (_dataSourceArr==nil) {

        // 获取plist文件的路径
        NSString *path = [[NSBundle mainBundle] pathForResource:@"apps.plist" ofType:nil];

        // 通过路径,获取到plist文件中的数组
        NSArray *rootArr = [NSArray arrayWithContentsOfFile:path];

        // 定义一个可变数组,向这个数组中添加模型
        NSMutableArray *tmpM = [NSMutableArray arrayWithCapacity:rootArr.count];

        // 遍历数组,取出数组中的字典
        [rootArr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            // 拿到字典之后,将字典转换成对应的模型
            AppInfo *appInfo = [AppInfo appInfoWithDict:obj];

            // 将模型添加到数据源数组中
            [tmpM addObject:appInfo];
        }];

        // 将可变数组,变成不可变 : 将线程不安全的类,变成了线程安全的类.同时,不可变的数组,外界不能修改的.
        _dataSourceArr = tmpM.copy;
    }

    return _dataSourceArr;
}
  • arrayWithCapacity方法实例化可变数组的效率高.
    • 因为在实例化可变数组的同时就指定了数组的容量.当在添加元素的时候,就不用再临时的申请内存空间.
    • 当容量满了以后,再添加元素时,会在再一次性开辟成倍的内存空间.
  • 使用块代码遍历的效率比 for循环 要快.

重构获取模型数组

  • 获取数据是数据模型的事情.数据模型是专门用来获取和处理数据的.
  • 数据模型不能光定义几个属性就不管了.而是要充分发挥其作用.
  • 模型内部封装返回模型数组的类方法.
  • 数据模型应该负责所有数据准备工作,并且在需要时被调用.

模型类声明获取模型数组的方法

  • 定义成类方法可以更加方便的供外界调用并获取到数据.
/// 获取模型数据
+ (NSArray *)appInfos;

获取模型数组的方法的实现

  • 返回已经存放了模型的模型数组
+ (NSArray *)appInfos
{
    // 获取plist文件的路径
    NSString *path = [[NSBundle mainBundle] pathForResource:@"apps.plist" ofType:nil];

    // 通过路径,获取到plist文件中的数组
    NSArray *rootArr = [NSArray arrayWithContentsOfFile:path];

    // 定义一个可变数组,向这个数组中添加模型
    NSMutableArray *modelArrM = [NSMutableArray arrayWithCapacity:rootArr.count];

    // 遍历数组,取出数组中的字典
    [rootArr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        // 拿到字典之后,将字典转换成对应的模型
        AppInfo *appInfo = [AppInfo appInfoWithDict:obj];

        // 将模型添加到数据源数组中
        [modelArrM addObject:appInfo];
    }];

    // 将可变数组,变成不可变 : 将线程不安全的类,变成了线程安全的类.同时,不可变的数组,外界不能修改的.
    return modelArrM.copy;
}

控制器中的懒加载

- (NSArray *)dataSourceArr
{
    if (_dataSourceArr==nil) {

        // 厨子,做饭去
        _dataSourceArr = [AppInfo appInfos];
    }

    return _dataSourceArr;
}

SB中加载原形cell

  • SB : storyboard

关联storyboard

数据源方法

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.dataSourceArr.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // 定义可重用的标示符
    static NSString *ID = @"AppCell";

    // 创建cell
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
    if (cell==nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:ID];
    }

    // 返回cell
    return cell;
}

SB中加载原形cell的分析和优化

  • 此处优化的内容是针对从SB中加载原形cell为例.

优化一 : 定义静态可重用标示符可省略.

  • Xcode3的遗留写法,现在已经不适用了.
  • 如果字符串常量的内容一样,那么他的内容地址也是一样的,这时候定义字符串常量是多余的.
  • 静态区的内存要等到程序退出了以后才能销毁的,将字符串常量保存在静态区,是对的内存的消耗
// 这个代码课注释掉
static NSString *ID = @"AppCell";

优化二 : SB中创建列表加载原形cell - 方案1

  • 如果正确的设置了可重用标示符,cell为空的判断操作是永远也不会执行的.
  • 如果没有正确的设置可重用标示符,cell为空的判断操作会执行,但是在SB中设置的cell样式会被代码创建的cell样式覆盖.
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
if (cell==nil) {
    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:ID];
}

SB中创建列表加载原形cell - 方案2 更优写法

  • 如果正确的设置了可重用标示符,下面的判断是永远也不会执行到得.
  • 如果没有正确的设置可重用标示符,会直接崩溃,并会提示崩溃原因.
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"AppCell" forIndexPath:indexPath];
if (cell==nil) {
    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:ID];
}

结论

  • 如果我们是从SB中加载的原形cell.无论采用哪种方案创建cell.只要可重用标示符设置正确,cell为空的判断就不会被执行.
  • 如果我们设置可重用标示符错误.要么cell样式不对.要么程序崩溃.这两种错误情况都在提示我们 : 要正确的设置cell的可重用标示符.

开发建议

  • 如果开发中是从SB中加载的原形cell.就直接采用以下方式创建cell即可.
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"AppCell" forIndexPath:indexPath];

同步下载图片

数据源方法

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // 创建cell
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"AppCell" forIndexPath:indexPath];

    // 获取对应cell的模型
    AppInfo *app = self.dataSourceArr[indexPath.row];

    // 给cell赋值
    cell.textLabel.text = app.name;
    cell.detailTextLabel.text = app.download;

    // 模拟网络延迟
    [NSThread sleepForTimeInterval:0.5];

    // 同步下载图片
    NSURL *url = [NSURL URLWithString:app.icon];
    NSData *data = [NSData dataWithContentsOfURL:url];
    UIImage *image = [UIImage imageWithData:data];

    // 给imageView赋值
    cell.imageView.image = image;

    // 返回cell
    return cell;
}

存在的问题

  • 如果网速慢,在滚动列表时会很卡.影响用户体验.

原因

  • 图片是同步下载的,图片不下载下来,无法返回cell.

解决办法

  • 异步下载图片.

异步下载图片

  • 准备全局并发队列
/// 全局并发队列
@property (nonatomic,strong) NSOperationQueue *queue;
  • 懒加载全局队列
- (NSOperationQueue *)queue
{
    if (_queue==nil) {
        _queue = [[NSOperationQueue alloc] init];
    }
    return _queue;
}

数据源方法 - 实现异步下载图片

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // 创建cell
    // UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"AppCell" forIndexPath:indexPath];

    AppCell *cell = [tableView dequeueReusableCellWithIdentifier:@"AppCell" forIndexPath:indexPath];

    // 获取对应cell的模型
    AppInfo *app = self.dataSourceArr[indexPath.row];

    // 给cell赋值
    cell.textLabel.text = app.name;
    cell.detailTextLabel.text = app.download;

    // 创建异步下载操作
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        // 模拟网络延迟
        [NSThread sleepForTimeInterval:0.5];

        // 同步下载图片
        NSURL *url = [NSURL URLWithString:app.icon];
        NSData *data = [NSData dataWithContentsOfURL:url];
        UIImage *image = [UIImage imageWithData:data];

        // 回到主线程更新UI
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            // 给imageView赋值
            cell.imageView.image = image;
        }];
    }];
    // 将操作添加到队列
    [self.queue addOperation:op];

    // 返回cell
    return cell;
}

存在的问题

  • 下载完成之后,不显示图片.当与cell产生交互(点击cell,滚动列表)的时候才能看到下载的图.

原因

  • 下载是异步的,在图片下载完成之前就已经把cell返回出去了.
  • cell上得系统子控件,比如imageView是懒加载上去的.当给这个空间设置了数据才会被加载到cell上.
  • 在返回cell的时候,imageView并没有被赋值,也就没有被加载出来,没有frame.
    • 查看视图层次结构验证cell子控件的懒加载原则.
  • 与cell交互的时候,会调用他的layoutSubviews方法,重新布局子控件.就重新计算frame.
    • 新建AppCell文件.与SB建立关联.重写layoutSubviews方法.观察与cell交互的时候,这个方法的调用情况.
@implementation AppCell

// 万不得已,不要重写这个方法,更不要在这个方法里面做耗时的操作.因为调用的频率是非常高的
- (void)layoutSubviews
{
    NSLog(@"%s",__FUNCTION__);

    [super layoutSubviews];
}

@end

解决办法

  • 异步下载图片之前先设置占位图片,把imageView先懒加载出来.

设置占位图

数据源方法

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // 创建cell
    AppCell *cell = [tableView dequeueReusableCellWithIdentifier:@"AppCell" forIndexPath:indexPath];

    // 获取对应cell的模型
    AppInfo *app = self.dataSourceArr[indexPath.row];

    // 给cell赋值
    cell.textLabel.text = app.name;
    cell.detailTextLabel.text = app.download;

    // 设置占位图 : 在cell返回之前设置
    cell.imageView.image = [UIImage imageNamed:@"user_default"];

    // 创建异步下载操作
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        // 模拟网络延迟
        [NSThread sleepForTimeInterval:0.5];

        // 同步下载图片
        NSURL *url = [NSURL URLWithString:app.icon];
        NSData *data = [NSData dataWithContentsOfURL:url];
        UIImage *image = [UIImage imageWithData:data];

        // 回到主线程更新UI
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            // 给imageView赋值
            cell.imageView.image = image;
        }];
    }];
    // 将操作添加到队列
    [self.queue addOperation:op];

    // 返回cell
    return cell;
}

存在的问题

  • 上下滑动列表时,图片每次都要重新下载.浪费用户流量.

原因

  • 没有建立缓存机制.每次展示cell,调用数据源方法时都会去建立下载操作下载图片.

解决办法

  • 实现图片缓存策略.
    • 图片下载完成之后先保存起来.然后在建立下载操作之前判断有没有保存的图片.要是有保存的图片就不用建立下载操作了.直接赋值并返回cell.
  • 图片缓存不能用数组作为保存图片的容器.
    • 图片下载是异步的.数组保存对象是用角标标识的,可能会出现图片保存的顺序不对的情况.
  • 图片缓存使用字典作为保存图片的容器更加合理.
    • 字典保存对象是用key标识的.我么使用图片的唯一的地址作为key来保存唯一的图片.跟图片的顺序就没有关系了.

字典实现图片内存缓存

准备图片缓存池

/// 图片缓存池
@property (nonatomic,strong) NSMutableDictionary *imageCache;
- (NSMutableDictionary *)imageCache
{
    if (_imageCache==nil) {
        _imageCache = [[NSMutableDictionary alloc] init];
    }
    return _imageCache;
}

数据源方法

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // 创建cell
    AppCell *cell = [tableView dequeueReusableCellWithIdentifier:@"AppCell" forIndexPath:indexPath];

    // 获取对应cell的模型
    AppInfo *app = self.dataSourceArr[indexPath.row];

    // 给cell赋值
    cell.textLabel.text = app.name;
    cell.detailTextLabel.text = app.download;

    // 设置占位图 : 在cell返回之前设置
    cell.imageView.image = [UIImage imageNamed:@"user_default"];

    // 在建立下载操作之前,判断图片缓存池内部是否有图片对象
    if ([self.imageCache objectForKey:app.icon]!=nil) {
        NSLog(@"从内存加载...%@",app.name);
        cell.imageView.image = [self.imageCache objectForKey:app.icon];
        return cell;
    }

    // 创建异步下载操作
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        // 模拟网络延迟
        [NSThread sleepForTimeInterval:0.5];

        NSLog(@"从网络加载...%@",app.name);

        // 同步下载图片
        NSURL *url = [NSURL URLWithString:app.icon];
        NSData *data = [NSData dataWithContentsOfURL:url];
        UIImage *image = [UIImage imageWithData:data];

        // 下载完成之后,回到主线程更新UI
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{

            // 给imageView赋值
            cell.imageView.image = image;

            // 保存图片到图片缓存池
            [self.imageCache setObject:image forKey:app.icon];
        }];
    }];
    // 将操作添加到队列
    [self.queue addOperation:op];

    // 返回cell
    return cell;
}

目前为止我们的需求实现了吗?

解决图片错行的问题

在下载操作中模拟网络延迟

// 模拟网络延迟
if (indexPath.row>9) {
    [NSThread sleepForTimeInterval:5.0];
}

问题

  • 当在5s之类快速的上下滚动列表时,会出现图片错行的问题.

分析问题

  • cell的重用.再加上重用的cell上可能绑定的有延迟的下载操作.
    • 如果第一行的cell重用的是第五行的cell.而且第五行的cell上正好绑定了一个延迟的下载操作.当5s时间到.第五行的图片下载完成就会覆盖第一行的cell上的图片;

解决问题

  • 哪个cell对应的图片下载完成了就去刷新那个对应的cell.
// 刷新对应的行
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];

数据源方法

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // 创建cell
    AppCell *cell = [tableView dequeueReusableCellWithIdentifier:@"AppCell" forIndexPath:indexPath];

    // 获取对应cell的模型
    AppInfo *app = self.dataSourceArr[indexPath.row];

    // 给cell赋值
    cell.textLabel.text = app.name;
    cell.detailTextLabel.text = app.download;

    // 设置占位图 : 在cell返回之前设置
    cell.imageView.image = [UIImage imageNamed:@"user_default"];

    // 在建立下载操作之前,判断图片缓存池内部是否有图片对象
    if ([self.imageCache objectForKey:app.icon]!=nil) {
        NSLog(@"从内存加载...%@",app.name);
        cell.imageView.image = [self.imageCache objectForKey:app.icon];
        return cell;
    }

    // 创建异步下载操作
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        // 模拟网络延迟
        if (indexPath.row>9) {
            [NSThread sleepForTimeInterval:5.0];
        }

        NSLog(@"从网络加载...%@",app.name);

        // 同步下载图片
        NSURL *url = [NSURL URLWithString:app.icon];
        NSData *data = [NSData dataWithContentsOfURL:url];
        UIImage *image = [UIImage imageWithData:data];

        // 回到主线程更新UI
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{

            // 给imageView赋值
//            cell.imageView.image = image;

            // 保存图片到图片缓存池
            [self.imageCache setObject:image forKey:app.icon];

            // 刷新对应的行
            [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
        }];
    }];
    // 将操作添加到队列
    [self.queue addOperation:op];

    // 返回cell
    return cell;
}

注意

  • 每次下载完一张图片之后,就会先保存到内存,再刷新对应的行.
  • 刷新对应的行就会重新调用数据源方法.
  • 所以imageView的赋值就没有必要在图片下载完成之后了.而是要放在刷新对应行时再次调用数据源方法时,从内存中获取到并赋值.否则,错行问题还是会存在.

目前为止我们的需求实现了吗?

解决图片重复下载的问题

在下载操作中模拟网络延迟

// 模拟网络延迟
if (indexPath.row>9) {
    [NSThread sleepForTimeInterval:20.0];
}

监听cell的点击事件,获取队列的操作计数

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSLog(@"队列中的操作个数 %zd",self.queue.operationCount);
}

问题

  • 当在20s之类快速的上下滚动列表时,会出现多个下载操作下载同一张图片的问题.

分析问题

  • 每次展示一个cell,就会建立对应的下载操作去下载cell上的图片.
  • 但是,当有网络延迟时.如果这个cell上的图片还没有下载完成,那么这个cell每出现一次就会建立一个下载操作去重复的下载同一张图片.

解决问题

  • 使用字典建立下载操作缓存池,用图片地址作key,将图片对应的下载操作保存起来.
  • 在建立下载操作之前先判断这个图片对应的下载操作有没有,如果有就不再建立下载操作.反之,就建立新的下载操作去下载这个图片.

准备下载操作缓冲池

/// 下载操作缓冲池
@property (nonatomic,strong) NSMutableDictionary *operationCache;
- (NSMutableDictionary *)operationCache
{
    if (_operationCache==nil) {
        _operationCache = [[NSMutableDictionary alloc] init];
    }
    return _operationCache;
}

数据源方法

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // 创建cell
    AppCell *cell = [tableView dequeueReusableCellWithIdentifier:@"AppCell" forIndexPath:indexPath];

    // 获取对应cell的模型
    AppInfo *app = self.dataSourceArr[indexPath.row];

    // 给cell赋值
    cell.textLabel.text = app.name;
    cell.detailTextLabel.text = app.download;

    // 设置占位图 : 在cell返回之前设置
    cell.imageView.image = [UIImage imageNamed:@"user_default"];

    // 在建立下载操作之前,判断图片缓存池内部是否有图片对象
    if ([self.imageCache objectForKey:app.icon]!=nil) {
        NSLog(@"从内存加载...%@",app.name);
        cell.imageView.image = [self.imageCache objectForKey:app.icon];
        return cell;
    }

    // 在建立下载操作之前,对应的图片的判断下载操作有没有
    if ([self.operationCache objectForKey:app.icon]!=nil) {
        NSLog(@"%@ 正在下载中...",app.name);
        return cell;
    }

    // 创建异步下载操作
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        // 模拟网络延迟
        if (indexPath.row>9) {
            [NSThread sleepForTimeInterval:20.0];
        }

        NSLog(@"从网络加载...%@",app.name);

        // 同步下载图片
        NSURL *url = [NSURL URLWithString:app.icon];
        NSData *data = [NSData dataWithContentsOfURL:url];
        UIImage *image = [UIImage imageWithData:data];

        // 图片下载完成,回到主线程更新UI
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{

            // 保存图片到图片缓存池
            [self.imageCache setObject:image forKey:app.icon];

            // 下载完成之后,清理对应的下载操作
            [self.operationCache removeObjectForKey:app.icon];

            // 刷新对应的行
            [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
        }];
    }];

    // 将下载操作添加到缓冲池
    [self.operationCache setObject:op forKey:app.icon];
    // 将操作添加到队列 : 操作执行结束之后,会自动从队列中移除,一旦移除,就解除了循环引用
    [self.queue addOperation:op];

    // 返回cell
    return cell;
}
  • 注意 : 图片下载完成之后,一定要将操作从操作缓存池移除.管理好内存.

到目前为止我们的需求实现了吗?

处理内存警告

  • 当我们在做数据缓存时,不能只考虑数据存储的问题.数据应该是有进有出.
  • 无论是磁盘缓存还是内存缓存,内存空间都是有限的.所以需要一个清理缓存的机制.

内存警告

  • 当程序收到内存警告的通知时,就是我们清理内存缓存的时候.

处理内存警告的实现

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.

    // 清理图片缓冲池
    [self.imageCache removeAllObjects];
    // 清理下载操作缓冲池
    [self.operationCache removeAllObjects];
    //取消所有的下载操作 : 正在下载的无法取消的.我们需要自定义下载操作才可以取消正在下载的操作
    [self.queue cancelAllOperations];
}

到目前为止我们的需求实现了吗?

解除循环引用

循环引用的分析

循环引用分析.png
  • 通过对循环引用的分析发现,确实存在循环引用.

代码验证循环引用

- (void)dealloc
{
    NSLog(@"%s",__FUNCTION__);
}
  • 设置导航控制器,showpop控制器.观察dealloc方法的执行情况.
  • **结论 : ** 代码验证出没有循环引用.

为什么代码验证没有循环引用?

  1. 队列queue对下载操作的强引用关系.当下载操作完成之后,下载操作会自动从队列中移除,强引用关系也就解除了.
  2. 下载操作缓存池对下载操作的强引用关系.当图片下载完成之后,下载操作已经手动的从下载操作缓存池中移除了.
// 图片下载完成,回到主线程更新UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{

    // 保存图片到图片缓存池
    [self.imageCache setObject:image forKey:app.icon];

    // 下载完成之后,清理对应的下载操作
    [self.operationCache removeObjectForKey:app.icon];

    // 刷新对应的行
    [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}];

隐含的问题

  • 如果不做任何处理,循环引用确实是可以解除.但是是有一定条件的.就是必须要等到图片下载完成之后.循环引用才可以解除.
  • 实际开发中pop操作时,我们是希望控制器可以尽快的销毁掉的.

解除循环引用

// 可以及时的解除循环引用
__weak typeof(self) weakSelf = self;

// 创建异步下载操作
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
    // 模拟网络延迟
    if (indexPath.row>9) {
        [NSThread sleepForTimeInterval:5.0];
    }

    NSLog(@"从网络加载...%@",app.name);

    // 同步下载图片
    NSURL *url = [NSURL URLWithString:app.icon];
    NSData *data = [NSData dataWithContentsOfURL:url];
    UIImage *image = [UIImage imageWithData:data];

    // 回到主线程更新UI
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{

        // 保存图片到图片缓存池
        [weakSelf.imageCache setObject:image forKey:app.icon];

        // 下载完成之后,清理对应的下载操作 : 也可以解除循环引用
        [weakSelf.operationCache removeObjectForKey:app.icon];

        // 刷新对应的行
        [weakSelf.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
    }];
}];

到目前为止我们的需求实现了吗?

断网测试

  • 在实际开发中,我们要考虑到用户在使用APP时可能出现的突发情况.比如:用户突然断网.
  • 已知 : 数组和字典中不能保存空对象.

问题

  • 当用户正在下载图片时突然断网,图片下载的结果就是nil.将nil添加到图片缓存池程序会崩溃的.
  • 所以,在保存图片之前一定要做为空的判断.

解决问题

__weak typeof(self) weakSelf = self;

// 创建异步下载操作
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
    // 模拟网络延迟
    if (indexPath.row>9) {
        [NSThread sleepForTimeInterval:5.0];
    }

    NSLog(@"从网络加载...%@",app.name);

    // 同步下载图片
    NSURL *url = [NSURL URLWithString:app.icon];
    NSData *data = [NSData dataWithContentsOfURL:url];
    UIImage *image = [UIImage imageWithData:data];

    // 回到主线程更新UI
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{

        // 断网测试
        if (image!=nil) {
            // 保存图片到图片缓存池
            [weakSelf.imageCache setObject:image forKey:app.icon];

            // 下载完成之后,清理对应的下载操作 : 也可以解除循环引用
            [weakSelf.operationCache removeObjectForKey:app.icon];

            // 刷新对应的行
            [weakSelf.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
        }
    }];
}];

到目前为止我们的需求实现了吗?

自定义Cell

  • 对比需求图发现,我们实现的界面和需求图不一样.因为我们现在使用的还是cell的系统控件.
  • 当系统的cell实现不了需求就得自定义cell了.

实现自动布局

自动布局.png

连线使自定义cell子控件建立关联

@interface AppCell : UITableViewCell

/// App图标
@property (weak, nonatomic) IBOutlet UIImageView *iconImageView;
// App名字
@property (weak, nonatomic) IBOutlet UILabel *nameLabel;
// App下载量
@property (weak, nonatomic) IBOutlet UILabel *downloadLabel;

/// 模型
@property (nonatomic,strong) AppInfo *appInfo;

@end

给自定义cell上的子控件赋值

@interface AppCell ()

@end

@implementation AppCell

- (void)setAppInfo:(AppInfo *)appInfo
{
    // 给cell赋值
    self.nameLabel.text = appInfo.name;
    self.downloadLabel.text = appInfo.download;
}

@end

数据源方法

  • 注意 :
    • iconImageView的赋值不能够放在自定义的cell里面,因为在刷新对应行的时候,iconImageView赋值需要跟indexPathtableView紧密联系起来.
    • iconImageViewindexPathtableView耦合性太强.
    • 目前没有更好的办法将iconImageView的赋值封装到自定义的cell里面去
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // 创建cell
    AppCell *cell = [tableView dequeueReusableCellWithIdentifier:@"AppCell" forIndexPath:indexPath];

    // 获取对应cell的模型
    AppInfo *app = self.dataSourceArr[indexPath.row];

    // 给cell传递数据
    cell.appInfo = app;

    // 设置占位图 : 在cell返回之前设置
    cell.iconImageView.image = [UIImage imageNamed:@"user_default"];

    // 在建立下载操作之前,判断内存缓存内部是否有图片对象
    if ([self.imageCache objectForKey:app.icon]!=nil) {
        NSLog(@"从内存加载...%@",app.name);
        cell.iconImageView.image = [self.imageCache objectForKey:app.icon];
        return cell;
    }

    // 在建立下载操作之前,对应的图片的判断下载操作有没有
    if ([self.operationCache objectForKey:app.icon]!=nil) {
        NSLog(@"%@ 正在下载中...",app.name);
        return cell;
    }

    // 可以及时的解除循环引用
    __weak typeof(self) weakSelf = self;
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        // 模拟网络延迟
        if (indexPath.row>9) {
            [NSThread sleepForTimeInterval:0.0];
        }

        NSLog(@"从网络加载...%@",app.name);

        // 同步下载图片
        NSURL *url = [NSURL URLWithString:app.icon];
        NSData *data = [NSData dataWithContentsOfURL:url];
        UIImage *image = [UIImage imageWithData:data];

        // 回到主线程更新UI
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{

            // 断网测试
            if (image!=nil) {
                // 保存图片到图片缓存池
                [weakSelf.imageCache setObject:image forKey:app.icon];

                // 下载完成之后,清理对应的下载操作 : 也可以解除循环引用
                [weakSelf.operationCache removeObjectForKey:app.icon];

                // 刷新对应的行
                [weakSelf.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
            }
        }];
    }];

    // 将下载操作添加到缓冲池
    [self.operationCache setObject:op forKey:app.icon];
    // 将操作添加到队列 : 操作执行结束之后,会自动从队列中移除,一旦移除,就解除了循环引用
    [self.queue addOperation:op];

    // 返回cell
    return cell;
}

到目前为止我们的需求实现了吗?

沙盒演练

沙盒目录介绍

  • Documents

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

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

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

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

沙盒演练

  • 文件保存到Documents目录
- (void)appendDocumentsPath
{
    // 图片地址
    NSString *icon = @"http://p16.qhimg.com/dr/48_48_/t0125e8d438ae9d2fbb.png";

    // 获取Documents文件目录
    NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    // 获取图片的名字
    NSString *fileName = [icon lastPathComponent];
    // Documents文件目录拼接图片的名字 == 图片保存到沙盒的路径
    NSString *filePath = [documentsPath stringByAppendingPathComponent:fileName];
}
  • 文件保存到Cache目录
- (void)appendCachePath
{
    // 图片地址
    NSString *icon = @"http://p16.qhimg.com/dr/48_48_/t0125e8d438ae9d2fbb.png";

    // 获取Cache文件目录
    NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    // 获取图片的名字
    NSString *fileName = [icon lastPathComponent];
    // cache文件目录拼接图片的名字 == 图片保存到沙盒的路径
    NSString *filePath = [cachePath stringByAppendingPathComponent:fileName];
}
  • 文件保存到Tmp目录
- (void)appendTmpPath
{
    // 图片地址
    NSString *icon = @"http://p16.qhimg.com/dr/48_48_/t0125e8d438ae9d2fbb.png";

    // 获取Tmp文件目录
    NSString *tmpPath = NSTemporaryDirectory();
    // 获取图片的名字
    NSString *fileName = [icon lastPathComponent];
    // Tmp文件目录拼接图片的名字 == 图片保存到沙盒的路径
    NSString *filePath = [tmpPath stringByAppendingPathComponent:fileName];
}

沙盒实现磁盘缓存

创建NSString+path分类

  • NSString+path.h文件
@interface NSString (path)

/// 文件保存到Documents目录
- (NSString *)appendDocumentsPath;
/// 文件保存到Cache目录
- (NSString *)appendCachePath;
/// 文件保存到Tmp目录
- (NSString *)appendTmpPath;

@end
  • NSString+path.m文件
@implementation NSString (path)

/// 文件保存到Documents目录
- (NSString *)appendDocumentsPath
{
    // 获取Documents文件目录
    NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    // 获取图片的名字
    NSString *fileName = [self lastPathComponent];
    // Documents文件目录拼接图片的名字 == 图片保存到沙盒的路径
    NSString *filePath = [documentsPath stringByAppendingPathComponent:fileName];

    return filePath;
}

/// 文件保存到Cache目录
- (NSString *)appendCachePath
{
    // 获取Cache文件目录
    NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    // 获取图片的名字
    NSString *fileName = [self lastPathComponent];
    // cache文件目录拼接图片的名字 == 图片保存到沙盒的路径
    NSString *filePath = [cachePath stringByAppendingPathComponent:fileName];

    return filePath;
}

/// 文件保存到Tmp目录
- (NSString *)appendTmpPath
{
    // 获取Tmp文件目录
    NSString *tmpPath = NSTemporaryDirectory();
    // 获取图片的名字
    NSString *fileName = [self lastPathComponent];
    // Tmp文件目录拼接图片的名字 == 图片保存到沙盒的路径
    NSString *filePath = [tmpPath stringByAppendingPathComponent:fileName];

    return filePath;
}

@end

分类使用

导入头文件 NSString+path

  • 保存data数据到caches目录
// 可以及时的解除循环引用
__weak typeof(self) weakSelf = self;
// 创建异步下载操作
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
    // 模拟网络延迟
    if (indexPath.row>9) {
        [NSThread sleepForTimeInterval:0];
    }

    NSLog(@"从网络加载...%@",app.name);

    // 同步下载图片
    NSURL *url = [NSURL URLWithString:app.icon];
    NSData *data = [NSData dataWithContentsOfURL:url];
    UIImage *image = [UIImage imageWithData:data];

    // 图片下载完成之后,做沙盒缓存
    if (image!=nil) {
        [data writeToFile:[app.icon appendCachePath] atomically:YES];
    }

    // 回到主线程更新UI
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{

        // 断网测试
        if (image!=nil) {
            // 保存图片到图片缓存池
            [weakSelf.imageCache setObject:image forKey:app.icon];

            // 下载完成之后,清理对应的下载操作 : 也可以解除循环引用
            [weakSelf.operationCache removeObjectForKey:app.icon];

            // 刷新对应的行
            [weakSelf.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
        }
    }];
}];

// 将下载操作添加到缓冲池
[self.operationCache setObject:op forKey:app.icon];
// 将操作添加到队列 : 操作执行结束之后,会自动从队列中移除,一旦移除,就解除了循环引用
[self.queue addOperation:op];
  • 读取caches目录数据
// 在建立下载操作之前,判断内存缓存内部是否有图片对象
if ([self.imageCache objectForKey:app.icon]!=nil) {
    NSLog(@"从内存加载...%@",app.name);
    cell.iconImageView.image = [self.imageCache objectForKey:app.icon];
    return cell;
}

// 判断沙盒有没有缓存图片
NSData *data = [NSData dataWithContentsOfFile:[app.icon appendCachePath]];
UIImage *image = [UIImage imageWithData:data];
if (image!=nil) {
    NSLog(@"从沙盒加载...%@",app.name);
    // 在内存中保存一份
    [self.imageCache setObject:image forKey:app.icon];

    cell.iconImageView.image = image;
    return cell;
}

// 在建立下载操作之前,对应的图片的判断下载操作有没有
if ([self.operationCache objectForKey:app.icon]!=nil) {
    NSLog(@"%@ 正在下载中...",app.name);
    return cell;
}
  • 注意 :
    • 判断沙盒缓存的位置要在判断内存缓存之后,建立下载操作之前.因为从内存中取数据比沙盒快.如果内存中有数据就没有必要再从沙盒中取数据了.
    • 当从沙盒中取得图片之后,要在内存中再保存一份.因为内存缓存效率比沙盒缓存高.

到目前为止我们的需求实现了吗?

代码重构

重构目的

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

重构的步骤

  • 新建一个方法
    • 新建方法
    • 把要抽取的代码,直接复制到新方法中
    • 根据需求调整参数
  • 调整旧代码
    • 注释原代码,给自己一个后悔的机会
    • 调用新方法
  • 测试
  • 优化代码
    • 在原有位置,因为要照顾更多的逻辑,代码有可能是合理的
    • 而抽取之后,因为代码少了,可以检查是否能够优化
    • 分支嵌套多,不仅执行性能会差,而且不易于阅读
  • 测试
  • 修改注释
    • 在开发中,注释不是越多越好
    • 如果忽视了注释,有可能过一段时间,自己都看不懂那个注释
    • .m 关键的实现逻辑,或者复杂代码,需要添加注释,否则,时间长了自己都看不懂!
    • .h 中的所有属性和方法,都需要有完整的注释,因为 .h 文件是给整个团队看的.
  • 重构一定要小步走,要边改变测试.

到目前为止我们的需求实现了吗?

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

推荐阅读更多精彩内容