(译)File System Programming Guide之Techniques for Reading and Writing Files Without File Coordinators

读取和写入文件主要涉及到代码和底层磁盘之间的字节传输。这是文件管理的最低级形式,但也是更复杂技术的基础。在某些时候,即使是最复杂的数据结构也必须先转换成一系列字节,然后才能存储在磁盘上。类似地,那些数据也必须从磁盘上以一系列字节的形式读出后,才能用它们来重建它们之前所表示的复杂的数据结构。

用来读/写文件的内容的技术有很多种,iOS和MacOS几乎支持所有这些技术。他们本质上都是做相同的事情,但在处理方式上稍有不同。有些技术要求你按顺序读/写文件,而另一些技术则允许你对文件的某个部分进行操作。有些技术提供异步读写的自动支持,而另一些的读写则是同步的,以便你可以更好地控制。

选择哪个技术用来操作文件是个需要好好考虑的问题,你需要考虑在读写过程中你想要控制的部分有多少,也需要考虑你准备花多少精力管理你的文件操作代码。一些高层的技术像Cocoa streams,虽然它限制了灵活性,但是提供了简单易用的接口。一些底层的技术,像POSIXGCD,它们提供了最大的灵活性和强大的功能,但是需要你编写更多的代码。

文件的异步读/写

由于文件操作涉及到磁盘访问(也可能是网络服务器上的一个磁盘)。因此,更推荐异步的执行这些操作。像Cocoa streamsGCD这两个技术被设计为在任何时候都是异步执行的,这使你可以专注于读/写文件数据,而不用担心代码执行的位置。

Processing an Entire File Linearly Using Streams

如果你始终是从头到尾读/写文件,那么Streams则提供了一些简单的异步接口用于完成这个功能。Streams通常用来操作那些随时产生数据的数据源,比如sockets等。但是,you can also use streams to read or write an entire file in one or more bursts(bursts 不会翻译。。) 有两种类型的Streams可用:

  • 使用NSOutputStream按顺序将数据写入磁盘。
  • 使用NSInputStream按顺序从磁盘读取数据。

Stream对象使用当前所处线程的run loop对读/写操作进行调度。当有可读的数据时,input stream会唤醒run loop并通知到它的代理。当有空间可以写数据时,output stream会唤醒run loop并通知到它的代理。在操作文件时,这种行为通常意味着run loop会被快速连续地唤醒多次,也就意味着你设置的代理会被重复调用多次,直到你关闭stream。如果是input stream,则是直到读取到文件的结尾。

有关如何设置和使用流对象读/写数据,请参考Stream Programming

使用GCD处理文件

GCD提供了几种不同的方式来异步读/写文件:

  • 使用dispatch I/O channel读/写数据。
  • 使用dispatch_readdispatch_write遍历函数来执行单个的异步操作。
  • 使用dispatch source调度自定义的事件处理逻辑,并在事件处理逻辑中使用标准的POSIX接口对文件进行读/写操作。

更加推荐使用Dispatch I/O channel读/写文件,因为它既可以让你控制文件操作的发生时间也可以让你在dispatch queue上异步的处理数据。
Dispatch I/O channel是一个dispatch_io_t类型的结构体,它用来标识你想要读/写的文件。Channel可以配置成使用stream-basedrandom-access的形式访问文件。基于stream-basedchannel会强制你按顺序读/写文件,与之相反,random-accesschannel可以让你从文件的任何位置开始读/写。

如果你不想创建和管理Dispatch I/O channel,则可以使用dispatch_readdispatch_write函数对文件进行读写。 使用Dispatch I/O channel会带来一些管理上的开销,如果你不想要这些开销成本的话,则可以使用上述的两个函数。但是,只有在对文件执行单个的读取或写入操作时才应使用它们。如果你需要在同一个文件上执行多个操作,那么创建一个Dispatch I/O channel则会高效得多。

dispatch sources可以让你用使用类似于Cocoa stream对象的方式处理文件。与流对象类似,它们通常用于偶尔发送和接收数据的socket或其他数据源,但它们也可用来处理文件。每当有数据等待读取或有空间可被写入数据时,dispatch source就会执行与之关联的事件处理逻辑。 对于文件来说,这意味着事件处理逻辑会被反复地快速连续调度,直到你显式地取消dispatch source或读取到的文件末尾。

更多有关如何创建和使用dispatch sources,请参考Concurrency Programming Guide。

dispatch_readdispatch_write或其他GCD的函数,请参考Grand Central Dispatch (GCD) Reference。

创建和使用Dispatch I/O Channel

要创建一个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-accesschannel,则可以在任何位置开始读/写。如果您创建是基于stream-basedchannel,那么你指定的任何起始偏移量都会被忽略,系统会在当前位置进行读/写。例如,要从基于random-accesschannel中读取第二个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-accesschannel)以及接收到进度时的处理逻辑。你可以使用dispatch_io_write函数执行写入操作,详见Grand Central Dispatch (GCD) Reference

处理I/O channel 的数据

所有在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_readdispatch_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对象来执行代码。

在内存中构建你的内容并将其一次写入磁盘

管理文件数据读取和写入的最简单的方法是两者一起进行。这适用于那些磁盘上的比较小的自定义类型的文档。你应该不会用这种方式处理多个文件或那些很大的文件。

对于二进制文件或私有格式的文件,可以使用NSDataNSMutableData类从磁盘读/写数据。你可以用许多不同的方式创建新的数据对象。例如,可以使用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.

有关NSDataNSMutableData类的更多信息,请参考 Foundation Framework Reference

For more information about using the document infrastructure in a macOS app, see Mac App Programming Guide.

使用NSFileHandle读写文件

使用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

POSIX级别的磁盘读写管理

如果你更喜欢在文件管理代码中使用C的函数,那么在POSIX级别提供了用于处理文件的标准函数。在POSIX级别,使用文件描述符来标识文件,文件描述符是一个整数值,用来标识一个打开的文件,它在你的应用中是唯一的。你可以将此文件描述符传递给任何需要它的其他函数。以下列表包含用于操作文件的主要POSIX函数:
- 使用open函数从文件创建文件描述符。
- 使用preadreadreadv函数从打开的文件描述符中读取数据。
- 使用pwritewritewritev函数将数据写入打开的文件描述符。
- 使用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文件的一部分资源。

更多有关NSURLNSFileManager类以及可以从文件和目录获取的属性,参考:NSURL Class ReferenceNSFileManager Class Reference

你可能感兴趣的:(文章翻译)