NSOperation的简单介绍

NSOperation是一个抽象类,代表与单个任务相关联的代码和数据。

由于NSOperation是一个抽象类,所以不直接使用它而是使用它的子类或者系统定义的子类(NSInvocationOperation或者NSBlockOperation)来执行任务,尽管是一个抽象类,NSOperation的基本实现包含了重要的逻辑来协调任务安全的执行。这种内置的逻辑使得我们可以专注于任务的具体实现。

operation对象是一次性对象,也就是说它只执行一次任务,不能重复使用。通常把operation添加到一个 操作队列(NSOperationQueue 类的实例)来执行。 操作队列 可以在子线程中直接执行操作,或者使用GCD间接的执行。

如果不想使用操作队列,我们可以直接在代码中调用operation的start方法来执行操作。 手动执行操作确实给代码带来更多的负担。因为启动一个未ready状态的操作会引发异常。ready属性指示操作的准备情况

操作依赖性

依赖性是以特定顺序执行操作的便捷方式。 可以使用- (void)addDependency:(NSOperation *)op;- (void)removeDependency:(NSOperation *)op; 方法来为操作添加或移除依赖。默认情况下,operation对象直到它依赖的所有的operation对象都执行完毕之后才会被视为准备就绪。一旦最后依赖的操作完成,该operation对象就准备就绪并能够执行

NSOperation的依赖关系不区分关联操作是否成功finished。(换句话说,取消某个操作也会被标记为finished)。在其依赖操作被取消或未成功完成任务的情况下,是否继续执行具有依赖关系的操作取决于我们自己。这可能需要将一些额外的错误跟踪功能添加到operation对象

KVO 兼容属性

NSOperation类的几个属性是支持KVC和KVO的,根据需要,我们可以监测这些属性来控制应用程序的其他部分。使用一下路径监测属性:

  • isCancelled - read-only

  • isAsynchronous - read-only

  • isExecuting - read-only

  • isFinished - read-only

  • isReady - read-only

  • dependencies - read-only

  • queuePriority - readable and writable

  • completionBlock - readable and writable

虽然我们可以观察这些属性,但是不应该将这些属性跟UI元素绑定,因为通常情况下UI元素的操作只能在主线程,而KVO通知可能在任何线程中。

如果为任何上述属性提供自定义实现,则自定义实现必须支持KVC和KVO。 如果自定义的NSOperation对象定义了其他属性,建议也使这些属性支持KVC和KVO。

多核考虑

NSOperation类本身是多核的。因此多线程访问是安全的,不需要创建额外的锁来同步访问。之所以这么设计是因为operation通常在创建并正在监视它的单独线程中运行。

当子类化NSOperation类时,必须确保重写的方法是线程安全的。如果在子类中实现了自定义的方法,必须确定这些方法是线程安全的。因此,必须同步访问operation的任何数据变量,以防止潜在的数据损坏。

异步和同步操作

如果要手动执行operation,而不是将其添加到一个队列中。则可以将operation设计为是同步或异步执行。 operation对象默认是同步的,在同步操作中,operation对象不会创建一个单独的线程来执行任务。当你调用同步操作的start方法时,该操作会在当前线程立即执行。当对象的start方法将控制权交还给调用者时,它本身的任务就完成了

当调用异步操作的start方法时,该方法可能会在其相应的任务完成之前返回。异步操作对象负责在单独的线程上调度它的任务。该操作可以通过直接开启新的线程,调用异步方法,或将block提交到调度队列执行来实现。当控制权返回给调用者时,操作是否正在进行并不重要,只是它可能正在运行。

如果使用队列来执行operation,使用同步操作更简单。如果手动执行操作,则可能需要将操作定义为异步。 定义一个异步操作需要更多工作,因为必须监测任务的持续状态,并且使用KVO通知监测该状态下的更改。但是,如果想要确保手动执行的操作不会阻塞线程,异步操作将会很有用。

当添加一个操作到操作队列中,队列会忽略操作的asynchronous属性的值, 并始终从一个单独的线程调用start方法。 因此,如果将operation添加到操作队列中来执行的话,就没有理由使这些operation异步。

子类化说明

NSOperation类提供了追踪操作执行状态的基本逻辑,但是除此之外,必须子类化来完成实际工作。如何创建子类取决于操作是并发执行还是非并发执行。

重写的方法

对于非并发操作,通常只需要重写一个方法:

- (void)main;

在这个方法中,放置执行给定任务所需的代码。当然,也可以自定义初始化方法,来方便的创建实例。我们可能还需要定义一些getter和setter方法来访问operation中的数据,但是,如果定义了自定义的getter/setter方法,必须保证这些方法是线程安全的

如果创建并发操作,则需要至少覆盖以下方法和属性

  • - (void)start;
  • @property(readonly, getter=isAsynchronous) BOOL asynchronous;
  • @property(readonly, getter=isExecuting) BOOL executing;
  • @property(readonly, getter=isFinished) BOOL finished;

在并发操作中,start方法负责以异步方式启动操作。无论是生成一个线程还是调用一个异步方法,都可以通过该方法来完成。开始操作时,start方法应该更新executing属性(表示操作的执行状态)。可以通过发送KVO通知,让外部知道操作正在运行。executing属性必须是线程安全的。

完成或取消任务后,并发操作对象必须为isExecutingisFinished路径生成KVO通知,标记操作状态的最终更改。(在取消的情况下,即使操作没有结束它的任务,也要更新isFinished。 排队的操作在从队列中删除之前必须是已经finished)。除了生成KVO通知,重写的executingfinished属性也应该根据操作状态作出相应的更改。

重点:

在自定义的start方法中,任何时候都不应该调用super。当定义一个并发操作,需要自己提供跟默认start方法相同的行为,其中包括启动任务并生成合适的KVO通知。自定义的start方法也应该检查在实际启动任务之前操作本身是否已经被取消。

即使是并发队列,也很少需要重写上述描述以外的方法。 然而,如果你自定义operation 的依赖功能,则还必须重写其他的方法和提供KVO通知。在有依赖关系的情况下,可能只需要提供isReady路径的通知。 因为dependencies属性包含了依赖操作的列表,对它的更改已由默认的NSOperation类处理

维护operation对象状态

operation对象在内部维护状态信息以确定什么时候执行操作是安全的,并通过operation的生命周期通知外部客户的进展情况。自定义的子类需要维护该状态信息以确保代码中的操作正确的执行。 相关的operation状态的key path:

  • @property (readonly, getter=isReady) BOOL ready;

    isReady 路径指示操作何时可以执行。ready 属性在操作准备好执行的时候为true,如果仍存在有未完成的操作依赖于它则为false。

  • @property(readonly, getter=isExecuting) BOOL executing;

    isExecuting属性指示操作是否正在处理其分配的任务。executing属性在operation正在执行任务时为true,否则为false

    如果你自定义了operation对象的start方法,则当operation的执行状态更改时,还必须更新executing属性并生成KVO通知。

  • @property(readonly, getter=isFinished) BOOL finished;

    isFinished路径指示某个操作已成功完成它的任务,或者已取消并正在退出。 operation对象不会清除一个依赖,直到isFinished为true。同样的,一个操作队列不会把一个operation出列,直到finished属性为true。因此,将operation标记为finished 对于防止队列进行备份或取消操作至关重要。

    如果你替换了start方法或操作对象,当操作结束执行或取消操作时生成KVO通知

    如果替换start方法或操作对象,则还必须替换finished属性,并在操作结束执行或取消操作时生成KVO通知。

  • @property (readonly, getter=isCancelled) BOOL cancelled;

    isCancelled路径指示operation的取消被请求。不必为该路径发送KVO通知。

响应取消命令

一旦添加operation到队列中,operation就脱离你的控制了。队列会接管并处理任务的调度。但是,如果稍后决定不想执行operation,可能是因为用户点击取消按钮或退出了应用程序,比如,你可以取消operation来防止它不必要的占用CPU时间。可以通过operation对象的cancel方法或- (void)cancelAllOperations;方法。

取消一个operation不会立即强制停止正在执行的操作。代码中必须显式的检查该属性的值并根据需要中止。NSOperation的默认实现包括检查取消状态。比如,如果在调用start方法之前取消了一个操作,start方法将退出而不启动该任务。

在任何自定义代码中,都应该始终支持取消语义。特别的,主要任务的代码应该定期性的检查cancelled属性的值。如果为YES,operation对象应该尽可能快的清理并退出。如果实现了自定义的start方法,那么该方法应该适当的提前检查取消状态。

除了当操作取消时退出操作,将取消的操作设置为一个合适的最终状态也是很重要的。如果我们自己管理finishedexecuting属性的值(可能是因为正在执行并发操作), 必须相应地更新这些属性。特别的,必须将finished属性的值返回为YES,executing属性的值为NO。即使操作在开始执行之前就被取消也必须进行这些更改。

几个方法的简单介绍

执行操作的方法
  • - (void)start;
    开始执行操作。 该方法的默认实现会更改operation的执行状态,并调用接受者的main方法。 该方法还会执行若干项检查确保操作能够实际运行。比如,如果receiver被取消或者已经finished,该方法将不会调用main方法而直接return。如果操作正在执行或者尚未准备执行,该方法或抛出 NSInvalidArgumentException 异常。

    注意:
    如果一个操作依赖的其他操作没有finished,则该认为该操作还没有准备好执行

    如果实现了并发操作,必须覆盖start方法,并使用该方法开启操作。该方法自定义的实现在任何时候都不应该调用 super。 除了为你的任务配置执行环境,该方法的自定义实现也必须跟踪operation 的状态并提供适当的状态转变。当操作执行并完成它的工作之后,还应该分别为isExecutingisFinished路径生成KVO通知。

    如果想要手动执行操作的话可以显式的调用该方法。但是,对一个已经在操作队列里operation对象调用该方法,或者在调用该方法之后对操作进行排队,都是编程错误。一旦将operation添加到队列,队列将负责管理操作

  • - (void)main;
    执行receiver的非并发任务。该方法的默认实现是实际上什么都没做。我们应该覆盖该方法来执行所需的任务。在自定义实现中,不应该调用super。该方法将会在 NSOperation提供的 autorelease pool 中自动执行,所以不需要我们创建 autorelease pool。

    如果要实现并发操作,不需要覆盖该方法。但是如果从自定义的start方法调用的话可能就需要重写该方法了

  • @property(copy) void (^completionBlock)(void);

    操作主要任务完成之后执行的block。每当一个NSOperation执行完毕,它就会调用它的completionBlock属性一次。 该block的执行上下文并不确定,但通常是在子线程中。因此,不应该使用该block执行任何需要在特定执行上下文的工作。相反,应该该工作分配给主线程或者能够处理该任务的特定线程中。

    finished属性值为YES时,该block将执行。因为block在operation完成其任务后执行,所以不能使用该block来排队等待被认为是该任务一部分的其他任务。finished属性为YES的operation对象必须完成它所有的任务。该block应该通知相应的对象工作已经完成,或者执行其他的一些任务(与operation实际任务相关,但不属于操作的一部分)

    在iOS 8以后和 macOS 10.10以后,在该block执行之后将该属性置为nil.

取消操作的方法
  • - (void)cancel;
    通知operation对象停止执行其任务。该方法不会强制停止operation的代码。相反的,它更新对象的内部标志来反应状态的变化。如果操作已经finished,该方法将不会起作用。取消一个在操作队列中,但还没有开始执行的operation,可以更快地从队列中移除操作。
属性
  • @property(readonly, getter=isCancelled) BOOL cancelled;

    指示操作是否已经取消的布尔值。

    默认是NO,调用cancel方法将该属性设为YES。 一旦取消,operation必须转至finished状态。

    取消一个operation不会直接停止receiver代码的执行。如果返回YES,operation对象负责定期地调用该方法并自行停止。

    在执行operation的任务之前,应该检查该属性的值。通常是在自定义的main方法中的开头处检查。在开始执行操作或者在执行的任意时候,操作都可能被取消。因此,在main方法的开头检查该属性的值(并在该方法中定期的检查),可以在操作取消时尽可能快的退出。

  • @property(readonly, getter=isExecuting) BOOL executing;

    指示操作是否正在执行的布尔值。

    如果operation正在执行它的主任务,属性值为YES,否则为NO。

    当实现了一个并发操作对象,必须覆盖该属性的实现,以便于可以返回操作对象的执行状态。在自定义的实现中,当操作对象的执行状态改变的时候必须为isExecuting路径生成KVO通知

    对于非并发操作不需要重新实现该属性

  • @property(readonly, getter=isFinished) BOOL finished;

    指示操作是否已完成其任务。

    如果operation已经完成它的主任务,该属性值为YES. 如果正在执行或还没有开始执行则为NO

    当实现一个并发操作时,必须重写该属性的实现,以便返回operation的完成状态。在自定义实现中,如果操作的finished状态改变的话,必须为isFinished路径生成KVO通知。

    对于非并发操作无需重写该属性的实现

  • `@property(readonly, getter=isAsynchronous) BOOL asynchronous;

    指示操作是否异步执行其任务。

    对于当前线程异步执行的操作,该属性值为YES。对于当前线程同步执行的操作为NO。默认为NO

    当实现一个异步操作对象,必须实现该属性并返回YES

  • @property(readonly, getter=isReady) BOOL ready;

    指示现在是否可以执行操作的布尔值

    操作的准备情况取决于它们对其他操作的依赖,以及你定义的自定义的条件。NSOperation类管理对其他操作的依赖关系,并根据这些依赖关系报告receiver的准备情况。

    如果想使用自定义条件来定义操作对象的准备情况,需要重写该属性的实现,并返回一个准确反映receiver的准备情况的值。如果这样做,自定义的实现必须从super获取默认的属性值,并将该值和冰岛该属性的新值中。在自定义实现中,当操作对象的准备状态改变的时候,必须为isReady路径生成KVO通知

  • @property(copy) NSString *name;

    operation的名称。 为操作对象分配一个名称,方便debug。

管理依赖
  • - (void)addDependency:(NSOperation *)op;

    使receiver依赖于指定操作的完成

    参数:
    operation: receiver应该依赖的操作。不应该向receiver重复添加相同的依赖,这样做的结果是undefined

    直到依赖的所有操作都执行结束后,receiver才会被认为准备好执行。如果receiver已经在执行其任务,添加依赖将不会有效果。该方法可能会改变receiver的isReadydependencies属性。

    注意不要造成循环依赖。这样做的话会造成操作间的死锁,并可能会freeze你的程序。

  • - (void)removeDependency:(NSOperation *)op;

    移除receiver对特定操作的依赖。

    该方法可能会更改receiver的isReadydependencies属性。

  • @property(readonly, copy) NSArray<NSOperation *> *dependencies;

    在当前对象开始执行之前必须完成执行的操作对象数组。

    该属性是包含的NSOperation对象的数组。使用addDependency:方法为该数组添加对象。

    直到依赖的操作对象都执行结束后,operation才可以执行。操作完成执行时也不会从该列表中移除。可以使用该列表跟踪所有的依赖操作,包括那些已经完成的操作。唯一能从该列表中移除operation的方法是调用removeDependency:方法。

NSBlockOperation

NSBlockOperation类是NSOperation的一个具体子类,用于管理一个或多个block的并发执行。可以使用该对象一次执行数个block,而不必为每个block创建单独的操作对象。当执行多个block时,只有当所有的block执行结束之后,该操作对象才会被视为finished。

添加到NSBlockOperation的block以默认优先级调度到适当的工作队列。block本身不应该对其执行环境做任何假设

方法介绍

  • + (instancetype)blockOperationWithBlock:(void (^)(void))block;

    创建并返回一个NSBlockOperation对象,并为其添加指定的block。

  • - (void)addExecutionBlock:(void (^)(void))block;

    添加指定block到receiver要执行的的block列表中。

    指定的block不应该对它的执行环境做任何假设。

    当receiver正在执行或者已经finished时调用该方法会抛出NSInvalidArgumentException异常

  • @property(readonly, copy) NSArray<void (^)(void)> *executionBlocks;

    与receiver相关联的block数组

    该数组中的block是调用addExecutionBlock:方法添加的block的副本。

NSInvocationOperation

NSInvocationOperation类是NSOperation的一个具体子类,用于开启一个操作,该操作包括在指定对象上调用一个selector。

方法

  • - (instancetype)initWithTarget:(id)target selector:(SEL)sel object:(id)arg;

    参数:

    target: 定义指定selector的对象。

    sel: 运行该操作对象时会调用该selector。selector可能会有0或1个参数。如果有参数,参数的类型必须是 id。 方法的返回类型可以是 void、基本数据类型,或是可以作为id类型返回的对象。

    arg: 传递给selector的参数对象。如果selector不需要参数,将arg置为nil即可。

    该方法返回一个 NSInvocationOperation对象,或者nil(如果target对象没有实现指定的selector)

    如果selector的返回类型是 非void,我们可以在操作完成执行后调用result方法来获取返回值。

  • - (instancetype)initWithInvocation:(NSInvocation *)inv;

    使用指定的NSInvocation对象初始化一个NSInvocationOperation对象。

    返回一个NSInvocationOperation对象或者nil(如果对象不能被初始化)

  • @property(readonly, retain) id result;

    invocation 或方法的结果。

    该方法返回的对象或者返回值的NSValue对象(如果返回值不是对象会转成NSValue对象)。如果方法或者invocation未完成执行,则为nil。

    如果在执行方法或调用期间发生异常,访问该属性将会再次引发该异常。如果操作被取消或者invocation或方法返回类型为void,访问该属性也会引发异常。

NSOperation与GCD

尽管GCD对于内嵌异步操作十分理想,NSOperation依旧提供更复杂、面向对象的计算模型,它对于涉及到各种类型数据、需要重复处理的任务是更加理想的

什么时候使用GCD

Dispatch queues, groups, semaphores(信号量), sources, 和 barriers 组成了一组基本的并发基元,其上构建了所有的系统框架。

对于一次性的计算或简单的加速现有方法,使用轻量级的GCD比使用NSOperation更方便。

什么时候使用NSOperation

NSOperation 可以按照特定的队列优先级和服务质量(qualityOfService)来调度一组依赖关系。与在GCD队列中的block不同, NSOperation可以被取消,以及查询其操作状态。通过继承,NSOperation可以将其工作结果与自身关联起来,供将来参考。


资料来源:
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容