运行在iPhone OS系统上的应用程序可以通过各种Core OS和Core Services框架来访问本地的文件系统和网络。读写本地文件系统的能力使您可以保存用户数据和应用程序状态,以备后用;而访问网络的能力则使您可以和网络服务器进行交流,进而实现远程操作的执行和数据的收发。


文件和数据管理

iPhone OS系统上的文件和用户的媒体数据及个人文件共享闪存上的空间。出于安全的目的,您的应用程序被放在其自己的目录下,并且只能对该目录进行读写。本章的下面部分将描述应用程序本地文件系统的结构及几个读写文件的技术。


常用目录

出于安全的目的,应用程序只能将自己的数据和偏好设置写入到几个特定的位置上。当应用程序被安装到设备上时,系统会为其创建一个家目录。表6-1列出了应用程序家目录下的一些重要子目录,您的程序可能需要对其进行访问。表中还描述了每个目录的设计目的和访问限制,以及iTunes是否对该目录下的内容进行备份。有关备份和恢复过程的更多信息,请参见“备份和恢复” 部分;有关应用程序家目录本身的信息,则请参见 “应用程序沙箱”部分。


表 6-1  iPhone应用程序的目录

目录

描述

<Application_Home>
/
AppName
.app

这是程序包目录,包含应用程序的本身。由于应用程序必须经过签名,所以您在运行时不能对这个目录中的内容进行修改,否则可能会使应用程序无法启动。

在iPhone OS 2.1及更高版本的系统,iTunes不对这个目录的内容进行备份。但是,iTunes会对在App Store上购买的应用程序进行一次初始的同步。

<Application_Home>
/Documents/

您应该将所有的应用程序数据文件写入到这个目录下。这个目录用于存储用户数据或其它应该定期备份的信息。有关如何取得这个目录路径的信息,请参见“获取应用程序目录的路径”部分。

iTunes会备份这个目录的内容。

<Application_Home>
/Library/Preferences

这个目录包含应用程序的偏好设置文件。您不应该直接创建偏好设置文件,而是应该使用
NSUserDefaults
类或CFPreferences API来取得和设置应用程序的偏好,详情请参见“添加Settings程序包”部分。

iTunes会备份这个目录的内容。

<Application_Home>
/Library/Caches

这个目录用于存放应用程序专用的支持文件,保存应用程序再次启动过程中需要的信息。您的应用程序通常需要负责添加和删除这些文件,但在对设备进行完全恢复的过程中,iTunes会删除这些文件,因此,您应该能够在必要时重新创建。您可以使用“获取应用程序目录的路径” 部分描述的接口来获取该目录的路径,并对其进行访问。

在iPhone OS 2.2及更高版本,iTunes不对这个目录的内容进行备份。

<Application_Home>
/tmp/

这个目录用于存放临时文件,保存应用程序再次启动过程中不需要的信息。当您的应用程序不再需要这些临时文件时,应该将其从这个目录中删除(系统也可能在应用程序不运行的时候清理留在这个目录下的文件)。有关如何获得这个目录路径的信息,请参见“获取应用程序目录的路径”部分。

在iPhone OS 2.1及更高版本,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
目录下。


请注意:如果您的应用程序需要创建数据量大或频繁变化的文件,则应该考虑将它们存储在Application Home
/Library/Caches
目录下,而不是<Application_Home>
/Documents
目录。备份大数据文件会使备份过程显著变慢,备份频繁变化(因此必须频繁备份)的文件也同样如此。将这些文件放到
Caches
目录下可以避免每次同步都对其进行备份(在iPhone OS 2.2及更高版本)。


有关如何在应用程序中使用目录的更多信息,请参见表6-1


在应用程序更新过程中被保存的文件

更新应用程序就是将用户下载的新版应用程序代替之前的版本。在这个过程中,iTunes会将更新过的应用程序安装到新的应用程序目录下,并在删除老版本之前,将用户数据文件转移到新的应用程序目录下。在更新的过程中,iTunes保证如下目录中的文件会得以保留:


<Application_Home>
/Documents


<Application_Home>
/Library/Preferences


虽然其它用户目录下的文件也可能被转移,但是您不应该假定更新之后该文件还仍然存在。


Keychain数据

keychain是一个安全、经过加密保护的容器,用于保存密码和其它秘密信息。应用程序的keychain数据存储在应用程序沙箱之外。如果应用程序被卸载,则该数据会自动被删除。当用户通过iTunes备份应用程序数据时,keychain数据也会被备份。然而,keychain数据只能被恢复到之前做备份的设备上。应用程序的更新并不影响其keychain数据。

有关iPhone OS keychain的更多信息,请参见Keychain服务编程指南文档中的“Keychain服务的概念”部分。


获取应用程序目录的路径

系统在各个级别上都提供了用于获取应用程序沙箱目录路径的编程方法。然而,取得这些路径的推荐方式还是使用Cocoa编程接口。
NSHomeDirectory
函数(在Foundation框架中)负责返回顶级家目录的路径—也就是包含应用程序、
Documents

Library
、和
tmp
目录的路径。除了这个函数,您还可以用
NSSearchPathForDirectoriesInDomains

NSTemporaryDirectory
函数来取得
Documents

Caches
、和
tmp
目录的准确路径。


NSHomeDirectory

NSTemporaryDirectory
函数都通过
NSString
对象返回正确格式的路径。您可以通过
NSString
类提供的与路径相关的方法来修改路径信息或创建新的路径字符串。举例来说,在取得临时的目录路径之后,您可以附加一个文件名,并用结果字符串在临时目录下创建给定名称的文件。


请注意:如果您使用带有ANSI C编程接口的框架—包括那些接受路径参数的接口—请记住
NSString
对象和其在Core Foundation框架中的等价类型之间是“免费桥接”的。这意味着您可以将一个
NSString
对象(比如上述某个函数的返回结果)强制类型转换为一个
CFStringRef
类型,如下面的例子所示:


CFStringRef homeDir = (CFStringRef)NSHomeDirectory();

有关免费桥接的更多信息,请参见 Carbon-Cocoa集成指南文档。

Foundation框架中的
NSSearchPathForDirectoriesInDomains
函数用于取得几个应用程序相关目录的全路径。在iPhone OS上使用这个函数时,第一个参数指定正确的搜索路径常量,第二个参数则使用
NSUserDomainMask
常量。表6-2列出了大多数常用的常量及其返回的目录。


表6-2  常用的搜索路径常量

常量

目录


NSDocumentDirectory

<Application_Home>
/Documents


NSCachesDirectory

<Application_Home>
/Library/Caches


NSApplicationSupportDirectory

<Application_Home>
/Library/Application Support


由于
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
路径,但是,您的应用程序不能往该位置写入任何文件。

另外一个需要记住的考虑是,不同平台的目录位置是不一样的。
NSSearchPathForDirectoriesInDomains

NSHomeDirectory

NSTemporaryDirectory
、和其它类似函数的返回路径取决于应用程序运行在设备还是仿真器上。作为例子,程序清单6-1上显示的函数调用在设备上返回的路径(
documentsDirectory
)大致如下:


/var/mobile/Applications/30B51836-D2DD-43AA-BCB4-9D4DADFED6A2/Documents

但是,它在仿真器上返回的路径则具有如下的形式:


/Volumes/Stuff/Users/johnDoe/Library/Application Support/iPhone Simulator/User/Applications/118086A0-FAAF-4CD4-9A0F-CD5E8D287270/Documents

在读写用户偏好设置时,请使用
NSUserDefaults
类或
CFPreferences
 API。这些接口使您免于构造
Library/Preferences/
目录路径和直接读写偏好文件。有关使用这些接口的更多信息,请参见“添加Settings程序包”部分。

如果应用程序的程序包中包含声音、图像、或其它资源,则应该使用
NSBundle
类或
CFBundleRef
封装类型来装载那些资源。程序包知道应用程序内部资源应该在什么位置上,此外,它还知道用户的语言偏好,能够自动选择本地化的资源。有关程序包的更多信息,请参见“应用程序的程序包”部分。


文件数据的读写

iPhone OS提供了如下几种读、写、和管理文件的方法:


Foundation框架:


如果您可以将应用程序数据表示为一个属性列表,则可以用
NSPropertyListSerialization
 API来将属性列表转换为一个
NSData
对象,然后通过
NSData
类的方法将数据对象写入磁盘。


如果应用程序的模型对象采纳了
NSCoding
协议,则可以通过
NSKeyedArchiver
类、特别是它的
archivedDataWithRootObject:
方法将模型对象图进行归档。


Foundation框架中的
NSFileHandle
类提供了随机访问文件内容的方法。


Foundation框架中的
NSFileManager
类提供了在文件系统中创建和操作文件的方法。


Core OS调用:


诸如
fopen

fread
、和
fwrite
这些调用可以用于对文件进行顺序或随机读写。



mmap

munmap
调用是将大文件载入内存并访问其内容的有效方法。


请注意:上面的Core OS调用列表只是列举一些较为常用的例子。更完全的可用函数列表请参见iPhone OS手册的第三部分中的函数列表。


本章的下面部分将描述如何使用一些高级技术来进行文件的读写。有关Foundation框架中与文件相关类的更多信息,请参见Foundation框架参考


属性列表数据的读写

属性列表是一种数据表示形式,用于封装几种Foundation(及 Core Foundation)的数据类型,包括字典、数组字符串、日期、二进制数据、数值及布尔值。属性列表通常用于存储结构化的配置数据。举例来说,每个Cocoa和iPhone应用程序中都有一个
Info.plist
文件,它就是用于存储应用程序本身配置信息的属性列表。您自己也可以用属性列表来存储其它信息,比如应用程序退出时的状态等。

在代码中,属性列表的构造通常从构造一个字典或数组、并将它作为容器对象开始,然后在容器中加入其它的属性列表对象,(可能)包含其它的字典和数组。字典的键必须是字符串对象,键的值则是
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
类的更多信息,请参见属性列表编程指南


用归档器进行数据读写

归档器的作用是将任意的对象集合转换为字节流。这听起来像是
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];

更多如何使用归档器和如何使对象支持
NSCoding
协议的信息,请参见Cocoa的归档和序列化编程指南


将数据写到Documents目录

有了封装应用程序数据的
NSData
对象(或者是档案,或者是序列化了的属性列表)之后,您就可以调用程序清单6-4所示的方法来将数据写到应用程序的
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:
方法来将文件映射到内存。

到底是采用映射文件还是直接装载取决于您的考虑。如果只需要少量(3-4)内存页面,则将整个文件载入内存相对安全一些。但是,如果您的文件需要数十或上百个页面,则将文件映射到内存可能更为有效一些。当然,无论采用什么方法,您都应该测量应用程序的性能,确定装载文件和为其分配必要内存需要多长时间。


文件访问的指导原则

在您创建文件或写入文件数据时,请记住下面这些指导原则:


使写入磁盘的数据量尽可能少。文件操作速度相对较慢,且涉及到Flash盘的写操作,有一定的寿命限制。下面这些具体的小贴士可以帮助您最少化与文件相关的操作:


只写入发生变化的文件部分,但要尽可能对变化进行累计,避免在只有少数字节发生改变时对整个文件进行写操作。


在定义文件格式时,将频繁变化的内容放在一起,以便使每次需要写入磁盘的总块数最少。


如果您的数据是需要随机访问的结构化内容,则可以将它们存储在Core Data持久仓库或SQLite数据库中。如果您处理的数据量可能增长到数兆以上,这一点尤其重要。


避免将缓存文件写入磁盘。这个原则的唯一例外是:在应用程序退出时,您需要写入某些状态信息,使程序在下次启动时可以回到之前的状态。



保存状态信息

当用户按下Home键时,iPhone OS会退出您的应用程序,返回到Home屏幕。类似地,如果您的应用程序打开一个由其它应用程序处理的URI模式,iPhone OS也会退出您的应用程序,在相应的应用程序上打开该URI。换句话说,在Mac OS X上引起应用程序挂起或转向后台的动作,在iPhone OS上都会使其退出。这些动作在移动设备上经常发生,因此,您的应用程序必须改变管理可变数据和程序状态的方式。

大多数桌面应用程序由用户手工选择将文件存入磁盘的时机,与此不同的是,iPhone应用程序应该在工作流的关键点上自动保存已发生的变化。究竟何时保存数据由您自己来决定,但是有两个潜在的时间点:或者在用户做出改变之后马上进行保存;或者将同一页面上的变化累计成批,然后在退出该页面、显示新页面、或者应用程序退出的时候进行保存。在任何情况下,您不应该让用户漫游到新的页面而不保存之前页面的内容。

当您的应用程序被要求退出时,应该将当前状态保持到临时的缓存文件或偏好数据库中。在用户下次启动应用程序时,可以根据这些信息将程序恢复到之前的状态。您保持的状态信息应该尽可能少,但同时又足够使应用程序恢复到恰当的点。您不必一定要显示用户上次退出时操作的页面,如果那样做并不合理的话。比如,如果一个用户在编辑某个联系人的时候离开了Phone程序,那么在下次运行时,Phone程序显示的是联系人的顶级列表,而不是该联系人的编辑屏幕。


大小写敏感性

iPhone OS设备的文件系统是大小写敏感的。在处理文件名的任何时候,您都应该确保大小写准确匹配,否则可能不能打开或访问文件。


网络

iPhone OS的网络栈中包含几个基于(iPhone和iPod touch设备上的)无线通讯硬件的编程接口。主编程接口是CFNetwork框架,该框架在BSD套接字和Core Foundation框架的封装类型之上,实现了网络实体间的通讯。您也可以用Foundation框架的
NSStream
类和位于系统Core OS层中的BSD套接字来进行通讯。

本文的下面部分将为需要集成网络功能的开发者提供一些专门针对iPhone的贴士。有关如何通过CFNetwork框架实现网络通讯的信息,请参见CFNetwork编程指南CFNetwork框架参考;有关如何使用
NSStream
类的信息,则请参见Foundation框架参考


有效进行网络通讯的贴士

在实现收发网络数据的代码时,请记住这是设备上最耗电的操作之一。最少化收发数据的时间有助于提高电池的使用寿命。为此,您在编写与网络相关的代码时需要考虑如下贴士:


对于您自己控制的协议,请将数据格式定义得尽可能紧凑。


避免使用聊天式的协议进行通讯。


在任何可能的时候,将数据包成群传输。


蜂窝网和Wi-Fi无线网都被设计为在没有数据传输活动时关闭电源。然而,根据无线网络的不同,这样做可能需要花几秒钟的时间。如果您的应用程序每隔数秒就发送少量的数据,则即使无线装置实际上并没做什么,也会一直保持电源打开,持续耗电。相比于经常性地传输少量数据,一次性传递所有数据或间隔时间较长但每次传递数据量较大是更好的选择。

在进行网络通讯时,意识到数据包在任何时候都可能丢失是很重要的。在编写网络通讯代码时,请务必在出现错误时进行处理,使程序尽可能强壮。实现响应网络条件变化的处理程序是完全合理的,但如果这些处理程序始终没有被调用,也不要觉得奇怪。举例来说,在网络服务消失时,Bonjour的网络回调函数并不总是立即被调用。当接收到某个服务即将消失的通告时,Bonjour系统服务确实立即调用浏览回调函数(browsing callbacks),然而,网络服务可能没有通告就消失了,如果设备提供的网络服务意外地丢掉网络连接,或者通告在传递中丢失,就可能出现这种情况。


使用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连接。


有关
UIRequiresPersistentWiFi
键及
Info.plist
文件中其它键的更多信息,请参见“信息属性列表”部分。


飞行模式警告

当应用程序启动时,如果设备处于飞行模式,系统可能会显示一个对话框通知用户。系统仅在下面的所有条件都满足时才会显示这个通知对话框:


应用程序的信息属性列表(
Info.plist
) 文件包含
UIRequiresPersistentWiFi
键,且该键的值被设置为true。


应用程序启动的同时设备处于飞行模式。


在切换到飞行模式后设备上的Wi-Fi还没有被手工激活。


posted on 2011-05-25 16:41  一个人的天空@  阅读(898)  评论(0编辑  收藏  举报