多线程异步加载图片async_pictures
异步加载图片
- 目标:在表格中异步加载网络图片
目的:
- 模拟
SDWebImage基本功能实现 - 理解
SDWebImage的底层实现机制 SDWebImage是非常著名的网络图片处理框架,目前国内超过90%公司都在使用!
- 模拟
要求:
- 不要求能够打出来
- 需要掌握思路
- 需要知道开发过程中,每一个细节是怎么递进的
- 需要知道每一个隐晦的问题是如何发现的
搭建界面&数据准备
代码
数据准备
@interface AppInfo : NSObject
/// App 名称
@property (nonatomic, copy) NSString *name;
/// 图标 URL
@property (nonatomic, copy) NSString *icon;
/// 下载数量
@property (nonatomic, copy) NSString *download;
+ (instancetype)appInfoWithDict:(NSDictionary *)dict;
/// 从 Plist 加载 AppInfo
+ (NSArray *)appList;
@end
+ (instancetype)appInfoWithDict:(NSDictionary *)dict {
id obj = [[self alloc] init];
[obj setValuesForKeysWithDictionary:dict];
return obj;
}
/// 从 Plist 加载 AppInfo
+ (NSArray *)appList {
NSURL *url = [[NSBundle mainBundle] URLForResource:@"apps.plist" withExtension:nil];
NSArray *array = [NSArray arrayWithContentsOfURL:url];
NSMutableArray *arrayM = [NSMutableArray arrayWithCapacity:array.count];
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
[arrayM addObject:[self appInfoWithDict:obj]];
}];
return arrayM.copy;
}
视图控制器数据
/// 应用程序列表
@property (nonatomic, strong) NSArray *appList;
- 懒加载
- (NSArray *)appList {
if (_appList == nil) {
_appList = [AppInfo appList];
}
return _appList;
}
表格数据源方法
#pragma mark - 数据源方法
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.appList.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"AppCell"];
// 设置 Cell...
AppInfo *app = self.appList[indexPath.row];
cell.textLabel.text = app.name;
cell.detailTextLabel.text = app.download;
return cell;
}
知识点
- 数据模型应该负责所有数据准备工作,在需要时被调用
- 数据模型不需要关心被谁调用
- 数组使用
[NSMutableArray arrayWithCapacity:array.count];的效率更高- 使用块代码遍历的效率比 for 要快
@"AppCell"格式定义的字符串是保存在常量区的- 在 OC 中,懒加载是无处不在的
- 设置
cell内容时如果没有指定图像,择不会创建imageView
# 同步加载图像
- 设置
// 同步加载图像
// 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;
注意:之前没有设置
imageView时,imageView并不会被创建
存在的问题
- 如果网速慢,会卡爆了!影响用户体验
- 滚动表格,会重复下载图像,造成用户经济上的损失!
解决办法
- 异步下载图像
异步下载图像
全局操作队列
/// 全局队列,统一管理所有下载操作
@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 交互,会调用 cell 的 layoutSubviews 方法,重新调整 cell 的布局
解决办法
- 使用占位图像
- 自定义 Cell
注意演示不在主线程更新图像的效果
占位图像
// 0. 占位图像
UIImage *placeholder = [UIImage imageNamed:@"user_default"];
cell.imageView.image = placeholder;
问题
- 因为使用的是系统提供的 cell
- 每次和 cell 交互,
layoutSubviews方法会根据图像的大小自动调整imageView的尺寸
解决办法
- 自定义 Cell
自定义 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
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 (nonatomic, strong) NSMutableDictionary *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时会崩溃=>不能向字典中插入 nilimage == nil时会重复刷新表格,陷入死循环
解决办法
- 修改主线程回调代码
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
if (image != nil) {
// 设置模型中的图像
[weakSelf.imageCache setObject:image forKey:app.icon];
// 刷新表格
[weakSelf.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
}];
代码重构
代码重构介绍
重构目的
- 相同的代码最好只出现一次
- 主次方法
- 主方法
- 只包含实现完整逻辑的子方法
- 思维清楚,便于阅读
- 次方法
- 实现具体逻辑功能
- 测试通过后,后续几乎不用维护
- 主方法
重构的步骤
- 新建一个方法
- 新建方法
- 把要抽取的代码,直接复制到新方法中
- 根据需求调整参数
- 调整旧代码
- 注释原代码,给自己一个后悔的机会
- 调用新方法
- 测试
- 优化代码
- 在原有位置,因为要照顾更多的逻辑,代码有可能是合理的
- 而抽取之后,因为代码少了,可以检查是否能够优化
- 分支嵌套多,不仅执行性能会差,而且不易于阅读
- 测试
- 修改注释
- 在开发中,注释不是越多越好
- 如果忽视了注释,有可能过一段时间,自己都看不懂那个注释
- .m 关键的实现逻辑,或者复杂代码,需要添加注释,否则,时间长了自己都看不懂!
- .h 中的所有属性和方法,都需要有完整的注释,因为 .h 文件是给整个团队看的
重构一定要小步走,要边改变测试
重构后的代码
- (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;
}
沙盒缓存实现
沙盒目录介绍
- Documents
- 保存由应用程序产生的文件或者数据,例如:涂鸦程序生成的图片,游戏关卡记录
- iCloud 会自动备份 Document 中的所有文件
- 如果保存了从网络下载的文件,在上架审批的时候,会被拒!
tmp
- 临时文件夹,保存临时文件
- 保存在 tmp 文件夹中的文件,系统会自动回收,譬如磁盘空间紧张或者重新启动手机
- 程序员不需要管 tmp 文件夹中的释放
Caches
- 缓存,保存从网络下载的文件,后续仍然需要继续使用,例如:网络下载的离线数据,图片,视频…
- 缓存目录中的文件系统不会自动删除,可以做离线访问!
- 要求程序必需提供一个完善的清除缓存目录的”解决方案”!
Preferences
- 系统偏好,用户偏好
- 操作是通过
[NSUserDefaults standardDefaults]来直接操作
iOS 不同版本间沙盒目录的变化
- iOS 7.0及以前版本
bundle目录和沙盒目录是在一起的 - iOS 8.0之后,
bundle目录和沙盒目录是分开的
NSString+Path
#import "NSString+Path.h"
@implementation NSString (Path)
- (NSString *)appendDocumentPath {
NSString *dir = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
return [dir stringByAppendingPathComponent:self.lastPathComponent];
}
- (NSString *)appendCachePath {
NSString *dir = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject;
return [dir stringByAppendingPathComponent:self.lastPathComponent];
}
- (NSString *)appendTempPath {
return [NSTemporaryDirectory() stringByAppendingPathComponent:self.lastPathComponent];
}
@end
沙盒缓存
- 将图像保存至沙盒
if (data != nil) {
[data writeToFile:app.icon.appendCachePath atomically:true];
}
- 检查沙盒缓存
// 判断沙盒文件是否存在
UIImage *image = [UIImage imageWithContentsOfFile:app.icon.appendCachePath];
if (image != nil) {
NSLog(@"从沙盒加载图像 ... %@", app.name);
// 将图像添加至图像缓存
[self.imageCache setObject:image forKey:app.icon];
cell.iconView.image = image;
return cell;
}
iOS6 的适配问题
面试题:iOS 6.0 的程序直接运行在 iOS 7.0 的系统中,通常会出现什么问题
状态栏高度 20 个点是不包含在
view.frame中的,self.view的左上角原点的坐标位置是从状态栏下方开始计算- iOS 6.0 程序直接在 iOS 7.0 的系统中运行最常见的问题,就是少了20个点
如果包含有
UINavigationController,self.view的左上角坐标原点从状态栏下方开始计算- 因此,iOS 6.0的系统无法实现表格从导航条下方穿透的效果
如果包含有
UITabBarController,self.view的底部不包含 TabBar- 因此,iOS 6.0的系统无法实现表格从 TabBar 下方穿透效果
小结
代码实现回顾
- 从
tableView数据源方法入手 - 根据
indexPath异步加载网络图片 - 使用
操作缓冲池避免下载操作重复被创建 - 使用
图像缓冲池实现内存缓存,同时能够对内存警告做出响应 - 使用
沙盒缓存实现再次运行程序时,直接从沙盒加载图像,提高程序响应速度,节约用户网络流量
遗留问题
- 代码耦合度太高,由于下载功能是与数据源的
indexPath绑定的,如果想将下载图像抽取到cell中,难度很大!
SDWebImage初体验
简介
- iOS中著名的牛逼的网络图片处理框架
- 包含的功能:图片下载、图片缓存、下载进度监听、gif处理等等
- 用法极其简单,功能十分强大,大大提高了网络图片的处理效率
- 国内超过90%的iOS项目都有它的影子
- 框架地址:https://github.com/rs/SDWebImage
演示 SDWebImage
- 导入框架
- 添加头文件
#import "UIImageView+WebCache.h"
- 设置图像
[cell.iconView sd_setImageWithURL:[NSURL URLWithString:app.icon]];
思考:SDWebImage 是如何实现的?
- 将网络图片的异步加载功能封装在
UIImageView的分类中 - 与
UITableView完全解耦
要实现这一目标,需要解决以下问题:
- 给
UIImageView下载图像的功能 - 要解决表格滚动时,因为图像下载速度慢造成的图片错行问题,可以在给
UIImageView设置新的URL时,取消之前未完成的下载操作
目标锁定:取消正在执行中的操作!
多线程异步加载图片async_pictures的更多相关文章
- 实例演示Android异步加载图片
本文给大家演示异步加载图片的分析过程.让大家了解异步加载图片的好处,以及如何更新UI.首先给出main.xml布局文件:简单来说就是 LinearLayout 布局,其下放了2个TextView和5个 ...
- 实例演示Android异步加载图片(转)
本文给大家演示异步加载图片的分析过程.让大家了解异步加载图片的好处,以及如何更新UI.首先给出main.xml布局文件:简单来说就是 LinearLayout 布局,其下放了2个TextView和5个 ...
- 演化理解 Android 异步加载图片
原文:http://www.cnblogs.com/ghj1976/archive/2011/05/06/2038738.html#3018499 在学习"Android异步加载图像小结&q ...
- Android 多线程 异步加载
Android 应用中需要显示网络图片时,图片的加载过程较为耗时,因此加载过程使用线程池进行管理, 同时使用本地缓存保存图片(当来回滚动ListView时,调用缓存的图片),这样加载和显示图片较为友好 ...
- Android之ListView异步加载图片且仅显示可见子项中的图片
折腾了好多天,遇到 N 多让人崩溃无语的问题,不过今天终于有些收获了,这是实验的第一版,有些混乱,下一步进行改造细分,先把代码记录在这儿吧. 网上查了很多资料,发现都千篇一律,抄来抄去,很多细节和完整 ...
- android listview 异步加载图片并防止错位
网上找了一张图, listview 异步加载图片之所以错位的根本原因是重用了 convertView 且有异步操作. 如果不重用 convertView 不会出现错位现象, 重用 convertVie ...
- [Android]异步加载图片,内存缓存,文件缓存,imageview显示图片时增加淡入淡出动画
以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/3574131.html 这个可以实现ImageView异步加载 ...
- ListView异步加载图片,完美实现图文混排
昨天参加一个面试,面试官让当场写一个类似于新闻列表的页面,文本数据和图片都从网络上获取,想起我还没写过ListView异步加载图片并实现图文混排效果的文章,so,今天就来写一下,介绍一下经验. Lis ...
- 软引用SoftReference异步加载图片
HashMap<String, SoftReference<Drawable>> imageCache 关于SoftReference这个类多少知道些机制,会用就ok了. 机制 ...
随机推荐
- 第一课 opengl简介
1. 什么是opengl: opengl是图形硬件的一种软件接口. 2. opengl对场景中的图像进行渲染时所执行的主要图形操作 1)根据几何图元创建形状,从而建立物体的数学描述. 2)在三维空间中 ...
- 医失眠灵验方--五味子50g 茯神50g 合欢花15g 法半夏15g
方药:五味子50g 茯神50g 合欢花15g 法半夏15g 水煎服 主治:失眠健忘 此方为已故名老中医李培生之验方,用于临床治疗失眠健忘症,疗效显著,其主药为五味子,滋阴和阳,敛阳 ...
- [译]BEAST还是一个威胁吗?
原文链接:https://community.qualys.com/blogs/securitylabs/2013/09/10/is-beast-still-a-threat 原文发表时间:2013. ...
- 慕课网-安卓工程师初养成-1-6 MyEclipse的使用简介
来源 http://www.imooc.com/video/1414 http://www.my-eclipse.cn/ MyEclipse 2014 官方版下载地址 声明:MyEclipse 20 ...
- 检测zookeeper和kafka是否正常
cd $(dirname $) source ~/.bash_profile count_zoo=`ps -ef | grep "config/zookeeper.properties&qu ...
- C#中List集合转换JSON
#region 将List<>转换为Json public string List2JSON(List<object> objlist, string classname) { ...
- 【Linux】Linux字体颜色
转自:http://onlyzq.blog.51cto.com/1228/546459 echo显示带颜色,需要使用参数-e格式如下:echo -e "\033[字背景颜色;文字颜色m字符串 ...
- 手机h5 页面 iPhone 下 手机号码 蓝色字体 黑色字体
在手机端 苹果系统下 手机号码会变成蓝色的 ,如何不让手机号变成蓝色 黑色 或者其他颜色 , 苹果真是的 原因是识别成了电话号码,然后成为了链接.解决方法: 更改链接的颜色 a{ color: re ...
- CSS强制文本在一行内显示若有多余字符则使用省略号表示
这篇文章主要介绍了强制文本在一行内显示,多余字符使用省略号,设置或检索是否使用一个省略标记(...)标示对象内文本的溢出.对应的脚本特性为textOverflow 设置或检索是否使用一个省略标记(.. ...
- boost:exception使用实例
/************************************************************************/ /*功能描述: boost exception使用 ...