tableView加载网络图片
- 需求的效果图
- 数据结构
获取模型数组
准备模型
.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];
}
到目前为止我们的需求实现了吗?
解除循环引用
循环引用的分析
- 通过对循环引用的分析发现,确实存在循环引用.
代码验证循环引用
- (void)dealloc
{
NSLog(@"%s",__FUNCTION__);
}
- 设置导航控制器,
show
和pop
控制器.观察dealloc
方法的执行情况. - **结论 : ** 代码验证出没有循环引用.
为什么代码验证没有循环引用?
- 队列
queue
对下载操作的强引用关系.当下载操作完成之后,下载操作会自动从队列中移除,强引用关系也就解除了. - 下载操作缓存池对下载操作的强引用关系.当图片下载完成之后,下载操作已经手动的从下载操作缓存池中移除了.
// 图片下载完成,回到主线程更新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了.
实现自动布局
连线使自定义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
赋值需要跟indexPath
和tableView
紧密联系起来. -
iconImageView
与indexPath
和tableView
耦合性太强. - 目前没有更好的办法将
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 文件是给整个团队看的.
- 重构一定要小步走,要边改变测试.
到目前为止我们的需求实现了吗?