domenica 9 settembre 2012

Core Data and Memory Management using large objects - some tips

Ok, let's talk about Core Data.
Core Data is not simply a Database, but an entire framework that takes care about the entire lifecycle of your objects, even ensuring persistency (which means: you can save your objects state and keep it between an app launch and another).
When you decide to manage an object using Core Data (creating an instance of NSManagedObject), you can fetch it from the persistent store, use predicate to filter objects, add, update, delete your objects and then save again the context. You don't have to manage the database select/insert/update.
Remember:

1-fetch or create the objects
2- do your stuff (change properties, read, ecc)
3- save the context

All simple and useful.
But how about memory management? In some cases, it could be not simple to manage memory.
Core Data use a caching system that should do all the work "automagically"...but it won't work if you don't take some care.

We can use an example.
We want to use Core Data to store a simple web image cache. Create an entity:

Entity name: ImageCache
Entity properties:
    NSString *url;
    NSData *imageData;

Store a lot of image inside it (like, for example, 1GB), and try to fetch all: your app will crash ;-)
Sure, you can filter images and fetch only the image you need, but why do this?

If you have read Core Data documentation, you should know about Faulting: when you fetch an item, the item properties are not fully loaded into memory. The property contains only a "link" to the real data. When you try to access it the first time, Core Data transparently loads it from the persistent store (or  from the cache, if the item is cached).
For this reason, you should not care too much about memory, because if you fetch all properties, image data should be loaded only at the first access to the property imageCacheInstance.imageData

Now, come back to our test: if you try to fetch all images, the app will crash. Why?
Let's open Instruments, write some NSLog lines and check.
When you fetch your data, the imageData property is correctly faulted (you can check calling isFault method). But all data are loaded into memory.

First rule: don't store your data inside a "top level object"
Change your Core Data Model to something like this:

Entity 1 Name: ImageCache
Entity 1 Properties:
    NSString *url;
    id imageDataReference;

Entity 2 Name: ImageData
Entity 2 Properties:
    NSData *image;
    id imageDataReference;

Two entities, one 1:1 relation.
Now, if you fetch all ImageCache values, you should see into memory only few data.
Then, if you access to imageCacheInstance.imageDataReference.image, the ImageData instance is faulted and the data loaded from the persistent store into memory.

When you have finished, you can fault again the ImageData instance and free memory. Simply send this message (to each ImageCache instance):

[_managedObjectContext refreshObject:imageCacheInstance mergeChanges:NO];

note: don't call this on the ImageData Instance, because the cache will not be freed (even if the object will be faulted, all the data will remain in memory)
Call it on the "father" object and all should work correctly.

Nessun commento:

Posta un commento