ios7中引入的新類:nsprogress介紹

openglnewbee發表於2014-05-22

蘋果公司在 iOS 7 and OS X 10.9引入NSProgress類,目標是建立一個標準的機制用來報告長時間執行的任務的進度。NSProgress引入之後,其最重要的作用是可以在一個app的多個不需要緊耦合的模組之間產生進度報告。舉個例子,一個執行在後臺佇列中的圖片操作,這個操作應該能夠把它的進度通知給一個檢視控制器 (並且這個檢視控制器也可以暫停或者終止該操作),甚至兩個物件不可能持有對方的引用。

 

設計目標

有關NSProgress的最好的文件現在可以在Foundation release notes for OS X 10.9.中找到。該類的參考文件也有,不過我發現不大好理解它和其他的類是如何一塊使用的 。

 

在釋出說明中,蘋果公司闡述了關於NSProgress四個主要的設計目的:

. 鬆耦合

. 組合性

. 重用性

. 可用性

 

 讓我們看看以上這幾個用處。以下全部引自Foundation釋出說明。

 

鬆耦合

鬆耦合。執行良好的程式碼能夠報告事情的進度,不管是否正在觀察它或者甚至它是否被觀察到。從一個較低層次看,程式碼用來觀察進度並把進度呈現給使用者,但是不需要報告程式碼如何構建的。大部分目標容易實現,我們可以通過使用一個可被多種進度報告者和觀察者使用的單獨的NSProgress類來實現。

 

蘋果公司正在制定一個新標準,將有希望被開源社群廣泛採納。如果你寫的程式碼潛在受益於報告其做的事情進度,你應該優先考慮加入對NSProgress的支援。像我們將要看到的,你也許甚至不需要修改你程式碼的公共API來做這件事。在大多數情況下,僅僅增加幾行程式碼到你的方法中就已經足夠完成實際工作了。通過使用一個已經建立的標準, 你的程式碼將會更加容易被別的程式設計師使用,並且更容易和其他元件整合。

 

可組合性

可組合性。執行良好的程式碼可以報告進度,而不需要考慮到是否這個任務實際上僅僅是一個大型操作的一部分,該大型操作的全部進度才是使用者真正感興趣的。為此,每一個NSProgresse能夠有一個父程式和多個子程式。如果有子操作,那麼他們的進度會被NSProgresses子程式顯示。進度報告被從子進度傳遞給父進度,取消請求從父進度傳遞給子進度。一個進度樹的進度細分能夠解決諸如此類問題,比如不同的程式碼片段完成的工作進度如何被用來計算一個全域性的用來展現給使用者的進度值。例如,像-[NSProgress fractionCompleted],這個方法返回一個考慮到接收者和其子進度的值。

 

NSProgress物件活躍在一個層次結構中,該結構不同於UIKit下的檢視層次結構。在該層次樹的根部,UI圖層可以建立一個進度物件而不論它何時想要監控任務的進度。藉助於這樣的物件(也就是所謂的在呼叫完成任務的方法之前的當前進度),這個物件將會自動成為任何被低階程式碼建立的子進度例項的父物件。在工作進行時,子進度會更新,同時更新也會被傳遞給父進度。因此,藉助觀察根進度物件(通過KVO),UI 層能夠顯示子進度的組合進度。並且,還可以讓根進度物件終止或者暫停,該UI 圖層也有能力通過進度層次結構和執行程式碼互動。

 

和檢視層次結構不同,不存在某種公共API,其可以從父到子或者從子到父貫穿進度層次結構。進度物件不需要考慮他們在層級樹中的位置,以及他們是否有一個實際上的父或者子。事件傳遞和整體進度計算完全在後臺完成。

 

重用性

重用性。這意味著NSProgress幾乎能被所有能夠連結到Foundation框架的程式碼使用,並且生成使用者可見的進度 。在Mac OS X中, NSProgress包括一個在一個程式中釋出進度並且觀察其他程式中進度的機制。看到這裡,如果你的程式碼做任何形式的進度報告,你將會考慮引入NSProgress。

 

可用性

可用性。在許多情況下使用NSProgress的巨大障礙,是組織程式碼準確地找到用來報告進度的NSProgress的例項。這個障礙有很多方面,像如何層次化你的程式碼(你一定要通過多層函式或者方法將NSProgress作為一個引數來傳遞嗎?),它是如何真正被多個專案使用的(你甚至能夠在不影響原有事情的基礎上增加NSProgress引數嗎?),等等。為了有助於越過這個障礙,有一個“當前進度”的概念要知道,它是NSProgress的例項,它將是任何新的用來顯示細分工作的進度物件的父物件。你可以設定一個進度物件作為當前的進度物件,然後呼叫框架或者程式碼的其他部分。如果它支援進度報告,它可以使用currentProgress方法找到當前進度物件,如果需要可附加上它自己的子物件做其自身的事情。

 

“當前進度”的思想(每個執行緒可以有其自己的當前進度)極大的解放了開發者,從而不用在不同的程式碼層中前後傳遞NSProgress(例如就像我們經常用NSError物件做的事情)。 這個設計考慮到了一個事實,就是顯示進度的程式碼(就是UI)經常是從做實際執行的程式碼中被剝離出來的多個層次。另一方面,看起就像程式碼的風格。當前進度是基於一個執行緒自身的全域性變數,有時候開發者一般會被告知要避免它。 

 

這樣的設計也意味著,如果在一個執行任何型別長時間執行的任務的庫中支援NSProgress,通常不需要開發者去改變庫的公共API。雖然這通常是一件好事,但是這有可能成為一個主要的發現性問題。由於這個API 不包括任何對NSProgress的引用,所以該庫的開發者必須格外注意用來支援NSProgress 的文件。

 

舉個例子,你知道NSData現在通過dataWithContentsOfURL:options:error:方法對NSProgress的內建支援嗎?我之前一直不清楚,直到我偶然在Foundation release notes中遇到了阻礙--- NSData類引用文件沒有涉及到它。另一個方面,看到那些,甚至是另人尊敬的NSData現在使用NSProgress,這便引導我假設NSURLSession---一個全新的類和NSProgress一同被引進---當然有NSProgress開箱即用的支援。我花費幾個小時嘗試,最後證明行不通。

 

使用 NSProgress

讓我看看你將會如何在實際開發中使用NSProgress。再強調下,最好的示例程式碼來自蘋果官方,現在可以在Foundation release notes中找到。可以從兩個角度考慮:在UI中顯示進度和在一個完成工作的方法中報告進度。我將會逐個討論他們。

 

在UI中顯示進度

以下有幾個在檢視或者檢視控制器中顯示進度的步驟:

1.在你呼叫一個長時間執行的任務之前,藉助+progressWithTotalUnitCount:.方法建立一個NSProgress例項。 引數totalUnitCount將會包括“要完成的總工作單元的數量”。

 

有一點很重要,要從UI圖層的角度完全理解這個數值;你不會被要求猜測有多少個實際工作物件以及有多少種類的工作單元(位元組?畫素?文字行數?)。如果你遍歷集合並且計劃為每一個集合元素呼叫該例項物件,該引數經常會是1或者也許是一個集合中的元素的數量 。

 

2.使用KVO註冊一個進度的fractionCompleted屬性的觀察者。類似於NSOperation,NSProgress被設計藉助KVO來使用。在MAC,這使得通過Cocoa Bindings繫結一個NSProgress例項到一個進度條或者標籤上變得非常容易。在iOS上,你將會在KVO observer handle中手動更新你的UI。

 

除了fractionCompleted, completedUnitCount和totalUnitCount屬性之外,NSProgress也有一個localizedDescription (@"50% completed"),並且還有一個localized Additional Description (@"3 of 6"),其能夠被繫結到文字標籤。KVO通知在改變NSProgress物件屬性值的執行緒中傳送,因此確保在你的主執行緒中手動更新UI。

 

3.當前的進度物件通過呼叫-becomeCurrentWithPendingUnitCount:方法建立新的進度物件。在這裡,pendingUnitCount這個引數相當於“是要被接收者完成的總的工作單元的量要完成的工作的一部分”。你可以多次呼叫這個方法並且每次傳遞totalUnitCount(本次程式碼完成的佔比)的一部分。在集合元素的迭代示例中,我們將會在每一次迭代中呼叫[progress becomeCurrentWithPendingUnitCount:1];

 

4.呼叫工作物件的方法。由於當前進度是一個區域性執行緒概念,你必須在你呼叫becomeCurrentWithPendingUnitCount:的相同的執行緒中做這個事情。如果工作物件的API被設計成在主執行緒中呼叫,那這就不是一個問題,就像我對大部分API的看法那樣(Brent Simmons 也這麼認為)。

 

但是如果你的UI 層正在建立一個後臺佇列並且呼叫工作物件來同步那個佇列,那要確保將 becomeCurrentWithPendingUnitCount:和resignCurrent放到相同的dispatch_async()塊中呼叫。

 

5.在你的進度物件中呼叫-resignCurrent。這個方法是和-becomeCurrentWith PendingUnitCount:相對應的,並且會呼叫相同的次數 。你可以在實際工作被完成以前呼叫resignCurrent,因此你不需要等待,直到你得到一個來自工作物件的完成通知。

 

在becomeCurrent…/resignCurrent呼叫期間唯一發生的事情是工作物件必須建立一個或者多個子進度(往下面看)。如果工作物件不做這事情,resignCurrent將會考慮被完成的任務,並通過申請單位計數來自動增加completedUnitCount。

  1. static void *ProgressObserverContext = &ProgressObserverContext; 
  2.  
  3. - (void)startFilteringImage 
  4.     NSProgress *progress = [NSProgress progressWithTotalUnitCount:1]; 
  5.     [progress addObserver:self 
  6.                forKeyPath:NSStringFromSelector(@selector(fractionCompleted)) 
  7.                   options:NSKeyValueObservingOptionInitial 
  8.                   context:ProgressObserverContext]; 
  9.     [progress becomeCurrentWithPendingUnitCount:1]; 
  10.      
  11.     // ImageFilter is a custom class that performs the work 
  12.     ImageFilter *imageFilter = [[ImageFilter alloc] initWithImage:self.image]; 
  13.     [imageFilter filterImageWithCompletionHandler: 
  14.         ^(UIImage *filteredImage, NSError *error)  
  15.     { 
  16.         [progress removeObserver:self 
  17.             forKeyPath:NSStringFromSelector(@selector(fractionCompleted)) 
  18.             context:ProgressObserverContext]; 
  19.  
  20.         // Image filtering finished 
  21.         ... 
  22.     }]; 
  23.      
  24.     [progress resignCurrent]; 
  25.  
  26. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object 
  27.     change:(NSDictionary *)change context:(void *)context 
  28.     if (context == ProgressObserverContext) 
  29.     { 
  30.         [[NSOperationQueue mainQueue] addOperationWithBlock:^{ 
  31.             NSProgress *progress = object; 
  32.             self.progressBar.progress = progress.fractionCompleted; 
  33.             self.progressLabel.text = progress.localizedDescription; 
  34.             self.progressAdditionalInfoLabel.text = 
  35.                 progress.localizedAdditionalDescription; 
  36.         }]; 
  37.     } 
  38.     else 
  39.     { 
  40.         [super observeValueForKeyPath:keyPath ofObject:object 
  41.             change:change context:context]; 
  42.     } 

6.當工作物件已經完成自身的工作,會從進度物件的KVO中登出掉。在程式碼中,它看起來像一個假設的影象過濾任務。

 

報告進度

工作物件在其被呼叫時將會完成這些步驟。

 

1.藉助+progressWithTotalUnitCount:方法建立一個NSProgress物件。totalUnitCount將會是代表你的運演算法則的專案的工作單位,因此根據於你完成的工作型別,它可能會是一個用位元組表示的檔案長度,圖片中的畫素數量,字串中的位元組數量,或者其他的。該方法將會呼叫-[NSProgress initWithParent:userInfo:],並且把+[NSProgress currentProgress]作為父引數傳遞,因此在當前進度之間建立一個父子關係(這裡當前進度是我們的UI 層的進度物件),並且工作物件相會使用新建立的程式例項來報告其自身的進度。

 

因為當前進度是執行緒特有的,有一點很重要,即工作物件在被呼叫的相同執行緒中建立其自身的進度物件。否則,父子關係將不會被正確建立。關係一旦被建立,NSProgress物件是執行緒安全的。工作物件可以在來自任何執行緒/佇列的進度上延遲更新屬性。

 

如果你還不知道totalUnitCount(例如,因為你正在下載一個檔案並且在得到一個網路響應之前檔案尺寸未知),可以使用NSProgress *progress = [[NSProgress alloc] initWithParent:[NSProgress currentProgress] userInfo:nil];.建立進度物件。

 

2.可以選擇的是,通過設定類似cancellable 和pausable的屬性來配置程式物件。當處理檔案時,你也可以設定kind=NSProgressKindFile, 在這種情況下,進度的localizedDescription和localizedAdditionalDescription將會返回更多的特殊文字。想實現這一點,你也可能至少想要增加NSProgressFileOperationKindKey鍵到進度的使用者資訊字典。仔細從文件中找找。

 

3.在後臺佇列中完成實際的工作。定期更新你的進度的completedUnitCount。這將會自動傳遞到父進度中並且通過在上文中討論過的KVO的設定觸發一個UI更新。

 

可選擇的是,如果你的工作包括幾個更小的子任務,你可以不費力建立一個更深的進度層級結構。正如上邊的UI層,你的工作物件將會呼叫becomeCurrentWithPendingUnitCount:來使得其自身成為當前進度,然後使用NSProgress輪流觸發子任務來報告其自身的進度到進度鏈中。然而你不應該混淆一個進度物件的所有方法。如果在呼叫becomeCurrentWithPendingUnitCount和resignCurrent期間沒有建立子進度,那麼該resignCurrent呼叫將會自動設定接收者的fractionCompleted到 1。

 

如果你的進度可終止或者暫停,那你也應該週期性地檢查進度終止或者暫停屬性是否是YES ,並且做出相應的反應。這些屬性改變傳遞是從父進度到子進度(通常在響應使用者動作的時候)。正確的終止響應是停止你正在做的事情並且及時報告一個NSError。

 

示例程式碼:

  1. @implementation ImageFilter 
  2.  
  3. ... 
  4.  
  5. - (void)filterImageWithCompletionHandler: 
  6.     (void(^)(UIImage *filteredImage, NSError *error))completionHandler 
  7.     int64_t numberOfRowsInImage = (int64_t)self.image.size.height; 
  8.     NSProgress *progress = [NSProgress progressWithTotalUnitCount: 
  9.         numberOfRowsInImage]; 
  10.     progress.cancellable = YES; 
  11.     progress.pausable = NO; 
  12.  
  13.     dispatch_queue_t backgroundQueue = 
  14.         dispatch_queue_create("image filter queue", DISPATCH_QUEUE_SERIAL); 
  15.     dispatch_async(backgroundQueue, ^{ 
  16.         NSError *error = nil; 
  17.         UIImage *filteredImage = nil; 
  18.         for (int64_t row = 0; row < numberOfRowsInImage; row++) { 
  19.             // Check if cancelled 
  20.             if (progress.cancelled) { 
  21.                 error = [NSError errorWithDomain:NSCocoaErrorDomain 
  22.                     code:NSUserCancelledError userInfo:nil]; 
  23.                 break
  24.             } 
  25.  
  26.             // Do the work for this row of pixels 
  27.             ... 
  28.              
  29.             // Update progress 
  30.             progress.completedUnitCount++; 
  31.         } 
  32.          
  33.         // We assume work is complete and either filteredImage has been set  
  34.         // or the task has been cancelled and error has been set above. 
  35.         if (completionHandler) { 
  36.             dispatch_async(dispatch_get_main_queue(), ^{ 
  37.                 completionHandler(filteredImage, error); 
  38.             }); 
  39.         } 
  40.     }); 
  41.  
  42. @end 

結論

我認為NSProgresss 是Foundation的一個令人興奮的新特性,它將會對蘋果的app開發者越來越有用,並且蘋果本身和開源社群將會廣泛使用它。理解其設計,特別是一個執行緒“當前進度物件”的思想,是一個有效使用它的基本需求。

 

如果你實現了一個受益於程式報告的API,那你應該優先考慮增加NSProgress支援。如果你這麼做了,確保清晰記錄這個API,這樣就不會是僅從API中告知使用者了。蘋果公司在這方面應該以身作則,但是不幸的是當前文件不全。除了在Foundation釋出說明中關於NSData的標記和一個Multipeer Connectivity framework中的顯式的使用,我在其他API中找不到任何提及它的內容。


1.在MAC上,NSProgress甚至可以在程式間交換資料。Safari 使用這個特點將下載加進度告知finder,並且允許使用者終止一個來自finder 或者Dock的正在執行的下載任務。

 

2.仍然很有可能以傳統方式使用NSProgress,把NSProgress例項作為代理方法或者block的引數傳遞給APP的其他部分。以模擬NSURLConnection設計的方式使用NSProgress,一直用來藉助於在一個專門物件中封裝進度資訊的條件,以connection:didSendBodyData:totalBytesWritten:totalBytesExpectedToWrite:代理方法的形式報告物件 。蘋果公司在Multipeer Connectivity framework中使用這個方法(檢視MCSessionDelegate協議)。

 

原文:NSProgress

相關文章