您的位置:首页 > 其它

多线程异步加载图片async_pictures

2015-08-15 22:12 363 查看

异步加载图片

目标:在表格中异步加载网络图片

目的:

模拟
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
时会崩溃=>不能向字典中插入 nil

image == 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
时,
取消之前未完成的下载操作


目标锁定:取消正在执行中的操作!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: