This is a follow-up to Asynchronously decrypt a large file with RNCryptor on iOS
I've managed to asynchronously decrypt a large, downloaded file (60Mb) with the method described in this post, corrected by Calman in his answer.
It basically goes like this:
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];
However, I'm still facing a problem: the whole data is loaded in memory before being decrypted. I can see a bunch of "Sent X bytes to decryptor", and after that, the same bunch of "Decryptor recevied X bytes" in the console, when I'd like to see "Sent, received, sent, receives, ...".
That's fine for small (2Mb) files, or with large (60Mb) files on simulator; but on a real iPad1 it crashes due to memory constraints, so obviously I can't keep this procedure for my production app.
I feel like I need to send the data to the decryptor by using dispatch_async
instead of blindly sending it in the while
loop, however I'm completely lost. I've tried:
- creating my own queue before the
while
, and usingdispatch_async(myQueue, ^{ [decryptor addData:data]; });
- the same, but dispatching the whole code inside of the
while
loop - the same, but dispatching the whole
while
loop - using
RNCryptor
-providedresponseQueue
instead of my own queue
Nothing works amongst these 4 variants.
I don't have a complete understanding of dispatch queues yet; I feel the problem lies here. I'd be glad if somebody could shed some light on this.
Cyrille,
The reason your app is crashing due to memory constraints is that the RNCryptor buffer grows beyond the capabilities of the device.
Basically, you're reading the content of the file much faster than RNCryptor can handle it. Since it can't decrypt fast enough it buffers the incoming stream until it can process it.
I haven't yet have time to dive into the RNCryptor code and figure out exactly how it's using GCD to manage everything, but you can use a semaphore to force the reads to wait until the previous block was decrypted.
The code below can successfully decrypt a 225MB file on an iPad 1 without crashing.
It has a few issues that I'm not quite happy with, but it should give you a decent starting point.
Some things to note:
Personally I feel that there must be a better solution for this, but I haven't yet had the time to research it a bit more.
I hope this helps.
If you only want to process one block at a time, then only process a block when the first block calls you back. You don't need a semaphore to do that, you just need to perform the next read inside the callback. You might want an
@autoreleasepool
block inside ofreadStreamBlock
, but I don't think you need it.When I have some time, I'll probably wrap this directly into RNCryptor. I opened Issue#47 for it. I am open to pull requests.
After spending the last 2 days trying to get my MBProgress hud to update its progress with Calman's code i came up with the following. The memory used still stays low and the UI updates
}