0

I'm saving images to photo library using ALAssetsLibrary. When when it gets in loop it runs simultaneously and causing memory problems. How can I run this without causing memory problems

ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
for(UIImage *image in images){
    [library writeImageToSavedPhotosAlbum:[image CGImage] 
                              orientation:(ALAssetOrientation)[image imageOrientation]       
                          completionBlock:^(NSURL *assetURL, NSError *error) 
    {
        ... //
    }];
}
CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67
Dulguun Otgon
  • 1,925
  • 1
  • 19
  • 38

3 Answers3

5

If you want to ensure that these writes happen serially, you could use a semaphore to wait for the completion of the image before initiating the next write:

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

for (UIImage *image in images) {
    [library writeImageToSavedPhotosAlbum:[image CGImage] orientation:(ALAssetOrientation)[image imageOrientation] completionBlock:^(NSURL *assetURL, NSError *error) {
        dispatch_semaphore_signal(semaphore);                  // signal when done
    }];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // wait for signal before continuing
}

And, since you probably don't want to block the main queue while this is going on, you might want to dispatch that whole thing to some background queue:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    for (UIImage *image in images) {
        [library writeImageToSavedPhotosAlbum:[image CGImage] orientation:(ALAssetOrientation)[image imageOrientation] completionBlock:^(NSURL *assetURL, NSError *error) {
            dispatch_semaphore_signal(semaphore);                  // signal when done
        }];
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // wait for signal before continuing
    }
});

You could, alternatively, wrap this writeImageToSavedPhotosAlbum in a custom NSOperation that doesn't post isFinished until the completion block, but that seems like overkill to me.


Having said that, I worry a little about this array of images, where you're holding all of the UIImage objects in memory at the same time. If they are large, or if you have many images, that could be problematic. Often you would want to simply maintain an array of, say, image names, and then instantiate the images one at a time, e.g.:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    for (NSString *path in imagePaths) {
        @autoreleasepool {
            ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];

            UIImage *image = [UIImage imageWithContentsOfFile:path];

            [library writeImageToSavedPhotosAlbum:[image CGImage] orientation:(ALAssetOrientation)[image imageOrientation] completionBlock:^(NSURL *assetURL, NSError *error) {
                dispatch_semaphore_signal(semaphore);                  // signal when done
            }];

            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // wait for signal before continuing
        }
    }
});

One should generally be wary of any coding pattern that presumes the holding an array of large objects, like images, in memory at the same time.

If you need to frequently access these images, but don't always want to re-retrieve it from persistent storage every time, you could employ a NSCache pattern (e.g. try to retrieve image from cache; if not found, retrieve from persistent storage and add it to the cache; upon memory pressure, empty cache), that way you enjoy the performance benefits of holding images in memory, but gracefully handle situation where the image cache consumes too much memory.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Thank you I think my code got improved with your help. But it still gets shuts down after saving 3 images – Dulguun Otgon Nov 28 '13 at 09:07
  • It closes and generates error message in Xcode says 'Terminated due to memory pressure'. BTW just found out simultaneous calls to `library writeImageToSavedPhotosAlbum` causes more higher memory spike after each time it get's called regardless of the time interval it gets called. – Dulguun Otgon Nov 28 '13 at 11:13
  • Is your app in the background when Xcode logs 'Terminated due to memory pressure'? – CouchDeveloper Nov 28 '13 at 11:27
  • @DulguunLst You can probably then solve that by moving the instantiation of the library into the autorelease pool. There's probably a modest performance hit for that (re-instantiating it for every image), but should solve memory issue. See revised answer (the last piece of code). – Rob Nov 28 '13 at 11:43
  • Tried putting instantiation into autorelease pool and set it to Nil after every completion and recreating it on the next image. Result came out same starting to suspect this is xcode or ios bug. Or I'm missing some call to clear some unknown stuff. Really confused now. – Dulguun Otgon Nov 30 '13 at 06:37
  • @DulguunLst Are you absolutely sure that this is the source of your memory leak? – Rob Nov 30 '13 at 13:56
  • Yes, without it application's memory usage doesn't spike – Dulguun Otgon Dec 02 '13 at 03:37
1

Another approach is to implement an "asynchronous loop":

typedef void(^completion_t)(id result, NSError* error);

- (void) saveImages:(NSMutableArray*)images 
     toAssetLibrary:(ALAssetsLibrary*)library 
         completion:(completion_t)completionHandler
{
    if ([images count] > 0) {
        UIImage* image = [images firstObject];
        [images removeObjectAtIndex:0];
        
        [library writeImageToSavedPhotosAlbum:[image CGImage]
                                  orientation:(ALAssetOrientation)[image imageOrientation]
                              completionBlock:^(NSURL *assetURL, NSError *error)
        {
            if (error) {
                
            }
            else {
            }
            [self saveImages:images toAssetLibrary:library completion:completionHandler];
        }];
    }
    else {
        // finished
        if (completionHandler) {
            completionHandler(@"Images saved", nil);
        }
    }
}

Notice:

  • The method saveImages:toAssetLibrary:completion: is an asynchronous method.

  • It sequentially processes a list of images.

  • The completion handler will be called when all images have been saved.

In order to accomplish this, the implementation above invokes itself in the completion handler of writeImageToSavedPhotosAlbum:orientation:completionBlock:.

This is NOT a recursive method invocation, though: when the completion handler invokes the method saveImages:toAssetLibrary:completion:, the method already returned.

Possible improvements:

  • For brevity, the sample has no error handling. This should be improved in a real implementation.

  • Instead of having a list of images, you'd better off using a list of URLs to the images.

Usage

You may use it like this:

ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];   
[self saveImages:[self.images mutableCopy] toAssetLibrary:library 
completion:^(id result, NSError*error) {
    ... 
}];
Community
  • 1
  • 1
CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67
  • I implemented something like this but without the completion handler and it still caused memory warning. And tried yours, still caused memory warning. I think it's because of this ALAssetsLibrary. What a nasty thing it is. – Dulguun Otgon Nov 28 '13 at 09:55
  • Perhaps using a list of URLs instead of a list of images? (see Rob's second suggestion) – CouchDeveloper Nov 28 '13 at 10:16
  • Yeah I used list of URLs as NSStrings. But I think I might have found little improvement. Tried letting method create it's own ALAssetsLibrary and setting it `Nil` before calling itself again. Application received memory warning but didn't shut down sometimes. – Dulguun Otgon Nov 28 '13 at 10:37
  • BTW just found out simultaneous calls to `library writeImageToSavedPhotosAlbum` causes more higher memory spike after each time it get's called regardless of the time interval it gets called. – Dulguun Otgon Nov 28 '13 at 11:13
  • @DulguunLst Ensure, `writeImageToSavedPhotosAlbum:..` will be invoked one after the other. Ensure, you use URLs and load the image before writing to the asset library. When done writing to the library, set the image to nil. In addition to that, use an autorelease pool where you wrap all these methods to ensure temporary objects get freed. If this does not help, you need to dig deeper and try to figure the cause of the memory consumption utilizing Instruments. – CouchDeveloper Nov 28 '13 at 11:23
  • Tried putting instantiation of ALAssetsLibrary into autorelease pool and set it to Nil after every completion and recreating it on the next image. Result came out same (memory spike gets higher on every call to `writeImageToSavedPhotosAlbum`) starting to suspect this is xcode or ios bug. Or I'm missing some call to clear some unknown stuff. Really confused now. – Dulguun Otgon Nov 30 '13 at 06:38
0

If you want the update to use dispatch_async instead.

// This will wait to finish
dispatch_async(dispatch_get_main_queue(), ^{
    // Update the UI on the main thread.
});
codercat
  • 22,873
  • 9
  • 61
  • 85