您的位置:首页 > 移动开发 > IOS开发

ios开发进阶之多线程02 NSOperation

2015-08-27 21:22 531 查看

一 单例模式

单例模式的作用

可以保证在程序运行过程,一个类只有一个实例,而且该实例易于供外界访问

从而方便地控制了实例个数,并节约系统资源

ARC中,单例模式的实现

在.m中保留一个全局的static的实例

static id _instance;

重写allocWithZone:方法,在这里创建唯一的实例(注意线程安全)

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [super allocWithZone:zone];
});
return _instance;
}


提供1个类方法让外界访问唯一的实例

+ (instancetype)sharedInstance
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[self alloc] init];
});
return _instance;
}


实现copyWithZone:方法

- (id)copyWithZone:(struct _NSZone *)zone
{
return _instance;
}


注意点

不要继承单例类

先创建子类永远是子类对象

线创建父类永远是父类对象

如何判断是否是ARC

#if __has_feature(objc_arc)
//ARC
#else
//MRC
#endif


二 NSOperation基本使用

NSOperation的作用

配合使用NSOperation和NSOperationQueue也能实现多线程编程

NSOperation和NSOperationQueue实现多线程的具体步骤

先将需要执行的操作封装到一个NSOperation对象中

然后将NSOperation对象添加到NSOperationQueue中

系统会自动将NSOperationQueue中的NSOperation取出来

将取出的NSOperation封装的操作放到一条新线程中执行

NSOperation的子类

NSOperation是个抽象类,并不具备封装操作的能力,必须使用它的子类

使用NSOperation子类的方式有3种

NSInvocationOperation

NSBlockOperation

自定义子类继承NSOperation,实现内部相应的方法

NSInvocationOperation

// 1.将操作封装到Operation中
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(demo) object:nil];
// 2.执行封装的操作
// 如果直接执行NSInvocationOperation中的操作, 那么默认会在主线程中执行
[op1 start];


NSBlockOperation

// 1.封装操作
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1- %@", [NSThread currentThread]);
}];

// 2.添加操作
[op1 addExecutionBlock:^{
NSLog(@"2- %@", [NSThread currentThread]);
}];
[op1 addExecutionBlock:^{
NSLog(@"3- %@", [NSThread currentThread]);
}];

// 2.执行操作
// 如果只封装了一个操作, 那么默认会在主线程中执行
// 如果封装了多个操作, 那么除了第一个操作以外, 其它的操作会在子线程中执行
[op1 start];


自定义子类继承NSOperation

自定义类继承于NSOperation, 那么需要将操作写到自定义类的main方法中

@implementation XMOperation

- (void)main
{
NSLog(@"%s, %@", __func__,[NSThread currentThread]);
}


三 NSOperationQueue

NSOperationQueue的作用

NSOperation可以调用start方法来执行任务,但默认是同步执行的

如果将NSOperation添加到NSOperationQueue(操作队列)中,系统会自动异步执行NSOperation中的操作

添加操作到NSOperationQueue中

- (void)addOperation:(NSOperation *)op;
- (void)addOperationWithBlock:(void (^)(void))block;


NSInvocationOperation

// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.封装任务
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(demo) object:nil];
// 3.将任务添加到队列中
// 只要将一个任务添加到alloc/init的队列中, 那么队列内部会自动调用start
// 只要将一个任务添加到alloc/init的队列中, 就会开启一个新的线程执行队列
[queue addOperation:op1];


NSBlockOperation

// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.封装任务
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1 = %@", [NSThread currentThread]);
}];
// 3.将任务添加到队列中
[queue addOperation:op1];


另一种简洁写法:

// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.将任务添加到队列中
// addOperationWithBlock方法会做两件事情
// 1.根据传入的block, 创建一个NSBlockOperation对象
// 2.将内部创建好的NSBlockOperation对象, 添加到队列中
[queue addOperationWithBlock:^{
NSLog(@"1 = %@", [NSThread currentThread]);
}];


自定义子类继承NSOperation

// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.封装任务
XMGOperation *op1 = [[XMGOperation alloc] init];
// 3.将任务添加到队列中
[queue addOperation:op1];


四 最大并发数

什么是并发数:同时执行的任务数

maxConcurrentOperationCount 默认等于 -1, 代表不限制, 可以创建N多线程

默认就是并发

如果想实现串行, 那么就设置maxConcurrentOperationCount = 1

注意: 最大并发数, 不能设置为0, 否则任务不会被执行

五 队列的暂停、取消、恢复

暂停-恢复

不会暂停当前正在执行的任务,会暂停队列中的下一个任务

恢复任务,会从第一个未执行的任务恢复执行

// 如果是YES, 代表需要暂停
// 如果是NO ,代表恢复执行
self.queue.suspended = YES;
self.queue.suspended = !self.queue.suspended;


取消

不会取消当前正在执行的任务

取消后任务不能恢复

耗时操作应该每执行一段判断一次

// 内部会调用所有任务的cancel方法
[self.queue cancelAllOperations];


六 NSOperation线程间通信

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 开启子线程下载图片
[queue addOperationWithBlock:^{
NSString *urlStr = @"http://...";
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:urlStr];
NSData *data = [NSData dataWithContentsOfURL:url];

//生成下载好的图片
UIImage *image = [UIIMage imageWithData:data]
// 回到主线程更新UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.imageView.image = image;
}];

}];


七 NSOperation依赖关系

依赖和监听

只有被依赖的任务完成, 才会执行当前任务

可以跨队列依赖

[operationB addDependency:operationA]; // 操作B依赖于操作A


op1.completionBlock = ^{
NSLog(@"第一张图片下载完毕");
};
op2.completionBlock = ^{
NSLog(@"第二张图片下载完毕");
};


图片合成案例代码

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
//    queue.maxConcurrentOperationCount = 1;

__block UIImage *image1 = nil;
__block UIImage *image2 = nil;
// 1.开启一个线程下载第一张图片
NSOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:@"http://www.ibayue.com/images/fileup/201411/20141103150824.jpg"];
NSData *data = [NSData dataWithContentsOfURL:url];
// 2.生成下载好的图片
UIImage *image = [UIImage imageWithData:data];
image1 = image;
}];

// 2.开启一个线程下载第二长图片
NSOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:@"http://i0.letvimg.com/lc02_yunzhuanma/201503/07/00/48/170acdff2830753f89957742b0e8f5b8_25431356/thumb/2_400_225.jpg"];
NSData *data = [NSData dataWithContentsOfURL:url];
// 2.生成下载好的图片
UIImage *image = [UIImage imageWithData:data];
image2 = image;

}];
// 3.开启一个线程合成图片
NSOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
UIGraphicsBeginImageContext(CGSizeMake(200, 200));
[image1 drawInRect:CGRectMake(0, 0, 100, 200)];
[image2 drawInRect:CGRectMake(100, 0, 100, 200)];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

// 4.回到主线程更新UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"回到主线程更新UI");
self.imageView.image = newImage;
}];
}];

// 监听任务是否执行完毕
op1.completionBlock = ^{
NSLog(@"第一张图片下载完毕");
};
op2.completionBlock = ^{
NSLog(@"第二张图片下载完毕");
};

// 添加依赖
// 只要添加了依赖, 那么就会等依赖的任务执行完毕, 才会执行当前任务
// 注意:
// 1.添加依赖, 不能添加循环依赖
// 2.NSOperation可以跨队列添加依赖
[op3 addDependency:op1];
[op3 addDependency:op2];

// 将任务添加到队列中
[queue addOperation:op1];
[queue addOperation:op2];
[queue2 addOperation:op3];
}


八 多图片下载

字典转模型

#import "XMGApp.h"

@implementation XMGApp

- (instancetype)initWithDict:(NSDictionary *)dict{
if (self = [super init]) {
[self setValuesForKeysWithDictionary:dict];
}
return self;
}

+ (instancetype)appWithDict:(NSDictionary *)dict
{
return [[self alloc] initWithDict:dict];
}
@end


懒加载

#pragma mark - lazy
- (NSArray *)apps
{
if (!_apps) {
// 1.从plist中加载数组
NSString *path = [[NSBundle mainBundle] pathForResource:@"apps.plist" ofType:nil];
NSArray *arr = [NSArray arrayWithContentsOfFile:path];

// 2.定义数组保存转换好的模型
NSMutableArray *models = [NSMutableArray arrayWithCapacity:arr.count];

// 3.遍历数组中所有的字典, 将字典转换为模型
for (NSDictionary *dict in arr) {
XMGApp *app = [XMGApp appWithDict:dict];
[models addObject:app];
}
_apps = [models copy];
}
return _apps;
}

- (NSMutableDictionary *)imageCaches
{
if (!_imageCaches) {
_imageCaches = [NSMutableDictionary dictionary];
}
return _imageCaches;
}


#pragma mark - UITableViewDatasource

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

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//    NSLog(@"%s", __func__);
// 1.获取cell
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"app"];

// 2.设置数据
XMGApp *app = self.apps[indexPath.row];

cell.textLabel.text = app.name;
cell.detailTextLabel.text = [NSString stringWithFormat:@"下载:%@", app.download];

// 设置图片
/*
存在的问题:
1.在主线程中下载图片, 可能会阻塞主线程
2.重复下载
*/
// 1.先从内存缓存中获取, 如果没有才去下载
UIImage *image = self.imageCaches[app.icon];
if (image == nil) {

// 2.再从磁盘缓存中获取, 如果没有才去下载
NSString *filePath = [app.icon cacheDir];
NSData *data = [NSData dataWithContentsOfFile:filePath];

if (data == nil) {
NSLog(@"下载图片");
// 内存缓存中没有值, 需要下载
NSURL *url = [NSURL URLWithString:app.icon];
data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];

// 将下载好的图片缓存到内存缓存中
self.imageCaches[app.icon] = image;

// 将下载好的图片写入到磁盘
[data writeToFile:filePath atomically:YES];

// 更新UI
cell.imageView.image = image;
}else
{
NSLog(@"使用磁盘缓存");
NSData *data = [NSData dataWithContentsOfFile:filePath];
UIImage *image = [UIImage imageWithData:data];

// 将下载好的图片缓存到内存缓存中
self.imageCaches[app.icon] = image;

// 更新UI
cell.imageView.image = image;
}
}else
{
NSLog(@"使用内存缓存");
// 更新UI
cell.imageView.image = image;
}
// 3.返回cell
return cell;
}


目录结构

Documents

需要保存由”应用程序本身”产生的文件或者数据,例如:游戏进度、涂鸦软件的绘图

目录中的文件会被自动保存在 iCloud

注意:不要保存从网络上下载的文件,否则会无法上架!

Caches

保存临时文件,”后续需要使用”,例如:缓存图片,离线数据(地图数据)

系统不会清理 cache 目录中的文件

就要求程序开发时,”必须提供 cache 目录的清理解决方案”

Preferences

用户偏好,使用 NSUserDefault 直接读写!

如果要想数据及时写入磁盘,还需要调用一个同步方法

tmp

保存临时文件,”后续不需要使用”

tmp 目录中的文件,系统会自动清理

重新启动手机,tmp 目录会被清空

系统磁盘空间不足时,系统也会自动清理

封装获取文件路径方法

- (NSString *)cacheDir
{
// 1.获取cache目录
NSString *dir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
// 2.生成绝对路径
return [dir stringByAppendingPathComponent:[self lastPathComponent]];
}
- (NSString *)documentDir {
NSString *dir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
return [dir stringByAppendingPathComponent:[self lastPathComponent]];
}

- (NSString *)tmpDir {
NSString *dir = NSTemporaryDirectory();
return [dir stringByAppendingPathComponent:[self lastPathComponent]];
}


优化版本:

#pragma mark - UITableViewDatasource

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

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//    NSLog(@"%s", __func__);
// 1.获取cell
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"app"];

// 2.设置数据
XMGApp *app = self.apps[indexPath.row];

cell.textLabel.text = app.name;
cell.detailTextLabel.text = [NSString stringWithFormat:@"下载:%@", app.download];
cell.imageView.image = [UIImage imageNamed:@"abc"];  //开始没有图片,搞占位图片

// 设置图片
/*
存在的问题:
1.在主线程中下载图片, 可能会阻塞主线程
2.重复下载
*/
// 1.先从内存缓存中获取, 如果没有才去下载
UIImage *image = self.imageCaches[app.icon];
if (image == nil) {

// 2.再从磁盘缓存中获取, 如果没有才去下载
NSString *filePath = [app.icon cacheDir];
__block NSData *data = [NSData dataWithContentsOfFile:filePath];

if (data == nil) {
NSLog(@"下载图片");
/*
存在的问题:
1.重复设置
2.重复下载
*/
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

// 3.判断当前图片是否有任务正在下载
NSBlockOperation *op = self.operations[app.icon];
if (op == nil) {
// 没有对应的下载任务
op = [NSBlockOperation blockOperationWithBlock:^{
// 开启子线程下载
// 内存缓存中没有值, 需要下载
NSURL *url = [NSURL URLWithString:app.icon];
data = [NSData dataWithContentsOfURL:url];
if (data == nil) {
// 如果下载失败, 应该将当前图片对应的下载任务从缓存中移除 \以便于下次可以再次尝试下载
[self.operations removeObjectForKey:app.icon];
return;
}
UIImage *image = [UIImage imageWithData:data];
// 将下载好的图片缓存到内存缓存中
self.imageCaches[app.icon] = image;

// 将下载好的图片写入到磁盘
[data writeToFile:filePath atomically:YES];

// 回到主线程更新UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"更新UI");

// 刷新指定的行,网速慢的情况下防止图片跳来跳去
[tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];

// 从缓存中将当前图片对应的下载任务移除
[self.operations removeObjectForKey:app.icon];
}];
}];

// 先将下载任务保存到缓存中
self.operations[app.icon] = op;

// 将任务添加到队列中
[queue addOperation:op];
}

}else
{
NSLog(@"使用磁盘缓存");
NSData *data = [NSData dataWithContentsOfFile:filePath];
UIImage *image = [UIImage imageWithData:data];

// 将下载好的图片缓存到内存缓存中
self.imageCaches[app.icon] = image;

// 更新UI
cell.imageView.image = image;
}
}else
{
NSLog(@"使用内存缓存");
// 更新UI
cell.imageView.image = image;
}

// 3.返回cell
return cell;
}


图片下载注意点(面试4点)

重复下载问题

定义字典保存下载好的图片

磁盘缓存问题

内存没有尝试从磁盘获取

阻塞主线程问题

新建NSOperationQueue下载图片

重复设置问题

reloadRowsAtIndexPaths

逻辑1 - 从来没下载过
1.查看内存缓存是否有图片
2.查看磁盘缓存是否有图片
3.查看时候有任务正在下载当前图片
4.开启任务下载图片
5.写入磁盘
6.缓存到内存
7.移除下载操作
8.显示图片

逻辑2 - 已经下载过
1.查看内存缓存是否有图片
2.查看磁盘缓存是否有图片
3.使用磁盘缓存
4.将图片缓存到内存中
5.更新UI

逻辑3 - 已经下载过, 并且不是重新启动
1.查看内存缓存是否有图片
2.更新UI


九 SDWebImage架构

SDWebImageManager

SDImageCache

SDWebImageDownloader

SDWebImageDownloaderOperation

SDWebImage常见面试题

默认缓存时间多少

一周

缓存的地址

NSString *fullNamespace = [@”com.hackemist.SDWebImageCache.” stringByAppendingString:ns];

cleanDisk如何清理过期图片

删除早于过期日期的文件

保存文件属性以计算磁盘缓存占用空间

如果剩余磁盘缓存空间超出最大限额,再次执行清理操作,删除最早的文件

clearDisk如何清理磁盘

删除缓存目录

新建缓存目录

SDWebImage如何播放图片

取出gif中每一帧, 生成一张可动画图片

SDWebImage如何判断图片类型

判断图片二进制前8个字节

kPNGSignatureBytes[8] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};

多图片下载案例

- (void)viewDidLoad
{
[super viewDidLoad];
self.tableView.rowHeight = 150;
// 直接下载一张图片
/*
第1个参数: 需要下载图片的URL
第2个参数: 下载的配置信息(例如是否需要缓存等等)
第3个参数: 下载过程中的回调
第4个参数: 下载完成后的回调
*/
NSURL *url = [NSURL URLWithString:@"http://ia.topit.me/a/f9/0a/1101078939e960af9ao.jpg"];
[[SDWebImageManager sharedManager] downloadImageWithURL:url options:kNilOptions progress:^(NSInteger receivedSize, NSInteger expectedSize) {
// receivedSize : 已经接受到的数据大小
// expectedSize : 需要下载的图片的总大小
NSLog(@"正在下载 %zd %zd", receivedSize, expectedSize);
} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
// image : 下载好的图片
// error: 错误信息
// cacheType: 缓存的类型
// finished: 是否下载完成
// imageURL: 被下载的图片的地址
NSLog(@"下载成功 %@", image);
}];
}


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// 1.获取cell
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"app"];

// 2.设置数据
XMGApp *app = self.apps[indexPath.row];

cell.textLabel.text = app.name;
cell.detailTextLabel.text = [NSString stringWithFormat:@"下载:%@", app.download];

// 在老版本的SDWebImage中, 以下方法是没有sd_前缀的
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:app.icon] placeholderImage:[UIImage imageNamed:@"abc"]];

// 3.返回cell
return cell;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: