8

Ich habe NSProgress untersucht, aber die vorhandene Dokumentation, Klassenreferenz und Anleitungen fehlen. Ich frage mich hauptsächlich, ob meine NSProgress auf meinen Anwendungsfall anwendbar ist. Die Klassenreferenzdokumentation bezieht sich alternativ auf suboperations oder subtasks, ich mag mich irren, aber ich interpretierte suboperations als einen Fall, in dem eine NSOperation eine Gruppe von anderen NSOperations verwaltet. Ein Beispiel für meinen Anwendungsfall ist wie folgt:Verwenden von NSProgress mit geschachtelten NSOperationen

  • Erstellen Sie eine Upload All Items in Group Operation für jede Gruppe, die existiert.
  • Fügen Sie diese Operationen zu NSOperationQueue hinzu.
  • Jede Upload All Items in Group Operation wird eine Upload Item Operation für jedes Element in ihrer Gruppe erstellen. Diese werden alle zu einem von der Operation verwalteten NSOperationQueue hinzugefügt.

würde ich NSProgress erwartet, dies zu unterstützen, und mir erlauben Fortschritte aus den verschachtelten Operationen zu propagieren (Upload Item Betrieb) an den Mutterbetrieb und dann schließlich zu dem Haupt-Thread und die Benutzeroberfläche. Aber ich hatte Schwierigkeiten, dies zu implementieren, es scheint, als ob NSProgress eher für lange Operationen gedacht ist, die ihren gesamten Code auf einem Hintergrundthread ausführen, aber separate "Sektionen" haben, die es leicht machen zu bestimmen, wann der Fortschritt gemacht wurde ist der Fall, dann ist die Verwendung des Begriffs suboperation ein bisschen irreführend, da es an die Verwendung von verschachtelten NSOperations erinnert.

Vielen Dank für Ihre Hilfe, und lassen Sie mich wissen, wenn zusätzliche Details benötigt werden.

Antwort

13

NSProgress weiß nichts über NSOperations - die beiden Dinge sind orthogonal - aber das heißt nicht, dass es nicht mit ihnen verwendet werden kann. Die Idee hinter der Verschachtelung von NSProgress "Aufgaben" ist, dass die innere Aufgabe nichts über die äußere Aufgabe weiß, und die äußere Aufgabe keinen direkten Zugriff auf die innere Aufgabe benötigt, um Updates für sie einzuholen NSProgress. Ich kochte ein kleines Beispiel folgenden:

// 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 

Eines ist zu beachten, dass, wenn NSProgress benutzt wird Status der Benutzeroberfläche zu vermitteln, dann werden Sie sicherstellen möchten, dass Sie jedes Mal das NSProgress Objekt aktualisieren, was Sie tun also vom Hauptthread, sonst wirst du viele komische Abstürze bekommen.

Alternativ könnten Sie einfach NSURLConnection verwenden, um Dateien herunterzuladen und haben dann einen Delegaten wie folgt aus:

@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 

Hoffnung, das hilft.

+0

Sollten Sie '[p ancomeCurrentWithPendingUnitCount: numGroups]; 'nicht außerhalb der ersten for-Schleife aufrufen? – Eric

+2

@Eric Das würde die Beziehung zwischen den Teilprogresses (potenziell) ungleich in Bezug auf ihren Anteil des übergeordneten Fortschritts machen. Anders gesagt, wenn Sie möchten, dass jede Datei 1 Einheit Fortschritt im Elternteil darstellt, müssen Sie dies auf diese Weise tun. Wenn Sie * sicher * sind, dass die Unterfortschritte in einigen gemeinsam genutzten Einheiten wie Bytes angegeben werden (wahrscheinlich eine sichere Annahme, aber nicht überall) und Sie diese Einheit als Teil der Elternfortschrittsberichte verfügbar machen wollen, dann ja, Du könntest es nach draußen bewegen. – ipmcc

+0

Das ist eine tolle Antwort, danke. – Sam

Verwandte Themen