9

I'm trying to use RNCryptor to encrypt and decrypt large files (600+MB) on iOS. On the github I found example code on how to use the library asynchronously on streams. This code is similar to the answer of Rob Napier on a question about this same subject.

However, although I think I implemented the code correctly, the app uses up to 1.5 GB of memory (in the iPad 6.1 simulator). I thought the code was supposed to prevent the app from keeping more than one block of data in-memory? So what is going wrong?

In my controller, I create a 'CryptController' which I message with encrypt/decrypt requests.

  // Controller.m
  NSString *password = @"pw123";
  self.cryptor = [[CryptController alloc] initWithPassword:password];

  //start encrypting file
  [self.cryptor streamEncryptRequest:self.fileName andExtension:@"pdf" withURL:[self samplesURL]];

  //wait for encryption to finish
  NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:1];
  do {
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                             beforeDate:timeout];
  } while (![self.cryptor isFinished]);

In CryptController I have:

- (void)streamEncryptionDidFinish {
  if (self.cryptor.error) {
    NSLog(@"An error occurred. You cannot trust decryptedData at this point");
  }
  else {
    NSLog(@"%@ is complete. Use it as you like", [self.tempURL lastPathComponent]);
  }
  self.cryptor = nil;
  self.isFinished = YES;
}

- (void) streamEncryptRequest:(NSString *)fileName andExtension:(NSString *)ext withURL:(NSURL *)directory {

  //Make sure that this number is larger than the header + 1 block.
  int blockSize = 32 * 1024;

  NSString *encryptedFileName = [NSString stringWithFormat:@"streamEnc_%@", fileName];
  self.tempURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
  self.tempURL = [self.tempURL URLByAppendingPathComponent:encryptedFileName isDirectory:NO];
  self.tempURL = [self.tempURL URLByAppendingPathExtension:@"crypt"];

  NSInputStream *decryptedStream = [NSInputStream inputStreamWithURL:[[directory URLByAppendingPathComponent:fileName isDirectory:NO] URLByAppendingPathExtension:ext]];
  NSOutputStream *cryptedStream = [NSOutputStream outputStreamWithURL:self.tempURL append:NO];

  [cryptedStream open];
  [decryptedStream open];

  __block NSMutableData *data = [NSMutableData dataWithLength:blockSize];
  __block RNEncryptor *encryptor = nil;

  dispatch_block_t readStreamBlock = ^{
    [data setLength:blockSize];
    NSInteger bytesRead = [decryptedStream read:[data mutableBytes] maxLength:blockSize];
    if (bytesRead < 0) {
      //Throw an error
    }
    else if (bytesRead == 0) {
      [encryptor finish];
    }
    else {
      [data setLength:bytesRead];
      [encryptor addData:data];
      //NSLog(@"Sent %ld bytes to encryptor", (unsigned long)bytesRead);
    }
  };

  encryptor = [[RNEncryptor alloc] initWithSettings:kRNCryptorAES256Settings
                                           password:self.password
                                            handler:^(RNCryptor *cryptor, NSData *data) {
                                              //NSLog(@"Encryptor received %ld bytes", (unsigned long)data.length);
                                              [cryptedStream write:data.bytes maxLength:data.length];
                                              if (cryptor.isFinished) {
                                                [decryptedStream close];
                                                //call my delegate that i'm finished with decrypting
                                                [self streamEncryptionDidFinish];
                                              }
                                              else {
                                                readStreamBlock();
                                              }
                                            }];

  // Read the first block to kick things off
  self.isFinished = NO;
  readStreamBlock();
}

When I profile using the Allocation Instrument, the allocation categories I see consistently growing are malloc 32.50 KB, malloc 4.00 KB, NSConcreteData and NSSubrangeData. Especially the malloc 32.50 KB grows big, over 1 GB. The responsible caller is [NSConcreteData initWithBytes:length:copy:freeWhenDone:bytesAreVM:] For NSConcreteData the responsible caller is -[NSData(NSData) copyWithZone:].

When I profile using the Leaks Instrument, no leaks are found.

I'm new to Objective-C, and from what I understood, the new ARC is supposed to handle allocation and deallocation of memory. When googling on anything memory related, all the information I find is assuming you don't use ARC (or it didn't exist at time of writing). I sure am using ARC, since I get compile errors saying so when I try to manually deallocate memory.

If anyone can help me with this, it would be greatly appreciated! If any more information is needed, I'll be happy to provide it :) Also, I'm new to StackOverflow, so if there's anything I've overlooked that I should have done, kindly inform me!

Community
  • 1
  • 1
Johanneke
  • 5,443
  • 4
  • 20
  • 33
  • I just tried reading from and writing to streams, just to make sure the memory issue isn't somewhere in there. Memory didn't go over 1 MB, so the issue is with the encryption code somewhere. – Johanneke Mar 12 '13 at 09:32

4 Answers4

7

I finally tried the solution given here, which uses semaphores instead of depending on the callback to wait for the stream. This works perfectly :) The memory usage hovers around 1.1 MB according to the Allocations Instrument. It may not look as neat because of the semaphore syntax, but at least it does what I need it to do.

Other suggestions are still welcome of course :)

- (void)encryptWithSemaphore:(NSURL *)url {
  dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

  __block int total = 0;
  int blockSize = 32 * 1024;

  NSString *encryptedFile = [[url lastPathComponent] stringByDeletingPathExtension];
  NSURL *docsURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
  self.tempURL = [[docsURL URLByAppendingPathComponent:encryptedFile isDirectory:NO] URLByAppendingPathExtension:@"crypt"];

  NSInputStream *inputStream = [NSInputStream inputStreamWithURL:url];
  __block NSOutputStream *outputStream = [NSOutputStream outputStreamWithURL:self.tempURL append:NO];
  __block NSError *encryptionError = nil;

  [inputStream open];
  [outputStream open];

  RNEncryptor *encryptor = [[RNEncryptor alloc] initWithSettings:kRNCryptorAES256Settings
                                                        password:self.password
                                                         handler:^(RNCryptor *cryptor, NSData *data) {
                                                           @autoreleasepool {
                                                             [outputStream write:data.bytes maxLength:data.length];
                                                             dispatch_semaphore_signal(semaphore);

                                                             data = nil;
                                                             if (cryptor.isFinished) {
                                                               [outputStream close];
                                                               encryptionError = cryptor.error;
                                                               // call my delegate that I'm finished with decrypting
                                                             }
                                                           }
                                                         }];
  while (inputStream.hasBytesAvailable) {
    @autoreleasepool {
      uint8_t buf[blockSize];
      NSUInteger bytesRead = [inputStream read:buf maxLength:blockSize];
      if (bytesRead > 0) {
        NSData *data = [NSData dataWithBytes:buf length:bytesRead];

        total = total + bytesRead;
        [encryptor addData:data];

        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
      }
    }
  }

  [inputStream close];
  [encryptor finish];  
}
Community
  • 1
  • 1
Johanneke
  • 5,443
  • 4
  • 20
  • 33
4

Just run:

self.cryptorQueue = dispatch_queue_create([queueName cStringUsingEncoding:NSUTF8StringEncoding], NULL);

dispatch_async(self.cryptorQueue, ^{
        readStreamBlock();
    });

trouble: stack is growing and autorelease pull will not execute release for buffer.

solution: add async in the same queue.. this will let current block to finish execution.

Here is the code:

__block NSMutableData *data = [NSMutableData dataWithLength:blockSize];
__block RNDecryptor *decryptor = nil;


dispatch_block_t readStreamBlock = ^{

    [data setLength:blockSize];

    NSInteger bytesRead = [inputStream read:[data mutableBytes] maxLength:blockSize];
    if (bytesRead < 0) {
        // Throw an error
    }
    else if (bytesRead == 0) {
        [decryptor finish];
    }
    else {

        [data setLength:bytesRead];
        [decryptor addData:data];
    }
};

decryptor = [[RNDecryptor alloc] initWithPassword:@"blah" handler:^(RNCryptor *cryptor, NSData *data) {

        [decryptedStream write:data.bytes maxLength:data.length];
        _percentStatus = (CGFloat)[[decryptedStream propertyForKey:NSStreamFileCurrentOffsetKey] intValue] / (CGFloat)_inputFileSize;
        if (cryptor.isFinished)
        {
            [decryptedStream close];
            [self decryptFinish];
        }
        else
        {
            dispatch_async(cryptor.responseQueue, ^{
                readStreamBlock();
            });
            [self decryptStatusChange];
        }

}];


// Read the first block to kick things off

decryptor.responseQueue = self.cryptorQueue;
[self decryptStart];
dispatch_async(decryptor.cryptorQueue, ^{
    readStreamBlock();
});
alecxe
  • 462,703
  • 120
  • 1,088
  • 1,195
Eugene_skr
  • 79
  • 3
  • I was running into the same problem as the OP and this fixed it for me, specifically putting `readStreamBlock()` into an async call. This enables the thread to go back to the main loop, which allows the data blocks to be autoreleased, before reading the next block of data. Thank you! – Matt Mc Sep 13 '13 at 01:43
  • Thanks for that code - it works well and solves the memory spike issue. But I'm still having problems with downloading large files and encrypting them at the same time as RNCryptor does't have streams support. Has anyone solved it? – Kostia Kim Nov 08 '13 at 06:45
0

I may be wrong, but I think your do...while loop prevents the autorelease pool from draining frequently enough.

Why are you using this loop to wait for the decryptor to finish? You should use the completion block to notify your controller that the drcryptor has finished, instead.

(BTW, welcome to SO, your question is really well asked and that's highly appreciated).

Cyrille
  • 25,014
  • 12
  • 67
  • 90
  • I am using the loop, because I saw that in an example of RNCryptor code, [testAsync](https://github.com/rnapier/RNCryptor/blob/master/RNCryptorTests/RNCryptorTests.m#L168). How do I notify the controller? And does that mean the controller should simply wait for that notification without polling the boolean? – Johanneke Mar 11 '13 at 11:12
  • Yep, just wait for the completion block to fire some method of your controller. You can also use NSNotifications, but currently you've got a `[self streamEncryptionDidFinish]` that seems to do the job. – Cyrille Mar 11 '13 at 11:30
  • Ok, thank you. I'll try it out and see if it solves the problem. When I know more, I'll post back here. – Johanneke Mar 11 '13 at 12:14
  • I now have a method in `Controller.m`, called `returnToMain` which just shows an alert (since my Output log in XCode is not working when profiling I needed a simple way to know what was happening). The `do...while` loop is gone, and the mainthread is just idle I guess? After the call to streamEncryptRequest there is simply no code in `Controller.m`. In `CryptController.m` I do `[Controller performSelectorOnMainThread:@selector(returnToMain) withObject:nil waitUntilDone:NO];` Unfortunately, this does not solve the memory problem. It just accumulates until everything is released at once. – Johanneke Mar 11 '13 at 12:44
0

I might again be wrong, but in your readStreamBlock, data should be a parameter to the block, rather than a reference to a __block NSMutableData declared outside of it. As you can see, the RNEncryptor handler provides its own data variable, which is different from the one you've declared yourself.

Ideally, put all your readStreamBlock directly inside the handler, without even declaring it a block.

Cyrille
  • 25,014
  • 12
  • 67
  • 90
  • I think the `data` variable in `readStreamBlock` is what is passed to the handler, instead of the other way round. In `readStreamBlock`, a block from the inputstream is read into data, after which `[encryptor addData:data]` is called. I thought this meant that `data` is passed to the handler here, so it can be encrypted and written to file using the outputstream. And I could put all of `readStreamBlock` directly inside the handler, but then I'd need to duplicate some code to get the whole process started. As it is I can just call `readStreamBlock` (as I do at the end of the method). – Johanneke Mar 11 '13 at 13:16
  • But I will try to play around with the `data` variable and see if that helps. I just don't think it will. – Johanneke Mar 11 '13 at 13:17
  • The variables declared outside the block and used inside the block (`data`, namely) are captured when the block is created. Thus the `data` variable used inside the block is the one you declared yourself, not the one passed as a parameter of the handler. – Cyrille Mar 11 '13 at 13:45
  • I understand, what I meant was that the variable `data` that is declared outside and used inside the block, is passed to the handler as an argument. So instead of giving the block an argument and passing the `data` parameter from the handler to the block, the block is passing to the handler (through `[encryptor addData:data]`) – Johanneke Mar 11 '13 at 14:02
  • I tried what you suggested, passing the `data` parameter from the handler to the `readStreamBlock`. Problem is that the parameter is an `NSData`, and I need an `NSMutableData` in the block. So I pass unmutable data and then initialize an `NSMutableData` with the contents. Unfortunately, the memory is still accumulating until the process is finished and everything is released. `void (^readStreamBlock)(NSData*) = ^(NSData *dataIn){ NSMutableData *data = [NSMutableData dataWithData:dataIn]; //rest of block }` – Johanneke Mar 11 '13 at 16:12
  • Your problem might not be here, but that seems less error-prone to me this way. – Cyrille Mar 11 '13 at 20:16
  • What if you wrap the code of your `readStreamBlock` inside an `@autoreleasepool` to help draining faster? – Cyrille Mar 11 '13 at 20:18
  • We’ll have to wait for Rob’s expert eyes, I fear. – Cyrille Mar 12 '13 at 08:45