8

我一直在调查NSProgress,但发现现有的文档,类参考和教程缺乏。我主要想知道我的NSProgress是否适用于我的用例。类别参考文件可选地指suboperationssubtasks,我可能会误解,但我将suboperations解释为NSOperation管理一组其他NSOperations的情况。我的使用案例如下:使用NSProgress与嵌套的NSOperations

  • 为每个存在的组创建一个Upload All Items in Group操作。
  • 将这些操作中的每一个添加到NSOperationQueue
  • 每个Upload All Items in Group操作将为其组中的每个项目创建一个Upload Item操作。这些全部被添加到由操作管理的NSOperationQueue

我本来期望NSProgress终于支持这一点,让我从嵌套操作(Upload Item运转)向母公司操作传播的进步,然后在主线程和UI。但是我很难实现这一点,似乎NSProgress更适合于在一个后台线程上执行所有代码的长操作,但它们有单独的“部分”,可以很容易地确定何时取得进展,如果是这样是这种情况,那么使用术语suboperation有点让人误解,因为它让人想起使用嵌套NSOperations

感谢您提供的任何帮助,如果需要更多详细信息,请告知我们。

回答

13

NSProgressNSOperations一无所知 - 两件事情是正交的 - 但这并不意味着它不能与它们一起使用。嵌套0​​“任务”背后的想法是,内部任务不知道任何有关外部任务的信息,而外部任务不需要直接访问内部任务的NSProgress来为其提供更新。我做了一个小例子:

// Outer grouping 
NSProgress* DownloadGroupsOfFiles(NSUInteger numGroups, NSUInteger filesPerGroup) 
{ 
    // This is the top level NSProgress object 
    NSProgress* p = [NSProgress progressWithTotalUnitCount: numGroups]; 

    for (NSUInteger i = 0; i < numGroups; ++i) 
    { 
     // Whatever DownloadFiles does, it's worth "1 unit" to us. 
     [p becomeCurrentWithPendingUnitCount: 1]; 

     DownloadFiles(filesPerGroup); 

     [p resignCurrent]; 
    } 

    return p; 
} 

// Inner grouping 
void DownloadFiles(NSUInteger numberOfFiles) 
{ 
    NSProgress* p = [NSProgress progressWithTotalUnitCount: numberOfFiles]; 
    NSOperationQueue* opQueue = [[NSOperationQueue alloc] init]; 

    // Make the op queue last as long as the NSProgress 
    objc_setAssociatedObject(p, NULL, opQueue, OBJC_ASSOCIATION_RETAIN); 

    // For each file... 
    for (NSUInteger i = 0; i < numberOfFiles; ++i) 
    { 
     // Whatever this DownloadOperation does is worth 1 "unit" to us. 
     [p becomeCurrentWithPendingUnitCount: 1]; 

     // Make the new operation 
     MyDownloadOperation* op = [[MyDownloadOperation alloc] initWithName: [NSString stringWithFormat: @"File #%@", @(i+1)]]; 
     [opQueue addOperation: op]; 

     [p resignCurrent]; 
    } 
} 

// And then the DownloadOperation might look like this... 
@interface MyDownloadOperation : NSOperation 
@property (nonatomic, readonly, copy) NSString* name; 
- (id)initWithName: (NSString*)name; 
@end 

@implementation MyDownloadOperation 
{ 
    NSProgress* _progress; 
    NSString* _name; 
} 

- (id)initWithName:(NSString *)name 
{ 
    if (self = [super init]) 
    { 
     _name = [name copy]; 
     // Do this in init, so that our NSProgress instance is parented to the current one in the thread that created the operation 
     _progress = [NSProgress progressWithTotalUnitCount: 1]; 
    } 
    return self; 
} 

- (void)dealloc 
{ 
    _name = nil; 
    _progress = nil; 
} 

- (void)main 
{ 
    // Fake like we're doing something that takes some time 

    // Determine fake size -- call it 768K +- 256K 
    const NSUInteger size = 512 * 1024 + arc4random_uniform(512*1024); 
    const NSUInteger avgBytesPerSec = 1024 * 1024; 
    const NSTimeInterval updatePeriod = 1.0/60.0; 

    // Make sure all the updates to the NSProgress happen on the main thread 
    // in case someone is bound to it. 
    dispatch_async(dispatch_get_main_queue(), ^{ 
     _progress.totalUnitCount = size; 
     _progress.completedUnitCount = 0; 
    }); 

    NSUInteger bytesRxd = 0; 
    do 
    { 
     // Sleep for a bit... 
     usleep(USEC_PER_SEC * updatePeriod); 

     // "Receive some data" 
     NSUInteger rxdThisTime = updatePeriod * avgBytesPerSec; 

     // Never report more than all the bytes 
     bytesRxd = MIN(bytesRxd + rxdThisTime, size); 

     // Update on the main thread... 
     dispatch_async(dispatch_get_main_queue(), ^{ 
      [_progress setCompletedUnitCount: bytesRxd]; 
     }); 
    } while (bytesRxd < size); 
} 

@end 

有一点需要注意的是,如果NSProgress被用来传达状态的UI,那么你将要确保每次更新NSProgress对象,你做所以从主线程,否则你会得到很多奇怪的崩溃。

或者你可以只使用NSURLConnection的下载文件,然后有一个这样的委托:

@interface MyURLConnectionProgressReporter : NSObject <NSURLConnectionDownloadDelegate> 
@property (nonatomic, readwrite, assign) id<NSURLConnectionDownloadDelegate> delegate; 
@end 

NSProgress* DownloadABunchOfFiles(NSArray* arrayOfURLs) 
{ 
    arrayOfURLs = arrayOfURLs.count ? arrayOfURLs : @[ [NSURL URLWithString: @"http://www.google.com"] ]; 

    NSProgress* p = [NSProgress progressWithTotalUnitCount: arrayOfURLs.count]; 

    for (NSURL* url in arrayOfURLs) 
    { 
     [p becomeCurrentWithPendingUnitCount: 1]; 

     MyURLConnectionProgressReporter* delegate = [[MyURLConnectionProgressReporter alloc] init]; 
     NSURLConnection* conn = [[NSURLConnection alloc] initWithRequest: [NSURLRequest requestWithURL: url] delegate: delegate]; 
     [conn start]; 

     [p resignCurrent]; 
    } 

    return p; 

} 

@implementation MyURLConnectionProgressReporter 
{ 
    NSProgress* _progress; 
} 

static void EnsureMainThread(dispatch_block_t block); 

- (id)init 
{ 
    if (self = [super init]) 
    { 
     _progress = [NSProgress progressWithTotalUnitCount: 1]; 
     EnsureMainThread(^{ 
      _progress.kind = NSProgressKindFile; 
      [_progress setUserInfoObject:NSProgressFileOperationKindDownloading forKey:NSProgressFileOperationKindKey]; 
     }); 
    } 
    return self; 
} 

- (id)forwardingTargetForSelector:(SEL)aSelector 
{ 
    id retVal = [super forwardingTargetForSelector:aSelector]; 
    if (!retVal && [self.delegate respondsToSelector: _cmd]) 
    { 
     retVal = self.delegate; 
    } 
    return retVal; 
} 

- (void)p_updateWithTotalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long) expectedTotalBytes 
{ 
    // Update our progress on the main thread... 
    EnsureMainThread(^{ 
     if (!expectedTotalBytes) 
      _progress.totalUnitCount = -1; 
     else 
      _progress.totalUnitCount = MAX(_progress.totalUnitCount, expectedTotalBytes); 

     _progress.completedUnitCount = totalBytesWritten; 
    }); 
} 

- (void)connection:(NSURLConnection *)connection didWriteData:(long long)bytesWritten totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long) expectedTotalBytes 
{ 
    // Update our progress 
    [self p_updateWithTotalBytesWritten: totalBytesWritten expectedTotalBytes: expectedTotalBytes]; 

    // Then call on through to the other delegate 
    if ([self.delegate respondsToSelector: _cmd]) 
    { 
     [self.delegate connection:connection didWriteData:bytesWritten totalBytesWritten:totalBytesWritten expectedTotalBytes:expectedTotalBytes]; 
    } 
} 

- (void)connectionDidResumeDownloading:(NSURLConnection *)connection totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long) expectedTotalBytes 
{ 
    // Update our progress 
    [self p_updateWithTotalBytesWritten: totalBytesWritten expectedTotalBytes: expectedTotalBytes]; 

    // Then call on through to the other delegate 
    if ([self.delegate respondsToSelector: _cmd]) 
    { 
     [self.delegate connectionDidResumeDownloading:connection totalBytesWritten:totalBytesWritten expectedTotalBytes:expectedTotalBytes]; 
    } 
} 

- (void)connectionDidFinishDownloading:(NSURLConnection *)connection destinationURL:(NSURL *) destinationURL 
{ 
    // We're done, so we want (_progress.completedUnitCount == _progress.totalUnitCount) 
    EnsureMainThread(^{ 
     _progress.completedUnitCount = _progress.totalUnitCount; 
    }); 

    if ([self.delegate respondsToSelector: _cmd]) 
    { 
     [self.delegate connectionDidFinishDownloading:connection destinationURL:destinationURL]; 
    } 
} 

static void EnsureMainThread(dispatch_block_t block) 
{ 
    if (!block) 
     return; 
    else if ([NSThread isMainThread]) 
     block(); 
    else 
     dispatch_async(dispatch_get_main_queue(), block); 
} 

@end 

希望有所帮助。

+0

不应该调用'[p becomeCurrentWithPendingUnitCount:numGroups];'在第一个for循环之外? – Eric

+2

@Eric这将使子进程之间的关系(可能)在父进程比例方面不相等。换句话说,如果你想让每个文件在父代中代表1个单位的进度,那么你需要这样做。如果您确定子进程是在像字节这样的共享单元中指定的(这里可能是一个安全的假设,但不是在任何地方),并且您希望将该单元作为父进程报告的一部分公开,那么是的,你可以将它移到外面。 – ipmcc

+0

这是一个很棒的答案,谢谢。 – Sam