Использование NSProgress с вложенными NSOperations

Я расследовал NSProgress но обнаружили, что существующая документация, ссылки на классы и учебные пособия отсутствуют. Мне в основном интересно, применим ли мой NSProgress к моему сценарию использования. Ссылочная документация по классу альтернативно относится к suboperations или же subtasksЯ могу ошибаться, но я истолковал suboperations означать случай, когда NSOperation управляет группой других NSOperations, Пример моего варианта использования следующий:

  • Создать Upload All Items in Group операция для каждой группы, которая существует.
  • Добавьте каждую из этих операций в NSOperationQueue,
  • каждый Upload All Items in Group операция создаст Upload Item операция для каждого элемента в своей группе. Все они добавляются в NSOperationQueue управляемый операцией.

Я бы ожидал NSProgress чтобы поддержать это, и позвольте мне распространять прогресс от вложенных операций (Upload Item операция) к родительской операции, а затем, наконец, к основному потоку и пользовательскому интерфейсу. Но мне было трудно реализовать это, кажется, как будто NSProgress больше предназначен для длинных операций, которые выполняют весь свой код в одном фоновом потоке, но имеют отдельные "разделы", которые позволяют легко определить, когда был достигнут прогресс, если это так, то использование термина suboperation немного вводит в заблуждение, так как напоминает использование вложенных NSOperations,

Спасибо за любую помощь, которую вы можете оказать, и дайте мне знать, если понадобятся дополнительные детали.

1 ответ

Решение

NSProgress ничего не знает о NSOperations - две вещи ортогональны - но это не значит, что их нельзя использовать с ними. Идея вложенности NSProgress "Задачи" в том, что внутренняя задача ничего не знает о внешней задаче, а внешняя задача не нуждается в прямом доступе к внутренней задаче. 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 используется для передачи статуса в пользовательский интерфейс, тогда вы захотите убедиться, что каждый раз, когда вы обновляете 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

Надеюсь, это поможет.

Другие вопросы по тегам