访问目录和文件

《Objective C Phrasebook 2nd - 13 Accessing Directories and Files》

 

当使用AppKit时,对大多数文件系统相关的任务来说有两种选择:一是使用NSFileManager类,它属于Foundation框架,提供了很多操作文件和目录的底层方法。另外一个是使用NSWorkspace类,来自Application Kit,提供了更多的抽象形式。

NSWorkspace的许多函数都被委托给了workspace进程。在以前的NeTX系统上,这是一个单独的程序,但在OS X上,各种功能都由Windows服务器和Finder来实现了。

正因为如此,在不能保证当前使用workspace进程时,考虑使用更高版本是没有意义的。例如,命令行工具可能运行在远程连接上,或者在服务器代码中。

UIKit框架中没有和NSWorkspace相同的东西。如果开发目标是Cocoa Touch,那你就不得不使用底层API。

 

读取文件

NSData *copy = [NSData dataWithContentsOfFile: file];
NSData *mapped = [NSData dataWithContentsOfMappedFile: file];
NSData *read = [[NSFileHandle fileHandleForReadingAtPath: file] readDataToEndOfFile];


UNIX系统访问文件采用两种典型方式:read()系统调用和mmap()系统调用。大多数其他系统,至少是那些运行在使用内存管理单元(MMU)的硬件上的系统 ,都采用这两种方式。前者以字节流方式将文件复制到缓冲区,后者将文件数据映射到某段内存中。在Foundation中,有两个类来实现上述类似操作。NSData类用来封装一段用于存放无类型数据的内存,可以通过文件创建该类的实例,文件有多种选项。

对于小文件来说,只需要使用read()将文件数据的副本读入内存。对于大一点的文件,就需要使用+dataWithContentsOfMappedFile:方法了。

后面这种方法的好处在于它能够与操作系统的虚拟内存子系统默契合作。当应用程序用完物理内存时,你也无须分配交换空间,只需要将那些包含文件映射数据的过期页丢弃即可,当需要再次访问这些数据时可以将它们重新读取回来。

这么做对于那些内存很小且没有虚拟内存的平台来说很有用。iPhone在3GS版本之前,仅有128M的RAM,并且没有任何交换空间。如果将一个4M大小的文件读到内存,那么应用程序需要占用大量的空间。

相比而言,如果你为文件创建一个映射数据对象,系统将使用4KB大小的块按需读取数据,并在不需要时释放它。如果创建新对象时用完了内存,内核将释放内存中所映射的文件,以腾出空间。如果该文件已经被读取,就不能这么做,只能在用完内存后终止应用程序。

如果仅希望一次读取一些文件,可以使用NSFileHandle类。这个类里有一个文件描述符,并用Objective-C的实现了C标准库中的一些基本操作。你可以在一次操作中通过文件句柄来读取所有数据,但更多时候,你会逐个读取每个可用的bit。

通过给文件句柄发送-readDataOfLength: 消息,可以读取固定大小的数据块。该类的大多数其他方法一般不被用来从文件中读取数据,更多时候是用它们来和socket进行交互。

 

移动和复制文件

NSFileManager *fm = [NSFileManager new]; 
if (shouldMove){
    [fm copyItemAtPath: source toPath: destination error:nil];
}
else{
  [fm moveItemAtPath: source toPath:destination error:nil];
}


为便于操作,NSFileManager类将文件系统封装为一个整体。该类向Objective-C开发人员提供了与苹果Finder和Windows资源管理器相同的功能。

在MAC OS X10.5以前,所用方法的最后一个参数是对象,当完成复制或移动操作后,将向该对象发送消息。例如,发生错误时,这些消息会询问是否要进行处理。到了10.5,苹果对接口进行了重大修改,使用了新的错误报告模式,方法的最后一个参数变为指向NSError*的指针,用于返回错误对象,而消息则发给对象的委托。

这意味着现在只能在单线程中使用NSFileManager。10.5之前,这个类是单例模式(singleton pattern,即保证一个类仅有一个实例,并提供一个访问它的全局访问点)的一个例子。通过+defaultManager 方法缓存返回值的方式很常用,它返回的就是一个单例模式。现在,这么做已不安全,因为使用缓存版本的代码可能会被不同线程调用

在编写新的代码时,要确保每次使用NSFileManager前都要创建一个新的NSFileMangager实例,并在用完后销毁它,否则会因消息被发往错误的委托而终止程序。因为如果不这么做,两个线程可以同时为单个文件管理器设置委托,并且任何一个线程都会收到全部通知,而不是各自收到各自该收的通知。

如果使用文件管理器旧的被废弃的方法,对单例是线程安全的。如果你不调用任何发送委托消息的方法,那么使用单例仍然是安全的。但不幸的是,它意味着在复制文件时,需要创建一个新的NSFileManager实例,即使是已经有了一个指向单例实例的指针也不能例外。

注意:这个例子没有从软件设计的角度进行考虑。一个更好的方法是将委托存储在线程字典中。如果你需要对单例类做类似修改,可以考虑这种方法,或者简单地为回调函数保留额外参数。不必对已有代码做过多修改来支持新的接口。

 

获取文件属性

NSFileManager *fm = [NSFileManager defaultManager];
NSDictionary *attrs = [fm attributesOfItemAtPath: @"fileAttributes.m" error: NULL];
NSString *fileType = [attrs fileType];
NSNumber *creator = [attrs objectForKey: NSFileHFSCreatorCode];


NSFileManager类封装了大多数标准POSIX文件系统操作函数。在C中,一般都使用stat()函数来查询文件的信息。这个函数有一个参数是指向结构的指针,文件信息填写在该结构中。

这种方式的局限性显而易见:无法在不影响已有代码的情况先向结构中添加新的域成员。相较而言,NSFileManager则灵活许多。方法attributesOfItemAtPath:error: 返回一个NSDictionary对象,在无须破坏程序兼容性的情况下,可以随时向字典中添加额外的条目。该例中需要注意的一点是我们可以发送-fileType消息给字典,以获得相关值。这个方法是NSDictionary的一部分,并返回对应于NSFileType键值的字典条目。这里还有很多其他类似的字典方法。

OS X有比POSIX平台更多的文件元数据。HFS+存储了每个文件的创建者和类型信息。这些现在已经用的不多,但是在经典的MacOS上,他们被用于取代文件扩展名,以此来决定选择正确的应用程序打开文件。

 

路径处理

NSString *home = @"~"; 
NSString *full = [home stringByExpandingTildeInPath];
NSString *users = [full stringByDeletingLastPathComponent];
NSString *file = [users stringByAppendingPathComponent: @"users" ];
NSString *fileWithExtension = [file stringByAppendingPathExtension: @"db"];

很多Unix代码使用sscanf()和sprintf()函数来处理路径。但在将这些代码移植到Windows或Symbian系统上时会引发很多问题,例如,文件系统的布局和路径分隔符不同。

OpenStep在设计之初就面向跨平台。NSString类提供了很多处理路径的方法。

虽然这些方法是用来处理字符串的,但它们开放了一个更为抽象的接口。你可以添加或移除路径组件,也就是路径上的一些单独的条目(既可以是文件也可以是目录)。你还可以修改文件扩展名。

和其他UNIX系统一样,OS X有唯一的根文件夹,用斜线表示。子目录也用斜线分隔,文件扩展名则用点来分隔。而在Windows上,有很多根文件夹,路径的组成部分用反斜线来分隔,但文件扩展名也是用点来分隔。

如果使用NSString的路径处理方法,那么不论文件系统有何规定,你写的代码都能正确工作。

不幸地是,NSMutableString没有相应的操作。你可以通过使用+pathWithComponents:-pathComponents方法来避免因创建大量临时对象带来的负担,第一个方法用来通过路径组件数组来构造字符串,第二个方法用来创建路径组件数组。

无须过份担心处理路径代码的效率。几乎所有文件系统上的操作都会比创建一些临时对象代价大。

 

确定文件或目录是否存在

NSFileManager *fm = [NSFileManager defaultManager];
BOOL isDir;
if ([fm fileExistsAtPath: path isDirectory: &isDir])
{
   if (isDir)
      printf("Folder exists\n"); 
   else
      printf("File exists\n"); 
}
else
printf("File does not exist\n");

确定文件或目录是否存在的最简单方法就是尝试访问它,然后检查错误信息。这么做虽然是一种安全的方式,但很不优雅。如果你希望在尝试之前,能够向用户提供一些操作是否如期实现的反馈信息,那就要进行一个显式的测试。-fileExistsAtPath:isDirectory:方法能够实现。这个方法的名字可能会有些误导。它检查特定路径的文件或目录是否存在,并告诉你目标是否是目录。第二个参数是一个指向BOOL的指针。如果你用过C或C++,这看上去会非常熟悉,但对于使用过java之类的语言的人来说,这可能并不常见。C以及Objective-C仅支持返回单值,但这个方法需要返回两个东西:目标是否存在和它是否是目录。有很多方法可以实现这个功能。一种方式是定义一个枚举类型,包括什么都没有、存在文件、存在目录等元素。这么做会简化所有使用该方法的代码,因此为何没有这么做让人可能让人有些想不通。但当你发现你可以将NULL作为第二个参数传递进去时,答案就很明显了。如果这么做了,该方法不会干扰路径上的对象是否是目录的测试。这相对来说事个很小的改进,因此它是否是个很有意义的设计选择,还不好说。

注意:不要使用这种机制创建临时文件。这么做会在测试文件是否存在和是否创建文件之间潜在地存在竞争条件。可以使用C库的mkstemp()函数完成该功能。EtoileFoundation框架对此有一个封装,如果你希望使用对象,那么可以采用这个框架。

使用包(bundle)

NSBundle *mainBundle = [NSBundle mainBundle];
NSLog(@"%@ links against: %@", [mainBundle executablePath], [NSBundle allFrameworks]);
在苹果系统7及以前,文件系统支持两个分支(fork):代码分支和数据分支。数据分支用于存放任意资源。在Mac OS 8.1,苹果引入了HFS+,支持任意数量的分支,使得文件与目录一样有效率,能够容纳任意多数量的文件。NTFS具有相同的功能。

OS X仍然支持资源分支,并在不支持它的文件系统上将数据存储在一个隐藏文件中,例如UFS和FAT,但不推荐使用它们。将包含分支的文件拷贝到其他文件系统上会是一个问题。如果你从OS X上拷贝文件到FAT磁盘上,而后再从Windows上拷贝该文件,那么隐藏文件是不会被拷贝的,因此该分支中的数据将会丢失。

同时,NeXT走向了另一个方向。如果分支使得文件更象目录,为何不用目录来代替呢?NeXTSTEP在标准文件系统上层实现了类分支功能,就像使用目录代替了文件一样。

那些可以被当作文件的目录称为包(bundle)。对于原始底层文件系统操作以及其他的操作系统而言,它们看上去象目录。例如,如果你将一个OS X应用程序包复制到U盘上,然后在windows中查看它们,那么它们看上去和其他目录没什么两样。但当你在Finder中查看时,它就是一个单独的文件。

两种你几乎在所有Objective-C程序中都会用到的包分别是框架和应用程序。它们都包含一些可执行代码(一段程序或共享库)和一些资源集合。

确切的布局依赖于系统。GNUstep使用老一些的NeXT风格的包布局,OS X则使用稍微简单一些的方式。你可以很容易地生成既包含GNUstep又包含Cocoa布局的包,可以在两个系统间使用.app包而无须重新编译。

可以通过向NSBundle发送+mainBundle消息来获得应用程序的主包。NSBundle是用于封装包的类。它跟踪所有动态加载的包。

注意:命令行工具不存储在包中,但它们仍有一个主要包对象。该对象有一个包含主要执行体的目录作为其路径。

包可以很自然地被本地化。资源被存放在不同的子目录中,按需加载。-pathForResource:ofType:方法将返回已经命名后的资源的本地版本,带有特定的扩展,在包的资源目录中。

可以在代码中使用它获取存储在你的应用程序中或任何已加载的包中的资源文件的本地版本。如果代码在一个框架里,你可能希望从框架包中获取资源,这比从主包中获取代码稍微复杂一点。

+bundleForClass:方法提供给你一个包,包含了特定类的代码。如果你认为你将来可能将类移到一个框架里去,那么在装载资源时,使用该方法是个好的选择。

 

在系统位置查找文件

NSArray *dirs = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSAllDomainsMask, YES);

OS X上,文件系统布局定义地非常好。你可以硬编码路径,不影响其正常工作。但如果将代码移植到其他平台,就会出现大问题。

这是OpenStep程序员经常遇到的问题。例如,如果需要在Solaris、OPENSTEP、Windows NT间移植代码,那么就需要为每个平台硬编码路径。

苹果收购NeXT后,发布了一个OPENSTEP新版本,加载了一个MacOS兼容层和一个称之为Rhapsody的用户接口。它使用了和OPENSTEP一样的NeXT文件系统层级结构,在根下有一个/NeXT文件夹,包含了层级结构的非UNIX部分。

在Rhapsody DR2中,苹果对目录布局进行了重大修改,引入了OS X用户所熟悉的布局。为了便于使用,也为了支持Windows的YellowBox(Cocoa)(这个产品不久之后边停产了),他们引入了NSSearchPathForDirectoriesInDomains()函数。这个函数返回一个目录数组。当调用该函数时,第一个参数描述了你希望的目录类型。它可能是一个应用程序目录、库目录等。第二个参数是filesystem domain。

与OPENSTEP一样,OS X将文件系统层级结构划分为很多域,都有大致相同的内容,但用途不同。他们都是有序的,所以你应该更偏好高优先级域中的文件。这些域有以下四个:

  • User 这个域在用户的home目录下。里面的文件是用户私有的,并完全受控于用户。
  • Local 该域包含本地机器的文件。在OS X上,它包含了诸如/Library之类的目录,可以被本地系统管理员修改。
  • Network 在这个域下很少能看到什么东西。在独立的机器上或作为异构网络一部分的机器上,你看不到这个域。它被用于那些受网络管理员控制的目录。
  • System 这个域包含系统文件。通常,不应该在这里写入任何东西,尽管可以从这里读取。在这里所作的任何修改可能会下一次用户更新系统时被复原。

并非每个域中都存在每一种类型的目录。例如,在系统域中就没有document目录,如果有也没有什么意义。

最后一个参数告诉函数是否要在路径上扩展波浪号,这个参数应该始终设为YES。

frameworkLoader.m例子展示了这一章所描述的一些内容。这个例子加载了一个已命名框架。因为框架是包,你可以使用NSBundle来加载他们。

这个例子首先获取系统Library目录下的所有库列表,然后在其中查看,测试是否存在包含框架名和.framework扩展名的目录。

如果文件存在,程序将通知NSBundle加载它。你可以使用这段代码在运行时加载框架,而不用显式地对其进行连接。另外,你也可以通过修改,使之为库目录中的应用程序查找插件目录,这种情况下你要使用NSApplicationSupportDirectory。如果将插件生成为包,也应该使用这种方式加载。

NSArray *dirs = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSAllDomainsMask, YES);
for (NSString *dir in dirs) {
    NSString *f = [[[dir stringByAppendingPathComponent: @"Frameworks"] stringByAppendingPathComponent: framework]
                    stringByAppendingPathExtension: @" framework"];
  // Check that the framework exists and is a directory.
    BOOL isDir = NO;
    if ([fm fileExistsAtPath: f isDirectory: &isDir] && isDir) {
        NSBundle *bundle = [NSBundle bundleWithPath: f];
        if ([bundle load]) {
           NSLog(@"Loaded bundle %@", f);
           return YES; 
        }
    } 
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值