File and Data Management
Files in iPhone OS share space with the user’s media and personal files on the flash-based memory. For security purposes, your application is placed in its own directory and is limited to reading and writing files in that directory only. The following sections describe the structure of an application’s local file system and several techniques for reading and writing files.
=> Commonly Used Directories
For security purposes, an application has only a few locations in which it can write its data and preferences.
When an application is installed on a device, a home directory is created for the application. some of the important subdirectories inside the home directory that you might need to access. This table describes the intended usage and access restrictions for each directory and whether the directory’s contents are backed up by iTunes.
When an application is installed on a device, a home directory is created for the application. some of the important subdirectories inside the home directory that you might need to access. This table describes the intended usage and access restrictions for each directory and whether the directory’s contents are backed up by iTunes.
Table 6-1 Directories of an iPhone application
=> Backup and Restore
You do not have to prepare your application in any way for backup and restore operations. In iPhone OS 2.2 and later, when a device is plugged into a computer and synced, iTunes performs an incremental backup of all files, except for those in the following directories:
■ <Application_Home>/AppName.app
■ <Application_Home>/Library/Caches
■ <Application_Home>/tmp
Although iTunes does back up the application bundle itself, it does not do this during every sync operation.
Applications purchased from the App Store on the device are backed up when that device is next synced with iTunes. Applications are not backed up during subsequent sync operations though unless the application bundle itself has changed (because the application was updated, for example).
To prevent the syncing process from taking a long time, you should be selective about where you place files inside your application’s home directory. The <Application_Home>/Documents directory should be used to store user data files or files that cannot be easily recreated by your application. Files used to store temporary data should be placed inside the Application Home/tmp directory and deleted by your application when they are no longer needed. If your application creates data files that can be used during subsequent launches, it should place those files in the Application Home/Library/Caches directory.
■ <Application_Home>/AppName.app
■ <Application_Home>/Library/Caches
■ <Application_Home>/tmp
Although iTunes does back up the application bundle itself, it does not do this during every sync operation.
Applications purchased from the App Store on the device are backed up when that device is next synced with iTunes. Applications are not backed up during subsequent sync operations though unless the application bundle itself has changed (because the application was updated, for example).
To prevent the syncing process from taking a long time, you should be selective about where you place files inside your application’s home directory. The <Application_Home>/Documents directory should be used to store user data files or files that cannot be easily recreated by your application. Files used to store temporary data should be placed inside the Application Home/tmp directory and deleted by your application when they are no longer needed. If your application creates data files that can be used during subsequent launches, it should place those files in the Application Home/Library/Caches directory.
Note: If your application creates large data files, or files that change frequently, you should consider storing them in the Application Home/Library/Caches directory and not in the <Application_Home>/Documents directory. Backing up large data files can slow down the backup process significantly. The same is true for files that change regularly (and therefore must be backed up frequently). Placing these files in the Caches directory prevents them from being backed up (in iPhone OS 2.2 and later) during every sync operation.
=> Files Saved During Application Updates
Updating an application replaces the previous application with the new one downloaded by the user. During this process, iTunes installs the updated application in a new application directory. It then moves the user’s data files from the old installation over to the new application directory before deleting the old installation.
Files in the following directories are guaranteed to be preserved during the update process:
■ <Application_Home>/Documents
■ <Application_Home>/Library/Preferences
Although files in other user directories may also be moved over, you should not rely on them being present after an update.
Files in the following directories are guaranteed to be preserved during the update process:
■ <Application_Home>/Documents
■ <Application_Home>/Library/Preferences
Although files in other user directories may also be moved over, you should not rely on them being present after an update.
=> Keychain Data
A keychain is a secure, encrypted container for passwords and other secrets. The keychain data for an application is stored outside of the application sandbox. If an application is uninstalled, that data is automatically removed. When the user backs up application data using iTunes, the keychain data is also backed up. However, it can only be restored to the device from which the backup was made. An upgrade of an application does not affect its keychain data.
=> Getting Paths to Application Directories
At various levels of the system, there are programmatic ways to obtain file-system paths to the directory locations of an application’s sandbox. However, the preferred way to retrieve these paths is with the Cocoa programming interfaces. The NSHomeDirectory function (in the Foundation framework) returns the path to the top-level home directory—that is, the directory containing the application, Documents, Library, and tmp directories. In addition to that function, you can also use the NSSearchPathForDirectoriesInDomains and NSTemporaryDirectory functions to get exact paths to your Documents, Caches, and tmp directories.
Both the NSHomeDirectory and NSTemporaryDirectory functions return the properly formatted path information in an NSString object. You can use the path-related methods of NSString to modify the path information or create new path strings. For example, upon retrieving the temporary directory path, you could append a file name and use the resulting string to create a file with the given name in the temporary directory.
Note: If you are using frameworks with ANSI C programmatic interfaces including those that take paths recall that NSString objects are “toll-free bridged” with their Core Foundation counterparts. This means that you can cast a NSString object (such as the return by one of the above functions) to a CFStringRef type, as shown in the following example:
CFStringRef homeDir = (CFStringRef)NSHomeDirectory();
The NSSearchPathForDirectoriesInDomains function of the Foundation framework lets you obtain the full path to several application-related directories. To use this function in iPhone OS, specify an appropriate search path constant for the first parameter and NSUserDomainMask for the second parameter. Table lists several of the most commonly used constants and the directories they return.
Table: Commonly used search path constants
Because the NSSearchPathForDirectoriesInDomains function was designed originally for Mac OS X, where multiple such directories could exist, it returns an array of paths rather than a single path. In iPhone OS, the resulting array should contain the single path to the desired directory. Programshows a typical use of this function.
Getting a file-system path to the application’s Documents/ directory
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
You can call NSSearchPathForDirectoriesInDomains using a domain-mask parameter other than NSUserDomainMask or a directory constant other than those in above Table, but the application will be unable to write to any of the returned directory locations. For example, if you specify NSApplicationDirectory as the directory parameter and NSSystemDomainMask as the domain-mask parameter, you get the path /Applications returned (on the device), but your application cannot write any files to this location.
Another consideration to keep in mind is the difference in directory locations between platforms. The paths returned by NSSearchPathForDirectoriesInDomains, NSHomeDirectory, NSTemporaryDirectory, and similar functions differ depending on whether you’re running your application on the device or on the Simulator. For example, take the function call shown in. On the device, the returned path (documentsDirectory) is similar to the following:
/var/mobile/Applications/30B51836-D2DD-43AA-BCB4-9D4DADFED6A2/Documents
However, on the Simulator, the returned path takes the following form:
/Volumes/Stuff/Users/johnDoe/Library/Application Support/iPhone
Simulator/User/Applications/118086A0-FAAF-4CD4-9A0F-CD5E8D287270/Documents
To read and write user preferences, use the NSUserDefaults class or the CFPreferences API. These interfaces eliminate the need for you to construct a path to the Library/Preferences/ directory and read and write preference files directly. If your application contains sound, image, or other resources in the application bundle, you should use the NSBundle class or CFBundleRef opaque type to load those resources. Bundles have an inherent knowledge of where resources live inside the application. In addition, bundles are aware of the user’s language preferences and are able to choose localized resources over default resources automatically.
Another consideration to keep in mind is the difference in directory locations between platforms. The paths returned by NSSearchPathForDirectoriesInDomains, NSHomeDirectory, NSTemporaryDirectory, and similar functions differ depending on whether you’re running your application on the device or on the Simulator. For example, take the function call shown in. On the device, the returned path (documentsDirectory) is similar to the following:
/var/mobile/Applications/30B51836-D2DD-43AA-BCB4-9D4DADFED6A2/Documents
However, on the Simulator, the returned path takes the following form:
/Volumes/Stuff/Users/johnDoe/Library/Application Support/iPhone
Simulator/User/Applications/118086A0-FAAF-4CD4-9A0F-CD5E8D287270/Documents
To read and write user preferences, use the NSUserDefaults class or the CFPreferences API. These interfaces eliminate the need for you to construct a path to the Library/Preferences/ directory and read and write preference files directly. If your application contains sound, image, or other resources in the application bundle, you should use the NSBundle class or CFBundleRef opaque type to load those resources. Bundles have an inherent knowledge of where resources live inside the application. In addition, bundles are aware of the user’s language preferences and are able to choose localized resources over default resources automatically.
=> Reading and Writing File Data
The iPhone OS provides several ways to read, write, and manage files.
■ Foundation framework:
❏ If you can represent your application’s data as a property list, convert the property list to an NSData object using the NSPropertyListSerialization API. You can then write the data object to disk using the methods of the NSData class.
❏ If your application’s model objects adopt the NSCoding protocol, you can archive a graph of these model objects using the NSKeyedArchiver class, and especially its archivedDataWithRootObject: method.
❏ The NSFileHandle class in Foundation framework provides random access to the contents of a file.
❏ The NSFileManager class in Foundation framework provides methods to create and manipulate files in the file system.
■ Core OS calls:
❏ Calls such as fopen, fread, and fwrite also let you read and write file data either sequentially or via random access.
❏ The mmap and munmap calls provide an efficient way to load large files into memory and access their contents.
Note: The preceding list of Core OS calls is just a sample of the more commonly used calls. For a more complete list of the available functions, see the list of functions in section 3 of iPhone OS Manual Pages.
The following sections show examples of how to use some of the higher-level techniques for reading and writing files.
■ Foundation framework:
❏ If you can represent your application’s data as a property list, convert the property list to an NSData object using the NSPropertyListSerialization API. You can then write the data object to disk using the methods of the NSData class.
❏ If your application’s model objects adopt the NSCoding protocol, you can archive a graph of these model objects using the NSKeyedArchiver class, and especially its archivedDataWithRootObject: method.
❏ The NSFileHandle class in Foundation framework provides random access to the contents of a file.
❏ The NSFileManager class in Foundation framework provides methods to create and manipulate files in the file system.
■ Core OS calls:
❏ Calls such as fopen, fread, and fwrite also let you read and write file data either sequentially or via random access.
❏ The mmap and munmap calls provide an efficient way to load large files into memory and access their contents.
Note: The preceding list of Core OS calls is just a sample of the more commonly used calls. For a more complete list of the available functions, see the list of functions in section 3 of iPhone OS Manual Pages.
The following sections show examples of how to use some of the higher-level techniques for reading and writing files.
=> Reading and Writing Property List Data
A property list is a form of data representation that encapsulates several Foundation (and Core Foundation) data types, including dictionaries, arrays, strings, dates, binary data, and numerical and Boolean values.
Property lists are commonly used to store structured configuration data. For example, the Info.plist file found in every Cocoa and iPhone applications is a property list that stores configuration information about the application itself. You can use property lists yourself to store additional information, such as the state of your application when it quits.
In code, you typically construct a property list starting with either a dictionary or array as a container object.
You then add other property-list objects, including (possibly) other dictionaries and arrays. The keys of dictionaries must be string objects. The values for those keys are instances of NSDictionary, NSArray, NSString, NSDate, NSData, and NSNumber.
For an application whose data can be represented by a property-list object (such as an NSDictionary object), you could write that property list to disk using a method such as the one shown in below code. This
method serializes the property list object into an NSData object, then calls the writeApplicationData:toFile:
Converting a property-list object to an NSData object and writing it to storage
- (BOOL)writeApplicationPlist:(id)plist toFile:(NSString *)fileName {
NSString *error;
NSData *pData = [NSPropertyListSerialization dataFromPropertyList:plist
format:NSPropertyListBinaryFormat_v1_0 errorDescription:&error];
if (!pData) {
NSLog(@"%@", error);
return NO;
}
return ([self writeApplicationData:pData toFile:(NSString *)fileName]);
}
Property lists are commonly used to store structured configuration data. For example, the Info.plist file found in every Cocoa and iPhone applications is a property list that stores configuration information about the application itself. You can use property lists yourself to store additional information, such as the state of your application when it quits.
In code, you typically construct a property list starting with either a dictionary or array as a container object.
You then add other property-list objects, including (possibly) other dictionaries and arrays. The keys of dictionaries must be string objects. The values for those keys are instances of NSDictionary, NSArray, NSString, NSDate, NSData, and NSNumber.
For an application whose data can be represented by a property-list object (such as an NSDictionary object), you could write that property list to disk using a method such as the one shown in below code. This
method serializes the property list object into an NSData object, then calls the writeApplicationData:toFile:
Converting a property-list object to an NSData object and writing it to storage
- (BOOL)writeApplicationPlist:(id)plist toFile:(NSString *)fileName {
NSString *error;
NSData *pData = [NSPropertyListSerialization dataFromPropertyList:plist
format:NSPropertyListBinaryFormat_v1_0 errorDescription:&error];
if (!pData) {
NSLog(@"%@", error);
return NO;
}
return ([self writeApplicationData:pData toFile:(NSString *)fileName]);
}
When writing property list files in iPhone OS, it is important to store your files in binary format. You do this by specifying the NSPropertyListBinaryFormat_v1_0 key in the format parameter of the dataFromPropertyList:format:errorDescription: method. The binary property-list format is much more compact than the other format options, which are text based. This compactness not only minimizes the amount of space taken up on the user’s device, it also improves the time it takes to read and write the property list.
Below Code shows the corresponding code for loading a property-list file from disk and reconstituting the
objects in that property list.
Reading a property-list object from the application’s Documents directory
- (id)applicationPlistFromFile:(NSString *)fileName {
NSData *retData;
NSString *error;
id retPlist;
NSPropertyListFormat format;
retData = [self applicationDataFromFile:fileName];
if (!retData) {
NSLog(@"Data file not returned.");
return nil;
}
retPlist = [NSPropertyListSerialization propertyListFromData:retData
mutabilityOption:NSPropertyListImmutable format:&format errorDescription:&error];
if (!retPlist){
NSLog(@"Plist not returned, error: %@", error);
}
return retPlist;
}
Below Code shows the corresponding code for loading a property-list file from disk and reconstituting the
objects in that property list.
Reading a property-list object from the application’s Documents directory
- (id)applicationPlistFromFile:(NSString *)fileName {
NSData *retData;
NSString *error;
id retPlist;
NSPropertyListFormat format;
retData = [self applicationDataFromFile:fileName];
if (!retData) {
NSLog(@"Data file not returned.");
return nil;
}
retPlist = [NSPropertyListSerialization propertyListFromData:retData
mutabilityOption:NSPropertyListImmutable format:&format errorDescription:&error];
if (!retPlist){
NSLog(@"Plist not returned, error: %@", error);
}
return retPlist;
}
=> Using Archivers to Read and Write Data
An archiver converts an arbitrary collection of objects into a stream of bytes. Although this may sound similar to the process employed by the NSPropertyListSerialization class, there is an important difference.
A property-list serializer can convert only a limited set of (mostly scalar) data types. Archivers can convert arbitrary Objective-C objects, scalar types, arrays, structures, strings, and more.
The key to the archiving process is in the target objects themselves. The objects manipulated by an archiver must conform to the NSCoding protocol, which defines the interface for reading and writing the object’s state. When an archiver encodes a set of objects, it sends an encodeWithCoder: message to each one, which the object then uses to write out its critical state information to the corresponding archive. The unarchiving process reverses the flow of information. During unarchiving, each object receives an initWithCoder: message, which it uses to initialize itself with the state information currently in the archive.
Upon completion of the unarchiving process, the stream of bytes is reconstituted into a new set of objects that have the same state as the ones written to the archive previously.
The Foundation framework supports two kinds of archivers sequential and keyed. Keyed archivers are more flexible and are recommended for use in your application. The following example shows how to archive a graph of objects using a keyed archiver. The representation method of the _myDataSource object returns a single object (possibly an array or dictionary) that points to all of the objects to be included in the archive.
The data object is then written to a file whose path is specified by the myFilePath variable.
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:[_myDataSource
representation]];
[data writeToFile:myFilePath atomically:YES];
A property-list serializer can convert only a limited set of (mostly scalar) data types. Archivers can convert arbitrary Objective-C objects, scalar types, arrays, structures, strings, and more.
The key to the archiving process is in the target objects themselves. The objects manipulated by an archiver must conform to the NSCoding protocol, which defines the interface for reading and writing the object’s state. When an archiver encodes a set of objects, it sends an encodeWithCoder: message to each one, which the object then uses to write out its critical state information to the corresponding archive. The unarchiving process reverses the flow of information. During unarchiving, each object receives an initWithCoder: message, which it uses to initialize itself with the state information currently in the archive.
Upon completion of the unarchiving process, the stream of bytes is reconstituted into a new set of objects that have the same state as the ones written to the archive previously.
The Foundation framework supports two kinds of archivers sequential and keyed. Keyed archivers are more flexible and are recommended for use in your application. The following example shows how to archive a graph of objects using a keyed archiver. The representation method of the _myDataSource object returns a single object (possibly an array or dictionary) that points to all of the objects to be included in the archive.
The data object is then written to a file whose path is specified by the myFilePath variable.
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:[_myDataSource
representation]];
[data writeToFile:myFilePath atomically:YES];
Note: You could also send a archiveRootObject:toFile: message to the NSKeyedArchiver object to create the archive and write it to storage in one step.
To load the contents of an archive from disk, you simply reverse the process. After loading the data from disk, you use the NSKeyedUnarchiver class and its unarchiveObjectWithData: class method to get back the model-object graph. For example, to unarchive the data from the previous example, you could use the following code:
To load the contents of an archive from disk, you simply reverse the process. After loading the data from disk, you use the NSKeyedUnarchiver class and its unarchiveObjectWithData: class method to get back the model-object graph. For example, to unarchive the data from the previous example, you could use the following code:
NSData* data = [NSData dataWithContentsOfFile:myFilePath];
id rootObject = [NSKeyedUnarchiver unarchiveObjectWithData:data];
id rootObject = [NSKeyedUnarchiver unarchiveObjectWithData:data];
=> Writing Data to Your Documents Directory
After you have an NSData object encapsulating the application data (either as an archive or a serialized property list), you can call the method shown in Listing 6-4 to write that data to the application Documents directory.
Writing data to the application’s Documents directory
- (BOOL)writeApplicationData:(NSData *)data toFile:(NSString *)fileName {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
if (!documentsDirectory) {
NSLog(@"Documents directory not found!");
return NO;
}
NSString *appFile = [documentsDirectory
stringByAppendingPathComponent:fileName];
return ([data writeToFile:appFile atomically:YES]);
}
Writing data to the application’s Documents directory
- (BOOL)writeApplicationData:(NSData *)data toFile:(NSString *)fileName {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
if (!documentsDirectory) {
NSLog(@"Documents directory not found!");
return NO;
}
NSString *appFile = [documentsDirectory
stringByAppendingPathComponent:fileName];
return ([data writeToFile:appFile atomically:YES]);
}
=> Reading Data from the Documents Directory
To read a file from your application’s Documents directory, construct the path for the file name and use the desired method to read the file contents into memory. For relatively small files that is, files less than a few memory pages in size you could use the code in Listing 6-5 to obtain a data object for the file contents.
This example constructs a full path to the file in the Documents directory, creates a data object from it, and then returns that object.
Reading data from the application’s Documents directory
- (NSData *)applicationDataFromFile:(NSString *)fileName {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *appFile = [documentsDirectory
stringByAppendingPathComponent:fileName];
NSData *myData = [[[NSData alloc] initWithContentsOfFile:appFile]
autorelease];
return myData;
}For files that would require multiple memory pages to hold in memory, you should avoid loading the entire file all at once. This is especially important if you plan to use only part of the file. For larger files, you should consider mapping the file into memory using either the mmap function or the initWithContentsOfMappedFile: method of NSData.
Choosing when to map files versus load them directly is up to you. It is relatively safe to load a file entirely into memory if it requires only a few (3-4) memory pages. If your file requires several dozen or a hundred pages, however, you would probably find it more efficient to map the file into memory. As with any such determination, though, you should measure your application’s performance and determine how long it takes to load the file and allocate the necessary memory.
This example constructs a full path to the file in the Documents directory, creates a data object from it, and then returns that object.
Reading data from the application’s Documents directory
- (NSData *)applicationDataFromFile:(NSString *)fileName {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *appFile = [documentsDirectory
stringByAppendingPathComponent:fileName];
NSData *myData = [[[NSData alloc] initWithContentsOfFile:appFile]
autorelease];
return myData;
}For files that would require multiple memory pages to hold in memory, you should avoid loading the entire file all at once. This is especially important if you plan to use only part of the file. For larger files, you should consider mapping the file into memory using either the mmap function or the initWithContentsOfMappedFile: method of NSData.
Choosing when to map files versus load them directly is up to you. It is relatively safe to load a file entirely into memory if it requires only a few (3-4) memory pages. If your file requires several dozen or a hundred pages, however, you would probably find it more efficient to map the file into memory. As with any such determination, though, you should measure your application’s performance and determine how long it takes to load the file and allocate the necessary memory.
=> File Access Guidelines
When creating files or writing out file data, keep the following guidelines in mind:
■ Minimize the amount of data you write to the disk. File operations are relatively slow and involve writing to the Flash disk, which has a limited lifespan. Some specific tips to help you minimize file-related operations include:
❏ Write only the portions of the file that changed, but aggregate changes when you can. Avoid writing out the entire file just to change a few bytes.
❏ When defining your file format, group frequently modified content together so as to minimize the overall number of blocks that need to be written to disk each time.
❏ If your data consists of structured content that is randomly accessed, store it in a Core Data persistent store or a SQLite database. This is especially important if the amount of data you are manipulating could grow to be more than a few megabytes in size.
■ Avoid writing cache files to disk. The only exception to this rule is when your application quits and you need to write state information that can be used to put your application back into the same state when it is next launched.
■ Minimize the amount of data you write to the disk. File operations are relatively slow and involve writing to the Flash disk, which has a limited lifespan. Some specific tips to help you minimize file-related operations include:
❏ Write only the portions of the file that changed, but aggregate changes when you can. Avoid writing out the entire file just to change a few bytes.
❏ When defining your file format, group frequently modified content together so as to minimize the overall number of blocks that need to be written to disk each time.
❏ If your data consists of structured content that is randomly accessed, store it in a Core Data persistent store or a SQLite database. This is especially important if the amount of data you are manipulating could grow to be more than a few megabytes in size.
■ Avoid writing cache files to disk. The only exception to this rule is when your application quits and you need to write state information that can be used to put your application back into the same state when it is next launched.
No comments:
Post a Comment