读取和写入文件主要涉及到代码和底层磁盘之间的字节传输。这是文件管理的最低级形式,但也是更复杂技术的基础。在某些时候,即使是最复杂的数据结构也必须先转换成一系列字节,然后才能存储在磁盘上。类似地,那些数据也必须从磁盘上以一系列字节的形式读出后,才能用它们来重建它们之前所表示的复杂的数据结构。
用来读/写文件的内容的技术有很多种,iOS和MacOS几乎支持所有这些技术。他们本质上都是做相同的事情,但在处理方式上稍有不同。有些技术要求你按顺序读/写文件,而另一些技术则允许你对文件的某个部分进行操作。有些技术提供异步读写的自动支持,而另一些的读写则是同步的,以便你可以更好地控制。
选择哪个技术用来操作文件是个需要好好考虑的问题,你需要考虑在读写过程中你想要控制的部分有多少,也需要考虑你准备花多少精力管理你的文件操作代码。一些高层的技术像Cocoa streams,虽然它限制了灵活性,但是提供了简单易用的接口。一些底层的技术,像POSIX和GCD,它们提供了最大的灵活性和强大的功能,但是需要你编写更多的代码。
由于文件操作涉及到磁盘访问(也可能是网络服务器上的一个磁盘)。因此,更推荐异步的执行这些操作。像Cocoa streams和GCD这两个技术被设计为在任何时候都是异步执行的,这使你可以专注于读/写文件数据,而不用担心代码执行的位置。
如果你始终是从头到尾读/写文件,那么Streams则提供了一些简单的异步接口用于完成这个功能。Streams通常用来操作那些随时产生数据的数据源,比如sockets等。但是,you can also use streams to read or write an entire file in one or more bursts(bursts 不会翻译。。) 有两种类型的Streams可用:
Stream对象使用当前所处线程的run loop对读/写操作进行调度。当有可读的数据时,input stream会唤醒run loop并通知到它的代理。当有空间可以写数据时,output stream会唤醒run loop并通知到它的代理。在操作文件时,这种行为通常意味着run loop会被快速连续地唤醒多次,也就意味着你设置的代理会被重复调用多次,直到你关闭stream。如果是input stream,则是直到读取到文件的结尾。
有关如何设置和使用流对象读/写数据,请参考Stream Programming
GCD提供了几种不同的方式来异步读/写文件:
更加推荐使用Dispatch I/O channel读/写文件,因为它既可以让你控制文件操作的发生时间也可以让你在dispatch queue上异步的处理数据。
Dispatch I/O channel是一个dispatch_io_t
类型的结构体,它用来标识你想要读/写的文件。Channel可以配置成使用stream-based或random-access的形式访问文件。基于stream-based的channel会强制你按顺序读/写文件,与之相反,random-access的channel可以让你从文件的任何位置开始读/写。
如果你不想创建和管理Dispatch I/O channel,则可以使用dispatch_read
或dispatch_write
函数对文件进行读写。 使用Dispatch I/O channel会带来一些管理上的开销,如果你不想要这些开销成本的话,则可以使用上述的两个函数。但是,只有在对文件执行单个的读取或写入操作时才应使用它们。如果你需要在同一个文件上执行多个操作,那么创建一个Dispatch I/O channel则会高效得多。
dispatch sources可以让你用使用类似于Cocoa stream对象的方式处理文件。与流对象类似,它们通常用于偶尔发送和接收数据的socket或其他数据源,但它们也可用来处理文件。每当有数据等待读取或有空间可被写入数据时,dispatch source就会执行与之关联的事件处理逻辑。 对于文件来说,这意味着事件处理逻辑会被反复地快速连续调度,直到你显式地取消dispatch source或读取到的文件末尾。
更多有关如何创建和使用dispatch sources,请参考Concurrency Programming Guide。
dispatch_read
、dispatch_write
或其他GCD的函数,请参考Grand Central Dispatch (GCD) Reference。
要创建一个dispatch I/O Channel,你必须提供待打开文件的描述符或者名称。如果你已经有一个打开的文件描述符,将它传给channel则会改变它的所有权,dispatch I/O Channel会全权管理该描述符,以便根据需要重新配置该文件描述符。例如,channel通常会使用O_NONBLOCK标志重新配置文件描述符,以便后续的读/写操作不会阻塞当前线程。使用文件路径创建channel时,channel会自动创建文件描述符以便对其进行控制。
Listing 7-1显示了如何使用NSURL对象创建一个dispatch I/O Channel。此时,channel被配置为random-access。如果在channel创建期间或生命周期结束时发生错误,则指定的block会被分派到相应的queue中被执行以进行一些清理操作。如果在创建过程中发生错误,你可以使用错误码来判断原因。 错误代码为0通常表示调用dispatch_io_close
函数后,channel放弃其文件描述符的控制权,所以此时,你就可以安全的关闭通道。
Listing 7-1 创建dispatch I/O channel
-(void)openChannelWithURL:(NSURL*)anURL {
NSString* filePath = [anURL path];
self.channel = dispatch_io_create_with_path(DISPATCH_IO_RANDOM,
[filePath UTF8String], // Convert to C-string
O_RDONLY, // Open for reading
0, // No extra flags
dispatch_get_main_queue(),
^(int error){
// Cleanup code for normal channel operation.
// Assumes that dispatch_io_close was called elsewhere.
if (error == 0) {
dispatch_release(self.channel);
self.channel = NULL;
}
});
}
创建完dispatch I/O channel后,你需要保存该函数生成的dispatch_io_t
结构体的对象,以便后面用它来调用读/写函数。如果你创建的是基于random-access的channel,则可以在任何位置开始读/写。如果您创建是基于stream-based的channel,那么你指定的任何起始偏移量都会被忽略,系统会在当前位置进行读/写。例如,要从基于random-access的channel中读取第二个1024长度的字节,你的代码可能会像下面这样:
dispatch_io_read(self.channel,
1024, // 1024 bytes into the file
1024, // Read the next 1024 bytes
dispatch_get_main_queue(), // Process the bytes on the main thread
^(bool done, dispatch_data_t data, int error){
if (error == 0) {
// Process the bytes.
}
}****);
‘写’操作会要求你指定要写入的字节、开始写入的位置(用random-access的channel)以及接收到进度时的处理逻辑。你可以使用dispatch_io_write
函数执行写入操作,详见Grand Central Dispatch (GCD) Reference
所有在channel上的操作都是使用dispatch_data_t
这个数据结构来处理读/写。dispatch_data_t
结构体是一个不透明类型,它管理着一个或多个连续的内存缓冲区。使用不透明类型话,即使GCD在底层处理的是一些分散的缓冲区,但是对你的应用来说,却好像是在处理一些连续的缓冲区。dispatch_data_t
数据结构的内部实现细节并不重要,重要的是我们需要知道如何创建和使用它。
要想将数据写入dispatch I/O channel,你的代码必须提供一个包含了待写入字节的dispatch_data_t
结构体。你可以使用dispatch_data_create
函数创建一个dispatch_data_t
,这个函数需要两个参数:一个指向buffer的指针、指向的buffer的大小。返回的dispatch_data_t
对象则封装了该buffer的数据。dispatch_data_t
对象如何包装 buffer数据取决于你在调用dispatch_data_create
时传入的析构函数。如果使用默认的析构函数,dispatch_data_t
对象会创建一个buffer副本,同时会在适当的
时机释放buffer。如果你不希望dispatch_data_t
对象复制你提供的buffer,那么你得自己提供一个析构函数用于在dispatch_data_t
对象释放时进行一些清理操作。
注意:如果你想将多个buffer的数据当成一个连续的数据块写入文件时,你可以创建一个dispatch_data_t
对象用来包装所有这些buffer。你可以使用dispatch_data_create_concat
函数将额外的buffer添加到某个dispatch_data_t
的对象中。
虽然各个buffer之间是相互独立的,并且在内存上也是位于不同的位置,但是dispatch_data_t
可以将它们包装为一个单独的实体。(你甚至可以使用dispatch_data_create_map
函数将你提供的那些分散的buffer生成一个在内存分布上连续的buffer)。特别是在磁盘操作的时候,如果buffer是连续的,那么你只需要调用一次dispatch_io_write
就可以将数据写入,如果buffer是分散的,那么就得每个buffer都调用一次dispatch_io_write
,很明显,前者会高效很多。
要从dispatch_data_t
中提取数据,可以使用dispatch_data_apply
函数。因为dispatch_data_t
封装了底层细节,所以你可以使用这个函数遍历dispatch_data_t
中的所有buffer,然后通过一个回调进行处理。如果dispatch_data_t
中的buffer是单个且连续的,你提供的回调逻辑只会被执行一次。如果有多个buffer,那么有多少个buffer你的回调逻辑就会被调用多少次。每次回调时,都会传给你一个数据buffer 和一些该buffer的相关信息。
Listing 7-2展示了这样一个例子:
打开一个channel,读取一个UTF8格式的文本文件的内容到NSString对象中。 这个例子中每次读取1024个字节,这是随意取的一个大小,可能性能不是最好的。但是演示了如何使用dispatch_io_read
和dispatch_data_apply
函数读取数据并将数据转换为你的应用所需要的格式。这个例子中,使用dispatch_data_t
对象的buffer初始化一个string对象,然后将这个string对象传给addString:toFile:
方法以便后面使用。
Listing 7-2 Reading the bytes from a text file using a dispatch I/O channel
- (void)readContentsOfFile:(NSURL*)anURL {
// Open the channel for reading.
NSString* filePath = [anURL path];
self.channel = dispatch_io_create_with_path(DISPATCH_IO_RANDOM,
[filePath UTF8String], // Convert to C-string
O_RDONLY, // Open for reading
0, // No extra flags
dispatch_get_main_queue(),
^(int error){
// Cleanup code
if (error == 0) {
dispatch_release(self.channel);
self.channel = nil;
}
});
// If the file channel could not be created, just abort.
if (!self.channel)
return;
// Get the file size.
NSNumber* theSize;
NSInteger fileSize = 0;
if ([anURL getResourceValue:&theSize forKey:NSURLFileSizeKey error:nil])
fileSize = [theSize integerValue];
// Break the file into 1024 size strings.
size_t chunkSize = 1024;
off_t currentOffset = 0;
for (currentOffset = 0; currentOffset < fileSize; currentOffset += chunkSize) {
dispatch_io_read(self.channel, currentOffset, chunkSize, dispatch_get_main_queue(),
^(bool done, dispatch_data_t data, int error){
if (error)
return;
// Build strings from the data.
dispatch_data_apply(data, (dispatch_data_applier_t)^(dispatch_data_t region,
size_t offset, const void *buffer, size_t size){
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
NSString* aString = [[[NSString alloc] initWithBytes:buffer
length:size encoding:NSUTF8StringEncoding] autorelease];
[self addString:aString toFile:anURL]; // Custom method.
[pool release];
return true; // Keep processing if there is more data.
});
});
}
更多关于如何处理dispatch_data_t
对象的信息,参考Grand Central Dispatch (GCD) Reference
那些和文件相关的用来处理数据的同步接口,可以让你灵活的自行设置代码执行的上下文。同步执行并不意味着它们会比异步执行的效率低,同步执行只是表示这些接口不会自动提供异步执行的上下文环境。如果你想要使用这些技术异步的读写数据,并获得与异步接口同样的好处,则必须自己提供异步执行上下文。 当然,最好的方法是使用GCD调度队列或operation对象来执行代码。
管理文件数据读取和写入的最简单的方法是两者一起进行。这适用于那些磁盘上的比较小的自定义类型的文档。你应该不会用这种方式处理多个文件或那些很大的文件。
对于二进制文件或私有格式的文件,可以使用NSData
或NSMutableData
类从磁盘读/写数据。你可以用许多不同的方式创建新的数据对象。例如,可以使用keyed archiver
对象将对象图转换为数据对象中的线性字节流。如果你有结构化良好的二进制文件格式,你可以将字节附加到一个NSMutableData
对象,然后逐步的构建你的数据对象。当你准备将数据对象写入磁盘时,请使用writeToURL:atomically:
或writeToURL:options:error:
方法。这些方法允许你仅用一步就完成相应磁盘文件的创建。
注意: 在iOS中,writeToURL:options:error:
方法的options
参数其中有个选项是用来指定是否希望加密文件的内容。如果设置了这个选项,系统就会对文件内容进行加密以确保文件的安全性。
要从磁盘读取数据,请使用initWithContentsOfURL:options:error:
方法,它会根据文件内容创建数据对象。你可以使用这个数据对象来反转创建它时所做的处理(译者注:苹果的措辞老复杂了。。。这里意思就是说:你怎么从一个对象得到一个data,那么你就可以用这个data再构造出那个对象)。因此,如果你使用keyed archiver
创建数据对象,那么就可以使用keyed unarchiver
重新创建对象图。如果你是逐步地输出数据,那么就可以解析数据对象中的字节流,并使用它重建数据结构。
Apps that use the NSDocument infrastructure typically interact with the file system indirectly using NSData objects. When the user saves a document, the infrastructure prompts the corresponding NSDocument object for a data object to write to disk. Similarly, when the user opens an existing document, it creates the document object and passes it a data object with which to initialize itself.
有关NSData
和NSMutableData
类的更多信息,请参考 Foundation Framework Reference
For more information about using the document infrastructure in a macOS app, see Mac App Programming Guide.
使用NSFileHandle
类对文件进行读写与在POSIX级读写文件的过程非常相似。基本过程是:打开文件,执行读取或写入,完成后关闭文件。使用NSFileHandle
时,创建该类的实例时会自动打开相应的文件。文件句柄对象相当于是对文件的封装,它为你管理着底层的文件描述符。
Listing 7-3演示了一个非常简单的使用文件句柄对象读取文件全部内容的方法。fileHandleForReadingFromURL:error:
创建的是一个会自动释放的对象,这会导致它在该方法返回后的某个时刻自动释放。
- (NSData*)readDataFromFileAtURL:(NSURL*)anURL {
NSFileHandle* aHandle = [NSFileHandle fileHandleForReadingFromURL:anURL error:nil];
NSData* fileContents = nil;
if (aHandle)
fileContents = [aHandle readDataToEndOfFile];
return fileContents;
}
NSFileHandle
类的更多信息,参考NSFileHandle Class Reference
如果你更喜欢在文件管理代码中使用C的函数,那么在POSIX级别提供了用于处理文件的标准函数。在POSIX级别,使用文件描述符来标识文件,文件描述符是一个整数值,用来标识一个打开的文件,它在你的应用中是唯一的。你可以将此文件描述符传递给任何需要它的其他函数。以下列表包含用于操作文件的主要POSIX函数:
- 使用open
函数从文件创建文件描述符。
- 使用pread
,read
或readv
函数从打开的文件描述符中读取数据。
- 使用pwrite
,write
或writev
函数将数据写入打开的文件描述符。
- 使用lseek
函数重新定位当前的文件指针并更改读取或写入数据的位置。
- 完成后使用pclose
函数关闭文件描述符。
重要提示:在某些文件系统上,不能保证write
调用成功就会真正将字节写入文件系统。对于某些网络文件系统,被写入的数据可能会在write
调用后的某个时间点才被发送到服务器。要验证数据是否确实传送到文件,请使用fsync
函数强制数据发送到服务器或关闭文件并确保它已成功关闭(关闭函数未返回-1)。
Listing 7-4演示了一个简单的函数,它使用POSIX调用来读取文件的前1024个字节并将它们设置到NSData对象中。如果文件少于1024个字节,则该方法读取尽可能多的字节并将数据对象截断为实际的字节数。
- (NSData*)readDataFromFileAtURL:(NSURL*)anURL {
NSString* filePath = [anURL path];
fd = open([filePath UTF8String], O_RDONLY);
if (fd == -1)
return nil;
NSMutableData* theData = [[[NSMutableData alloc] initWithLength:1024] autorelease];
if (theData) {
void* buffer = [theData mutableBytes];
NSUInteger bufferSize = [theData length];
NSUInteger actualBytes = read(fd, buffer, bufferSize);
if (actualBytes < 1024)
[theData setLength:actualBytes];
}
close(fd);
return theData;
}
由于应用程序可以打开的文件描述符的数量是有限制的,因此你应该在使用完文件描述符后立即将其关闭。文件描述符不仅用于打开的文件,还用于其他通信通道:例如套接字(sockets)和管道(pipes)。同时,为你的应用创建文件描述符的也并不只有你的代码。每次加载资源文件或使用需要通过网络通信的框架时,系统都会为你创建文件描述符。如果在你的代码中打开大量套接字或文件并且从不关闭它们,系统可能无法在关键时刻创建文件描述符。
文件包含许多有用的信息,文件系统也是如此。对于每个文件和目录,文件系统存储着例如:大小、创建日期、所有者、权限、文件是否被锁定或文件扩展名是否隐藏等元信息。有很多种方法可以获取和设置这些信息,但最好的方法是:
NSURL
类获取并设置大多数内容元数据信息(包括Apple特定的属性)。NSFileManager
类获取和设置基本的文件相关信息。NSURL
类提供了很多的文件相关信息,包括文件系统的标准信息(如文件大小、类型、所有者和权限),还提供了许多Apple特定的信息(例如分配的标签、本地化名称、与文件关联的图标、文件是否为package等等)。另外,一些将URL作为参数的方法可以让你在文件或目录上执行其他操作时缓存属性。特别是在访问大量文件时,这种类型的缓存行为可以通过减少磁盘相关操作数量来提高性能。无论属性是否缓存,都可以使用getResourceValue:forKey:error:
方法从NSURL对象中检索它们,并使用setResourceValue:forKey:error:
或setResourceValues:error:
方法为某些属性设置新值。
即使你没有使用NSURL类,你也可以使用NSFileManager
类的attributesOfItemAtPath:error:
和setAttributes:ofItemAtPath:error:
方法来获取和设置一些与文件相关的信息。这些方法可让你获取一些文件相关的信息,例如类型、大小以及当前支持的访问级别。你还可以使用NSFileManager
类获取文件系统本身的常规信息,例如其大小、可用的空间、文件系统中的节点数等等。请注意,你只能获取iCloud文件的一部分资源。
更多有关NSURL
和NSFileManager
类以及可以从文件和目录获取的属性,参考:NSURL Class ReferenceNSFileManager Class Reference