小时光上传队列分享——NSOperation的使用
上传队列分享——NSOperation的使用
1. 需求分析
发一条记录的流程(并且可以同时发多条记录)
其他产品/技术需求(影响到设计所以也加在这)
- 并发要求——图片肯定要支持并发上传、记录串行上传(并行也可以,但是图片已经支持并发了,所以没意义,除非每条记录都是一张照片)。
- 任务可取消——用户可以暂停、继续、删除一条任务(影响技术选型)。
- 支持杀死app后,启动app继续任务。
2. 设计(上传队列)
不管你在何处工作,构建些什么,用何种编程语言,在软件开发上,一直伴随你的那个不变真理是什么?—— 《Head First 设计模式》
A. 年轻时的想法(针对实现设计)
维护两种队列,暂且称为一级队列和二级队列。数据模型的设计和看到的界面(业务)是对应的,思路比较自然。现在我称之为针对实现设计。优缺点我们后面陈述。
B. 稍后来的想法(面向抽象设计/分层)
主要分为两层:
- 上传层(只负责上传图片,有上传单张图片的任务扔过来就好)
- 日记队列层(面向业务)
两者最主要的区别:
计A每一处设计都是面向业务的,没有抽象;设计B中抽象层是把“上传图片功能”抽象了出来,可以设计的完全不知道业务(什么叫不知道业务?)
B的设计有什么优点?
- 易扩充——由于是抽象出来的通用模块,App中其他的上传图片业务(比如更改头像、支持H5上传一张图片)都可以不改代码直接使用,甚至换成别的App也可以直接拿过来使用。
- 易维护——假如上传图片不使用七牛了,换成了腾讯云,只需要更改上传层的代码就好了,不然的话,可能会改很多地方。
上传层其实还可以抽象出很多层
3. 技术实现
Why NSOperation?
最关键是实现上传队列,第一反应肯定是考虑GCD或者NSOperation。这里说下最终选择NSOperation的原因:
- NSOperation天然的OOP,我们只需要将单个上传的逻辑封装在一个NSOperation即可,而使用GCD,需要额外很多代码来封装。
- 需要支持cancel一个任务,所以选择NSOperation会方便些。
NSOperation类介绍
对于NSOperation,需要了解以下几件事:
- NSOperation为抽象类,我们直接使用NSOperation,一般都是自定义NSOperation的子类,或者使用系统提供的两个子类,NSInvocationOperation和NSBlockOperation。(后两个其实都不怎么常用,自己查资料了解)。
- 两种执行NSOperation的方式:扔到NSOperation Queue里或者调用“start”方法手动触发。
- Dependencies,使用NSOperation可以方便的设置任务之间的依赖关系(面试总爱问,咱们没有用,其实有个场景想一下觉得很合适?)
- 如果手动调用start执行一个任务,那么默认任务将会在调用start的线程,同步执行。(isAsynchronous属性)
- When you add an operation to an operation queue, the queue ignores the value of the asynchronous property and always calls the start method from a separate thread.
自定义NSOperation子类(今天讲的重点)
1. 使用好处
- 天然分离代码到一个类中,不容易造成代码臃肿
- 对于子线程里做异步任务,能够比较灵活的处理(每个NSOperation里执行的依然是一个异步任务、token、七牛上传),换句话讲,能够自己来控制每个NSOperation的状态。
2. 典型做法
先回忆一下,咱们要干什么 —— 用一个队列维护一个或者多个任务,每个任务就是上传一个图片到七牛
先来解决“每个任务就是上传一个图片到七牛”这件事,会想到3点要面临的挑战:
- 上传这个耗时的任务,放哪,怎么处理。
- 怎么得知任务已经完成。
- 如果中途取消了怎么处理。
下面通过代码来看下使用NSOperation自定义类,怎么处理这三个问题,代码来自Apple官方文档《Concurrency Programming Guide》,可以对照着看下咱们的代码。
@interface MyOperation : NSOperation {
BOOL executing;
BOOL finished;
}
- (void)completeOperation;
@end
@implementation MyOperation
- (id)init {
self = [super init];
if (self) {
executing = NO;
finished = NO;
}
return self;
}
- (BOOL)isConcurrent { // ------------2
return YES;
}
- (BOOL)isExecuting { // ------------2
return executing;
}
- (BOOL)isFinished { // ------------2
return finished;
}
- (void)start { // ------------1
// Always check for cancellation before launching the task.
if ([self isCancelled]) // ------------3
{
// Must move the operation to the finished state if it is canceled.
[self willChangeValueForKey:@"isFinished"];
finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}
// If the operation is not canceled, begin executing the task.
[self willChangeValueForKey:@"isExecuting"];
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
executing = YES;
[self didChangeValueForKey:@"isExecuting"];
}
- (void)main { // ------------1
@try {
// Do the main work of the operation here.
[self completeOperation];
}
@catch(...) {
// Do not rethrow exceptions.
}
}
- (void)completeOperation {
[self willChangeValueForKey:@"isFinished"]; // ------------4
[self willChangeValueForKey:@"isExecuting"];
executing = NO;
finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
@end
代码解读
- 前面已经说过NSOperation是抽象类了,所以子类肯定要实现它的抽象方法,一般需要实现start、main方法。start方法里一般,更改状态,main用来执行主要任务(上传图片放这里!!)
- 覆写几个状态的getter方法。指定用来记录状态的变量,比如executing、finished,所以系统可以根据NSOperation的状态,进行相应的处理。
- 阶段性检查任务是否被cancel,被cancel之后要把executing、finished改为对应的状态值。(这一步很重要)。
- 手动触发KVO,告诉系统任务的状态发生了改变。然后系统调用getter方法时,发现isFinished状态变成了YES,则认为这个任务完成,移除NSOperation Queue。
剩余一些细节
-
上传七牛之前,需要先通过咱们自己API获取token。如果不能自己灵活控制NSOperation的状态,很多步骤都要做出成同步的(自定义NSOperation给我们更大的灵活性,无论这个任务有多复杂,是同步还是异步,都可以控制它状态的变化)
-
我们知道,每个Operation的main方法,肯定是会并发运行的,而token的获取其实只要获取一次,就好了,所以,我们使用了信号量dispatch_semaphore来确保只之执行一次token的请求。
-
然后,每张图片上传之前,会使用系统方法,做一次人脸识别、写入一次Exif信息,这两部都是非常占用内存的。如果并发执行,很有可能让内容冲到一定高度而Out Of Memory,为了避免这个问题,一个是人脸识别只使用一张小图进行识别(不超过640*640),并且对于这两个过程,加锁。(关于iOS里几种锁的用法和优缺点,建议了解一下,面试特别爱问)
3. 将任务放到Queue中
现在我们已经知道一个任务如何实现了,只需要将NSOperation扔到NSOperation Queue中,就会自动执行了。并发数可以使用NSOperation Queue的maxConcurrentOperationCount
来控制并发数。
考虑一个问题:什么时候往Queue里添加NSOperation?(一次性全加入?还是?)
4. 多线程其他知识
异步任务的同步处理几种方法
- NSOperation自定义子类
- 使用GCD group的时候,可以对其中异步任务使用dispatch_group_enter和dispatch_group_leave
- GCD信号量——dispatch_semaphore ['seməfɔ:]
手动触发KVO
- (void)setHTTPShouldHandleCookies:(BOOL)HTTPShouldHandleCookies {
[self willChangeValueForKey:NSStringFromSelector(@selector(HTTPShouldHandleCookies))];
_HTTPShouldHandleCookies = HTTPShouldHandleCookies;
[self didChangeValueForKey:NSStringFromSelector(@selector(HTTPShouldHandleCookies))];
}
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([AFHTTPRequestSerializerObservedKeyPaths() containsObject:key]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
5. 推荐工具
- SpaceLauncher
- Magnet
- MWeb(强烈推荐,几大功能:预览主题、一键把网页变成Markdown、导出、侧边栏)
- 小技巧——写markdown的时候,怎么方便的对齐代码