iOS多线程 一一 多图下载程序

来源:互联网 发布:任加踢法 知乎 编辑:程序博客网 时间:2024/06/04 09:54


效果图如下:

打印效果:


上图打印效果,展现了滚动tableView重复从网络中下载数据的现象,在后面会对上面打印做介绍.


涉及到的知识点:

     01 字典转模型     02 存储数据到沙盒,从沙盒中加载数据     03 占位图片的设置(cell的刷新问题)     04 如何进行内存缓存(使用NSDictionary)     05 在程序开发过程中的一些容错处理     06 如何刷新tableView的指定行(解决数据错乱问题)     07 NSOperation以及线程间通信相关知识

看效果图,感觉很简单,创建一个UITableView,在cell上面设置数据. 以前在都是一些现成的数据,这次试用的数据(图片)是通过URL从网络中下载来的,因此会出现很多问题!

比如:

1. UI很不流畅--------> 开子线程下载图片

2. 图片重复从网络中下载 --------> 把下载过的图片保存起来

3. 图片不会自动刷新

4. 当网络延迟时,图片又会重复下载

5. 数据错乱现象.

首先不考虑上面出现的问题,先把上面的效果图做好.然后再根据上面问题逐一解决.

storyboard





ZYTableViewController文件

这个tableViewController和storyboard中的控制器是绑定好的.

////  ZYTableViewController.h//  00-掌握-多图下载综合案例-数据展示////  Created by 朝阳 on 2017/11/22.//  Copyright © 2017年 sunny. All rights reserved.//#import <UIKit/UIKit.h>@interface ZYTableViewController : UITableViewController@end#import "ZYTableViewController.h"#import "ZYApps.h"@interface ZYTableViewController ()@property (nonatomic, strong) NSArray *apps;@end@implementation ZYTableViewController#pragma -mark lazy loading- (NSArray *)apps{    if (!_apps) {        // 加载plist文件        NSArray *arrayM = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"apps.plist" ofType:nil]];        // 创建一个临时可变数组        NSMutableArray *tempArray = [NSMutableArray array];        for (NSDictionary *dict in arrayM) {            [tempArray addObject:[ZYApps appWithDict:dict]];        }        _apps = tempArray;    }    return _apps;}- (void)viewDidLoad {    [super viewDidLoad];    }#pragma mark - Table view data source- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {    return 1;}- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {    return self.apps.count;}- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {        static NSString *ID = @"app";    // 创建cell    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID forIndexPath:indexPath];        // 设置数据给cell    ZYApps *app = self.apps[indexPath.row];    cell.textLabel.text = app.name;    cell.detailTextLabel.text = app.download;        NSURL *url = [NSURL URLWithString:app.icon];    NSData *iconData = [NSData dataWithContentsOfURL:url];    UIImage *image = [UIImage imageWithData:iconData];    cell.imageView.image = image;    //    NSLog(@"%ld----",indexPath.row);/* 存在两个严重问题: 1. UI很不流畅 ----> 开子线程下载图片 2. 图片重复下载 ----> 先把之前已经下载的图片保存起来  */    // 返回cell    return cell;}@end

模型数据ZYApps文件

////  ZYApps.h//  00-掌握-多图下载综合案例-数据展示////  Created by 朝阳 on 2017/11/22.//  Copyright © 2017年 sunny. All rights reserved.//#import <Foundation/Foundation.h>@interface ZYApps : NSObject/** app名称 */@property(nonatomic, strong) NSString * name;/** app下载量 */@property(nonatomic, strong) NSString * download;/** app图标 */@property(nonatomic, strong) NSString * icon;+ (instancetype)appWithDict:(NSDictionary *)dict;@end@implementation ZYApps+ (instancetype)appWithDict:(NSDictionary *)dict{    ZYApps *apps = [[ZYApps alloc] init];    // 利用KVC    [apps setValuesForKeysWithDictionary:dict];    return apps;}@end


以上代码就可以实现效果图.但是存在两个严重的问题:

1. 图片被重复下载

2. UI很不流畅

问题1. 图片被重复下载

因为当滚动tableView的时候,会重复下载网络中的图片.----解决---> 先把下载好的图片保存起来

具体解决:

当应用程序第一次下载下来的时候,tableView中的图片,需要从网络中下载下来.然后把图片保存到内存缓存一份,把图片也写入到沙盒中一份.

当来回滚动tableView的时候,下载过的图片已经在内存中缓存过了,因此获取内存中的图片就可以了.由此防止了重复下载图片的现象.把图片的二进制写入到沙盒中,原因是

因为当应用程序重新启动的时候,在应用程序内存中缓存的图片都清空了,因此还需要重新从网络上下载图片,保存到沙盒中就是为了当重新启动应用程序的时候,数据可以从沙盒中读取,防止重复下载.

此时 tableView:cellForRowAtIndexPath:方法中代码.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {        static NSString *ID = @"app";    // 创建cell    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID forIndexPath:indexPath];        // 设置数据给cell    ZYApps *app = self.apps[indexPath.row];    cell.textLabel.text = app.name;    cell.detailTextLabel.text = app.download;        // 设置图标    // 查看内存缓存中该图片是否存在,若存在直接用,否则去磁盘缓存中查看是否有缓存\    如果有磁盘缓存,就保存一份到内存.设置图片,否则下载        // 从内存缓存中读取    UIImage *image = [self.images objectForKey:app.icon];    // 是否内存中存在已下载的图片    if (image) {        cell.imageView.image = image;        NSLog(@"使用内存缓存中的图片---%ld",indexPath.row);    }else{        // 保存图片到沙盒缓存        /*         arg1: 沙盒的哪个目录         arg2: 去主目录下去搜索,默认就是NSUserDomainMask         arg3: 是否展开路径         */        NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];        // 获得图片名称,不能包含/        NSString *fileName = [app.icon lastPathComponent];        // 拼接图片的全路径        NSString *fullPath = [caches stringByAppendingPathComponent:fileName];                // 检查磁盘缓存        NSData *imageData = [NSData dataWithContentsOfFile:fullPath];        if (imageData) {            UIImage *image = [UIImage imageWithData:imageData];            // 设置图标            cell.imageView.image = image;            NSLog(@"%ld--使用了磁盘缓存的图片--",indexPath.row);            // 把图片保存到内存中一份            [self.images setObject:image forKey:app.icon];//            NSLog(@"%@",fullPath);                    }else{                        NSURL *url = [NSURL URLWithString:app.icon];            NSData *iconData = [NSData dataWithContentsOfURL:url];            UIImage *image = [UIImage imageWithData:iconData];            cell.imageView.image = image;            // 把图片保存到内存缓存            [self.images setObject:image forKey:app.icon];            // 写数据到沙盒            [iconData writeToFile:fullPath atomically:YES];//            NSLog(@"%@",fullPath);            NSLog(@"%ld--下载--",indexPath.row);        }    }        /*     存在两个严重问题:     1. UI很不流畅 ----> 开子线程下载图片     2. 图片重复下载 ----> 先把之前已经下载的图片保存起来     */        // 返回cell    return cell;}

这样就解决了重复下载图片.


解决上面的5个问题 和 ZYTableViewController文件

问题:

1. UI很不流畅 --->开子线程下载图片

2. 图片重复下载 --->先把之前已经下载的图片保存起来(字典)

内存缓存 ---> 磁盘缓存


3. 图片不会自动刷新:

原因:因为cellsubTitle类型的,subTitle类型中的image默认frame是(0,0,0,0)的,当显示cell的时候,imageframe还是(0,0,0,0),此时有图片已经下载完了.因为是开子线程下载图片的,程序是异步的,因此先return cell,此时的cellImageframe0,图片设置上去也是不显示的.

解决:手动刷新每一行cell. reloadRowsAtIndexPaths:withRowAnimation:,这个方法会调用cellForRow方法,因此会重新创建cell,cellImage此时已经在内存缓存了.


4.(当网络延迟时)图片重复下载:

因为当下载一个cell的图片时候需要2s,当这个cell下载到1s的时候,用户滚动速度较快,此时整个cell被存放到缓存池中了(此时cell的图片还没有下载完),当下一个cell显示的时候,会从缓存池中取,此时缓存池中没有下载好图片的cell.因此会出现重复下载现象


5. 数据错乱

原因: cell的复用问题造成的,当从缓存池中复用cell的同时,把复用的cell的图片也复用过来了.因此出现数据紊乱现象

解决:cell需要下载新的图片之前,清空cell原来的图片(设置占位图片)

#import "ZYTableViewController.h"#import "ZYApps.h"@interface ZYTableViewController ()/** 模型数组 */@property (nonatomic, strong) NSArray *apps;/** 存放下载过的图片 */@property (nonatomic,strong) NSMutableDictionary *images;/** 队列 */@property (nonatomic,strong) NSOperationQueue *queue;/** 操作缓存 */@property (nonatomic,strong) NSMutableDictionary *operations;@end@implementation ZYTableViewController#pragma -mark lazy loading- (NSArray *)apps{    if (!_apps) {        // 加载plist文件        NSArray *arrayM = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"apps.plist" ofType:nil]];        // 创建一个临时可变数组        NSMutableArray *tempArray = [NSMutableArray array];        for (NSDictionary *dict in arrayM) {            [tempArray addObject:[ZYApps appWithDict:dict]];        }        _apps = tempArray;    }    return _apps;}- (NSMutableDictionary *)images{    if (!_images) {        _images = [NSMutableDictionary dictionary];    }    return _images;}- (NSOperationQueue *)queue{    if(!_queue){        _queue = [[NSOperationQueue alloc] init];        // 设置最大并发数:并行执行的任务数        _queue.maxConcurrentOperationCount = 5;    }    return _queue;}- (NSMutableDictionary *)operations{    if (!_operations) {        _operations = [NSMutableDictionary dictionary];    }    return _operations;}- (void)viewDidLoad {    [super viewDidLoad];    }- (void)didReceiveMemoryWarning{    // 当发生内存警告的时候    [self.images removeAllObjects];    // 取消队列中所有的操作    [self.queue cancelAllOperations];}#pragma mark - Table view data source- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {        return self.apps.count;}- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {        static NSString *ID = @"app";    // 创建cell    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID forIndexPath:indexPath];        // 设置数据给cell    ZYApps *app = self.apps[indexPath.row];    cell.textLabel.text = app.name;    cell.detailTextLabel.text = app.download;        // 设置图标    // 查看内存缓存中该图片是否存在,若存在直接用,否则去磁盘缓存中查看是否有缓存\    如果有磁盘缓存,就保存一份到内存.设置图片,否则下载        // 从内存缓存中读取    UIImage *image = [self.images objectForKey:app.icon];    // 是否内存中存在已下载的图片    if (image) {        cell.imageView.image = image;        NSLog(@"使用内存缓存中的图片---%ld",indexPath.row);    }else{        // 保存图片到沙盒缓存        /*         arg1: 沙盒的哪个目录         arg2: 去主目录下去搜索,默认就是NSUserDomainMask         arg3: 是否展开路径         */        NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];        // 获得图片名称,不能包含/        NSString *fileName = [app.icon lastPathComponent];        // 拼接图片的全路径        NSString *fullPath = [caches stringByAppendingPathComponent:fileName];                // 检查磁盘缓存        NSData *imageData = [NSData dataWithContentsOfFile:fullPath];        // 废除//        imageData = nil;        if (imageData) {            UIImage *image = [UIImage imageWithData:imageData];            // 设置图标            cell.imageView.image = image;            NSLog(@"%ld--使用了磁盘缓存的图片--",indexPath.row);            // 把图片保存到内存中一份            [self.images setObject:image forKey:app.icon];            //            NSLog(@"%@",fullPath);                    }else{                        // 创建队列(注意:在这里会创建很多个队列);            //            NSOperationQueue *queue = [[NSOperationQueue alloc] init];                        //## 检查该图片是否正在下载,如果是那么就什么都不做,否则再添加下载任务            NSBlockOperation *downloadImage = [self.operations objectForKey:app.icon];            if (downloadImage) {                // 什么都不做            }else{                                // 清空cell之前的图片//                cell.imageView.image = nil;                // 占位图片//                cell.imageView.image = [UIImage imageNamed:@"qq"];                                // 创建操作                downloadImage = [NSBlockOperation blockOperationWithBlock:^{                                        NSURL *url = [NSURL URLWithString:app.icon];                    NSData *iconData = [NSData dataWithContentsOfURL:url];                    UIImage *image = [UIImage imageWithData:iconData];                                        //NSLog(@"%ld--下载--",indexPath.row);                                        // 容错处理                    if (image == nil) {                        [self.operations removeObjectForKey:app.icon];                        return;                    }                                        // 演示网络延迟                    //[NSThread sleepForTimeInterval:2.0];                                        // 线程间通信                    [[NSOperationQueue mainQueue] addOperationWithBlock:^{                                                // cell.imageView.image = image;                        // 刷新一行(会重新调用cellForRow方法)                        [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationRight];                    }];                                        // 把图片保存到内存缓存                    [self.images setObject:image forKey:app.icon];                    // 写数据到沙盒                    [iconData writeToFile:fullPath atomically:YES];                    //            NSLog(@"%@",fullPath);                    NSLog(@"%ld--下载--",indexPath.row);                                        // 移除图片的下载操作                    [self.operations removeObjectForKey:app.icon];                                    }];                                // 添加操作到操作缓存中                [self.operations setObject:downloadImage forKey:app.icon];                                // 把操作添加到队列中                [self.queue addOperation:downloadImage];                            }          }    }        // 返回cell    return cell;}@end


沙盒

 Documents: 会备份,不允许

 tmp: 临时路径(随时会被删除)

 Libray

 Preferences: 偏好设置,保存账号密码

 caches: 缓存文件




原创粉丝点击