Chapter 9. Core Data Migration and Performance

WHAT'S IN THIS CHAPTER?

  • Managing database schema changes with versioning and migration

  • Implementing a threaded Core Data application

  • Understanding and optimizing Core Data performance and memory usage

  • Analyzing Core Data performance with the Instruments tool

In the preceding chapters, you learned about the Core Data framework and how to use it to provide the data storage capability in your applications. In this chapter, you will take a closer look at some advanced topics related to Core Data, including migration from one database schema to another and optimizing the performance of your Core Data–based application.

In this chapter, you learn how to handle change in your application data model using model versioning and migration. You also learn how to effectively manage memory when building a Core Data–based solution, safely implement a threaded Core Data application, and troubleshoot and analyze your Core Data application using the Instruments tool.

MODEL VERSIONING AND SCHEMA MIGRATION

It is very rare that a finished application turns out the way that you imagined when you started. In fact, many applications are never truly finished. If your customers like your application, you will almost certainly want to enhance it and build upon its existing feature set. Often, you will get feature requests from customers and clients, which can be a continuing source of revenue for you, the developer.

When you first sat down to design your application, you reviewed the requirements and designed the database that would store your data. As the application development process moves forward, things change, and you may need to modify your data store by adding entities and attributes, changing data types, or modifying relationships between your entities. This is a normal part of the development cycle. Although you may occasionally get a nasty error message from Core Data about using an incompatible data store, this is not a big deal. While you're developing, you can simply blow away your old application and its data, and your application will create a new data store that works with the current schema. This is not an option once you have shipped your application.

In general, users will not be very happy if they try to open an updated version of your application and it crashes because the data store schema does not match the schema of the data file. They will be even less happy if they contact you for support and you tell them that they have to delete the old application and all of their data to be able to run with the update. They will most likely not use your application, or any other applications that you build, ever again.

You are probably wondering how you can continue to extend your product while not losing all of your customer's existing work when he or she upgrades to the newest version of your application. Fortunately, Apple has provided a solution with Core Data Versioning.

Core Data implements a feature called model versioning, which allows you to specify and label different versions of your data model in your Xcode project. You can have many versions of the data model in your project. You need only specify which schema is the current model that your application should use.

Another tool provided by the framework is the mapping model. The mapping model lets you map the translation of data from one model into another. Finally, data migration is used with versioning and mapping to migrate a back-end data store from one schema version to another. I've illustrated this process in Figure 9-1.

The data migration process

Figure 9.1. The data migration process

The Core Data framework thus solves your application update problem. You can create a new version of your schema to support your application enhancements, and then use schema migration to migrate the existing data store on your customer's device to the new schema.

Model Versioning

To Core Data, two models are compatible if there are no changes to the model that will affect a change in the back-end data store. For instance, you can add a transient property while maintaining compatibility between the existing and the new data store. Remember, that transient properties are not stored in the data store, so a change to a transient property will not break compatibility with the original data store.

If you make a change that alters how your data is stored in the data store, however, you are breaking compatibility with the existing data store. Actions such as adding new attributes or entities, changing attribute types, and renaming entities or attributes will all change the structure of the data store and will break compatibility.

There are some rules that you can use to determine if two models will be compatible. For an entity, Core Data compares the name, parent, and isAbstract flags. Core Data does not care about className, userInfo, and validation predicates because these are not stored in the data store. These items are only available to the developer at runtime.

For every property, Core Data examines the name, isOptional, isTransient, and isReadOnly elements. For attributes, Core Data also looks at the attributeType, and for relationships, Core Data examines the destinationEntity, minCount, maxCount, deleteRule, and inverseRelationship. Core Data does not compare the userInfo and validation predicates.

If any of these attributes of a model differ for any entity or any property of any entity, the models are incompatible.

In Xcode, you can create and specify different versions of your application data model. You will look at this functionality now using the Tasks project. Although you will not be adding any additional functionality, you will be modifying the database to purposely break compatibility with the existing data store to illustrate versioning and migration.

Find your existing Tasks application and copy it into a new folder. Open up the new Tasks project in Xcode. Next, open the data model Tasks.xcdatamodel. To create a new version of the model, choose Design

Model Versioning

If you look in the Resources folder in Xcode, you will notice that the Tasks.xcdatamodel file is gone. Xcode has replaced this file with a new file called Tasks.xcdatamodeld. Tasks.xcdatamodeld is actually a bundle and not a file at all. If you click on the disclosure indicator to open the bundle, you will see two data model files, Tasks.xcdatamodel and Tasks 2.xcdatamodel, as shown in Figure 9-2. At this point, the two files are identical. You may also notice that the Tasks.xcdatamodel file has a little green checkmark on the file icon. The checkmark indicates that this file is the Current Version. This is the version of the model that Core Data will use at runtime.

Two versions of the Tasks model

Figure 9.2. Two versions of the Tasks model

You will be making some changes to the new version of the model, version 2. You will want to use the new version of the model in your application, so you have to set it as the current version. Select Tasks 2.xcdatamodel and choose Design

Two versions of the Tasks model

Now you will make a change to the model that will break compatibility with your existing Tasks data store. In the Tasks 2.xcdatamodel file, open the Location entity and add an address property of String type. Save the model. Make sure that you have the Tasks 2.xcdatamodel file selected as the current version. Clean your project by selecting Build

Two versions of the Tasks model

Because you have made a change to the model that breaks compatibility with the old data store, the application should raise an error and abort. In the console, you should see an error that looks like this:

2010-08-03 12:52:23.277 Tasks[6100:207] Unresolved error Error Domain=NSCocoaErrorDomain
Code=134100 "The operation couldn't be completed. (Cocoa error 134100.)"
UserInfo=0x5b298a0 {metadata=<CFBasicHash 0x5b2e1b0 [0x26db380]>{type = immutable dict,
count = 6,
entries =>
    0 : <CFString 0x5b2e1e0 [0x26db380]>{contents = "NSStoreModelVersionIdentifiers"} =
<CFArray 0x5b2e5e0 [0x26db380]>{type = immutable, count = 0, values = ()}
    2 : <CFString 0x5b2e550 [0x26db380]>{contents = "NSStoreModelVersionHashesVersion"}=
<CFNumber 0x5b1a8a0 [0x26db380]>{value = +3, type = kCFNumberSInt32Type}
    3 : <CFString 0x23dd324 [0x26db380]>{contents = "NSStoreType"} = <CFString 0x23dd2e4
[0x26db380]>{contents = "SQLite"}
    4 : <CFString 0x5b2e580 [0x26db380]>{contents = "NSPersistenceFrameworkVersion"} =
<CFNumber 0x5b2e230 [0x26db380]>{value = +320, type = kCFNumberSInt64Type}
    5 : <CFString 0x5b2e5b0 [0x26db380]>{contents = "NSStoreModelVersionHashes"} =
<CFBasicHash 0x5b2e6d0 [0x26db380]>{type = immutable dict, count = 2,
entries =>
    1 : <CFString 0x5b2e600 [0x26db380]>{contents = "Task"} = <CFData 0x5b2e630
[0x26db380]>{length = 32, capacity = 32, bytes = 0x4041451778c0bd9f84e09e2a91478c44 ...
abb9be2796761c30}
    2 : <CFString 0x5b2e610 [0x26db380]>{contents = "Location"} = <CFData 0x5b2e680
[0x26db380]>{length = 32, capacity = 32, bytes = 0xfa099c17c3432901bbaf6eb31dddc734 ...
97ebad533e2e5363}
}

    6 : <CFString 0x23dd464 [0x26db380]>{contents = "NSStoreUUID"} = <CFString 0x5b2e3a0
[0x26db380]>{contents = "6B5E801A-9B00-4F17-858D-726679EE28C3"}
}
, reason=The model used to open the store is incompatible with the one used to create
the store}, {
    metadata =     {
        NSPersistenceFrameworkVersion = 320;
        NSStoreModelVersionHashes =         {
            Location = <fa099c17 c3432901 bbaf6eb3 1dddc734 a9ac14d2 36b913ed 97ebad53
3e2e5363>;
            Task = <40414517 78c0bd9f 84e09e2a 91478c44 d85394f8 e9bb7e5a abb9be27
96761c30>;
        };
NSStoreModelVersionHashesVersion = 3;
        NSStoreModelVersionIdentifiers =         (
        );
        NSStoreType = SQLite;
        NSStoreUUID = "6B5E801A-9B00-4F17-858D-726679EE28C3";
    };
    reason = "The model used to open the store is incompatible with the one used to
create the store";
}

You'll fix this error in the next section.

Lightweight Migration

In order to make your existing data work with your new data model, you need to migrate the schema to a new data store. If the changes that you have made are not too drastic, you can easily accomplish this using a process called lightweight migration.

Lightweight migration is a feature of Core Data that helps you to automatically migrate a data store from one model version to another. In the previous section, I briefly mentioned the mapping model. Core Data uses the mapping model to determine how to map data from one schema model into another. Lightweight migration allows Core Data to infer the mapping model based on the changes that you made to the model from one version to another.

Lightweight migration is particularly handy during development because you won't have to regenerate your test data every time you make a change to your model. It is also fast to implement because you don't have to go through the trouble of creating your own mapping model to map your data from one version to another.

For lightweight migration to work, the changes to your model have to be simple. Generally, if you add an attribute, make a required attribute optional, or make an optional attribute required and add a default value, lightweight migration will work for you. If you change the name of an entity or attribute, you need to set the renaming identifier for the renamed attribute in the user info pane to the old name of the attribute in the source model.

In the Tasks example, the model has changed, but you are still trying to use a data store that works with the old model. In this instance, you can use lightweight migration because you have only added a new field to the data store.

To use lightweight migration, you need to make a couple of changes to your application source code. If you recall from Chapter 5, "Introducing Core Data," the Persistent Store Coordinator is a Core Data object that associates a managed object model to a back-end data store. You can see this relationship in Figure 9-3. In your code, when you add a persistent store to the coordinator, you can specify an NSDictionary of options in the addPersistentStoreWithType:configuration:URL:options:error: method call. You will modify the code in the TasksAppDelegate.m file to specify the options to initiate a lightweight migration.

Core Data objects

Figure 9.3. Core Data objects

Open the Tasks project and navigate to the TasksAppDelegate.m file. You will be adding code to the persistentStoreCoordinator accessor method. The first thing that you need to do is create an NSDictionary that contains the keys that you want to pass into the coordinator. The two keys that you will set are NSMigratePersistentStoresAutomaticallyOption and NSInferMappingModelAutomaticallyOption.

NSMigratePersistentStoresAutomaticallyOption tells the coordinator to automatically migrate the persistent store to a new model if the store is not compatible with the current model. Core Data will search the application bundle for a model that is capable of opening the existing data store and then it will search for a mapping model that maps from the model that can open the data store to the current model. Because you haven't created a mapping model, you have to add another option to your options dictionary.

You use the NSInferMappingModelAutomaticallyOption option key to tell Core Data to try to infer the mapping model from the differences between the model that can open the data store and the current model.

In the persistentStoreCoordinator method, after the line that allocates and initializes the NSPersistentStoreCoordinator, add the following code to initialize your options dictionary:

NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES],
                             NSMigratePersistentStoresAutomaticallyOption,
                             [NSNumber numberWithBool:YES],
                             NSInferMappingModelAutomaticallyOption, nil];
                                                         
Core Data objects

This code creates a new NSDictionary and populates it with the keys as discussed previously.

Next, you have to change the call to addPersistentStoreWithType:configuration:URL:options:error: to use the new options dictionary that you have created. Change the line to look like this:

if (![persistentStoreCoordinator
          addPersistentStoreWithType:NSSQLiteStoreType
          configuration:nil URL:storeUrl options:options error:&error])
                                                         
Core Data objects

The complete method should look like this:

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {

    if (persistentStoreCoordinator != nil) {
        return persistentStoreCoordinator;
    }

    NSURL *storeUrl =
        [NSURL fileURLWithPath: [[self applicationDocumentsDirectory]
            stringByAppendingPathComponent: @"Tasks.sqlite"]];

    NSError *error = nil;
    persistentStoreCoordinator =
        [[NSPersistentStoreCoordinator alloc]
         initWithManagedObjectModel:[self managedObjectModel]];

    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES],
                             NSMigratePersistentStoresAutomaticallyOption,
                             [NSNumber numberWithBool:YES],
                             NSInferMappingModelAutomaticallyOption, nil];

    if (![persistentStoreCoordinator
          addPersistentStoreWithType:NSSQLiteStoreType
          configuration:nil URL:storeUrl options:options error:&error]) {

        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

    return persistentStoreCoordinator;
}
                                                         
Core Data objects

Build and run your application. The migration should now succeed and the application should run just as it did before. Your error is gone, you've added a new field to the data store to support some new functionality, and your customer's data is intact.

Warning

Make sure that you have a few tasks in your data store because you will be transforming the existing data in the next section. If you have no data, you will not see any results when you apply the transformation.

You can check in advance if the lightweight migration will work by calling the inferredMappingModelForSourceModel:destinationModel:error: method. If the mapping will work and the migration will succeed, you will get back a reference to the new model, if not, the method returns nil.

Generating a Mapping Model

If your model changes are too extensive or otherwise don't meet the criteria supported for performing a lightweight migration, you will have to generate a mapping model. Remember that you use the mapping model to tell Core Data how to migrate your data from one store to another. You can also use the mapping model to perform data transformations as you move the data from one data store to another.

Because of the relationship between the mapping model and the data model, the classes used to build a mapping model correspond with the classes used to build a data model. NSMappingModel is like the data model, NSEntityMapping is like an entity, and NSPropertyMapping is like a property or attribute.

The NSEntityMapping class tells the migration process how to handle mapping a source entity in the destination data store. The mapping type determines what to do with the specific entity in the destination data store. The mapping types are: add, remove, copy, and transform. The add mapping means that this is a new entity in the destination and should be added to the destination data store. Remove means that the entity does not exist in the destination and exists only in the source. Copy indicates that the mapping should copy the source object identically to the destination. Transform means that the entity exists in the source and the destination and the mapping should transform the source in some way to get from the source to the destination. You will see an example of how to transform data as it is being migrated from one model to another later in this chapter.

The NSPropertyMapping class tells the migration process how to map source properties to destination properties. You can provide a value expression to transform values while moving the data from the source data store to the destination data store. You can specify a value expression in the mapping model editor in Xcode.

In some cases, you may want to do a custom migration where you will use these classes, so it is good to have at least a cursory understanding of which classes are involved in performing a migration. More often, you will not work with the mapping classes in code because Xcode includes a graphical tool that you can use to create and edit your mapping model. You will create a mapping model to migrate your data store back to the original version of the data model. You will also transform some of the data in the data store along the way to demonstrate the transformation capabilities of executing the migration process using a mapping model.

In Xcode, select the Tasks.xcdatamodel file and make it the current model by selecting Design

Generating a Mapping Model

You should now see the New Mapping Model File dialog. You will use this dialog to tell the mapping model which data model is the source and which is the destination. Open the Resources folder and the Tasks.xcdatamodeld bundle. Select the Tasks 2 model and click the Set Source Model button. Remember that you have a data store that is compatible with the Tasks 2 model and you want to migrate it to be compatible with the Tasks model. Finally, select the Tasks model and click the Set Destination model. Click the Finish button to have Xcode create your mapping model.

You should now see the mapping model tool. If you do not, make sure that you have selected the Map.xcmappingmodel file in the Resources folder in Xcode. You can see the mapping model tool in Figure 9-4.

The Tasks mapping model

Figure 9.4. The Tasks mapping model

At the top of the screen, the tool lists the source and destination models. Below that is a three-column view. The left column displays the entity mappings, which the framework uses to migrate entities between datastores. The middle column displays the property mappings for the currently selected entity. The right column shows the details of the currently selected item.

In the top-left corner of the window is a valuable tool, the Show Differences button. Click this button to open a window that graphically shows all of the differences in the two data models, as shown in Figure 9-5. You can use this display to help plan the mapping model and to verify the changes that you made between the two models. You can see in Figure 9-5 that the address attribute that existed in the Location entity in the source model is missing in the destination model.

Model Differences tool

Figure 9.5. Model Differences tool

The Mapping Model tool has created the default entity and property mappings for you. You can successfully run with the default mapping model. However, you will apply some transformations using value expressions to demonstrate how you can transform data while performing a migration.

In the mapping model, you will notice two entity mappings — one for Tasks and one for Locations. Select the TaskToTask entity mapping. This mapping maps Task entities in the source data store to Task entities in the destination. As you can probably guess, the LocationToLocation entity mapping maps Location entities in the source data store to Location entities in the destination.

Now you will create a few value expressions to learn how to transform your data during a migration. Value expressions are instances of the NSExpression class. If you recall from the last chapter, this is the same class, with its subclasses, that you use as the right-hand side of a predicate.

First, you will modify the priority value expression to set the priority of every task to Medium (priority 2). Select the TaskToTask mapping in the Entity Mappings pane. In the Property Mappings pane, select the priority attribute. You will notice that the right-hand pane is context-sensitive: Its contents will change depending on the item that you have selected in the left or middle pane. When you have an Entity Mapping selected, the right-hand pane will display the mapping attributes that pertain to an Entity Mapping. When you select the priority attribute, the pane changes to display the fields that pertain to an Attribute Mapping.

The default value expression for the priority attribute is $source.priority. This expression directly migrates the priority from the source to the destination. The special variable $source is used to indicate that the value for this field in the destination should be taken from the corresponding source object. In the case of priority, the expression $source.priority gets the priority field from the source entity. However, instead of taking the value from the source, you want to change the priority for every task to 2. So, in the Value Expression field in the right-hand pane, change the value expression to the number 2.

Next, select the text attribute in the Property Mappings pane. The default value expression for the text property migrates the text from the source $source to the destination. You want to change the text of each task to NEW TASK so that you can see the effects of executing a transformation when the Tasks application opens. In the text property mapping, change the value expression field in the right hand pane to NEW TASK. This will change the text of every task to NEW TASK.

Next, let's change the dueDate of all of your tasks to today. Select the dueDate attribute in the Property Mappings pane. The default value expression for the dueDate property migrates the dueDate from the source to the destination. You want to set the new dueDate to today's date. You don't want to hard code a date because you do not know when the user will run the migration on his or her data store. It would be better if you could specify the date using a function.

You can call arbitrary functions in a value expression using function expressions by using the FUNCTION keyword. The syntax is FUNCTION(receiver, selectorName, arguments, . . .). In this case, you want the receiver to be NSDate and the selector that you want to call is the date method, which returns the current system date.

This is not as straightforward to implement as you might imagine. Passing the string, NSDate to the FUNCTION expression will evaluate NSDate as a string literal and not the class NSDate. In effect, you would be executing a call that looks like this: [@"NSDate" date] which is not what you want to do. You need to convert the receiver into a class object, not a string.

There is another function expression that you can use called CAST that accepts two strings and has the form CAST('string', 'type'). Therefore, you can call the function CAST("NSDate","Class") to get the class NSDate and not the string NSDate. Your final value expression for the dueDate mapping should be FUNCTION (CAST("NSDate","Class"), 'date'). This will call the method [NSDate date], which is what you want. Update the value expression and save the mapping file.

You are finished with the mapping model. You now need to go back into the TasksAppDelegate.m file and make a code change. Because you have created a mapping model, you no longer need Core Data to infer the mapping model from the data model changes. In fact, if you have Core Data infer the changes, the framework will not execute your transformations.

Change the options dictionary entry for NSInferMappingModelAutomaticallyOption to nil. This will cause Core Data to search the application bundle for a mapping model instead of determining the mappings automatically. Again, you must do this to execute your data transformations and direct Core Data to use your mapping model. The options dictionary creation code should now look like this:

NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES],
                             NSMigratePersistentStoresAutomaticallyOption,
                             [NSNumber numberWithBool:NO],
                             NSInferMappingModelAutomaticallyOption, nil];
                                                         
Model Differences tool

You are ready to build the application and run it. You should see the text of all of the tasks change to NEW TASK and if you click any task, it should have a priority of Medium and the due date should be set to today's date.

SAFELY THREADING WITH CORE DATA

As you work through designing and developing your iPhone and iPad applications, you may begin to run into performance barriers that are difficult to overcome. For example, you may encounter situations where your user interface is unresponsive while waiting for a time-consuming operation to finish. Traditionally, moving these expensive operations off to their own thread is a technique that you could use to solve this and other performance issues. The iPhone and iPad are no exception.

Writing threaded code has typically been a difficult task. However, the iOS includes a framework that simplifies the task of creating threads and using these threads to execute your code. You will look at the classes that you can use in the iOS to implement concurrency in your application. Then, you will build a sample application that executes a time-consuming process. First you will run this process on the main thread; then you will move that process off to its own thread where you will see a significant improvement in the responsiveness of your application.

Designing for Threading

You should carefully consider the threading model for your application as early on in the design process as possible. It is somewhat difficult to retrofit threading into an application after you have completed the coding. If you can predict the operations that will be the most time consuming, or operations that are self-contained that you can run atomically in a concurrent fashion, you can design threading into your software from the beginning. While it is possible to implement threading in your application after you encounter a performance problem, your design will be better and your application code cleaner and more maintainable if you consider threading from the start.

As a designer, you should begin your consideration of threading once you have a thorough understanding of your application requirements. Based on these requirements, you should be able to determine the work that your application needs to perform. Once you understand this work, you should look to see if you could divide this work into atomic, self-contained operations. If you can easily break a task down to one or more atomic operations, it may be a good candidate for concurrency, particularly if the task is independent of other tasks.

A particularly useful case for implementing concurrency is to keep the main thread free to increase the responsiveness of your user interface. If you execute a long-running operation on the main application thread, the user will experience a frozen interface as the operation executes. This is potentially frustrating for users, as they cannot continue to work with your application while it is in this state.

In the example in this section, I will graphically illustrate this point. First, you will write an application that generates five random numbers and pauses for 1 second between each number. Because this operation will run on the main thread, you will be able to see that your user interface freezes while you are generating these numbers. I have illustrated this scenario on the left in Figure 9-6. Then, you will take the random number generation and move it to its own thread. After you do this, you will notice that the user interface is responsive as you generate the random numbers. You can see this expressed on the right in Figure 9-6.

Threaded versus non-threaded design

Figure 9.6. Threaded versus non-threaded design

Threading and Core Data

As you could probably guess, you will be using Core Data to store the data in your sample. The problem is that Core Data is not inherently thread-safe, mostly because the managed object context is not thread-safe. If you modify a managed object in a thread that is different from the thread on which you created the context, the context will not know about the changes to the managed object. This could cause your application to contain stale data. It is also possible to introduce data inconsistencies if you attempt to modify the same managed object on different threads. For these reasons, you should absolutely avoid passing Managed Objects across thread boundaries.

One approach to threading with Core Data is to create a separate managed object context on each thread. You can easily achieve this by passing (or obtaining) a reference to the Persistent Store Coordinator and manually creating a context in the thread. While the Persistent Store Coordinator is not thread-safe, the context knows how to lock the coordinator to enable concurrency using this method. You will follow this approach in the sample application. If you need to perform many operations on the context, you should create a new Persistent Store Coordinator on the thread as well. You will not be doing this in your sample.

While you never want to pass Managed Objects between threads, you can achieve a similar result by passing objectIDs. The objectID uniquely identifies a managed object. You can obtain a managed object from the objectID by calling the objectWithID: method on the thread's context.

Threading with NSOperation

The concurrency model implemented in iOS does not require that you create individual threads directly. Rather, the developer creates operations designed to run concurrently and then hands them off to the system. The system then configures the optimal number of threads to use to run the specified concurrent tasks. It is possible to create threads yourself. However, if you create threads yourself, you are responsible for determining the optimum number of threads to create based on workload and the number of available processor cores. This is a difficult determination to make in real time. By using the concurrency classes provided in the iOS, the system takes care of this complexity by creating and scheduling the threads for you.

When you decide that you need to move a particular block of code off the main thread and on to its own thread, look no further than the NSOperation class. NSOperation is an abstract base class that you subclass to define and implement atomic operations that you want to dispatch to a separate thread.

When deciding to implement an operation, you have a couple of architectural choices. If you have an existing method in a class that you would like to execute asynchronously, you can use the NSInvocationOperation class. NSInvocationOperation is a concrete subclass of NSOperation that allows you to create an operation out of a class method and queue it for execution on a separate thread. If you are introducing threading into an existing application, NSInvocationOperation may be your best bet because you probably already have methods that perform the work that you would like to thread. It is also useful when you want to choose the method to execute dynamically at runtime because it accepts a selector as the method to run.

If you are building your threading model from scratch, you should implement threading by creating discrete subclasses of NSOperation. This will give you the freedom to implement your operations as you wish and alter the way that the operation reports status if your application calls for it.

In order to implement a subclass of NSOperation, at a minimum, you need to implement a custom initializer method that you use to configure your object for use and a main method that will perform the task. You can also implement any other custom methods that you need such as accessor methods to get at the operation's data or dealloc to free memory allocated by the operation.

Once you have created your operation classes, you need to tell the system to execute them. You accomplish this by creating an instance of NSOperationQueue and adding your NSOperation subclasses to the queue. The system uses operation queues to schedule and execute your operations. The system manages the queues, which handle the details of scheduling and executing your threads for you. It is possible to configure dependencies between operations when adding operations to an operation queue. You can also prioritize operations when you add them to a queue.

Core Data Threading Example

As I mentioned, this example application will generate five random numbers and pause for 1 second between each number to simulate a time consuming synchronous operation. This code will block the main thread causing the user interface to become unresponsive while you are generating the random numbers. Then, you will take the random number generation and move it on to its own thread. Once you do this, you will see that the user interface remains responsive while you generate the random numbers.

Note

In this example, you will be inserting data into the Core Data context on an off thread. This is generally not recommended; however I am doing it in this example to demonstrate the mechanics of threading with Core Data. In production applications, you should generally only use threads to perform time consuming reads and queries from Core Data and not to perform inserts or updates.

To get started, open Xcode and start a new Navigation-based application. Make sure that "Use Core Data for storage" is checked. Call your new application RandomNumbers.

The first thing that you will do is update the default data model to hold your random numbers. Open the data model file RandomNumbers.xcdatamodel. In the Event entity, change the name of the timeStamp attribute to randomNumber. Change the type of the randomNumber attribute to Integer16.

Next, you need to make some changes to the RootViewController implementation file. In the configureCell:atIndexPath: method, change the reference to the old timeStamp attribute to randomNumber. The configureCell method should look like this:

- (void)configureCell:(UITableViewCell *)cell
          atIndexPath:(NSIndexPath *)indexPath {

    NSManagedObject *managedObject =
        [fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text =
        [[managedObject valueForKey:@"randomNumber"] description];
}
                                                         
Core Data Threading Example

You will also need to make this change in the fetchedResultsController accessor method:

- (NSFetchedResultsController *)fetchedResultsController {

    if (fetchedResultsController != nil) {
        return fetchedResultsController;
    }

    /*
     Set up the fetched results controller.
     */
    // Create the fetch request for the entity.
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    // Edit the entity name as appropriate.
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Event"
        inManagedObjectContext:managedObjectContext];
    [fetchRequest setEntity:entity];

    // Set the batch size to a suitable number.
    [fetchRequest setFetchBatchSize:20];

    // Edit the sort key as appropriate.
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc]
        initWithKey:@"randomNumber" ascending:NO];
    NSArray *sortDescriptors = [[NSArray alloc]
                                initWithObjects:sortDescriptor, nil];

    [fetchRequest setSortDescriptors:sortDescriptors];

    // Edit the section name key path and cache name if appropriate.
    // nil for section name key path means "no sections".
    NSFetchedResultsController *aFetchedResultsController =
        [[NSFetchedResultsController alloc]
         initWithFetchRequest:fetchRequest
         managedObjectContext:managedObjectContext
         sectionNameKeyPath:nil cacheName:@"Root"];

    aFetchedResultsController.delegate = self;
    self.fetchedResultsController = aFetchedResultsController;

    [aFetchedResultsController release];
    [fetchRequest release];
[sortDescriptor release];
    [sortDescriptors release];

    return fetchedResultsController;
}
                                                         
Core Data Threading Example

Blocking the Main Thread

Now you are ready to generate your random numbers and populate the Core Data database. You will implement the insertNewObject method, which runs when the user taps the plus button in the top-right corner of the application. Delete the existing implementation and add the following:

- (void)insertNewObject {

    // Create a new instance of the entity managed by the fetched results
    // controller.
    NSManagedObjectContext *context =
        [fetchedResultsController managedObjectContext];
    NSEntityDescription *entity =
        [[fetchedResultsController fetchRequest] entity];


    // Generate 5 random numbers waiting 1 second between them.
    for (int i=0; i<5; i++){
        NSManagedObject *newManagedObject =
            [NSEntityDescription insertNewObjectForEntityForName:[entity name]
                                          inManagedObjectContext:context];

        [newManagedObject
            setValue:[NSNumber numberWithInt:1 + arc4random() % 10000]
            forKey:@"randomNumber"];

        //  Simulate long synchronous blocking call
        sleep(1);

    }

    // Save the context.
    NSError *error = nil;
    if (![context save:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }
}
                                                         
Blocking the Main Thread

The method first obtains a reference to the context, and then creates an entity description based on the entity that the fetched results controller manages. In the for loop, you create a new managed object to hold your random number. Then, you generate a random number and assign it to the randomNumber attribute of your new managed object. Then you call the sleep function to sleep the thread for 1 second. Sleep is a blocking call, so you use it here to simulate some code that takes a significant amount of time to execute. Finally, you save the context.

Build and run the application. When the application starts, tap the plus sign in the top-right corner of the interface to invoke the insertNewObject method and generate some random numbers. Try to scroll the TableView while you generate the random numbers. Notice how the user interface becomes unresponsive while you are generating the random numbers. The synchronous call to sleep is blocking the main thread, which controls the UI. In about 5 seconds, you will see the random numbers appear in the interface and control will return to the user.

Moving the Blocking Call

Clearly, this is not a good situation for your application. Your users will be confused when the application doesn't respond when they try to scroll the list while the application is generating random numbers. You solve this problem by creating an NSOperation subclass and then moving the code to generate the random numbers onto a new thread.

Create a new Objective-C class that is a subclass of NSObject and call it RandomOperation. In the header file RandomOperation.h, change the base class from NSObject to NSOperation.

Remember that Core Data is inherently not thread-safe. When you use Core Data with threads, the key is to create a separate context on each thread. You create a context against a Persistent Store Coordinator. You need to write an initializer for your operation that accepts a Persistent Store Coordinator. You then use that coordinator to create a new context into which you will add your random numbers.

In the header file, add an instance variable and property for the coordinator. You should note that you do not need to retain the coordinator because you will only hold a pointer to the shared coordinator. Finally, add an init method called initWithPersistentStoreCoordinator that accepts NSPersistentStoreCoordinator as an input parameter. Here is the RandomOperation header:

#import <Foundation/Foundation.h>

@interface RandomOperation: NSOperation  {
    NSPersistentStoreCoordinator *coordinator;

}

-(id) initWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator*)coord;

@property (nonatomic,assign) NSPersistentStoreCoordinator *coordinator;

@end
                                                         
Moving the Blocking Call

Let's move on to the RandomOperation implementation file. In the implementation, synthesize the property and implement the initWithPersistentStoreCoordinator method like this:

@synthesize coordinator;

-(id) initWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator*)coord
{
    if (self == [super init]) {
        self.coordinator = coord;
    }

    return self;

}
                                                         
Moving the Blocking Call

All you are doing in this method is calling the superclass init method and then storing the NSPersistentStoreCoordinator input parameter in the class coordinator property.

Next, implement the dealloc method to call dealloc on the superclass:

-(void) dealloc {
    [super dealloc];
}
                                                         
Moving the Blocking Call

Now, you need to implement the main method to actually do the work:

-(void) main {

    // Create a context using the persistent store coordinator
    NSManagedObjectContext *managedObjectContext;

    if (self.coordinator != nil) {
        managedObjectContext = [[NSManagedObjectContext alloc] init];
        [managedObjectContext setPersistentStoreCoordinator: self.coordinator];
    }
    else {
        return;
    }

    // Generate 5 random numbers waiting 1 second between them.
    for (int i=0; i<5; i++){
        NSManagedObject *newManagedObject =
        [NSEntityDescription insertNewObjectForEntityForName:@"Event"
                             inManagedObjectContext:managedObjectContext];

        [newManagedObject
         setValue:[NSNumber numberWithInt:1 + arc4random() % 10000]
forKey:@"randomNumber"];

        //  Simulate long synchronous blocking call
        sleep(1);
    }

    // Save the context.
    NSError *error = nil;
    if (![managedObjectContext save:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

    // Clean up
    [managedObjectContext release];
}
                                                         
Moving the Blocking Call

Most of this method is the same as the insertNewObject method from the RootViewController. The major difference is that in the first part of the method, you use the class pointer to the Persistent Store Coordinator to create a new managed object context. There is some added safety code here to return from the method if you do not have a pointer to a valid coordinator. You then proceed to create Managed Objects containing the random numbers just as you did in insertNewObject. Finally, you call save on the context and release the local context. You are finished with the RandomOperation class.

Back in the RootViewController, you need to change the insertNewObject method to use your operation. At the top of the implementation file, add #include for the RandomOperation header and another #include to include the app delegate:

#import "RandomOperation.h"
#import "RandomNumbersAppDelegate.h"
                                                         
Moving the Blocking Call

You will get a reference to the coordinator from the app delegate and pass it into your RandomOperation in the initializer.

Delete all of the existing code from the insertNewObject method because you have moved this code into your RandomOperation class. You do, however, need to implement the insertNewObject method to use your RandomOperation class. The following is the implementation of insertNewObject:

- (void)insertNewObject {
    // Create an instance of NSOperationQueue
    NSOperationQueue* operationQueue = [[NSOperationQueue alloc] init];

    // Get a reference to the app delegate to get the coordinator
RandomNumbersAppDelegate* appDelegate =
        [UIApplication sharedApplication].delegate;

    // Create an instance of the operation
    RandomOperation* ourOperation =
        [[RandomOperation alloc]
            initWithPersistentStoreCoordinator:
                appDelegate.persistentStoreCoordinator];

    // Add the operation to the operation queue
    [operationQueue addOperation:ourOperation];

    // Clean up
    [ourOperation release];
    [operationQueue release];

}
                                                         
Moving the Blocking Call

First, you create an instance of the NSOperationQueue that you will use to hold and execute your operation. Next, you get a reference to the App Delegate that you will use to get a reference to the Persistent Store Coordinator. Then, you create an instance of your RandomOperation class and pass it a reference to the Persistent Store Coordinator. Finally, you call the addOperation method on the operation queue to add your operation to the queue and instruct the queue to execute your operation. Last, you release the variables that you allocated in the method.

Before you run the application, you need to modify the App Delegate to expose the Persistent Store Coordinator property. In the RandomNumbersAppDelegate, move the interface declaration for the Core Data Stack properties out of the implementation file and into the header. Remove the @interface and @end beginning and ending tags. You need the coordinator to be public so that you can retrieve it when you go to create your operation. The app delegate header file should look like this:

#import <UIKit/UIKit.h>
#import <CoreData/CoreData.h>

@interface RandomNumbersAppDelegate : NSObject <UIApplicationDelegate> {

    NSManagedObjectModel *managedObjectModel;
    NSManagedObjectContext *managedObjectContext;
    NSPersistentStoreCoordinator *persistentStoreCoordinator;

    UIWindow *window;
    UINavigationController *navigationController;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet UINavigationController
    *navigationController;

@property (nonatomic, retain, readonly) NSManagedObjectModel
    *managedObjectModel;
@property (nonatomic, retain, readonly) NSManagedObjectContext
    *managedObjectContext;
@property (nonatomic, retain, readonly) NSPersistentStoreCoordinator
    *persistentStoreCoordinator;

- (NSString *)applicationDocumentsDirectory;

@end
                                                         
Moving the Blocking Call

Delete your old version of the application from the simulator to delete the old Core Data database. Now run the program and click the plus button to add some new random numbers. You will notice that the interface is now responsive while you are generating the random numbers. However, there is one problem: the numbers never appear. If you wait a few seconds, quit the application, and then restart it, you will see five new numbers in the TableView. The problem is that the context in the RootViewController is unaware that you have added records to the Core Data database on another thread, so the FetchedResultsController has not updated the TableView.

Fortunately, there is a way to handle this. Whenever a context performs a save, the context sends out an NSManagedObjectContextDidSaveNotification notification. The NSManagedObjectContext class has a method -(void)mergeChangesFromContextDidSaveNotification:(NSNotification *)notification that accepts a notification and merges the changes contained in the notification into the context. Therefore, you need to write some code to listen for and handle the notification. When you receive the notification, you need to call mergeChangesFromContextDidSaveNotification: to merge the changes into the context on the main thread.

In the viewDidLoad method in the RootViewController, you need to get a reference to the default notification center and add self as an observer for the NSManagedObjectContextDidSaveNotification message. You do this at the end of viewDidLoad. The following is the revised viewDidLoad method:

- (void)viewDidLoad {
    [super viewDidLoad];

    // Set up the edit and add buttons.
    self.navigationItem.leftBarButtonItem = self.editButtonItem;

    UIBarButtonItem *addButton = [[UIBarButtonItem alloc]
        initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
                             target:self
                             action:@selector(insertNewObject)];
    self.navigationItem.rightBarButtonItem = addButton;
    [addButton release];

    NSError *error = nil;
    if (![[self fetchedResultsController] performFetch:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

    // Add an observer for the NSManagedObjectContextDidSaveNotification message
NSNotificationCenter* notificationCenter =
        [NSNotificationCenter defaultCenter];

    [notificationCenter addObserver:self
                           selector:@selector(contextSaved:)
                               name:NSManagedObjectContextDidSaveNotification
                             object:nil ];
}
                                                         
Moving the Blocking Call

Any time that the save method is called on any context, Core Data generates an NSManagedObjectContextDidSaveNotification message. You are now listening for this message, and when you receive it, you will execute the contextSaved method in the RootViewController. Now, all you have to do is implement the contextSaved method to take the notification and call mergeChangesFromContextDidSaveNotification:

-(void) contextSaved:(NSNotification*)notification
{
    [self.managedObjectContext
        mergeChangesFromContextDidSaveNotification:notification];
}
                                                         
Moving the Blocking Call

The last thing that you need to do is implement dealloc to remove self as an observer on the notification center:

- (void)dealloc {
    [fetchedResultsController release];
    [managedObjectContext release];
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [super dealloc];
}
                                                         
Moving the Blocking Call

Delete your old version of the application from the simulator to delete the old Core Data database. Build and run the application. Click the plus button. Notice how the UI is still responsive. In a few moments, the save occurs, the notification is sent, contextSaved is called, the changes are merged into the context, and the FetchedResultsController updates the table with the new random numbers. You have successfully moved a time-consuming blocking call off the main thread and on to another thread.

CORE DATA PERFORMANCE

When designing and building an application, one of your primary concerns should be performance. In the context of data-driven applications, let's look at a few specific aspects of overall application performance.

The first aspect to consider is perceived performance. Perceived performance can be defined as how quickly your application appears to complete a task. The key word in this definition is appears.

The threading program that you built in the last section is a perfect example of perceived performance. In the first phase of the example, when your user interface was frozen, you probably felt that the performance of the application was poor because it locked up for 5 seconds as it generated the random numbers. After you moved the random number generation to another thread, the application probably felt much snappier, even though the length of time from when you tapped the plus button to the time when the random numbers appeared in the TableView was exactly the same. The difference was that the interface was responsive so you perceived the application to be working more quickly.

You have already learned how to use threading as a tool for increasing the responsiveness and therefore the perceived performance of your application. In this section, I will focus on another aspect of iOS programming that has a great impact on performance — memory management. On embedded devices such as the iPhone and iPad, you need to be particularly concerned about memory as the iOS does not currently support virtual memory or paging. If your application runs low on memory, the OS will tell certain parts of your application to unload. This will cause a time-consuming re-initialization to occur when the user needs to use these parts of your application again. Additionally, it is possible that your application can run out of memory, causing the OS to terminate your application.

After you look at some memory management items, you will look at a few ways that you can increase the actual speed of your applications.

Faulting

Core Data uses a technique called faulting to reduce the memory footprint of your application by keeping unused data out of memory. Faulted objects are instances of NSManagedObject or your NSManagedObject subclass that are retrieved in a fetch request, but not all of the properties of the object are immediately loaded into memory. Core Data can conserve memory by loading only the parts of an object or a relationship that you need.

Core Data does not populate a faulted object's properties until you access them. This process, called fault realization, happens automatically. You do not have to execute a fetch to realize a fault, but behind the scenes, Core Data may need to execute additional fetches to realize faults.

Faulting most often occurs when objects are associated with relationships. If you recall the sample from Chapter 6, "Modeling Data in Xcode," you had two entities, Task and Location, as illustrated in Figure 9-7. On the All Tasks screen, you queried Core Data for a list of all Tasks. Because the Task entity has a relationship entity, you would think that Core Data would load the related Location object into memory as well. However, it does not. The Location entity in this instance is faulted. Core Data has no reason to load a related Location object into memory unless the code asks for it.

Tasks application data model

Figure 9.7. Tasks application data model

Core Data does not bring relationships to a faulted object into memory until you access them. This behavior allows Core Data to conserve memory by keeping unused objects out of memory until you need them.

In this example, Core Data is preventing only one object, the Location, from being loaded. You may be thinking that this doesn't save too much memory. That is true, but imagine if the location object were also related to a series of other objects, each with its own relations, and so on. In large object hierarchies, the memory conserved by faulting can be considerable.

Data Store Types

When using Core Data, you can specify the type of data store that Core Data uses to persist your data. Your options are SQLite, binary, and in-memory. Although you will usually use the SQLite store type, you will take a brief look at all of the types.

The in-memory store does not actually persist your data at all. As the name says, the data store is in memory. When your application quits, your data is gone. This could be useful in situations where you want to use Core Data to manage your application data when that data does not need to be persisted from session to session. This is typically not very useful in iOS applications as your application session could end at any time, particularly if the user of an iPhone gets a call.

The binary store type uses a proprietary binary format to store your data. The primary drawback of using the binary format is that all of the data stored in the data store is loaded into memory when the data store is loaded. There is no provision to leave parts of the data on disk. In contrast, an SQLite database remains largely left on disk and Core Data only brings the parts that you specifically query for into memory.

Although there is no "disk" on the iPhone or iPad, there is a difference between application memory and storage memory. When I say "on disk," I am referring to the space on the device that is used for data storage. The iPhone or iPad cannot use this storage space to execute applications, so there is a distinction between the application memory available for use by your application's execution and disk space, which is available for data storage.

Storing Binary Data

You may be tempted to store binary data such as images or sound clips in your Managed Objects using Core Data. In general, this is not a good idea. You are generally better off storing the binary data on disk and storing the path to the data in Core Data as you did in the catalog example earlier in the book because you may store a large chunk of data in an object and not realize that you are bringing all of that data into memory when Core Data loads the object. For example, if you stored the images for each of your catalog items in the database, but did not display them on the main catalog screen, these potentially large images would still be loaded into memory.

If you do store binary data in Core Data, do not store the data in frequently accessed rows. For example, you should not store an image in the same managed object that you use to display items in a table if you are not also displaying the image because the image will be loaded into memory regardless of whether you display the image or not.

Entity Inheritance

You may remember from Chapter 6 that you can implement an entity hierarchy in Core Data using inheritance and the parent field of an entity in the entity detail pane. I have illustrated a hypothetical entity hierarchy in Figure 9-8.

Entity inheritance hierarchy

Figure 9.8. Entity inheritance hierarchy

Although this is a feature of Core Data, you need to be aware of how the implementation uses memory in the back-end SQLite data store. An entity hierarchy is not the same as the equivalent object hierarchy in an object-oriented design. In the entity hierarchy, all of the attributes of all of the entities in the hierarchy are stored in the same database table. This can lead to inefficiency in how the data is stored on disk, causing excessive memory usage.

To illustrate this, imagine that in the hierarchy illustrated in Figure 9-8, the Product entity has attributes P1 and P2, the Hammer has attributes H1 and H2, and the Screw has attributes S1 and S2. In the current implementation of Core Data, Core Data stores the data for all three of these entities in a single table, as illustrated in Figure 9-9. You can see that the Hammer entities have unused space in the table for the Screw-related fields and vice versa for Screw objects. Although this is a simple example, it illustrates the storage issue. The problem gets worse as your inheritance hierarchy gets larger and deeper.

Core Data storage for entity inheritance

Figure 9.9. Core Data storage for entity inheritance

Runtime Performance

This chapter covered improving perceived performance with threading and being conscious of how Core Data is using memory. In this section, I will just lay out a few general tips that you can look to in an attempt to increase the runtime performance of your application.

First, when designing your model, avoid over-normalization of your data. Remember that you are not designing a relational database but an object persistence mechanism. Feel free to de-normalize your data if it makes building the displays for the application easier. You should try to find a balance between normalization and speeding up your queries for rapid display. Accessing data through a relationship is more expensive than retrieving an attribute. Consider this before normalizing your data. In addition, querying across relationships is expensive. Determine if you really need to do it, or de-normalize the data if it makes sense. For example, if you have an application that stores names and addresses, it may make sense from a UI perspective to keep the state name in the same table as the rest of the address as opposed to normalizing it out into its own table. You may not want to take the overhead penalty for following a relationship to a state table every time that you need to look up an address if you will always be showing the address and state together.

I know that it may seem obvious, but you should try to fetch only the data that you need when you need it. This tip ties in with the "Faulting" section from earlier in the chapter. Core Data will generally not bring in data that you are not going to use. Therefore, you should be careful to avoid building your queries in a way that forces Core Data to bring data into memory that you may not need.

Another thing that you can do to increase your application performance is take advantage of Core Data caching. The Persistent Store Coordinator holds fetched results in its caches. This is particularly useful when you can set up a background thread to fetch data while the foreground remains responsive. Then, the foreground thread can read the data from the persistent coordinator's cache when necessary, avoiding another trip to the data store.

The final tip has to do with the order of the items in your search predicate. Search order in predicates is important. Put likely-to-fail criteria first so the comparison can end quickly. The engine evaluates predicates in order, and if one part of the predicate fails, the engine will move on to the next record. You can potentially reduce query times by placing likely-to-fail criteria at the beginning of a predicate if it makes sense.

Managing Changes with the Fetched Results Controller

In this section, you will look at how you can use NSFetchedResultsController to update your TableView based on changes to a result set.

When the data managed by NSFetchedResultsController changes, the fetched results controller calls several delegate methods. You have the option to either use these delegate methods to update the associated TableView based on each individual change, or simply handle one delegate method and tell the TableView to reload its data.

Implementing the delegate methods to handle individual updates can yield better performance because you are not reloading all of the table data each time there is an update. There is a tradeoff, however, because if there are many simultaneous changes to the data, it may be cheaper to just reload all of the data because presenting many individual changes can be time consuming.

To keep the Tasks application simple, I took the latter approach. In the RootViewController, you implemented the controllerDidChangeContent delegate method to reload the data for the TableView. Here is the implementation:

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [self.taskTableView reloadData];
}
                                                         
Managing Changes with the Fetched Results Controller

Compare how you just reloaded all of the data for the table in the Tasks application to how you handle individual cell updates in the random numbers application. In the random numbers project, you handled each update individually by accepting the template code for the RootViewControllers. The delegate methods that the NSFetchedResultsController calls are controllerWillChangeContent:, controller:didChangeSection:atIndex:forChangeType, controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:, and controllerDidChangeContent:.

The FetchedResultsController calls the controllerWillChangeContent: and controllerDidChangeContent: methods to bracket the changes that are being made to the result set. You implement these methods to tell the table that changes will begin and end respectively. You can think of these methods as beginning and committing a transaction to the table. Here is the implementation in the RandomNumbers example:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView beginUpdates];
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView endUpdates];
}
                                                         
Managing Changes with the Fetched Results Controller

This code simply tells the TableView that updates are coming and that updates are finished. The endUpdates call triggers the TableView to display and optionally animate the changes to the data.

The FetchedResultsController calls the controller:didChangeSection:atIndex:forChangeType method when the sections of the TableView should change. The two types of changes that you will receive in this method are NSFetchedResultsChangeInsert and NSFetchedResultsChangeDelete. Your code will be able to determine if the changes to the data have added or removed a section from the TableView. Typically, you will implement a switch statement to handle these two cases, as shown in the RandomNumbers sample:

- (void)controller:(NSFetchedResultsController *)controller
     didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
              atIndex:(NSUInteger)sectionIndex
        forChangeType:(NSFetchedResultsChangeType)type {

    switch(type) {
        case NSFetchedResultsChangeInsert:
            [self.tableView insertSections:
             [NSIndexSet indexSetWithIndex:sectionIndex]
                          withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:
             [NSIndexSet indexSetWithIndex:sectionIndex]
                          withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}
                                                         
Managing Changes with the Fetched Results Controller

This code simply inserts or deletes the section that it receives in the method call.

Finally, the FetchedResultsController calls the controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: method when objects in the data store change. In this method, your code needs to handle these four operations: NSFetchedResultsChangeInsert, NSFetchedResultsChangeDelete, NSFetchedResultsChangeMove, and NSFetchedResultsChangeUpdate. Again, you will usually handle calls to this method in a switch statement and make the appropriate changes to your TableView as you did in the RandomNumbers example:

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath
        forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath {

    UITableView *tableView = self.tableView;

    switch(type) {

        case NSFetchedResultsChangeInsert:
            [tableView insertRowsAtIndexPaths:
             [NSArray arrayWithObject:newIndexPath]
                             withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:
             [NSArray arrayWithObject:indexPath]
                             withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeUpdate:
            [self configureCell:[tableView
                                 cellForRowAtIndexPath:indexPath]
                    atIndexPath:indexPath];
            break;

        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:
             [NSArray arrayWithObject:indexPath]
                             withRowAnimation:UITableViewRowAnimationFade];
            [tableView insertRowsAtIndexPaths:
             [NSArray arrayWithObject:newIndexPath]
                             withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}
                                                         
Managing Changes with the Fetched Results Controller

PERFORMANCE ANALYSIS USING INSTRUMENTS

When confronted with a performance issue in your application, the first place that you should turn is Instruments. Instruments is a GUI-based tool that you can use to profile your application in myriad ways. Instruments features a plug-in architecture that allows you to pick and choose from a library of recording instruments to use in profiling your application, as shown in Figure 9-10. Additionally, you can create custom instruments that use DTrace to examine the execution of your application.

Once you have selected the recording instruments that you would like to use, you can use Instruments to run your application, either in the iPhone Simulator or on the actual device, and gather the data that you need to optimize your application. After you start your application, Instruments will profile the application in the background as the application runs. As your application is running, you will see the graphs in Instruments react to operations that you perform in the application.

Instruments tool library

Figure 9.10. Instruments tool library

The main feature of the tool's interface is the timeline. The premise is that you are recording the operation of your application in real-time and can later go back and look at the state of your application at any time during your test run. This is an incredibly powerful tool to use when trying to find problems in your application.

Once you narrow down a particular problem to a specific time on the timeline, you can drill down into the application stack. The stack provides information on the code that was executing at the time that Instruments took the sample. You can select your code segments in the stack to view the source code.

Typically, the biggest problems that you will find in your application are issues with memory management and leaks. Instruments has a couple of tools, ObjectAlloc and Leaks, that can be extremely helpful in detecting and crushing these often-illusive bugs. You will take a closer look at using these tools in Appendix A.

Instruments is an incredibly powerful tool and I cannot recommend it enough. If you have never used Instruments, you owe it to yourself to take a closer look at the tool. In this section, I walk you through the use of Instruments to profile your Core Data application using the Core Data–related tools.

Starting Instruments

In general, you can either start the Instruments tool by simply launching the application that is located, by default, in /YourDriveName/Developer/Applications or by selecting Run

Starting Instruments

Even though Core Data is grayed out as an option in the "Run with Performance Tool" menu item, you can use the Core Data instruments with the Instruments tool. You first need to get Instruments to recognize your application as an instrument target. Open the RandomNumbers project in Xcode. The easiest way to get your project into instruments is to use the Run with Performance Tool

Starting Instruments

Because you want to use the Core Data tools and not the CPU sampler, you need to stop the Instruments session and add the Core Data tools to the instrumentation. Press the red button to stop recording. Now, Instruments knows where to find the RandomNumbers application.

Close the current window and discard the results of the CPU sampler run. From the Instruments menu bar, select File

Starting Instruments

You now have to tell Instruments which program you want to execute. Open the Default Target pull down at the top of the window. Select Launch Executable

Starting Instruments
Setting the Default Target

Figure 9.11. Setting the Default Target

You are now ready to begin your test run. Click the red Record button in the top left of the application to start your recording. You will see the RandomNumbers application start up in the simulator. Once the application starts, you should see the timeline begin to move indicating that Instruments is profiling your application. You should also notice a spike in the top instrument, Core Data Fetches. This indicates that a fetch has occurred in the application. Tap the plus button in the simulator to generate some random numbers. In 5 seconds, you should see a spike in the Core Data Saves instrument indicating that a save has occurred. Stop the recording by clicking the red button.

The Instruments Interface

Now that you have finished your test run, you will want to analyze your data. To do this, you should understand how to use the Instruments interface. You can see the interface as it should look having completed your test run in Figure 9-12.

The Instruments interface

Figure 9.12. The Instruments interface

The Instruments pane shows all of the instruments that are active for the current test run.

The Track pane shows a graph representing different things for different instruments. For the Core Data Fetches instrument, the Track pane shows the count of items fetched and the duration of the fetch. The Track pane displays the time that an event occurred during the test run. You can adjust the time scale using the slider below the Instruments pane. You can also scroll the Track pane using the scrollbar at the bottom of the pane.

Below the Track pane is the Detail pane. Like the Track pane, the Detail pane shows different details based on the tool that you have selected in the Instruments pane. For the Core Data Fetches instrument, the Detail pane displays a sequence number, the caller of the fetch method, the fetched entity, the count of the number of items returned in the fetch, and the duration of the fetch in microseconds.

You can select an item in the Detail pane to view more detail about the item in the Extended Detail pane. The Extended Detail pane is particularly useful because it shows a stack trace for the method call that you have selected in the Detail pane. Select a fetch in the Detail pane to view its stack trace.

I find it useful to display the file icons in the Extended Detail pane because doing this makes the calls in the stack that originated in your code obvious. You can enable the file icons by clicking on the gear icon in the Extended Details pane. In Figure 9-12, you can see that the fetch selected in the Detail pane is initiated in the code in the RootViewController viewDidLoad method. If you double-click on an item in the call stack that corresponds to one of your source code files, in this case the call to the RootViewController viewDidLoad method, Instruments will display the source code in the Detail pane, showing you the exact line of code that made the fetch request. Click the Event List View button at the bottom left of the Detail pane to get back from the source code to the list of events.

The Core Data Instruments

Four instruments are available for use with Core Data: Core Data Saves, Fetches, Faults, and Cache Misses.

Core Data Saves reports the methods that invoke the Core Data save operation. It also reports the duration of the operation in microseconds. Opening the extended detail view shows the stack trace for the call and the time at which the call occurred.

You can use this tool to discover how often you are saving data and find a balance between saving too much and not saving enough. Each save operation causes a disk write, which can hurt performance, but not saving often enough results in using excess memory to hold the data in memory.

Core Data Fetches reports all fetches made against Core Data. The tool reports the name of the fetched entity, the caller, the number of records returned, and the duration of the fetch in microseconds. Opening the extended detail view shows the stack trace for the call and the time at which the call occurred.

Fetch operations are costly as they read from disk. You can use this tool to help optimize your search predicates to limit the number of rows returned by a fetch request.

Core Data Faults reports fault events that occur as the result of needing to realize object references in to-many relationships. The tool reports the method causing the fault, the object that was faulted, the execution duration in microseconds, the relationship fault source, the name of the relationship, and the relationship fault duration in microseconds. You can open the extended detail view to see the stack trace for the call and the time at which the call occurred.

As discussed earlier in this chapter, faulting is a memory-saving technique that comes at the expense of time when Core Data realizes the fault. If you find that Core Data is spending an excessive amount of time resolving faults, but your memory consumption is low, you could consider pre-fetching related objects instead of having them realized through a fault.

The Cache Misses tool reports fault events that result in cache misses. This is a subset of the information provided by the Faults instrument. The tool displays the method that caused the cache miss, the duration of execution for the fault handler in microseconds, the relationship cache miss source, the name of the relationship, and the relationship cache miss duration. Opening the extended detail view shows the stack trace for the call and the time at which the call occurred.

Similar to the remedy for extensive faulting, you can mitigate cache misses by pre-fetching data. If data is not in the cache, it leads to an expensive read from disk operation.

MOVING FORWARD

In this chapter, you learned how to version your data models and migrate between versions to help you add new features to your database and application. I also covered how to safely use Core Data in a threaded application with NSOperation to increase performance. You looked at some Core Data performance considerations and tips. Finally, you learned how to use the Instruments tool to observe your application's performance when using Core Data.

This ends the section of the book on Core Data. You should now feel confident that you are able to implement a data-driven application using this exciting and useful technology. You should also have all of the tools in your arsenal to be able to debug and troubleshoot problems that may arise while you are using Core Data.

In the next section of the book, you learn how to use XML and Web Services in your applications to communicate with other applications over the Internet.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset