小时光上传队列分享——NSOperation的使用

上传队列分享——NSOperation的使用

1. 需求分析

发一条记录的流程(并且可以同时发多条记录)

img1

其他产品/技术需求(影响到设计所以也加在这)

  1. 并发要求——图片肯定要支持并发上传、记录串行上传(并行也可以,但是图片已经支持并发了,所以没意义,除非每条记录都是一张照片)。
  2. 任务可取消——用户可以暂停、继续、删除一条任务(影响技术选型)。
  3. 支持杀死app后,启动app继续任务。

2. 设计(上传队列)

不管你在何处工作,构建些什么,用何种编程语言,在软件开发上,一直伴随你的那个不变真理是什么?—— 《Head First 设计模式》

A. 年轻时的想法(针对实现设计)

维护两种队列,暂且称为一级队列和二级队列。数据模型的设计和看到的界面(业务)是对应的,思路比较自然。现在我称之为针对实现设计。优缺点我们后面陈述。

img2

B. 稍后来的想法(面向抽象设计/分层)

主要分为两层:

  • 上传层(只负责上传图片,有上传单张图片的任务扔过来就好)
  • 日记队列层(面向业务)

img3

两者最主要的区别:

计A每一处设计都是面向业务的,没有抽象;设计B中抽象层是把“上传图片功能”抽象了出来,可以设计的完全不知道业务(什么叫不知道业务?)

B的设计有什么优点?

  • 易扩充——由于是抽象出来的通用模块,App中其他的上传图片业务(比如更改头像、支持H5上传一张图片)都可以不改代码直接使用,甚至换成别的App也可以直接拿过来使用。
  • 易维护——假如上传图片不使用七牛了,换成了腾讯云,只需要更改上传层的代码就好了,不然的话,可能会改很多地方。

上传层其实还可以抽象出很多层

3. 技术实现

Why NSOperation?

最关键是实现上传队列,第一反应肯定是考虑GCD或者NSOperation。这里说下最终选择NSOperation的原因:

  1. NSOperation天然的OOP,我们只需要将单个上传的逻辑封装在一个NSOperation即可,而使用GCD,需要额外很多代码来封装。
  2. 需要支持cancel一个任务,所以选择NSOperation会方便些。

NSOperation类介绍

对于NSOperation,需要了解以下几件事:

  1. NSOperation为抽象类,我们直接使用NSOperation,一般都是自定义NSOperation的子类,或者使用系统提供的两个子类,NSInvocationOperation和NSBlockOperation。(后两个其实都不怎么常用,自己查资料了解)。
  2. 两种执行NSOperation的方式:扔到NSOperation Queue里或者调用“start”方法手动触发。
  3. Dependencies,使用NSOperation可以方便的设置任务之间的依赖关系(面试总爱问,咱们没有用,其实有个场景想一下觉得很合适?)
  4. 如果手动调用start执行一个任务,那么默认任务将会在调用start的线程,同步执行。(isAsynchronous属性)
  5. 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. 使用好处

  1. 天然分离代码到一个类中,不容易造成代码臃肿
  2. 对于子线程里做异步任务,能够比较灵活的处理(每个NSOperation里执行的依然是一个异步任务、token、七牛上传),换句话讲,能够自己来控制每个NSOperation的状态。

2. 典型做法

先回忆一下,咱们要干什么 —— 用一个队列维护一个或者多个任务,每个任务就是上传一个图片到七牛

先来解决“每个任务就是上传一个图片到七牛”这件事,会想到3点要面临的挑战:

  1. 上传这个耗时的任务,放哪,怎么处理。
  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

代码解读

  1. 前面已经说过NSOperation是抽象类了,所以子类肯定要实现它的抽象方法,一般需要实现start、main方法。start方法里一般,更改状态,main用来执行主要任务(上传图片放这里!!
  2. 覆写几个状态的getter方法。指定用来记录状态的变量,比如executing、finished,所以系统可以根据NSOperation的状态,进行相应的处理。
  3. 阶段性检查任务是否被cancel,被cancel之后要把executing、finished改为对应的状态值。(这一步很重要)。
  4. 手动触发KVO,告诉系统任务的状态发生了改变。然后系统调用getter方法时,发现isFinished状态变成了YES,则认为这个任务完成,移除NSOperation Queue。

剩余一些细节

  1. 上传七牛之前,需要先通过咱们自己API获取token。如果不能自己灵活控制NSOperation的状态,很多步骤都要做出成同步的(自定义NSOperation给我们更大的灵活性,无论这个任务有多复杂,是同步还是异步,都可以控制它状态的变化)

  2. 我们知道,每个Operation的main方法,肯定是会并发运行的,而token的获取其实只要获取一次,就好了,所以,我们使用了信号量dispatch_semaphore来确保只之执行一次token的请求。

  3. 然后,每张图片上传之前,会使用系统方法,做一次人脸识别、写入一次Exif信息,这两部都是非常占用内存的。如果并发执行,很有可能让内容冲到一定高度而Out Of Memory,为了避免这个问题,一个是人脸识别只使用一张小图进行识别(不超过640*640),并且对于这两个过程,加锁。(关于iOS里几种锁的用法和优缺点,建议了解一下,面试特别爱问)

3. 将任务放到Queue中

现在我们已经知道一个任务如何实现了,只需要将NSOperation扔到NSOperation Queue中,就会自动执行了。并发数可以使用NSOperation Queue的maxConcurrentOperationCount来控制并发数。

考虑一个问题:什么时候往Queue里添加NSOperation?(一次性全加入?还是?)

4. 多线程其他知识

异步任务的同步处理几种方法

  1. NSOperation自定义子类
  2. 使用GCD group的时候,可以对其中异步任务使用dispatch_group_enter和dispatch_group_leave
  3. 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. 推荐工具

  1. SpaceLauncher
  2. Magnet
  3. MWeb(强烈推荐,几大功能:预览主题、一键把网页变成Markdown、导出、侧边栏)
  4. 小技巧——写markdown的时候,怎么方便的对齐代码

6. 参考文档

  1. 苹果文档NSOperation
  2. https://developer.apple.com/library/content/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationObjects/OperationObjects.html#//apple_ref/doc/uid/TP40008091-CH101-SW1
posted @ 2017-12-28 15:52  张驰小方块  阅读(763)  评论(0编辑  收藏  举报