iPhone应用程序编程指南(文件和网络)
文件和数据管理
Phone OS系统上的文件和用户的媒体数据及个人文件共享闪存上的空间。出于安全的目的,您的应用程序被放在其自己的目录下,并且只能对该目录进行读写。
常用目录
出于安全的目的,应用程序只能将自己的数据和偏好设置写入到几个特定的位置上。
当应用程序被安装到设备上时,系统会为其创建一个家目录。
<Application_Home>/
AppName.app 这是程序包目录,包含应用程序的本身。
<Application_Home>/Documents/ 您应该将所有的应用程序数据文件写入到这个目录下。这个目录用于存储用户数据或其它应该定期备份的信息。iTunes会备份这个目录的内容。
<Application_Home>/Library/Preferences
这个目录包含应用程序的偏好设置文件。 iTunes会备份这个目录的内容。
<Application_Home>/Library/Caches
这个目录用于存放应用程序专用的支持文件,保存应用程序再次启动过程中需要的信息。iTunes不对这个目录的内容进行备份。
<Application_Home>/tmp/
这个目录用于存放临时文件,保存应用程序再次启动过程中不需要的信息。iTunes不对这个目录的内容进行备份。
备份和恢复
您不需要在应用程序中为备份和恢复操作做任何准备。在iPhone OS 2.2及更高版本的系统中,当设备被连接到计算机并完成同步时,iTunes会对除了下面这些目录之外的所有文件进行增量式的备份:
-
<Application_Home>
/
AppName.app
-
<Application_Home>
/Library/Caches
-
<Application_Home>
/tmp
虽然iTunes确实对应用程序的程序包本身进行备份,但并不是在每次同步时都进行这样的操作。通过设备上的App Store购买的应用程序在下一次设备和iTunes同步时进行备份。而在之后的同步操作中,应用程序并不进行备份,除非应用程序包本身发生了变化(比如由于应用程序被更新了)。
<Application_Home>/Documents
目录应该用于存放用户数据文件或不容易在应用程序中重新创建的文件。存储临时数据的文件应该放在Application Home/tmp
目录,而且应该在不需要的时候将其删除。如果您的应用程序需要创建用于下次启动的数据文件,则应该将那些文件放到Application Home/Library/Caches
目录下。
在应用程序更新过程中被保存的文件
在更新的过程中,iTunes保证如下目录中的文件会得以保留:
-
<Application_Home>
/Documents
-
<Application_Home>
/Library/Preferences
虽然其它用户目录下的文件也可能被转移,但是您不应该假定更新之后该文件还仍然存在。
Keychain数据
keychain是一个安全、经过加密保护的容器,用于保存密码和其它秘密信息。应用程序的keychain数据存储在应用程序沙箱之外。如果应用程序被卸载,则该数据会自动被删除。当用户通过iTunes备份应用程序数据时,keychain数据也会被备份。然而,keychain数据只能被恢复到之前做备份的设备上。应用程序的更新并不影响其keychain数据。
获取应用程序目录的路径
Foundation框架中的NSSearchPathForDirectoriesInDomains
函数用于取得几个应用程序相关目录的全路径。在iPhone OS上使用这个函数时,第一个参数指定正确的搜索路径常量,第二个参数则使用NSUserDomainMask
常量。表6-2列出了大多数常用的常量及其返回的目录。
常量 |
目录 |
---|---|
<Application_Home> |
|
<Application_Home> |
|
<Application_Home> |
由于NSSearchPathForDirectoriesInDomains
函数最初是为Mac OS X设计的,而Mac OS X上可能存在多个这样的目录,所以它的返回值是一个路径数组,而不是单一的路径。在iPhone OS上,结果数组中应该只包含一个给定目录的路径。程序清单6-1显示了这个函数的典型用法。
程序清单6-1 取得指向应用程序Documents
目录的文件系统路径
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); |
NSString *documentsDirectory = [paths objectAtIndex:0]; |
在调用NSSearchPathForDirectoriesInDomains
函数时,您可以使用NSUserDomainMask
之外的其它域掩码参数,或者使用表6-2之外的其它目录常量,但是应用程序不能向其返回的目录写入数据。举例来说,如果您指定NSApplicationDirectory
作为目录参数,同时指定NSSystemDomainMask
作为域掩码参数,则可以返回(设备上的)/Applications
路径,但是,您的应用程序不能往该位置写入任何文件。
在读写用户偏好设置时,请使用NSUserDefaults
类或CFPreferences
API。这些接口使您免于构造Library/Preferences/
目录路径和直接读写偏好文件。
如果应用程序的程序包中包含声音、图像、或其它资源,则应该使用NSBundle
类或CFBundleRef
封装类型来装载那些资源。程序包知道应用程序内部资源应该在什么位置上,此外,它还知道用户的语言偏好,能够自动选择本地化的资源。
文件数据的读写
iPhone OS提供了如下几种读、写、和管理文件的方法:
-
Foundation框架:
-
如果您可以将应用程序数据表示为一个属性列表,则可以用
NSPropertyListSerialization
API来将属性列表转换为一个NSData
对象,然后通过NSData
类的方法将数据对象写入磁盘。 -
如果应用程序的模型对象采纳了
NSCoding
协议,则可以通过NSKeyedArchiver
类、特别是它的archivedDataWithRootObject:
方法将模型对象图进行归档。 -
Foundation框架中的
NSFileHandle
类提供了随机访问文件内容的方法。 -
Foundation框架中的
NSFileManager
类提供了在文件系统中创建和操作文件的方法。
-
-
Core OS调用:
-
诸如
fopen
、fread
、和fwrite
这些调用可以用于对文件进行顺序或随机读写。 -
mmap
和munmap
调用是将大文件载入内存并访问其内容的有效方法。
-
属性列表数据的读写
在代码中,属性列表的构造通常从构造一个字典或数组、并将它作为容器对象开始,然后在容器中加入其它的属性列表对象,(可能)包含其它的字典和数组。字典的键必须是字符串对象,键的值则是NSDictionary
、NSArray
、NSString
、NSDate
、NSData
、和NSNumber
类的实例。
对于可以将数据表示为属性列表对象的应用程序(比如NSDictionary
对象),您可以用程序清单6-2所示的方法来将属性列表写入磁盘。该方法将属性列表序列化为NSData
对象,然后调用writeApplicationData:toFile:
方法(其实现如程序清单6-4所示)将数据写入磁盘。
程序清单6-2 将属性列表对象转换为NSData
对象并写入存储
- (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]); |
} |
在iPhone OS系统上保存属性列表文件时,采用二进制格式进行存储是很重要的。在编码时,可以通过为dataFromPropertyList:format:errorDescription:
方法的format 参数指定NSPropertyListBinaryFormat_v1_0
值来实现。二进制格式比其它基于文本的格式紧凑得多,这种紧凑不仅使属性列表在用户设备上占用的空间最小,还可以减少读写属性列表的时间。
程序清单6-3的代码展示了如何从磁盘装载属性列表,并重新生成属性列表中的对象。
程序清单 6-3 从应用程序的Documents
目录读取属性列表对象
- (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; |
} |
用归档器进行数据读写
归档器的作用是将任意的对象集合转换为字节流。这听起来像是NSPropertyListSerialization
类采用的过程,但它们之间有一个重要的区别。属性列表序列化只能转换一个有限集合的数据类型(大多数是数量类型),而归档器可以转换任意的Objective-C对象、数量类型、数组、结构、字符串、及更多其它类型。
归档过程的关键在于目标对象的本身。归档器操作的对象必须遵循NSCoding
协议,该协议定义了读写对象状态的接口。归档器在编码一组对象时,会向每个对象发送一个encodeWithCoder:
消息,目标对象则在这个方法中将自身的关键状态信息写入到对应的档案中。解档过程的信息流与此相反,在解档过程中,每个对象都会接收到一个initWithCoder:
消息,用于从档案中读取当前状态信息,并基于这些信息进行初始化。解档过程完成后,字节流就被重新组成一组与之前写入档案时具有相同状态的新对象。
Foundation框架支持两种归档器—顺序归档和基于键的归档。基于键的归档器更加灵活,是应用程序开发中推荐使用的归档器。下面的例子显示如何用一个基于键的归档器对一个对象图进行归档。_myDataSource
对象的representation
方法返回一个单独的对象(可能是一个数组或字典),指向将要包含到档案中的所有对象,之后该数据对象就被写入由myFilePath
变量指定路径的文件中。
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:[_myDataSource representation]]; |
[data writeToFile:myFilePath atomically:YES]; |
请注意:您还可以向NSKeyedArchiver
对象发送archiveRootObject:toFile:
消息,以便在一个步骤中完成档案的创建和将档案写入存储。
您可以简单地通过相反的流程来装载磁盘上的档案内容。在装载磁盘数据之后,可以通过NSKeyedUnarchiver
类及其unarchiveObjectWithData:
类方法来取回模型对象图。例如,您可以用下面的代码来解档之前例子中的数据:
NSData* data = [NSData dataWithContentsOfFile:myFilePath]; |
id rootObject = [NSKeyedUnarchiver unarchiveObjectWithData:data]; |
将数据写到Documents目录
程序清单6-4 将数据写到应用程序的Documents
目录
- (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]); |
} |
从Documents目录读取数据
为了从应用程序的Documents目录读取文件,您首先需要根据文件名构建相应的路径,然后以期望的方法将文件内容读入内存。对于相对较小的文件—也就是尺寸小于几个内存页面的文件—您可以用程序清单6-5中的代码来取得文件内容。该代码首先为Documents
目录下的文件构建一个全路径,并为这个路径创建一个数据对象,然后返回。
程序清单6-5 从应用程序的Documents
目录读取数据
- (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; |
} |
对于载入时需要多个内存页面的文件,应该避免一次性地装载整个文件。如果您只是计划使用部分文件,这一点就尤其重要。对于大文件,您应该考虑用mmap
函数或NSData
的initWithContentsOfMappedFile:
方法来将文件映射到内存。
文件访问的指导原则
在您创建文件或写入文件数据时,请记住下面这些指导原则:
-
使写入磁盘的数据量尽可能少。文件操作速度相对较慢,且涉及到Flash盘的写操作,有一定的寿命限制。下面这些具体的小贴士可以帮助您最少化与文件相关的操作:
-
只写入发生变化的文件部分,但要尽可能对变化进行累计,避免在只有少数字节发生改变时对整个文件进行写操作。
-
在定义文件格式时,将频繁变化的内容放在一起,以便使每次需要写入磁盘的总块数最少。
-
如果您的数据是需要随机访问的结构化内容,则可以将它们存储在Core Data持久仓库或SQLite数据库中。如果您处理的数据量可能增长到数兆以上,这一点尤其重要。
-
-
避免将缓存文件写入磁盘。这个原则的唯一例外是:在应用程序退出时,您需要写入某些状态信息,使程序在下次启动时可以回到之前的状态。
网络
蜂窝网和Wi-Fi无线网都被设计为在没有数据传输活动时关闭电源。
相比于经常性地传输少量数据,一次性传递所有数据或间隔时间较长但每次传递数据量较大是更好的选择。
使用Wi-Fi
如果您的应用程序通过Wi-Fi无线信号访问网络,则必须将这个事实通知系统,即在应用程序的Info.plist
文件中包含UIRequiresPersistentWiFi
键。包含这个键使系统知道在检测到活动的Wi-Fi 热区时应该弹出网络选择框,同时还使系统知道在您的应用程序运行时不应试图关闭Wi-Fi硬件。
为了防止Wi-Fi硬件消耗太多的电能,iPhone OS内置一个定时器,如果在30分钟内没有应用程序通过UIRequiresPersistentWiFi
键请求使用Wi-Fi,就会完全关闭该硬件。如果用户启动某个包含该键的应用程序,则在该程序的生命周期中,iPhone OS会有效地禁用该定时器。但是一旦该程序退出,系统就会重新启用该定时器。
请注意:即使UIRequiresPersistentWiFi
键的值为true
,在设备空闲(也就是处于屏幕锁定状态)时也是没有效果的。在那种情况下,应用程序被认为是不活动的,虽然它可能在某些级别上还在工作,但没有Wi-Fi连接。
飞行模式警告
当应用程序启动时,如果设备处于飞行模式,系统可能会显示一个对话框通知用户。系统仅在下面的所有条件都满足时才会显示这个通知对话框:
-
应用程序的信息属性列表(
Info.plist
) 文件包含UIRequiresPersistentWiFi
键,且该键的值被设置为true。 -
应用程序启动的同时设备处于飞行模式。
-
在切换到飞行模式后设备上的Wi-Fi还没有被手工激活。