Диспетчеризация очередей и асинхронный RNCryptor
Это продолжение асинхронного дешифрования большого файла с помощью RNCryptor на iOS
Мне удалось асинхронно дешифровать большой загруженный файл (60 МБ) с помощью метода, описанного в этом посте, исправленного Калманом в его ответе.
В основном это выглядит так:
int blockSize = 32 * 1024;
NSInputStream *cryptedStream = [NSInputStream inputStreamWithFileAtPath:...];
NSOutputStream *decryptedStream = [NSOutputStream output...];
[cryptedStream open];
[decryptedStream open];
RNDecryptor *decryptor = [[RNDecryptor alloc] initWithPassword:@"blah" handler:^(RNCryptor *cryptor, NSData *data) {
NSLog("Decryptor recevied %d bytes", data.length);
[decryptedStream write:data.bytes maxLength:data.length];
if (cryptor.isFinished) {
[decryptedStream close];
// call my delegate that I'm finished with decrypting
}
}];
while (cryptedStream.hasBytesAvailable) {
uint8_t buf[blockSize];
NSUInteger bytesRead = [cryptedStream read:buf maxLength:blockSize];
NSData *data = [NSData dataWithBytes:buf length:bytesRead];
[decryptor addData:data];
NSLog("Sent %d bytes to decryptor", bytesRead);
}
[cryptedStream close];
[decryptor finish];
Тем не менее, я все еще сталкиваюсь с проблемой: все данные загружаются в память перед расшифровкой. Я вижу группу "Отправлено X байтов в расшифровщик", и после этого та же самая группа "Дешифровщик получил X байтов" в консоли, когда я хочу видеть "Отправлено, получено, отправлено, получено, ...".
Это хорошо для небольших (2 МБ) файлов или для больших (60 МБ) файлов на симуляторе; но на реальном iPad1 происходит сбой из-за ограничений памяти, поэтому, очевидно, я не могу сохранить эту процедуру для своего производственного приложения.
Я чувствую, что мне нужно отправить данные в расшифровщик с помощью dispatch_async
вместо того, чтобы слепо отправить его в while
петля, однако я полностью потерян. Я пробовал:
- создавая свою очередь перед
while
и используяdispatch_async(myQueue, ^{ [decryptor addData:data]; });
- то же самое, но отправка всего кода внутри
while
петля - то же самое, но отправка целого
while
петля - с помощью
RNCryptor
-предоставленаresponseQueue
вместо моей собственной очереди
Ничего не работает среди этих 4 вариантов.
У меня пока нет полного понимания очередей отправки; Я чувствую, что проблема заключается здесь. Я был бы рад, если бы кто-то мог пролить свет на это.
3 ответа
Если вы хотите обрабатывать только один блок за раз, то обрабатывайте блок только тогда, когда первый блок перезвонит вам. Вам не нужен семафор, чтобы сделать это, вам просто нужно выполнить следующее чтение внутри обратного вызова. Вы могли бы хотеть @autoreleasepool
блок внутри readStreamBlock
Но я не думаю, что тебе это нужно.
Когда у меня будет время, я, вероятно, перенесу это прямо в RNCryptor. Я открыл выпуск № 47 для этого. Я открыт, чтобы тянуть запросы.
// Make sure that this number is larger than the header + 1 block.
// 33+16 bytes = 49 bytes. So it shouldn't be a problem.
int blockSize = 32 * 1024;
NSInputStream *cryptedStream = [NSInputStream inputStreamWithFileAtPath:@"C++ Spec.pdf"];
NSOutputStream *decryptedStream = [NSOutputStream outputStreamToFileAtPath:@"/tmp/C++.crypt" append:NO];
[cryptedStream open];
[decryptedStream open];
// We don't need to keep making new NSData objects. We can just use one repeatedly.
__block NSMutableData *data = [NSMutableData dataWithLength:blockSize];
__block RNEncryptor *decryptor = nil;
dispatch_block_t readStreamBlock = ^{
[data setLength:blockSize];
NSInteger bytesRead = [cryptedStream read:[data mutableBytes] maxLength:blockSize];
if (bytesRead < 0) {
// Throw an error
}
else if (bytesRead == 0) {
[decryptor finish];
}
else {
[data setLength:bytesRead];
[decryptor addData:data];
NSLog(@"Sent %ld bytes to decryptor", (unsigned long)bytesRead);
}
};
decryptor = [[RNEncryptor alloc] initWithSettings:kRNCryptorAES256Settings
password:@"blah"
handler:^(RNCryptor *cryptor, NSData *data) {
NSLog(@"Decryptor recevied %ld bytes", (unsigned long)data.length);
[decryptedStream write:data.bytes maxLength:data.length];
if (cryptor.isFinished) {
[decryptedStream close];
// call my delegate that I'm finished with decrypting
}
else {
// Might want to put this in a dispatch_async(), but I don't think you need it.
readStreamBlock();
}
}];
// Read the first block to kick things off
readStreamBlock();
Сирил,
Причиной сбоя вашего приложения из-за ограничений памяти является то, что буфер RNCryptor выходит за пределы возможностей устройства.
По сути, вы читаете содержимое файла гораздо быстрее, чем RNCryptor может его обработать. Поскольку он не может расшифровать достаточно быстро, он буферизует входящий поток, пока не сможет его обработать.
У меня еще нет времени погрузиться в код RNCryptor и выяснить, как именно он использует GCD для управления всем, но вы можете использовать семафор, чтобы заставить чтение ждать, пока предыдущий блок не был расшифрован.
Приведенный ниже код может успешно расшифровать файл размером 225 МБ на iPad 1 без сбоев.
У него есть несколько проблем, которыми я не совсем доволен, но он должен дать вам достойную отправную точку.
Некоторые вещи на заметку:
- Я обернул внутреннюю часть цикла while в блок @autoreleasepool для принудительного освобождения данных. Без этого релиз не произойдет, пока не закончится цикл while. (У Мэтта Галлоуэя есть отличный пост, объясняющий это здесь: Взгляд под капот ARC
- Вызов dispatch_semaphore_wait блокирует выполнение до тех пор, пока не будет получен dispatch_semaphore_signal. Это означает отсутствие обновлений пользовательского интерфейса и вероятность зависания приложения, если вы отправите слишком много (таким образом, проверка на bytesRead > 0).
Лично я чувствую, что должно быть лучшее решение для этого, но у меня еще не было времени, чтобы исследовать это немного больше.
Надеюсь, это поможет.
- (IBAction)decryptWithSemaphore:(id)sender {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block int total = 0;
int blockSize = 32 * 1024;
NSArray *docPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *input = [[docPaths objectAtIndex:0] stringByAppendingPathComponent:@"zhuge.rncryptor"];
NSString *output = [[docPaths objectAtIndex:0] stringByAppendingPathComponent:@"zhuge.decrypted.pdf"];
NSInputStream *cryptedStream = [NSInputStream inputStreamWithFileAtPath:input];
__block NSOutputStream *decryptedStream = [NSOutputStream outputStreamToFileAtPath:output append:NO];
__block NSError *decryptionError = nil;
[cryptedStream open];
[decryptedStream open];
RNDecryptor *decryptor = [[RNDecryptor alloc] initWithPassword:@"12345678901234567890123456789012" handler:^(RNCryptor *cryptor, NSData *data) {
@autoreleasepool {
NSLog(@"Decryptor recevied %d bytes", data.length);
[decryptedStream write:data.bytes maxLength:data.length];
dispatch_semaphore_signal(semaphore);
data = nil;
if (cryptor.isFinished) {
[decryptedStream close];
decryptionError = cryptor.error;
// call my delegate that I'm finished with decrypting
}
}
}];
while (cryptedStream.hasBytesAvailable) {
@autoreleasepool {
uint8_t buf[blockSize];
NSUInteger bytesRead = [cryptedStream read:buf maxLength:blockSize];
if (bytesRead > 0) {
NSData *data = [NSData dataWithBytes:buf length:bytesRead];
total = total + bytesRead;
[decryptor addData:data];
NSLog(@"New bytes to decryptor: %d Total: %d", bytesRead, total);
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
}
}
[cryptedStream close];
[decryptor finish];
dispatch_release(semaphore);
}
Потратив последние 2 дня, пытаясь заставить мой MBProgress hud обновить свой прогресс с помощью кода Calman, я пришел к следующему. Используемая память все еще остается низкой, а пользовательский интерфейс обновляется
- (IBAction)decryptWithSemaphore:(id)sender {
__block int total = 0;
int blockSize = 32 * 1024;
NSArray *docPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *input = [[docPaths objectAtIndex:0] stringByAppendingPathComponent:@"zhuge.rncryptor"];
NSString *output = [[docPaths objectAtIndex:0] stringByAppendingPathComponent:@"zhuge.decrypted.pdf"];
NSInputStream *cryptedStream = [NSInputStream inputStreamWithFileAtPath:input];
__block NSOutputStream *decryptedStream = [NSOutputStream outputStreamToFileAtPath:output append:NO];
__block NSError *decryptionError = nil;
__block RNDecryptor *encryptor=nil;
NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:input error:NULL];
__block long long fileSize = [attributes fileSize];
[cryptedStream open];
[decryptedStream open];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_async(queue, ^{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
encryptor = [[RNDecryptor alloc] initWithPassword:@"12345678901234567890123456789012" handler:^(RNCryptor *cryptor, NSData *data) {
@autoreleasepool {
NSLog(@"Decryptor recevied %d bytes", data.length);
[decryptedStream write:data.bytes maxLength:data.length];
dispatch_semaphore_signal(semaphore);
data = nil;
if (cryptor.isFinished) {
[decryptedStream close];
decryptionError = cryptor.error;
[cryptedStream close];
[encryptor finish];
// call my delegate that I'm finished with decrypting
}
}
}];
while (cryptedStream.hasBytesAvailable) {
@autoreleasepool {
uint8_t buf[blockSize];
NSUInteger bytesRead = [cryptedStream read:buf maxLength:blockSize];
if (bytesRead > 0) {
NSData *data = [NSData dataWithBytes:buf length:bytesRead];
total = total + bytesRead;
[encryptor addData:data];
NSLog(@"New bytes to decryptor: %d Total: %d", bytesRead, total);
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_main_queue(), ^{
HUD.progress = (float)total/fileSize;
});
}
}
}
});
}