Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

use xcassets without imageNamed to prevent memory problems?

Tags:

according to the apple documentation it is recommended to use xcassets for iOS7 applications and reference those images over imageNamed.

But as far as I'm aware, there were always problems with imageNamed and memory.

So I made a short test application - referencing images out of the xcassets catalogue with imageNamed and started the profiler ... the result was as expected. Once allocated memory wasn't released again, even after I removed the ImageView from superview and set it to nil.

I'm currently working on an iPad application with many large images and this strange imageView behavior leads to memory warnings.

But in my tests I wasn't able to access xcassets images over imageWithContentsOfFile.

So what is the best approach to work with large images on iOS7? Is there a way to access images from the xcassets catalogue in another (more performant) way? Or shouldn't I use xcassets at all so that I can work with imageWithContentsOfFile?

Thank you for your answers!

like image 374
appsperimental Avatar asked Apr 04 '14 07:04

appsperimental


1 Answers

UPDATE: Cache eviction works fines (at least since iOS 8.3).

I decided to go with the "new Images.xcassets" from Apple, too. Things started to go bad, when I had about 350mb of images in the App and the App constantly crashed (on a Retina iPad; probably because of the size of the loaded images).

I have written a very simple test app where I load the images in three different types (watching the profiler):

  1. imageNamed: loaded from an asset: images never gets released and the app crashes (for me I could load 400 images, but it really depends on the image size)

  2. imageNamed: (conventionally included to the project): The memory usage is high and once in a while (> 400 images) I see a call to didReceiveMemoryWarning:, but the app is running fine.

  3. imageWithContentsOfFile([[NSBundle mainBundle] pathForResource:...): The memory usage is very low (<20mb) because the images are only loaded once at a time.

I really would not blame the caching of the imageNamed: method for everything as caching is a good idea if you have to show your images again and again, but it is kind of sad that Apple did not implement it for the assets (or did not document it that it is not implemented). In my use-case, I will go for the non-caching imageWithData because the user won't see the images again.

As my app is almost final and I really like the usage of the loading mechanism to find the right image automatically, I decided to wrap the usage:

  • I removed the images.xcasset from the project-target-copy-phase and added all images "again" to the project and the copy-phase (simply add the top level folder of Images.xcassets directly and make sure that the checkbox "Add To Target xxx" is checked and "Create groups for any added folders" (I did not bother about the useless Contents.json files).
  • During first build check for new warnings if multiple images have the same name (and rename them in a consistent way).
  • For App Icon and Launch Images set "Don't use asset catalog" in project-target-general and reference them manually there.
  • I have written a shell script to generate a json-model from all the Contents.json files (to have the information as Apples uses it in its asset access code)

Script:

cd projectFolderWithImageAsset
echo "{\"assets\": [" > a.json
find Images.xcassets/ -name \*.json | while read jsonfile; do
  tmppath=${jsonfile%.imageset/*}
  assetname=${tmppath##*/}
  echo "{\"assetname\":\"${assetname}\",\"content\":" >> a.json
  cat $jsonfile >> a.json; 
  echo '},' >>a.json
done
echo ']}' >>a.json
  • Remove the last "," comma from json output as I did not bother to do it manually here.
  • I have used the following app to generate json-model-access code: https://itunes.apple.com/de/app/json-accelerator/id511324989?mt=12 (currently free) with prefix IMGA
  • I have written a nice category using method swizzling in order to not change running code (and hopefully removing my code very soon):

(implementation not complete for all devices and fallback mechanisms!!)

#import "UIImage+Extension.h"
#import <objc/objc-runtime.h>
#import "IMGADataModels.h"

@implementation UIImage (UIImage_Extension)


+ (void)load{
static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        Method imageNamed = class_getClassMethod(class, @selector(imageNamed:));
        Method imageNamedCustom = class_getClassMethod(class, @selector(imageNamedCustom:));
        method_exchangeImplementations(imageNamed, imageNamedCustom);
    });
}

+ (IMGABaseClass*)model {
    static NSString * const jsonFile = @"a";
    static IMGABaseClass *baseClass = nil;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSString *fileFilePath = [[NSBundle mainBundle] pathForResource:jsonFile ofType:@"json"];
        NSData* myData = [NSData dataWithContentsOfFile:fileFilePath];
        __autoreleasing NSError* error = nil;
        id result = [NSJSONSerialization JSONObjectWithData:myData
                                                    options:kNilOptions error:&error];
        if (error != nil) {
            ErrorLog(@"Could not load file %@. The App will be totally broken!!!", jsonFile);
        } else {
            baseClass = [[IMGABaseClass alloc] initWithDictionary:result];
        }
    });
    return baseClass;
}


+ (UIImage *)imageNamedCustom:(NSString *)name{

    NSString *imageFileName = nil;
    IMGAContent *imgContent = nil;
    CGFloat scale = 2;

    for (IMGAAssets *asset in [[self model] assets]) {
        if ([name isEqualToString: [asset assetname]]) {
            imgContent = [asset content];
            break;
        }
    }
    if (!imgContent) {
        ErrorLog(@"No image named %@ found", name);
    }

    if (is4InchScreen) {
        for (IMGAImages *image in [imgContent images]) {
            if ([@"retina4" isEqualToString:[image subtype]]) {
                imageFileName = [image filename];
                break;
            }
        }
    } else {
        if ( UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone ) {
            for (IMGAImages *image in [imgContent images]) {
                if ([@"iphone" isEqualToString:[image idiom]] && ![@"retina4" isEqualToString:[image subtype]]) {
                    imageFileName = [image filename];
                    break;
                }
            }
        } else {
            if (isRetinaScreen) {
                for (IMGAImages *image in [imgContent images]) {
                    if ([@"universal" isEqualToString:[image idiom]] && [@"2x" isEqualToString:[image scale]]) {
                        imageFileName = [image filename];
                        break;
                    }
                }
            } else {
                for (IMGAImages *image in [imgContent images]) {
                    if ([@"universal" isEqualToString:[image idiom]] && [@"1x" isEqualToString:[image scale]]) {
                        imageFileName = [image filename];
                        if (nil == imageFileName) {
                            // fallback to 2x version for iPad unretina
                            for (IMGAImages *image in [imgContent images]) {
                                if ([@"universal" isEqualToString:[image idiom]] && [@"2x" isEqualToString:[image scale]]) {
                                    imageFileName = [image filename];
                                    break;
                                }
                            }
                        } else {
                            scale = 1;
                            break;
                        }
                    }
                }
            }
        }
    }

    if (!imageFileName) {
        ErrorLog(@"No image file name found for named image %@", name);
    }

    NSString *imageName = [[NSBundle mainBundle] pathForResource:imageFileName ofType:@""];
    NSData *imgData = [NSData dataWithContentsOfFile:imageName];
    if (!imgData) {
        ErrorLog(@"No image file found for named image %@", name);
    }
    UIImage *image = [UIImage imageWithData:imgData scale:scale];
    DebugVerboseLog(@"%@", imageFileName);
    return image;
}

@end
like image 121
theguy Avatar answered Sep 20 '22 13:09

theguy