Использование 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
Надеюсь, это поможет.