多图下载综合示例程序


0. 实现效果

gif


1. 涉及知识点

  • 1. 字典转模型
  • 2. 图片重复下载---》内存缓存 和 沙盒缓存处理
  • 3. UI不流畅---》开子线程下载图片(注意线程间通信, 下载完成之后回到主线程刷新 UI)
  • 4. 图片下载任务被添加到队列中多次---》操作缓存处理
  • 5. 图片下载后不显示问题---》主动刷新指定行
  • 6. 图片加载中出现数据错乱问题---》设置占位图片
  • 7. 在程序开发过程中的一些容错处理

2. 思路分析

image

1. 使用 tableViewController 完成基本数据的展示
    问题1:UI不流畅, 解决办法在子线程中下载图片
    问题2:重复下载图片
2. 解决重复下载问题
    对图片进行内存缓存以解决重复下载问题
    对图片做磁盘缓存,进行离线缓存
        分析磁盘缓存的位置
            Documents
                该目录下面的数据在连接手机时会备份
                苹果官方不允许把下载的数据存放于该目录下
                如果存储在这里会被拒绝上架
            Libriary
                caches (只要这个文件夹才是给我们存储文件的)
                    一般要将用户要保存的下载文件放在这个文件夹
                perference
                    该目录用来存放偏好设置如登录名密码等等
            Tmp
                会随时被随机删除 (这个文件夹一般都是系统自己用, 我们结合系统的在用)
3. 解决UI不流畅问题
    解决思路
        把下载图片的操作放在子线程中处理
    新的问题
        ①图片不显示
            因为 cell 的图片的尺寸是根据图片的大小, 然而一开始没有图片, 所有 frame 的 大小是 0 ,
            图片下载是在子线程下载, 异步执行, 在下载图片完成之前已经设置了图片的 frame , 
            而且没有进行数据刷新
            解决办法, 只要再次刷新指定的行即可

        ②线程间的通信
            在子线程下载图片写入沙盒等,回到主线程刷新UI
        ③重复开启下载线程, 导致重复下载问题
            需要对操作进行缓存
        ④数据错乱问题
            本例中如果需要显示的图片还没下载完成, 则对应的 cell 的图片是空的, 
            然而对应要显示 cell 是复用上面之前有图片的 cell 这时就会显示复用 cell 的图片造成数据错乱,
            解决办法, 通过设置占位图片解决该问题(一般占位图是公司的 log)
    注意点
        图片下载完成之后把操作从缓存中移除
        图片下载完成后,做容错处理
        发生内存警告后的处理
            ①移除内存缓存中的所有元素
            ②取消下载图片队列中所有的操作
        做容错处理: 
            1. 如果 url 错误, 导致图片下载为 nil , 会导致图片设置失败, 程序闪退, 
                解决办法: 判断 image 是否为空, 为空则直接返回, 不做图片设置, 就不会导致程序闪退
                    if (image == nil) {
                        [self.operations removeObjectForKey:appM.icon];
                        return ;
                    }

            2. 有可能下载网络超时
            3. 内存警告的时候 清空缓存即可


4. 框架应用
    可以使用SDWebImage框架来下载图片,一行代码解决上面所有的问题
    注意: 在刷新的时候 cell 的 image 可能会出现图片缩放情况
    解决办法:自定义一个 cell 管理 storyboard/ xib 的 cell 
    重写相应的layoutSubview 方法, 给图片设置尺寸即可解决缩放情况
    (在实际开发中一般不需要设置这个尺寸, 美工提供的图片基本都是合标准的)
    SDWebImage做了容错处理
    如果传入的是 NSString 类型而不是 url 类型的数据, 只会报警告, 
    并且内部会将字符串转为 url 类型

3. 代码实现

//
//  ViewController.m
//  11-掌握-多图下载综合案例-数据展示
//
//  Created by apple on 16/6/20.
//  Copyright © 2016年 CDH. All rights reserved.
//

#import "ViewController.h"
#import "CDHApp.h"

@interface ViewController ()
@property (nonatomic,strong) NSArray *apps;
@property (strong, nonatomic) NSMutableDictionary*images;
@property (strong, nonatomic) NSMutableDictionary*operations;
@property (strong, nonatomic) NSOperationQueue *queue;
@end

@implementation ViewController

#pragma mark -------------------
#pragma mark lazy Loading
-(NSMutableDictionary *)images
{
    if (_images ==nil) {
        _images = [NSMutableDictionary dictionary];
    }
    return _images;
}
-(NSArray *)apps
{
    if (_apps == nil) {

        NSArray *apps = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"apps.plist" ofType:nil]];

        NSMutableArray *arrayM = [NSMutableArray arrayWithCapacity:apps.count];
        //字典转模型  字典数组--->模型数组
        for (NSDictionary *dict in apps) {
            [arrayM addObject:[CDHApp appWithDict:dict]];
        }
        _apps = arrayM;
    }
    return _apps;
}

-(NSOperationQueue *)queue
{
    if (_queue == nil) {
        _queue = [[NSOperationQueue alloc]init];
        _queue.maxConcurrentOperationCount = 5; //设置最大并发数, 注意: 最大并发数最多不要大于6
    }
    return _queue;
}

-(NSMutableDictionary *)operations
{
    if (_operations == nil) {
        _operations  = [NSMutableDictionary dictionary];
    }
    return _operations;
}

#pragma mark -------------------
#pragma mark UITableDataSource
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

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


-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    //1.创建cell
    static NSString *ID = @"app";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];

    //2.设置数据
    //2.0 先拿到该行cell对应的数据
    CDHApp *appM = self.apps[indexPath.row];

    //2.1 设置标题
    cell.textLabel.text = appM.name;

    //2.2 设置子标题
    cell.detailTextLabel.text = [NSString stringWithFormat:@"下载量:%@",appM.download];

    //2.3 设置图片
    //当图片下载完成之后显示图片,& 保存到字典中去(保证 KEY 唯一性, 名称可能会相关, 但下载的网络路径是唯一的)
    //当该图片需要显示的时候,先检查内存缓存,如果有那么就直接使用内存缓存,否则在下载
    UIImage *image = [self.images objectForKey:appM.icon];
    if(image)
    {
        cell.imageView.image = image;
        NSLog(@"%zd---内存缓存",indexPath.row);
    }else
    {
        //拼接caches路径
        NSString *cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];

        //拼接文件的全路径
        // 1. 先拿到icon 路径的最后一个节点(最后一个 / 后面的内容)
        NSString *fileName = [appM.icon lastPathComponent];
        // 2. 拼接文件的全路径,
        //    stringByAppendingString: 方法不能自动添加 '/'
        //    stringByAppendingPathComponent: 方法会添加有 '/'
        NSString *fullPath = [cachesPath stringByAppendingPathComponent:fileName];

        //检查磁盘缓存
        NSData *data = [NSData dataWithContentsOfFile:fullPath];
        //为了演示方便 先清空缓存
        data = nil;
        if (data) {
                //设置
            UIImage *image = [UIImage imageWithData:data];
            cell.imageView.image = image;

                //保存到内存缓存
            [self.images setObject:image forKey:appM.icon];

              NSLog(@"%zd---磁盘缓存",indexPath.row);
        }else
        {

            //处理image
            //cell.imageView.image = nil;

            //设置占位图片
            cell.imageView.image = [UIImage imageNamed:@"Snip20200808_29"];
            //检查该图片的下载操作是否已经存在,如果存在,那么什么都不做,等待
            NSBlockOperation *download = [self.operations objectForKey:appM.icon];

            if (download) {

            }else
            {
                //封装操作
                download = [NSBlockOperation blockOperationWithBlock:^{

                    NSURL *url = [NSURL URLWithString:appM.icon];
                    NSData *data = [NSData dataWithContentsOfURL:url];
                    // 用个 for 循环模拟下载(高清/大)图片耗时操作
                    for (NSInteger i = 0; i<1000000000; i++) {

                    }

                    UIImage *image = [UIImage imageWithData:data];

                     NSLog(@"%zd---开始下载-----",indexPath.row);

                    if (image == nil) {
                        [self.operations removeObjectForKey:appM.icon];
                        return ;
                    }
                    //保存到内存缓存
                    [self.images setObject:image forKey:appM.icon];

                    //保存到磁盘缓存
                    [data writeToFile:fullPath atomically:YES];


                    //回到主线程设置图片
                    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                        //cell.imageView.image = image;
                        // 注意: 这个方法第一个参数接收的是数组所以要写成数组格式 @[indexPath]
                        [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationMiddle];
                        NSLog(@"%zd---下载结束++++++",indexPath.row);
                        //NSLog(@"UI---%@",[NSThread currentThread]);
                    }];

                }];

                //做操作缓存
                [self.operations setObject:download forKey:appM.icon];
                //添加操作到队列中
                [self.queue addOperation:download];
            }
        }
    }
    //3.返回cell
    return cell;
}

-(void)didReceiveMemoryWarning
{
    [self.images removeAllObjects];
     self.images = nil;

    [self.queue cancelAllOperations];
}
//磁盘缓存的路径
/*
 Documents :会备份&不允许保存下载文件 (X) , 一般图片这样的下载文件都比较大
 Library
    caches 一般要将用户要保存的下载文件放在这个文件夹
    perference  偏好设置
 tmp :临时数据

 */
/*
 1)UI卡顿 --->开子线程下载图片
        图片不显示(fram=0)--->刷新指定的行
        重复下载的问题(因为图片下载操作需要花费时间,在该时间段内部此image又需要显示)
        对图片的下载操作进行缓存--->操作缓存
 2)重复下载的问题-->内存缓存
 */
//二级缓存
/*
 显示--> 内存缓存 -->下载
 显示--> 内存缓存 -->磁盘缓存 -->下载
 */
@end

results matching ""

    No results matching ""