苹果公司在 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。
- static void *ProgressObserverContext = &ProgressObserverContext;
- - (void)startFilteringImage
- {
- NSProgress *progress = [NSProgress progressWithTotalUnitCount:1];
- [progress addObserver:self
- forKeyPath:NSStringFromSelector(@selector(fractionCompleted))
- options:NSKeyValueObservingOptionInitial
- context:ProgressObserverContext];
- [progress becomeCurrentWithPendingUnitCount:1];
- // ImageFilter is a custom class that performs the work
- ImageFilter *imageFilter = [[ImageFilter alloc] initWithImage:self.image];
- [imageFilter filterImageWithCompletionHandler:
- ^(UIImage *filteredImage, NSError *error)
- {
- [progress removeObserver:self
- forKeyPath:NSStringFromSelector(@selector(fractionCompleted))
- context:ProgressObserverContext];
- // Image filtering finished
- ...
- }];
- [progress resignCurrent];
- }
- - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
- change:(NSDictionary *)change context:(void *)context
- {
- if (context == ProgressObserverContext)
- {
- [[NSOperationQueue mainQueue] addOperationWithBlock:^{
- NSProgress *progress = object;
- self.progressBar.progress = progress.fractionCompleted;
- self.progressLabel.text = progress.localizedDescription;
- self.progressAdditionalInfoLabel.text =
- progress.localizedAdditionalDescription;
- }];
- }
- else
- {
- [super observeValueForKeyPath:keyPath ofObject:object
- change:change context:context];
- }
- }
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。
示例代码:
- @implementation ImageFilter
- ...
- - (void)filterImageWithCompletionHandler:
- (void(^)(UIImage *filteredImage, NSError *error))completionHandler
- {
- int64_t numberOfRowsInImage = (int64_t)self.image.size.height;
- NSProgress *progress = [NSProgress progressWithTotalUnitCount:
- numberOfRowsInImage];
- progress.cancellable = YES;
- progress.pausable = NO;
- dispatch_queue_t backgroundQueue =
- dispatch_queue_create("image filter queue", DISPATCH_QUEUE_SERIAL);
- dispatch_async(backgroundQueue, ^{
- NSError *error = nil;
- UIImage *filteredImage = nil;
- for (int64_t row = 0; row < numberOfRowsInImage; row++) {
- // Check if cancelled
- if (progress.cancelled) {
- error = [NSError errorWithDomain:NSCocoaErrorDomain
- code:NSUserCancelledError userInfo:nil];
- break;
- }
- // Do the work for this row of pixels
- ...
- // Update progress
- progress.completedUnitCount++;
- }
- // We assume work is complete and either filteredImage has been set
- // or the task has been cancelled and error has been set above.
- if (completionHandler) {
- dispatch_async(dispatch_get_main_queue(), ^{
- completionHandler(filteredImage, error);
- });
- }
- });
- }
- @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