Core Data

In short, Core Data is not only data persistence on disk, it is also all the data objects in memory. It manages all the data objects and their relationships.

There are 3 types of relationships between 2 tables in CoreData:

  • One-to-One
  • One-to-Many
  • Many-to-Many

Every relationship should have an inverse (it’s important to reference each object in relationship from either side).

Core Data API has 3 primary pieces:

  1. NSPersistentStoreCoordinator
  2. NSManagedObjectModel
  3. NSManagedObjectContext
  • NSManagedObject:  each instance is one entity in CoreData repo. Use @dynamics to auto-generate getter/setters for Core-data properties, @dynamic create them like this:
    - (NSString *)name {
        [self willAccessValueForKey:@"name"];
        NSString *value = [self primitiveValueForKey:@"name"];
        [self didAccessValueForKey:@"name"];
    }
    
    - (void)setName:(NSString *)value {
        [self willChangeValueForKey:@"name"];
        [self setPrimitiveValue:value forKey:@"name"];
        [self didChangeValueForKey:@"name"];
    }

    These properties are notification-bound, so we can always listen to their changes.

  1. Like accessing attributes, accessing Relationships will fire the Notifications.
  2. didTurnIntoFault  &  –willTurnIntoFault: if your object is faulted, all stored values and relationships will be/did get out of the memory. 
  3. awakeFromInsert – called just after NSManagedObj is created from insert call, before any values are set – so it’s perfect to do the init stuffs, like setting default values. This is called just once in the lifetime of the object. Be sure to call [super awakeFromInsert] if you override it.
  4. awakeFromFetch – called every time the object is loaded from disk. This method is called before the data is loaded. Also call [super] if override.
  •  NSFetchRequestretrieve objects from CoreData. We setEntity (NSEntityDescription) and the filtering (NSPredicate). It’s possible to use stored fetch requests within the model (Add Fetch Request from main menu), and ask the NSManagedObjectModel for it’s reference :

NSManagedObjectContext *moc = [self managedObjectContext];

NSManagedObjectModel *mom = [self managedObjectModel];

NSFetchRequest *request = [mom fetchRequestTemplateForName:@”bigMeals”];

  • NSManagedObjectContext – the object to access when we want to save to disk, read data and create new objects. It’s the thing that our code directly contacts with. It’s NOT THREAD SAFE, so each thread should have it’s own Context. 
  • NSManagedObjectModel – the .xcdatamodel file is compiled into a .mom file, stored in Resources dir of the application bundle. So we can construct it using fileURLWithPath:[[NSBundle mainBundle] pathForResource:@”DataModel” ofType:@”mom” ];

This object is very useful to discover all the entities, relationships and properties, so while normally we don’t access it, it’s important, like to have stored fetch request.

  • NSPersistentStoreCoordinator – bottom of stack, persists data to disk (or others), also NOT THREAD SAFE, however the Contexts can lock each others so can access the same StoreCoordinator.

A. Use SQLITE

4 types of repo in CoreData are

  1. SQLite,
  2. XML,
  3. binary
  4. in-memory

Except SQLite, the other 3 are atomic stores (whole store saved atomically – which requires full loading into memory). SQLite utilise relational database -> can have a very large data size (we only load what we need). So the first performance tuning is to use SQLite.

B. Control what to loads 

1. Fault – faulted objects are un-realized objects until being used (its attributes not loaded): 

There are many ways to control how the objects are loaded into memory, leaving lots of things to be “fault”.

2. NSManagedObjectID objects:  when we need to load many records from disk, use the ID objects:

[fetchRequest setResultType: NSManagedObjectIDResultType];  Then we use –objectWithID to get the object. This would be very quick and saves lots of memory.

Using [fetchRequest setIncludesPropertyValues: NO]; will load the objects as faulted (the properties will be loaded when 1 attribute is accessed).

3. Loading relationships:  preload the relationships as faults:

NSArray *relationshipKeys = [NSArray arrayWithObject:@”events”];

[fetchRequest setRelationshipKeyPathsForPrefetching: relationshipKeys];

C. Avoid Disk operations 

Accessing the disk is thousands times slower than accessing objects in memory, so we should avoid access the disk!!!

Writing to disks is slower than reading from it, so:

  • Save in Batch !
  • Save in logical pauses of app flow.
  • Use Instruments to monitor “Cache miss” / “disk hits”.

D. Make Threading safe

  • Create Contexts on the same Persistent Store Coordinator (usually [appDelegate persistentStore]).
  • Keep Contexts on sync: every time a context saves, a notification NSManagedObjectContextDidSaveNotification is broadcasted (along with the data about the changed). 
  • Function –mergeChangesFromContextDidSaveNotification: will update the context with the change just made.

E. My experience improving Core Data app:

One of my most important experiences with Core Data was how I have improved the speed of processing server’s data from 14 seconds to 4 secs. We can run Core Data Instruments in iOS Simulator mode, there are 3 Instruments:

  1. Core Data Fetches
  2. Core Data Cache Misses
  3. Core Data Saves

Also shown is the time the simulator took to run these operations. Usually running on real devices is many times slower.

First, Cache Misses are easiest to improve. Fetching records with preloaded ‘needed’ relationships is effective. We can go 1 step further by preloading another level.

Second, Fetches are very much in need of optimisation. Sometimes, when the table is not large, we can fetch the whole table and store as a Dictionary – instead of fetching single key/value each time. New phones have large memory and if we can use them properly it saved lots of loading time.

Similarly, when we fetch the same table using different criteria, each fetch will need to access the disk – we can just fetch once and use predicate to filter by criteria – avoiding disk read.

Third, know when to save. Usually save should be performed when leaving the app. When using Magical Records, different local contexts just need to save to the main Context.

Advertisements