iOS操作隊(duì)列

Cocoa操作(operation)是一種面向?qū)ο蟮姆绞絹矸庋b您想要異步執(zhí)行的工作。操作被設(shè)計(jì)用來和操作隊(duì)列(operation queue)一起使用或者由他們自己使用。因?yàn)樗麄兪腔贠bjective-C,操作常用于基于Cocoa的OS X和iOS應(yīng)用程序。

下面介紹如何定義和使用操作。

關(guān)于操作對象

操作對象是NSOperation類(在Foundation框架中)的實(shí)例,使用它來封裝您想要應(yīng)用程序執(zhí)行的工作。NSOperation類本身是一個(gè)抽象基類,必須將其子類化才能做任何有用的工作。盡管是抽象的,這個(gè)類確實(shí)提供了大量的基礎(chǔ)結(jié)構(gòu),以減少您在自己子類中的工作量。此外,基礎(chǔ)框架提供了兩個(gè)具體子類,可以和您已經(jīng)存在的代碼一樣使用。下標(biāo)列出了這些類,和一些怎么使用他們的摘要信息。

描述
NSInvocationOperation 您可以使用此類從您應(yīng)用程序中基于一個(gè)對象(object)和選擇器(selector)創(chuàng)建一個(gè)操作對象。如果已經(jīng)存在一個(gè)執(zhí)行任務(wù)所需要的方法,您可以使用此類。因?yàn)樗恍枰宇惢部梢砸愿鼊討B(tài)的方式使用此類創(chuàng)建操作對象。有關(guān)如何使用這個(gè)類的信息,請參閱創(chuàng)建NSInvocationOperation對象。
NSBlockOperation 您可以使用此類并發(fā)的執(zhí)行一個(gè)或者多個(gè)塊對象(block object)。因?yàn)樘梢詧?zhí)行多個(gè)塊,所以塊操作對象使用組語義操作。只有當(dāng)所有相關(guān)聯(lián)的塊都執(zhí)行完成時(shí),操作本身才被認(rèn)為完成操作。有關(guān)如何使用這個(gè)類的信息,請參閱創(chuàng)建NSBlockOperation對象
NSOperation 用于定義自定義操作對象的基類,子類化NSOperation給你完全的控制權(quán)來實(shí)現(xiàn)您自己的操作,包括改變操作執(zhí)行的默認(rèn)行為和記錄其狀態(tài)。有關(guān)如何定義自定義操作對象的信息,請參閱定義自定義操作對象。

所有操作對象都支持以下主要特性:

  • 支持在操作對象間建立基于圖的依賴關(guān)系。這些依賴關(guān)系阻止給定的操作運(yùn)行,直到它所依賴的所有操作都運(yùn)行結(jié)束。有關(guān)如何配置依賴,請參閱配置并發(fā)執(zhí)行操作。
  • 支持一個(gè)可選的完成塊(completion block),在操作的主任務(wù)完成后執(zhí)行。有關(guān)如何設(shè)置完成塊,請參閱設(shè)置完成塊。
  • 支持使用KVO通知監(jiān)視操作執(zhí)行狀態(tài)變化。有關(guān)如何觀察KVO通知,請參閱維持KVO規(guī)范
  • 支持優(yōu)先級操作,從而影響相對執(zhí)行順序。欲了解跟多信息,請參閱改變操作的執(zhí)行優(yōu)先級
  • 支持取消語義,允許您在執(zhí)行操作時(shí)停止操作。有關(guān)如何取消操作,請參閱取消操作。有關(guān)如何在您自己的操作中支持取消,請參閱響應(yīng)取消事件

操作的設(shè)計(jì)是為了幫助您在您的應(yīng)用程序中提高并發(fā)的級別。操作也是一個(gè)很好的方式將您應(yīng)用程序的行為組織和封裝成簡單的離散塊。你可以提交一個(gè)或多個(gè)操作對象到隊(duì)列,并且讓相應(yīng)的工作在一個(gè)或多個(gè)單獨(dú)的線程上異步執(zhí)行,而不是在您應(yīng)用程序的主線程上運(yùn)行一些代碼。

并發(fā)操作與非并發(fā)操作

雖然您通常通過添加操作到操作隊(duì)列中來執(zhí)行操作,但這并不是必須的。也可以通過調(diào)用操作對象的start方法來手動執(zhí)行,但這么做并不能保證該操作和您其他的代碼并行運(yùn)行。NSOperation類的isConcurrent方法告訴您操作與調(diào)用start方法的線程是同步還是異步運(yùn)行。默認(rèn)情況下,該方法返回NO,這意味著操作在調(diào)用線程中同步運(yùn)行。

如果您想實(shí)現(xiàn)并發(fā)操作(也就是說一個(gè)和調(diào)用線程異步執(zhí)行的操作),您必須寫一些額外的代碼來異步開始操作。例如,您可能產(chǎn)生一個(gè)獨(dú)立的線程,調(diào)用一個(gè)異步的系統(tǒng)功能,或者做一些其他事情來確保start方法開始執(zhí)行任務(wù),并且很有可能在任務(wù)執(zhí)行結(jié)束之前就立即返回。

大多數(shù)開發(fā)人員應(yīng)該從不需要實(shí)現(xiàn)并發(fā)操作對象。如果您總是添加操作到操作隊(duì)列,則不需要實(shí)現(xiàn)并發(fā)操作。當(dāng)您提交一個(gè)非并發(fā)操作到操作隊(duì)列時(shí),隊(duì)列本身會創(chuàng)建一個(gè)線程來運(yùn)行您的操作。因此,添加一個(gè)非并發(fā)操作到隊(duì)列,結(jié)果仍然會異步執(zhí)行您操作對象中的代碼。只有在您需要異步執(zhí)行操作又不添加操作到隊(duì)列的地方才需要定義并發(fā)操作。

有關(guān)如何創(chuàng)建并發(fā)操作,請參閱配置并發(fā)執(zhí)行操作NSOperation類參考

創(chuàng)建NSInvocationOperation對象

NSInvocationOperation類是NSOperation類的具體子類,當(dāng)運(yùn)行時(shí),在您指定的對象上調(diào)用您指定的選擇器(selector)。使用此類來避免在您應(yīng)用程序中為每個(gè)任務(wù)定義大量的自定義操作對象。特別是如果您正在修改現(xiàn)有的應(yīng)用程序,并且已經(jīng)有了執(zhí)行必要任務(wù)所需要的方法和對象。當(dāng)您想要根據(jù)情況能夠改變調(diào)用方法時(shí),您也可以使用它。例如,您可能使用一個(gè)調(diào)用操作,基于用戶的輸入來動態(tài)選擇來執(zhí)行一個(gè)選擇器(selector)。

創(chuàng)建調(diào)用操作的過程是簡單的。您創(chuàng)建并初始化此類的一個(gè)新的實(shí)例,傳遞需要的對象和需要執(zhí)行的選擇器到初始化方法。下面代碼給出了演示創(chuàng)建過程的自定義類的兩個(gè)方法。taskWithData:方法創(chuàng)建一個(gè)新的調(diào)用對象,并且用另一個(gè)方法的名字提供給他,此方法包含任務(wù)的實(shí)現(xiàn)。

@implementation MyCustomClass
- (NSOperation*)taskWithData:(id)data {
    NSInvocationOperation* theOp = [[NSInvocationOperation alloc] initWithTarget:self
                    selector:@selector(myTaskMethod:) object:data];
 
   return theOp;
}
 
// 這是執(zhí)行任務(wù)實(shí)際工作的方法.
- (void)myTaskMethod:(id)data {
    // 執(zhí)行任務(wù).
}
@end

創(chuàng)建NSBlockOperation對象

NSBlockOperation類是NSOperation類的具體子類,它為一個(gè)或多個(gè)塊對象(block object)充當(dāng)封裝器。這個(gè)類為已經(jīng)使用操作隊(duì)列并且也不想創(chuàng)建調(diào)度隊(duì)列的應(yīng)用程序提供一個(gè)面向?qū)ο蟮姆庋b器。您也可以使用塊操作來利用操作依賴,KVO和可能不適用于調(diào)度隊(duì)列的其他特性。

當(dāng)您創(chuàng)建一個(gè)塊操作時(shí),您通常在初始化時(shí)添加至少一個(gè)塊,您稍后可以根據(jù)需要添加更多的塊。當(dāng)NSBlockOperation對象到了執(zhí)行的時(shí)間時(shí),塊操作對象提交它所有的塊到一個(gè)默認(rèn)優(yōu)先級的并發(fā)調(diào)度隊(duì)列。塊操作對象然后等待,直到所有的塊完成執(zhí)行。當(dāng)最后一個(gè)塊結(jié)束執(zhí)行的時(shí)候,操作對象標(biāo)記自己為已完成。因此,您可以使用快操作來跟蹤一組執(zhí)行塊,就像使用一個(gè)線程連接合并多個(gè)線程的結(jié)果一樣。區(qū)別是,因?yàn)閴K操作本身運(yùn)行在一個(gè)獨(dú)立的線程上,當(dāng)?shù)却龎K操作完成時(shí)您應(yīng)用程序的其他線程可以繼續(xù)工作。

下面代碼顯示了如何創(chuàng)建NSBlockOperation對象的簡單例子。塊本身沒有參數(shù)也沒有有意義的返回結(jié)果。

NSBlockOperation* theOp = [NSBlockOperation blockOperationWithBlock: ^{
      NSLog(@"Beginning operation.\n");
      // 做一些工作.
   }];

創(chuàng)建塊操作對象后,你可以使用addExecutionBlock:方法添加更多塊到操作對象。如果您需要串?行的執(zhí)行塊,您必須將他們直接提交到期望的調(diào)度隊(duì)列。

定義自定義操作對象

如果塊操作和調(diào)用操作對象不能夠完全滿足您應(yīng)用程序的需求,您可以直接子類化NSOperation,并添加您需要的任何行為。NSOperation類為所有操作對象提供通用的子類化要點(diǎn)。該類也提供大量有意義的基礎(chǔ)結(jié)構(gòu)為依賴和KVO通知處理大部分工作。然而,有時(shí)可能仍然需要您補(bǔ)充實(shí)現(xiàn)現(xiàn)有的基礎(chǔ)結(jié)構(gòu),以確保您的操作行為是正確的。您必須做的額外工作的工作量,取決于您實(shí)現(xiàn)的是非并發(fā)操作還是并發(fā)操作。

定義非并發(fā)操作比定義并發(fā)操作簡單的多。對于非并發(fā)操作,您索要做的是執(zhí)行主要任務(wù)并且適當(dāng)?shù)捻憫?yīng)取消事件;現(xiàn)有的類基礎(chǔ)結(jié)構(gòu)為您處理其他所有工作。對于并發(fā)操作,您必須使用自定義代碼替換一些現(xiàn)有的基礎(chǔ)結(jié)構(gòu)。以下部分為您說明怎么實(shí)現(xiàn)兩種類型的對象。

執(zhí)行主要任務(wù)

每個(gè)操作對象應(yīng)該至少實(shí)現(xiàn)以下方法:

  • 一個(gè)自定義的初始化方法
  • main

您需要一個(gè)自定義的初始化方法把您的操作對象放到一個(gè)已知的狀態(tài),和一個(gè)自定義的main方法來執(zhí)行您的任務(wù)。當(dāng)然,您可以根據(jù)需要實(shí)現(xiàn)其他方法,如下所示:

  • 您打算從您實(shí)現(xiàn)的main方法中調(diào)用的自定義方法
  • 用于設(shè)置數(shù)據(jù)和訪問操作結(jié)果的存取方法
  • 允許您來歸檔和解檔操作對象的NSCoding協(xié)議方法

下面代碼顯示了自定義NSOperation子類的一個(gè)原始模板。(這個(gè)代碼并沒有顯示如何處理取消,但顯示了您通常會有的方法,有關(guān)處理取消的信息,請參閱響應(yīng)取消事件。)這個(gè)類的初始化方法使用data參數(shù)接收一個(gè)對象,并且在操作對象內(nèi)部存儲對它的引用。在返回結(jié)果到您的應(yīng)用程序之前,main方法表面上為處理data對象。

@interface MyNonConcurrentOperation : NSOperation
@property id (strong) myData;
-(id)initWithData:(id)data;
@end
 
@implementation MyNonConcurrentOperation
- (id)initWithData:(id)data {
   if (self = [super init])
      myData = data;
   return self;
}
 
-(void)main {
   @try {
      // Do some work on myData and report the results.
   }
   @catch(...) {
      // Do not rethrow exceptions.
   }
}
@end

對于如何實(shí)現(xiàn)一個(gè)NSOperation子類的詳細(xì)例子,請參閱NSOperationSample

響應(yīng)取消事件

操作開始執(zhí)行后,它將繼續(xù)執(zhí)行它的任務(wù)直到它結(jié)束或者直到您的代碼顯式的取消操作。取消可以發(fā)生在任何時(shí)間,即使在操作開始執(zhí)行之前。雖然NSOperation為客戶提供一個(gè)方法來取消操作,但識別取消事件必然是自愿的。如果操作完全終止,則可能無法回收已經(jīng)分配的資源。因此,期望操作對象能夠檢查取消事件,并且在操作中發(fā)生取消時(shí)能夠優(yōu)雅的退出。

為了在操作對象中支持取消,您所要做的就是在您的自定義代碼中適時(shí)的調(diào)用對象的isCancelled方法,如果它返回YES,立即返回。支持取消是很重要的,不管您的操作持續(xù)的時(shí)間多長、您直接繼承NSOperation或使用其具體子類之一。isCancelled方法本身是非常輕量的,并且可以被頻繁調(diào)用而沒有任何顯著的性能損失。當(dāng)設(shè)計(jì)您的操作對象時(shí),應(yīng)該在您代碼的以下地方考慮調(diào)用isCancelled方法:

  • 執(zhí)行任何實(shí)際工作之前立即調(diào)用
  • 在循環(huán)的每次迭代期間至少調(diào)用一次,如果每次迭代相對較長,調(diào)用多次
  • 在您代碼中可能比較容易終止操作的任何地方調(diào)用

以下代碼提供了一個(gè)簡單例子,顯示在操作對象的main方法中如何響應(yīng)取消事件。在這種情況下,while的每次循環(huán)都調(diào)用isCancelled方法,在再次定期開始工作之前,允許快速退出。

- (void)main {
   @try {
      BOOL isDone = NO;
 
      while (![self isCancelled] && !isDone) {
          // Do some work and set isDone to YES when finished
      }
   }
   @catch(...) {
      // Do not rethrow exceptions.
   }
}

雖然前面的代碼不包含清除代碼,但您自己的代碼應(yīng)當(dāng)確保釋放由您的自定義代碼分配的任何資源。

配置并發(fā)執(zhí)行操作

操作對象默認(rèn)情況下是以同步方式執(zhí)行,也就是說,他們在調(diào)用start方法的線程上執(zhí)行他們的任務(wù)。因?yàn)椴僮麝?duì)列為非并發(fā)操作提供線程,因此,大部分操作還是異步運(yùn)行。然而,如果您打算手動執(zhí)行操作,并且想要他們異步執(zhí)行,您必須采用適當(dāng)?shù)拇胧﹣泶_保他們是異步執(zhí)行。您可以通過定義您的操作對象為并發(fā)操作來做到這一點(diǎn)。

下表列出了實(shí)現(xiàn)并發(fā)操作通常覆蓋的方法。

方法 描述
start (必須)所有并發(fā)操作必須覆蓋這個(gè)方法,并通過自定義實(shí)現(xiàn)替換默認(rèn)行為。通過調(diào)用start方法來手動執(zhí)行一個(gè)操作。因此,您的這個(gè)方法的實(shí)現(xiàn)是您的操作的起始點(diǎn),也是您執(zhí)行任務(wù)而設(shè)置線程或者其他操作環(huán)境的地方。您的實(shí)現(xiàn)任何時(shí)候都不能夠調(diào)用super方法。
main (可選)這個(gè)方法通常用來實(shí)現(xiàn)關(guān)聯(lián)到操作對象的任務(wù)。雖然您可以在start方法中執(zhí)行任務(wù),但使用這個(gè)方法來實(shí)現(xiàn)任務(wù)可以將您的設(shè)置和任務(wù)代碼單獨(dú)分離開。
isExecuting/isFinished (必須)并發(fā)操作負(fù)責(zé)設(shè)置他們的操作環(huán)境,并且向外部客戶報(bào)告環(huán)境狀態(tài)。因此,一個(gè)并發(fā)對象必須維護(hù)一些狀態(tài)信息來明確任務(wù)什么時(shí)候正在執(zhí)行、什么時(shí)候已經(jīng)執(zhí)行結(jié)束。然后必須通過這些方法來報(bào)告狀態(tài)。您的這些方法的實(shí)現(xiàn)必須能夠被其他線程同時(shí)安全調(diào)用。當(dāng)這些方法報(bào)告的狀態(tài)值變化時(shí),您必須為期望的鍵路徑(key path)生成適當(dāng)?shù)腒VO通知。
isConcurrent (必須)為了標(biāo)記一個(gè)操作是并發(fā)的,覆蓋這個(gè)方法并返回YES

剩下的這部分顯示了一個(gè)實(shí)現(xiàn)MyOperation類的例子,它示范了實(shí)現(xiàn)并發(fā)操作需要實(shí)現(xiàn)的基本代碼。MyOperation類在自己創(chuàng)建的一個(gè)單獨(dú)線程上簡單執(zhí)行了自己的main方法。main方法執(zhí)行的實(shí)際工作無關(guān)緊要。這個(gè)例子是為了示范在定義并發(fā)操作您需要提供的基礎(chǔ)結(jié)構(gòu)。

下面代碼展示了MyOperation類的接口和部分實(shí)現(xiàn)。MyOperation類的isConcurrent,isExecutionisFinished方法的實(shí)現(xiàn)都相當(dāng)簡單。isConcurrent方法簡單返回YES來標(biāo)示這是一個(gè)并發(fā)操作。isExecutionisFinished方法簡單返回存儲在類自身中實(shí)例變量的值。

@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 {
    return YES;
}
 
- (BOOL)isExecuting {
    return executing;
}
 
- (BOOL)isFinished {
    return finished;
}
@end

下面代碼展示了MyOperationstart方法。這個(gè)是方法的最小實(shí)現(xiàn),也是為了示范必須執(zhí)行的任務(wù)。在這種情況下,這個(gè)方法簡單啟動一個(gè)線程,并且配置它來調(diào)用main方法。這個(gè)方法還更新成員變量executing,并且為isExecuting鍵路徑生成KVO通知來反映該值的變化。在它的工作結(jié)束后,然后方法簡單返回,留下剛才的獨(dú)立線程來執(zhí)行實(shí)際任務(wù)。

- (void)start {
   // Always check for cancellation before launching the task.
   if ([self isCancelled])
   {
      // 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"];
}

下面代碼為MyOperation類展示了剩余的實(shí)現(xiàn)。如上面代碼看到的,main方法是新線程的入口。它執(zhí)行關(guān)聯(lián)到操作對象的工作,并在工作結(jié)束的時(shí)候調(diào)用自定義的completeOperation方法。completeOperation方法然后為isExecutingisFinished鍵路徑生成所需的KVO通知來反映操作狀態(tài)的變化

- (void)main {
   @try {
 
       // Do the main work of the operation here.
 
       [self completeOperation];
   }
   @catch(...) {
      // Do not rethrow exceptions.
   }
}
 
- (void)completeOperation {
    [self willChangeValueForKey:@"isFinished"];
    [self willChangeValueForKey:@"isExecuting"];
 
    executing = NO;
    finished = YES;
 
    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];
}

即使操作被取消,您應(yīng)該總是通知KVO監(jiān)聽者您的操作現(xiàn)在已經(jīng)完成。當(dāng)一個(gè)操作對象依賴于其他操作對象的結(jié)束時(shí),它監(jiān)視這些對象的isFinished鍵路徑。只有當(dāng)所有對象報(bào)告它們已經(jīng)結(jié)束,依賴操作才標(biāo)示它已經(jīng)準(zhǔn)備運(yùn)行。不生成結(jié)束通知會阻止您應(yīng)用程序中其他操作的執(zhí)行。

維持KVO規(guī)范

NSOperation類的以下鍵路徑是符合KVO規(guī)范的:

  • isCancelled
  • isConcurrent(iOS7之后使用isAsynchronous,iOS7之后concurrent屬性由asynchronous替代)
  • isExecuting
  • isFinished
  • isReady
  • dependencies
  • queuePriority
  • completionBlock

如果您覆蓋start方法,或者除了覆蓋main方法外做NSOperation對象的任何重要自定義,您必須確保您的自定義對象為他們的鍵路徑保持KVO規(guī)范。當(dāng)覆蓋start方法時(shí),您最需要注意的鍵路徑是isExecutingisFinished。它們是重新實(shí)現(xiàn)這些方法最經(jīng)常被影響到的鍵路徑。

如果您想實(shí)現(xiàn)支持依賴其他操作對象的一些東西,您也可以覆蓋isReady方法并且強(qiáng)制它返回NO,直到您的自定義依賴都滿足(如果您實(shí)現(xiàn)自定義依賴關(guān)系,且仍然支持NSOperation類提供的默認(rèn)依賴管理系統(tǒng),請確保在isReady方法中調(diào)用super方法)。當(dāng)操作對象的準(zhǔn)備就緒狀態(tài)改變時(shí),為isReady鍵路徑生成KVO通知來報(bào)告這些變化。除非你覆蓋addDependency:removeDependency:方法,否則您應(yīng)該不需要為dependencies鍵路徑生成KVO通知而擔(dān)心。

雖然您可以為NSOperation的其他鍵路徑生成KVO通知,但不大可能需要您一直這么做。如果您需要取消操作,您可以簡單的調(diào)用現(xiàn)有的cancel方法。同樣的,很少需要您修改操作對象的隊(duì)列優(yōu)先級信息。最后,除非您的操作有可能動態(tài)改變并發(fā)狀態(tài),否則您不需要為isConcurrent鍵路徑提供KVO通知。

有關(guān)怎么在您自定義對象中支持鍵值觀察的更多信息,請參閱鍵值觀察指南。

自定義操作對象的執(zhí)行行為

操作對象的配置發(fā)生在您已經(jīng)創(chuàng)建好它們之后,但在您添加它們到隊(duì)列之前。這一部分描述的配置可以被應(yīng)用到所有的操作對象中,不管是您自己子類的NSOperation還是使用現(xiàn)有的子類。

配置相互依賴關(guān)系

依賴關(guān)系是串行執(zhí)行離散操作對象的一種方式,一個(gè)依賴于其他操作的操作不能夠開始執(zhí)行,直到它依賴的所有操作都已經(jīng)執(zhí)行結(jié)束。因此,您可以使用依賴關(guān)系在兩個(gè)操作對象之間來創(chuàng)建簡單的一到一的依賴關(guān)系,或者創(chuàng)建復(fù)雜的對象依賴圖。

為了在兩個(gè)操作對象之間建立依賴關(guān)系,您可以使用NSOperationaddDependency:方法。這個(gè)方法從當(dāng)前操作對象到您指定作為參數(shù)的目標(biāo)操作創(chuàng)建一個(gè)單向依賴。這個(gè)依賴意味著當(dāng)前操作對象不能夠開始執(zhí)行,直到目標(biāo)操作對象結(jié)束執(zhí)行。依賴關(guān)系也不限制操作在同一個(gè)隊(duì)列。操作對象管理它們自己的依賴關(guān)系,所以在操作對象之間建立依賴關(guān)系并將它們都添加到不同的隊(duì)列,它是完全接受的。然而有一件事是不能夠接受的,就是在操作之間創(chuàng)建循環(huán)依賴關(guān)系。這么做是程序員的錯(cuò)誤,它將永遠(yuǎn)阻止受影響的操作執(zhí)行。

當(dāng)操作所有的依賴都已經(jīng)結(jié)束執(zhí)行時(shí),操作對象一般會變?yōu)闇?zhǔn)備執(zhí)行(如果您自定義isReady方法的行為,操作的準(zhǔn)備就緒由您設(shè)置的標(biāo)準(zhǔn)來決定)。如果操作對象在隊(duì)列中,隊(duì)列可能隨時(shí)開始執(zhí)行操作。如果您打算手動執(zhí)行操作,將由您來調(diào)用操作的start方法。

重要提示:在運(yùn)行操作或者將它們添加到隊(duì)列之前您應(yīng)該總是先配置依賴關(guān)系。在之后添加依賴關(guān)系可能阻止操作對象運(yùn)行。

每當(dāng)操作對象的狀態(tài)改變時(shí),每個(gè)操作對象發(fā)出適當(dāng)?shù)腒VO通知,依賴關(guān)系就依靠這些通知。如果您自定義操作對象的行為,為了避免引發(fā)依賴問題,您可能需要從自定義代碼發(fā)送適當(dāng)?shù)腒VO通知。更多有關(guān)KVO通知和操作對象的信息,請參閱維持KVO規(guī)范。有關(guān)配置依賴關(guān)系的其他信息,見NSOperation類參考。

改變操作的執(zhí)行優(yōu)先級

對于添加到隊(duì)列的操作,執(zhí)行順序首先由排隊(duì)的準(zhǔn)備就緒的操作來決定,其次取決于他們的相對優(yōu)先級。準(zhǔn)備就緒由操作依賴的其他操作來決定,但是優(yōu)先級是操作對象本身的一個(gè)屬性。默認(rèn)情況下,所有新的操作對象都有一個(gè)“標(biāo)準(zhǔn)”優(yōu)先級,但是您可以根據(jù)需要通過調(diào)用對象的setQueuePriority:方法來增加或者減少優(yōu)先級。

優(yōu)先級只適用于同一個(gè)操作隊(duì)列里的操作。如果您的應(yīng)用程序有多個(gè)操作隊(duì)列,隊(duì)列自己的每個(gè)對象的優(yōu)先級與其他任何隊(duì)列無關(guān)。因此,在不同隊(duì)列中,低優(yōu)先級的操作仍然有可能早于高優(yōu)先級的操作執(zhí)行。

優(yōu)先級并不是依賴的替代者。優(yōu)先級只決定隊(duì)列中當(dāng)前為準(zhǔn)備狀態(tài)的操作開始執(zhí)行的順序。例如,如果一個(gè)隊(duì)列包含一個(gè)高優(yōu)先級操作和一個(gè)低優(yōu)先級操作,并且兩個(gè)操作都為準(zhǔn)備狀態(tài),隊(duì)列先執(zhí)行高優(yōu)先級操作。然而,如果高優(yōu)先級操作不是準(zhǔn)備狀態(tài),而低優(yōu)先級是準(zhǔn)備狀態(tài),隊(duì)列先執(zhí)行低優(yōu)先級操作。如果您想要阻止一個(gè)操作開始,直到另外一個(gè)操作結(jié)束,您必須使用依賴關(guān)系(如配置相互依賴關(guān)系中所述)代替。

改變底層線程優(yōu)先級

在OS X v10.6及以后,可以配置操作的底層線程的優(yōu)先級。在系統(tǒng)中,線程策略本身由內(nèi)核管理,但通常高優(yōu)先級線程比低優(yōu)先級線程被給予更多的機(jī)會來運(yùn)行。在一個(gè)操作對象中,您使用0.0到1.0范圍的一個(gè)浮點(diǎn)型值來指定線程的優(yōu)先級,0.0代表最低優(yōu)先級,而1.0代表最高優(yōu)先級。如果您不指定一個(gè)明確的線程優(yōu)先級,操作以默認(rèn)的線程優(yōu)先級0.5運(yùn)行。

為了設(shè)置線程的優(yōu)先級,您必須在添加操作對象到隊(duì)列(或者手動執(zhí)行)之前調(diào)用操作對象的setThreadPriority:方法。當(dāng)需要執(zhí)行操作時(shí),默認(rèn)的start方法使用您指定的值來改變當(dāng)前線程的優(yōu)先級。這個(gè)新的優(yōu)先級只保持操作對象main方法持續(xù)的時(shí)長。所有其他代碼(包括操作對象的結(jié)束塊)使用默認(rèn)線程優(yōu)先級運(yùn)行。如果您創(chuàng)建一個(gè)并發(fā)操作,因此覆蓋start方法,您必須自己配置線程優(yōu)先級。

注意:threadPriority屬性在iOS8.0以后被廢棄,使用qualityOfService代替。它是位于NSObjCRuntime.h中的NSQualityOfService枚舉。

/* 以下服務(wù)質(zhì)量(QoS)分類用于向系統(tǒng)指示工作的性質(zhì)和重要性。它們被系統(tǒng)用于管理各種資源。在資源爭用期間,較高的QoS類別比較低的QoS類別接收更多的資源 */
typedef NS_ENUM(NSInteger, NSQualityOfService) {
    /* UserInteractive QoS用于直接涉及提供交互式UI的工作,例如處理事件或繪制到屏幕 */
    NSQualityOfServiceUserInteractive = 0x21,
    
    /* UserInitiated QoS用于執(zhí)行已經(jīng)由用戶明確請求的工作,并且為了允許進(jìn)一步的用戶交互,必須立即呈現(xiàn)結(jié)果。例如,用戶在郵件列表中選擇電子郵件后加載電子郵件 */
    NSQualityOfServiceUserInitiated = 0x19,
    
    /* Utility QoS用于執(zhí)行用戶不太可能立即等待結(jié)果的工作。該工作可能已經(jīng)由用戶請求或自動啟動,不阻止用戶進(jìn)一步交互,通常在用戶可見的時(shí)間段操作,并且可以通過非模態(tài)進(jìn)度指示器向用戶指示其進(jìn)度。這項(xiàng)工作將以節(jié)能的方式運(yùn)行,以便在資源受到約束時(shí)遵循更高的QoS工作。例如,定期內(nèi)容更新或批量文件操作,如介質(zhì)導(dǎo)入 */
    NSQualityOfServiceUtility = 0x11,

    /* Background QoS用于不是用戶啟動或可見的工作。一般來說,用戶甚至不知道這項(xiàng)工作發(fā)生,它將以最有效的方式運(yùn)行,同時(shí)給予更高的QoS工作最大的尊重。例如,預(yù)取內(nèi)容,搜索索引,備份以及與外部系統(tǒng)同步數(shù)據(jù) */
    NSQualityOfServiceBackground = 0x09,

    /* Default QoS表示沒有QoS信息。隨時(shí)可能從其他資源推斷QoS信息。如果這樣的推斷是不可能的,將使用UserInitiated和Utility之間的QoS。 */
    NSQualityOfServiceDefault = -1
} NS_ENUM_AVAILABLE(10_10, 8_0);

設(shè)置完成塊

在OS X v10.6及以后,當(dāng)操作的主任務(wù)執(zhí)行結(jié)束的時(shí)候可以執(zhí)行一個(gè)完成塊。您可以使用一個(gè)結(jié)束塊來執(zhí)行任何您認(rèn)為不是主任務(wù)部分的工作。例如,您可能使用這個(gè)塊來通知感興趣的用戶操作本身已經(jīng)完成。一個(gè)并發(fā)操作對象可能使用這個(gè)塊來生成它最后的KVO通知。

使用NSOperationsetCompletionBlock:方法來設(shè)置完成塊。傳遞到這個(gè)方法的塊不應(yīng)該有參數(shù)和返回值。

實(shí)現(xiàn)操作對象的小貼士

雖然操作對象的實(shí)現(xiàn)相當(dāng)簡單,但當(dāng)您寫自己的代碼的時(shí)候,有一些事情也應(yīng)該注意。當(dāng)為您的操作對象寫代碼時(shí),您應(yīng)該考慮下面部分描述的因素。

管理操作對象內(nèi)存

下面部分描述了在操作對象中內(nèi)存管理的關(guān)鍵因素。在Objective-C程序中關(guān)于內(nèi)存管理的一般信息,請參閱高級內(nèi)存管理編程指南

避免Per-Thread存儲

盡管大多數(shù)操作執(zhí)行在線程上,在非并發(fā)操作的情況下,該線程通常是由操作隊(duì)列提供。如果操作隊(duì)列為您提供一個(gè)線程,您應(yīng)該認(rèn)為線程為隊(duì)列所擁有,而不應(yīng)被您的操作觸碰。特別是,您永遠(yuǎn)不要關(guān)聯(lián)任何數(shù)據(jù)到不是您自己創(chuàng)建或者管理的線程上。由操作隊(duì)列管理的線程根據(jù)系統(tǒng)和您的應(yīng)用程序的需要進(jìn)出。因此,使用per-thread存儲在操作間傳遞數(shù)據(jù)是不可靠的而且可能會失敗。

在操作對象的時(shí)候,在任何情況下都不應(yīng)該以任何原因來使用per-thread存儲。當(dāng)您初始化一個(gè)操作對象時(shí),您應(yīng)該為對象提供處理工作所需要的所有東西。因此,操作對象自身提供您需要的上下文存儲。所有的傳入傳出數(shù)據(jù)都應(yīng)該存儲在那里,直到它可以被完整的返回到您的應(yīng)用程序或者不再需要。

根據(jù)需要保留對操作對象的引用

只因?yàn)椴僮鲗ο螽惒竭\(yùn)行,您不應(yīng)當(dāng)認(rèn)為您可以創(chuàng)建他們并遺忘他們。他們也是對象,并由您根據(jù)代碼需要來決定對他們的任何引用。如果您需要在操作對象執(zhí)行結(jié)束后從對象取回結(jié)果數(shù)據(jù),這將十分重要。

您應(yīng)該總是保持對操作對象的引用,其原因是稍后您可能沒有機(jī)會向隊(duì)列詢問對象。隊(duì)列竭盡全力盡快調(diào)度和執(zhí)行操作。在大多數(shù)情況下,操作被加入到隊(duì)列后,隊(duì)列幾乎馬上開始執(zhí)行操作。當(dāng)您自己的代碼返回到隊(duì)列獲去對操作引用時(shí)候,操作可能已經(jīng)結(jié)束并從隊(duì)列中移除。

處理錯(cuò)誤和異常

因?yàn)椴僮鞅举|(zhì)上是應(yīng)用程序中離散的實(shí)體,他們有責(zé)任處理產(chǎn)生的錯(cuò)誤或者異常。在OS X v10.6及以后,NSOperation類提供的默認(rèn)start方法不捕獲異常(在OS X v10.5, start方法捕獲并處理異常)。您自己的代碼應(yīng)該總是直接捕獲并處理異常。還應(yīng)該檢查錯(cuò)誤碼,并根據(jù)需要通知應(yīng)用程序的相應(yīng)部分。如果您替換start方法,在自定義實(shí)現(xiàn)中必須同樣捕獲任何異常,以防止它們離開底層線程的上下文。

在這些類型的錯(cuò)誤情況下,您應(yīng)當(dāng)處理以下幾種:

  • 檢查和處理UNIX errno風(fēng)格的錯(cuò)誤代碼。參閱usr/include/sys/errno.h
  • 檢查方法或者函數(shù)返回的明確的錯(cuò)誤代碼
  • 捕獲您自己代碼或者其他系統(tǒng)框架拋出的異常
  • 捕獲NSOperation類自己拋出的異常,在下列情況下拋出異常:
    • 當(dāng)操作還沒準(zhǔn)備好來執(zhí)行,它的start方法被調(diào)用
    • 當(dāng)操作正在執(zhí)行或者結(jié)束(可能是因?yàn)楸蝗∠?code>start方法再次別調(diào)用
    • 當(dāng)您試圖添加完成塊到已經(jīng)執(zhí)行或者結(jié)束的操作
    • 當(dāng)您試圖取回已取消的NSInvocationOperation對象的返回值

如果您的自定義代碼遇到異?;蛘咤e(cuò)誤,您應(yīng)當(dāng)根據(jù)需要采取任何步驟來傳遞錯(cuò)誤到程序的其余部分。NSOperation類沒有為傳遞錯(cuò)誤結(jié)果碼或者異常提供明確的方法。因此,如果這些信息對您的應(yīng)用程序非常重要,您必須提供必須要的代碼。

為操作對象確定合適的范圍

雖然有可能添加一個(gè)任意大數(shù)量的操作到到操作隊(duì)列,但這樣做往往是不切實(shí)際的。像任何對象一樣,NSOperation類的實(shí)例消耗內(nèi)存,以及和自己執(zhí)行相關(guān)的實(shí)際成本。如果每個(gè)操作對象只做少量的工作,創(chuàng)建成千上萬個(gè)操作,您可能會發(fā)現(xiàn)調(diào)度操作比做實(shí)際工作花費(fèi)更多的時(shí)間。如果您的應(yīng)用程序已經(jīng)內(nèi)存受限,您可能會發(fā)現(xiàn)內(nèi)存中只有成千上萬的操作,可能進(jìn)一步降低性能。

高效使用操作的關(guān)鍵是在您需要處理的工作數(shù)量和保持電腦忙碌之間找到一個(gè)合適的平衡。盡量確保您的操作處理合理數(shù)量的工作。例如,如果您的應(yīng)用程序創(chuàng)建100個(gè)操作對象在100個(gè)不同值上執(zhí)行相同的任務(wù),可以考慮創(chuàng)建10個(gè)操作對象,每個(gè)處理10個(gè)值來代替。

您也應(yīng)該避免一次性向操作隊(duì)列添加大量操作,或者避免連續(xù)向隊(duì)列添加操作對象的速度比操作對象能夠被處理的速度快。應(yīng)該批量創(chuàng)建操作對象,而不是用操作對象充滿隊(duì)列。隨著一批執(zhí)行結(jié)束,使用結(jié)束塊告訴您的應(yīng)用程序創(chuàng)建一個(gè)新批次。當(dāng)你有許多工作需要處理,你想要隊(duì)列保持充滿足夠多的操作,使計(jì)算機(jī)保持忙碌,但你千萬不要想一次創(chuàng)建如此多的操作,這樣您的應(yīng)用程序會用完內(nèi)存。

當(dāng)然,您創(chuàng)建的操作對象的數(shù)量,在每個(gè)操作對象中執(zhí)行工作的數(shù)量,是可變的并且完全取決于您的應(yīng)用程序。您應(yīng)該總是使用工具例如Instrument來幫助您在效率和速度之間找到合適的平衡。您可以使用Instrument和其他性能工具為您的代碼收集指標(biāo),請參閱性能概述。

執(zhí)行操作

最終,為了處理關(guān)聯(lián)的工作,您的應(yīng)用程序需要執(zhí)行操作。在這一部分中,將介紹幾種方式來執(zhí)行操作,以及怎么在運(yùn)行時(shí)巧妙處理操作。

添加操作到操作隊(duì)列

到目前為止,執(zhí)行操作最簡單的方式是使用操作隊(duì)列,它是NSOperationQueue類的實(shí)例。您的應(yīng)用程序負(fù)責(zé)創(chuàng)建和維護(hù)任何它打算使用的操作隊(duì)列。一個(gè)應(yīng)用程序可以有任意數(shù)量的隊(duì)列,但在給定的時(shí)間點(diǎn),有多少操作可能執(zhí)行是有實(shí)際限制的。操作隊(duì)列和系統(tǒng)一起工作,限制并發(fā)操作的數(shù)量到一個(gè)適合于可以用核心和系統(tǒng)負(fù)載的值。因此,創(chuàng)建更多的隊(duì)列并不意味著可以執(zhí)行更多操作。

要?jiǎng)?chuàng)建一個(gè)隊(duì)列,您在應(yīng)用程序中分配它,就和其他任何對象一樣:

NSOperationQueue* aQueue = [[NSOperationQueue alloc] init];

要將操作添加到隊(duì)列,需要使用addOperation:方法。在OS X v10.6及以后,您可以使用addOperations:waitUntilFinished:方法添加一組操作,或者使用addOperationWithBlock:方法直接將塊對象添加到隊(duì)列(沒有相關(guān)的操作對象)。每個(gè)方法對操作進(jìn)行排隊(duì)并通知隊(duì)列應(yīng)該開始處理他們。在大多情況下,操作被添加到隊(duì)列后不久就被執(zhí)行,但因?yàn)橐恍┰虿僮麝?duì)列可能延遲執(zhí)行排隊(duì)的操作。特別是,如果排隊(duì)的操作依賴于尚未完成的其他操作,操作可能被延遲。如果操作隊(duì)列本身被暫?;蛞呀?jīng)執(zhí)行到并發(fā)操作的最大值,執(zhí)行也有可能被延遲。下面例子顯示了添加操作到隊(duì)列的基本語法。

[aQueue addOperation:anOp]; // Add a single operation
[aQueue addOperations:anArrayOfOps waitUntilFinished:NO]; // Add multiple operations
[aQueue addOperationWithBlock:^{
   /* Do something. */
}];

重要提示:千萬不要修改已經(jīng)被添加到隊(duì)列后的操作對象。在隊(duì)列中等待時(shí),操作可能隨時(shí)開始執(zhí)行,因此改變依賴或者它包含的數(shù)據(jù)可能會有不利影響。如果想要知道操作對象的狀態(tài),可以使用NSOperation類的方法來確定操作正在運(yùn)行,等待運(yùn)行或者已經(jīng)結(jié)束。

雖然NSOperationQueue類是為操作并發(fā)執(zhí)行設(shè)計(jì)的,也可以強(qiáng)制單個(gè)隊(duì)列每次只運(yùn)行一個(gè)操作。setMaxConcurentOperationCount:方法可以配置操作隊(duì)列并發(fā)操作的最大值。傳遞1到這個(gè)方法會導(dǎo)致隊(duì)列每次只執(zhí)行一個(gè)操作。雖然可以每次只執(zhí)行一個(gè)操作,執(zhí)行順序仍然基于其他因素,如每個(gè)操作的準(zhǔn)備就緒狀態(tài)和分配給它的優(yōu)先級。因此,一個(gè)連續(xù)的操作隊(duì)列不能夠和GCD的串行調(diào)度隊(duì)列提供完全一樣的行為。如果操作對象的執(zhí)行順序?qū)δ鷣碚f非常重要,您應(yīng)當(dāng)在添加對象到隊(duì)列之前使用依賴關(guān)系來建立這個(gè)順序。有關(guān)配置依賴關(guān)系的信息,請參閱配置相互依賴關(guān)系。

有關(guān)使用操作隊(duì)列的信息,請參閱NSOperationQueue類參考。有關(guān)串行調(diào)度隊(duì)列的詳細(xì)信息,請參閱創(chuàng)建串行調(diào)度隊(duì)列。

手動執(zhí)行操作

雖然操作隊(duì)列是運(yùn)行操作對象最簡單的方法,但也可以不使用隊(duì)列來執(zhí)行操作。如果您選擇手動執(zhí)行操作,但是,有些注意事項(xiàng)在代碼中應(yīng)當(dāng)考慮進(jìn)去。尤其是,操作必須準(zhǔn)備好運(yùn)行,并且您必須使用它的start方法啟動它。

操作不被認(rèn)為能夠運(yùn)行,直到它的isReady方法返回YES。isReady方法被集成到NSOperation類的依賴管理系統(tǒng)中,提供操作對象的依賴關(guān)系狀態(tài)。只有當(dāng)它的依賴被清除,操作才可以不受約束的開始執(zhí)行。

當(dāng)手動執(zhí)行一個(gè)操作,您應(yīng)當(dāng)使用start方法來開始執(zhí)行。使用這個(gè)方法,而不是main或者其他方法,是因?yàn)?code>start方法在實(shí)際運(yùn)行您自定義代碼之前執(zhí)行多項(xiàng)安全檢查。特別是,默認(rèn)start方法生成操作需要正確處理依賴關(guān)系的KVO通知。如果操作已經(jīng)被取消,這個(gè)方法也能夠正確的避免操作執(zhí)行,并且,如果您的操作實(shí)際上沒有準(zhǔn)備好運(yùn)行,則拋出異常。

如果您的應(yīng)用程序定義并發(fā)操作對象,在啟動操作之前,您也應(yīng)當(dāng)考慮調(diào)用操作的isConcurrent方法。在這個(gè)方法返回NO的情況下,您本地代碼可以決定是否在當(dāng)前線程同步執(zhí)行操作,或者首先創(chuàng)建一個(gè)單獨(dú)的線程。然而,實(shí)現(xiàn)這種檢查完全取決于您。

下面代碼展示一個(gè)手動執(zhí)行操作前,進(jìn)行檢查的簡單例子。如果方法返回NO,您應(yīng)該安排一個(gè)計(jì)時(shí)器并且稍后重新調(diào)用這個(gè)方法。稍后您可能會重新安排計(jì)時(shí)器直到方法返回YES,因?yàn)椴僮鞅蝗∠@種情況可能會發(fā)生。

- (BOOL)performOperation:(NSOperation*)anOp {
   BOOL ranIt = NO;
 
   if ([anOp isReady] && ![anOp isCancelled]) {
      if (![anOp isConcurrent])
         [anOp start];
      else
         [NSThread detachNewThreadSelector:@selector(start)
                   toTarget:anOp withObject:nil];
      ranIt = YES;
   } else if ([anOp isCancelled]) {
      // If it was canceled before it was started,
      //  move the operation to the finished state.
      [self willChangeValueForKey:@"isFinished"];
      [self willChangeValueForKey:@"isExecuting"];
      executing = NO;
      finished = YES;
      [self didChangeValueForKey:@"isExecuting"];
      [self didChangeValueForKey:@"isFinished"];
 
      // Set ranIt to YES to prevent the operation from
      // being passed to this method again in the future.
      ranIt = YES;
   }
   return ranIt;
}

取消操作

一旦被添加到操作隊(duì)列,操作對象實(shí)際上是被隊(duì)列所擁有,并且不能夠被移除。將操作出隊(duì)的唯一方法就是取消它。您可以通過調(diào)用操作對象的cancel方法來取消一個(gè)單獨(dú)的操作對象,或者您可以通過調(diào)用隊(duì)列對象的cancelAllOperations方法來取消隊(duì)列里所有的操作對象。

只有當(dāng)您確信不再需要他們時(shí),您才能取消操作。發(fā)出取消命令將操作對象設(shè)置為“canceled”狀態(tài),這將阻止它永遠(yuǎn)執(zhí)行。因?yàn)橐粋€(gè)取消的操作也被認(rèn)為是“finished”,依賴于它的對象接收適當(dāng)?shù)腒VO通知來清除依賴。因此,為了響應(yīng)一些特殊事件,例如退出應(yīng)用程序或者用戶專門要求取消,更常見的是取消隊(duì)列里的所有操作而不是有選擇的取消操作。

等待操作完成

為了獲得最佳性能,您應(yīng)該盡可能設(shè)計(jì)自己的操作為異步的,當(dāng)操作執(zhí)行的時(shí)候,讓您的應(yīng)用程序自由的去做其他工作。如果創(chuàng)建操作的代碼也將處理操作的結(jié)果,您可以使用NSOperationwaitUntilFinished方法,以阻止該代碼,直到操作完成。一般情況下,如果您有其他方法,最好避免使用此方法。阻塞當(dāng)前線程可能是一個(gè)方便的解決方案,但是它確實(shí)引入了更多串行化到您的代碼中,并限制并發(fā)的總數(shù)量。

重要提示:永遠(yuǎn)不要等待您的應(yīng)用程序的主線程的操作。您應(yīng)當(dāng)只從輔助線程或者其他操作中這么做。阻塞主線程會阻止應(yīng)用程序響應(yīng)用戶事件,且可能使您的應(yīng)用程序表現(xiàn)為無響應(yīng)。

除了等待一個(gè)操作完成,您也可以在一個(gè)隊(duì)列中通過調(diào)用NSOperationQueuewaitUntilAllOperationAreFinished方法來等待所有的操作。當(dāng)?shù)却麄€(gè)隊(duì)列完成時(shí),請注意,您應(yīng)用程序的其他線程仍然可以添加操作到隊(duì)列,從而延長了等待。

暫停與恢復(fù)隊(duì)列

如果您想操作的執(zhí)行臨時(shí)停止,您可以使用setSuspended:方法暫停相應(yīng)的操作隊(duì)列。暫停隊(duì)列并不能夠使已經(jīng)在執(zhí)行中的操作暫停它們的任務(wù)。它僅僅阻止新的操作被調(diào)度執(zhí)行。您可能響應(yīng)用戶的請求暫停隊(duì)列來暫停正在進(jìn)行的工作,因?yàn)橛脩艨赡茏罱K想要繼續(xù)工作。

參考:

https://developer.apple.com/library/content/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationObjects/OperationObjects.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容