8

Ho indagato su NSProgress ma ho trovato la documentazione esistente, il riferimento di classe e le esercitazioni mancanti. Mi sto principalmente chiedendo se il mio NSProgress sia applicabile al mio caso d'uso. La documentazione di riferimento di classe fa riferimento alternativamente a suboperations o subtasks, Potrei sbagliarmi, ma ho interpretato suboperations come un caso in cui una NSOperation gestisce un gruppo di altri NSOperations. Un esempio del mio caso d'uso è il seguente:Utilizzo di NSProgress con NSOperations nidificate

  • Creare un'operazione Upload All Items in Group per ogni gruppo esistente.
  • Aggiungere ognuna di queste operazioni a NSOperationQueue.
  • Ogni operazione Upload All Items in Group creerà un'operazione Upload Item per ciascun elemento nel proprio gruppo. Questi vengono aggiunti a un NSOperationQueue gestito dall'operazione.

mi sarei aspettato NSProgress a sostegno di questa, e mi permette di propagare i progressi dalle operazioni nidificate (Upload Item funzionamento) al funzionamento genitore, e poi finalmente al thread principale e l'interfaccia utente. Ma ho avuto difficoltà nell'implementare questo, sembra che lo NSProgress significhi più operazioni lunghe che eseguono tutto il loro codice su un thread in background, ma hanno "sezioni" separate che rendono facile determinare quando sono stati fatti progressi, se questo è il caso quindi l'uso del termine suboperation è un po 'fuorviante in quanto ricorda l'uso di nidificato NSOperations.

Grazie per l'aiuto che puoi fornire e fammi sapere se sono necessari ulteriori dettagli.

risposta

13

NSProgress non sa nulla di NSOperations - le due cose sono ortogonali - ma ciò non significa che non possa essere utilizzato con esse. L'idea alla base della nidificazione delle "attività" NSProgress è che l'attività interna non conosce nulla sull'attività esterna e l'attività esterna non ha bisogno dell'accesso diretto all'unità NSProgress dell'attività interna per ottenere aggiornamenti per essa. Ho cucinato un piccolo esempio:

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

Una cosa da notare è che se NSProgress viene utilizzato per trasmettere stato per l'interfaccia utente, poi si vuole fare in modo che ogni volta che si aggiorna l'oggetto NSProgress, si fa così dal thread principale, altrimenti avrai un sacco di strani incidenti.

In alternativa si potrebbe utilizzare NSURLConnection per scaricare i file, e quindi avere un delegato come questo:

@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 

Speranza che aiuta.

+0

Non dovresti chiamare '[p becomeCurrentWithPendingUnitCount: numGroups];' al di fuori del primo ciclo for? – Eric

+2

@Eric Ciò renderebbe la relazione tra i sotto-progressi (potenzialmente) non uguali in termini della loro proporzione del progresso genitore. Metti diversamente, se vuoi che ogni file rappresenti 1 unità di progresso nel genitore, allora devi farlo in questo modo. Se sei * sicuro * che i sottoprocessi sono specificati in unità reciprocamente condivise come i byte, (probabilmente un presupposto sicuro qui, ma non ovunque) e vuoi esporre quell'unità come parte della segnalazione di progresso genitore, allora sì, potresti spostarlo all'esterno. – ipmcc

+0

Questa è una risposta fantastica, grazie. – Sam