object-c 第三章

第三章 框架

框架是构成支持所有Objective-C 开发的库的基础。其中包括提供了对诸如字符串,数组,字典,其他通用对象及其适配的方法的支持。

框架比Cocoa库处于更低的抽象层次。Cocoa库和UIKit套件关心的应用程序,视图以及用户输入,而框架则关注组织数据的底层任务。在本章中,你将学习框架提供的一些重点类,以及了解Cocoa 和 Cocoa Touch 所依赖的设计模式。

可变和不可变对象

框架里面几乎所有存储数据的对象都分成两种:可变和不可变。可变对象可在对象创建后被修改,不可变对象则不能。

例如,NSArray类,它把对象存储成一个列表,但你不能往里添加,删除,替换对象。如果你想改变它的内容,则需要使用NSMutableArray类。

为什么有可变和不可变对象呢?两个理由:

  1. 如果一个对象是不可变的,说明它所在内存是不能被修改的,因此更有效率。
  2. 如果你传递了一个不可变对象给另一个对象,可以明保证这个对象永远不能被其他对象修改。

你可以为一个已有对象创建其不可变的版本。(反之亦然-你可以创建其可变的版本)例如,通过NSArray 创建 NSutableArray(后续章节会更详细介绍):

// here, 'someArray' is an NSArray
NSMutableArray* mutableArray = [NSMutableArray arrayWithArray: someArray];

可变和不可变对象都有各自的用处。大多数情况,编写Mac 和 iOS 应用时用到的是不可变对象。Cocoa常把这些对象作为参数传递,保证类似传递NSArray对象给另一个方法时,不会修改其内容。如果你是java开发者,可能对这种从语言级别区分可变和不可变对象会感到一丝陌生,因为java里习惯使用可变对象。不过随着使用时间变长,你会发现这种区分很有用。

字符串

简单来说,字符串就是一块文本信息。Objective-C应用处理到文本的地方,基本都会用到NSString,以后你会慢慢熟悉NSString的。

由于字符串可存储文本,所以常用于存储我们所写的自然语言(一般用unicode编码)。

字符串的创建

字符串用NSString 类表示,使Objective-C 对待字符串像其他对象一样。你可以这样创建一个空字符串:

NSString* aString = [[NSString alloc] init];

不推荐这样做,因为NSString 类是不可变的,所以上面创建的空字符串不能被修改。其他的创建方法,比如通过加载文件内容创建,或在代码提供内容来创建,会更有用。

Objective-C 使用字符串是非常普遍的,故此提供了一种快速创建方法:

NSString* aString = @"Hello, World!";

注意双引号前面的‘@’ 符号,它告诉编译器创建的是NSString对象而不是C标准的字符串(不属于Objective-C 对象)。因为NSString 是符合Objective-C 标准的对象,所以它可以几首消息,并和应用中其他对象通信。比如,你可以获取新字符串又多少个字符:

NSInteger sizeofString = [@"Hello, world!" length];

用这种语法定义的字符串对象称为文本型字符串。

字符串的使用

字符串对象提供良好的扩展性和内置大量的方法。大多数情况,字符串有两种用途:处理自然语言和文件路径。这意味NSString 提供大量的方法用于处理这些数据。

NSString 创建的三种方法:

  1. 使用文本,如:@“this example”。
  2. 由其它数据加载,如文件
  3. 由已存在的字符串。

由文本创建字符串的语法:

NSString* constantString = @"Text of the string";

这种创建的字符串对象,可以直接添加到数组和字典里,后续详细介绍。

大小写和路径

由于字符要处理自然语言,所以提供一些使用的方法用做转换处理。
如大小写转换:

NSString* originalString = @"This is An Example";

// "THIS IS AN EXAMPLE"
NSString* uppercase = [originalString uppercaseString]

// "this is an example"
NSString* lowerCaseString = [originalString lowercaseString];

// "This Is An Example"
NSString* capitalizedString = [originalString capitalizedString];

注意,调用上述方法不会改变原字符串,因为它是不可变的;相反,凡涉及处理字符串内容的方法,都会返回一个新字符串对象。

查找子符

在使用字符串时,有时需要提取子串,例如从“hello world” 中的hello。

为了提取子串,可以指定从原字符串的某位置开始直到结尾或从开头到指定位置结束进行查找。也可以在指定的范围内的字符进行查找。

想得到一个字符串前5个字符,可以:

NSString* startSubstring = [originalString substringToIndex: 5]; // "This "

想得到一个字符串从第5个位置往后的所有字符,可以:

NSString* endSubstring = [originalString substringFromIndex: 5]; // "is An EXAMPLE"

想得到某范围内的字符,首先要创建一个 NSRange 的结构体,它定义了范围值。比如,指定从第3个字符开始长度为5的字串,可以:

NSRange theRange = NSMakeRange(2, 5);

NSRange 有两个变量:位置和长度。位置是从0开始,所以上面的定义需要从2开始。

一旦定义好了NSRange,就可以这样获取子串:

NSRange theRange = NSMakeRange(2, 5);
NSString* subString = [originalString substringWithRange: theRange]; //"is is"

比较字符串

字符串比较是常用的操作,不过,不能像下面的代码这样做:

// firstString contains "one" and secondString is another object,
// also containing "one"
if (firstString == secondString){
  // do something
}

因为 == 操作符只是比较两个变量的指针,实际上检查两个变量在内存位置是否一样,通常结果是否定的。

正确的做法是使用 isEqualToString: 方法:

if ([firstString isEqualToString: secondString]){
  // do something
}

当两个字符串内容完全相同,方法返回True,否则False。这个方法大小写敏感。

搜索字符串

除了检查两个字符串是否一样,你还可以获取字符串所包含更多信息。在Cocoa,你可以查找字符串是否包含特定的子串,或者对比两个字符串看看他们如何排序。

查找子串,还是使用rangeOfString 系列方法。调用这些方法,如果子串找到,会返回NSRange,否则,返回的NSRange 中的位置变量是一个NSNotFound常量。例如:

NSString* sourceString = @"Four score and seven ago";
NSRange range = [sourceString rangeOfString: @"seven"];
if(range.location == NSNotFound){
  // the string was not found
} else {
  // the string was found; 'range' variable contains info on where it is
}

搜索子串还可以限制范围,或提供可选参数。比如:可以在搜索子串时忽略大小写:

NSString* sourceString = @"Four score and seven years ago";
NSRange range = [sourceString rangeOfString: "SEVEN" options: NSCaseInsensitiveSearch];

数组

简而言之,数组是对象的列表。它按序存储对象,允许你一次引用一个或全部对象。

数组是Cocoa 里重要的容器类之一,因为它可以在内存允许的情况下存储任意多的对象。任何时候一个方法需要使用一个或多个对象,NSArray几乎都可以用于存储它们。

和NSString一样,NSArray是不可变的--一旦被创建,就不能新加或移除其中的对象。所以,创建NSArray时,需要提供要存储的对象。有多种方法可以实现,比如:使用其他数组的元素来创建。

只要是Objective-C 对象,都可以存储在数组,这些对象不需要是相同的类型--可以将NSString对象和NSView对象存储在同一数组。数组也可以存储其他数组,因为NSArray 本身也是对象。

类似NSString的对象,创建NSArray对象也是特别的:

NSArray* myArray = @[@"one", @"two", @"three"];

这样就创建了一个新的,不可变的,且包含NSString对象的数组。

要从数组中获取某对象,语法是:

NSString* oneString = myArray[0];
NSString* twoString = myArray[1];

数组需要注意的地方:

  • 数组从0开始计数,对应第一个对象,其他依此类推。
  • 如果使用无效的下标,会导致异常或崩溃。比如,一个有3个对象的数组,你想取下标为3的对象,就会导致异常(下标3对应第4个对象)。
  • iOS5 以下的版本不支持直接访问数组元素的,你需要使用较为啰嗦的方法objectAtIndex: ,如下:

    NSString* oneString = [myArray objectAtIndex: 0];
    

    这个方法兼容iOS 7, 所以如果你的代码要兼容其他版本,就可以用这个方法。

同样地,你也可以通过count 属性获取数组元素的个数。

int count = myArray.count;
// count now equals 3

由于NSArray 是对象,所以和其他对象一样可以接收消息。比如,要获知某对象在数组的下标可发送indexOfObject: 方法,如果不存在,方法就返回一个特殊值 NSNotFound:

NSArray* myArray = @[@"one", @"two", @"three"];
int index = [myArray indexOfObject: @"two"]; // should be equal to 1

if (index == NSNotFound){
    NSLog(@"Couldn't find the object!");
}

若要从其他数组创建一个新数组,可以使用subArrayWithRange: 方法,它需要一个NSRange 参数。新数组里面的对象不会复制过来–即两个数组共同引用这些对象,所以其中一个数组修改了某个对象的属性,必然影响另一个。

如果你想把对象拷贝过来,你可以发送copy 消息,它会返回对象一个副本。但不是所有对象都支持拷贝,需要对象实现NSCopying 协议。

下面是一个例子:

NSArray* myArray = @[@"one", @"two", @"three"];
NSRange subArrayRange = NSMakeRange(1,2);
NSArray* subArray = [myArray subArrayWithRange: subArrayRange];
// subArray now contains "two", "three"

快速列举

对于一个容器,有时需要访问它里面的每一个对象,Objective-C 有个特性叫快速列举,允许你快速高效遍历容器的对象。

遍历数组对象,你需要这样做:

NSArray* myArray = @[@"one", @"two", @"three"];

for(NSString* string in myArray){
  // this code repeated 3 times, one for each item in the array
}

编译器会自动产生低开销的代码遍历容器每一个项。

可变数组

之前讨论的都是不可变数组NSArray,不过有时想要对数组的元素进行增删改,要怎么做呢?使用NSMutableArray 类即可。

NSMutableArray 是NSArray 的子类,所以NSArray能做的,NSMutableArray 也能做到,同样,以NSArray 为参数的方法,可以把NSMutableArray 作为实参传递过去(类的向上转型)

通过addObject: 和 insertObject: atIndex: 方法,可以往NSMutableArray 添加对象。前者将新的对象添加到数组的结尾,而后者则把对象插入指定的位置:

NSMutableArray* myArray = [NSMutalbeArray arrayWithArray: @[@"One", "Two"]]

// Add "Three" to the end
[myArray addObject: @"Three"];

// Add "Zero" to the start
[myArray InsertObject: @"Zero" atIndex: 0];

删除对象,则对应用removeObject: 和removeObjectAtIndex: 方法:

NSMutableArray* myArray = [NSMutableArray arrayWithArray: @[@"One", @"Two", @"Three"]];

[myArray removeObject: @"One"]; // remove "One"
[myArray removeObjectAtIndex: 1]; // remove "Three", the second item in the array at this point
// The array now contains just "Two"

注意,removeObject: 方法会把相同的对象实例全部删除,比如,如果字符串对象@“One” 在数组中出现两次,调用方法时,两个都会被删除。

替换数组中的对象,可以使用replaceObjectAtIndex: withObject: 方法,这个方法需要传递位置和要替换的对象:

NSMutableArray* myArray = [NSMutableArray arrayWithArray: @[@"One", @"Two", @"Three"]];
[myArray replaceObjectAtIndex: 1 withObject: @"Bananas"];
// myArray is now "One", "Bananas", "Three"

也可以直接给可变数组赋值来替换对象:

myArray[0] = @"Null";

提示:只有可变数组才能这么做,不可变数组则不可以。你也不能用这种方法新增和删除对象,只能用于替换数组中一个已经存在的对象。

字典

数组只是简单地存储对象的列表,字典较之更为复杂。字典可通过键值(凡支持拷贝的对象都可以作为键值,通常用NSString)映射来存储对象。

你可以将字典看作一个表。比如你想存储联系人的信息,就可以用字典表示:

key            value
----------------------
Name        Cave Johnson
Company    Aperture Science
Likes        Science
Dislikes    Lemons

当你想找到联系人的公司信息,通过Company 这个键值找到对应的值 Aperture Science。

NSDictionary 和表的工作原理是一样的,而存储对象则和NSArray类似,不同的是多了个键值。

当创建NSDictionary 对象时,需要提供键值和要存储的对象,否则不能创建。

创建语法如下:

NSDictionary* translationDictionary = @{
    @“greeting”: @"Hello",
    @"farewell": @"Goodbye"
};

取字典存储对象:

NSDictionary* translationDictionary = @{@"greeting": @"Hello"};
NSString* greeting = translationDictionary[@“greeting”];

如果对象没找到,则返回nil。

字典也可以用快速列举,只需要遍历字典每一个键值。要得到对应对象值,可以用objectForKey: 方法:

// Here, aDictionary is an NSDictionary

for(NSString* key in aDictionary){
  NSObject* theValue = aDictionary[key];
  // do something with theValue
}

和NSArrays 一样,NSDictionary 也是不可变的,如果要创建可变字典,使用NSMutableDictionary 类,它是NSDictionary 子类,允许新增和删除字典存储的对象。

设置字典的值,可以使用和设置数组值的相同方法。插入对象时,需要要键值对应。如果对象已经存在,则会被替换。

NSMutableDictionary* aDictionary = @{};
aDictionary[@"greeting"] = @"Hello";
aDictionary[@"farewell"] = @"Goodbye";

NSValue 和NSNumber

诸如NSArray 和 NSDictionary等容器类只能存储Objective-C 对象。不过Objective-C里并不是所有的东西都是对象,比如整型和布尔型的值,再如之前讨论的NSRange,都不是对象,因此也就不能存储在数组和字典中。

为了解决这个问题,Cocoa 使用一些类来将非对象的值转换称对象进行存储,而这些对象,可以被存储在容器里。

NSValue 用于存储多种非对象类型值。NSNumber 只处理和数字相关的,它继承自NSValue。

由数字创建NSNumber 对象,只需在数字前使用‘@’,编译器自动推断数字的类型(double, float, character, boolean等):

NSNumber* theNumber = @123;

若要获得存储的数值,可以:

int myValue = [theNumber intValue];

NSNumber 实例对象,可以添加到任何容器类的对象:

// 'numbers' is an NSMutableArray
[numbers addObject: theNumber];

NSNumber 还可以通过表达式赋值:

int a = 100;
NSNumber* number = @(a + 1);
// 'number' contains 101

NSData

当你开发程序时,通常要处理一系列数据。大多数情况,这些数据是从磁盘加载并需要用于对象,或者准备把数据写到磁盘。比如,你加载了一张图片到内存,在内存的角度,图片只是一些字节,在使用这图片前,你必须把这些数据转换称UIIMage对象。

NSData被设计成数据的容器。它由字节构成,并且对字节的类型不做任何假设。无论你操作的是文件或者操作从网络加载的信息,接收后的信息都转换成NSData对象,然后再转换成其他对象。

NSData 是不可变对象,同样的,也有可变的,即NSMutableDAta。NSMutableData 在不能一次性加载所有字节的情况下比较有用,比如下载文件。

从文件和URL 加载数据。

大多数情况都会通过磁盘加载文件或URL来创建NSData对象,或者把其他对象转换成NSData对象,这样就能把数据写到磁盘,更多的细节后续会讲到。

从磁盘加载文件,你首先要有文件路径。你也可以通过URL来加载文件。

例如加载一个文本文件到NSData对象,可以这样:

// Assuming that there is a text file at /Examples/Test.txt:
NSString* fielPath = @"/Examples/Test.txt";
NSData* loadedData = [NSData dataWithContentsOfFile: filePath];

因为NSData 对象只能存储数据,接收字节以及写数据到磁盘而不能做其他事情,所以有很多类都设计了处理NSData对象的方法。

比如,把NSData对象转换成NSString,可以使用NSString的initWithData: encoding: 方法,参数分别为NSData 和 NSStringEncoding(指定了字节的解码方式):

NSString* loadedString = [[NSString alloc] initWithData: loadedData encoding: NSUTF8StringEncoding];

提示:上述方法加载字符串不是最高效的方式,只是为了说明NSData 的用法,更好的方法是stringWithContentsOfFile: encoding: error: , 一步到位。

你可以把NSData 对象写入磁盘,使用的方法是 writeToFile: atomically: 。方法需要提供写入路径和一个是否是原子操作的布尔值。如果这个布尔值为YES,NSData先把字节写到临时文件,然后把文件移到指定路径。如果是NO,直接写入目标文件。原子操作会使写入非常慢,因为文件系统需要做移动文件等额外工作,不过这样可以避免程序退出或中途崩溃带来的问题。

序列化和反序列化

当你用到自己所设计的类的对象时,若能把这些对象存储在磁盘并且在需要时再加载到内存是非常有用的。这就是所谓的序列化和反序列化。通过序列化,你可以将任何对象转换成NSData对象。一旦转换成NSData,你就可以像之前所说的进行读写磁盘。将一个NSData对象还原则称为反序列化。

要使用序列化,类必须实现NSCoding协议。序列化就是为相关对象编码成可写入的字节。反序列化则为读入的字节进行解码并重建对象。

NSCoding协议有两个主要的方法构成:

  • encodeWithCoder:
  • initWithCoder:

任何时候需要为类序列化时,可调用encodeWithCoder: 方法,它要传递NSKeyedArchiver 对象作为参数,用于为对象编码。

当从磁盘加载对象时,可调用initWithCoder: 方法,对象就可以根据encodeCoder: 方法序列化后的对象重建自己。这个方法要传递NSKeyedUnarchiver 对象作为参数,用于恢复所存数据。

当加载一个对象时,需调用initWithCoder: 方法而不是init方法意味着initWithCoder必须也要实现init方法所实现的逻辑。

NSKeyedArchiver 对象的工作原理跟一个可变字典比较类似-使用encodeObject: forKey: 方法并需要传递键值和对象两个参数(相关方法还有 encodeInteger: forKey: 和encodeFloat: forKey:)。方法返回的对象可被序列化以及能够存储到磁盘。

下面是实现encodeWithCoder: 方法的例子:

-(void) encodeWithCoder: (NSKeyedArchiver*) aCoder{
  // Store a string (or any other Objective-C object that supports coding)
  [aCoder encodeObject: myuStringVariable forKey: @"myString"];

  // Store a number
  [aCoder encodeInteger: myIntegerVariable forKey: @"anInteger"];
}

对应initWithCoder: 方法的实现:

- (id) iniWithCoder: (NSkeyedUnarchiver*) aDecoder {
  self = [super init];

  myStringVariable = [aDecoder decodeObjectForKey: @"myString"];
  myIntegerVariable = [aDecoder decodeObjectForKey: @"anInteger"];

  return self;
}

提示: 如果试图反序列化一个不存在的对象,解码器会抛出异常并且导致程序会崩溃。

大部分Cocoa对象都支持序列化和反序列化-比如支持序列化的有 NSArray, NSData和NSDictionary。其他对象是否支持序列化,可以查看类的文档确认是否实现了NSCoding协议。

把一个对象转换成可用NSData, 可以这样做:

// myObject is an object that
// conforms to NSCoding
NSData* object storedData = [NSKeyedArchiver archivedDataWithRootObject: myObject];

// storedData can now be written to a file

反过来,则需:

// loadedData is NSData loaded from somewhere, and SomeObject is
// a class that conforms to NSCoding
SomeObject* myObject = [NSKeyedUnarchiver unarchiveObjectWithData: loadedData];

设计模式

Cocoa 是由一系列设计模式构建而成的。下面将介绍三个主要的设计模式,分别为:模型-视图-控制器模式(MVC),delegation模式(委托模式)以及键-值观察者模式(key-value observing)。

模型-视图-控制器模式

这个模式是Cocoa最重要的模式,它将对象按职责分成三类:模型,视图,控制器。

  • 模型是包含数据的对象或者协调存储、管理和分发数据的对象。简单的模型对象可以是NSString,而更复杂的可以是整个数据库-它们的目的都是存储数据或者向其他对象提供数据。模型关心的是管理如何存储数据,而对其他对象如何处理这些数据则毫不关心。
  • 视图负责直接和用户交互,它给用户提供信息和接收用户输入。视图只负责展示数据而不管理数据并在和用户交互时负责通知其他对象。
  • 控制器是模型和视图之间的媒介,并且包含了应用的大量业务逻辑-即定义程序是什么和如何响应用户输入的逻辑。控制器至少需要负责从模型接收数据,并将数据提供给数据。它也会在用户和视图交互时负责向模型提供信息。

为了更好说明这个模式,就拿编辑器程序举个例子,比如,编辑器程序从磁盘加载文本文件并把文本内容展示给用户,用户修改了文本内容并且保存修改到磁盘。

我们可以将这个程序分成模型,视图和控制器对象:

  • 模型对象负责从磁盘加载数据和将数据写回磁盘以及负责把文本以NSString方式提供给任何需要的对象。
  • 视图对象则负责要求某对象提供NSString类型数据并且展示给用户以及负责接收来自用户的输入。无论何时,只要用户有输入,编辑器会通知某对象文本的变更,同样,用户需要保存更改时,编辑器也能通知到某对象。
  • 控制器对象则负责委托模型对象从磁盘加载文件,并将文本传递给视图对象。当文本有更改时,控制器就接收到更新消息。最后,用户要求保存更改会通过视图通知到控制器,这时,控制器会再次委托模型去实际执行把文件写回到磁盘的工作。

通过这样区分职责,修改起程序来就变得更容易了。

比如,开发者决定下一个版本的程序支持保存文件到互联网,此时只需修改模型,控制器和视图只需保持不变。

通过清晰定义对象的职责,在维护时修改起来就比较容易。比如,如果开发者决定添加拼写检查功能,应该修改控制器的代码,因为控制器跟如何展示文本和如何存储文本没有关系。

本章讲到的一些主要类如NSData,NSArray和NSDictionary,都可以作为模型。因为它们负责存储数据和向其他类展示信息。NSKeyedArchiver则可作控制器,因为它接收信息并且有处理信息的逻辑。NSButton 和UiTextField 则可作为视图的例子,因为它们仅仅是给用户展示信息。

委托模式

委托模式是指对象将某些职责委托给别的对象去完成。例如,UIApplication对象,它表示iOS的应用实例。当应用转到后台运行时,需要清楚什么是应该发生的。好多语言处理这用问题时用到的子类化-比如C++,会为UIApplication类定义一个空方法applicationDidEnterBackground,然后开发者需创建UIApplication 的子类,在覆盖applicationDidEnterBackground方法。

不过,这种方法特别勉强并且会导致额外的问题-增加了代码的复杂行,也说明如果你想覆盖两个类的方法时,你需要创建两个独立的子类。Objective-C则是基于运行时确定对象,无论另一个对象是否能够响应消息来解决这个问题。

一个对象想要另一个对象知道将要发生什么或已经发生了什么,可存储一个对象引用。这个被引用的对象则是委托对象。当事件发生时,它会检查委托的对象是否实现了匹配事件的方法-对于UIApplication类的委托对象,例如,检查委托对象是否实现了applicationEnterBackground方法。如果实现了,该方法则被调用。一个对象可同时作为多个对象的委托。

正是因为这种松耦合,一个对象作为多个对象的委托的可能的。比如,某个对象可以是一个声音回放对象和一个图片对象的委托,当声音回放完成和相机捕获完图像后,都能接收到通知。

委托里用到的具体方法通常在协议中列出。比如,如果你的对象想要作为AVAudioPlayer对象的委托,则它需要实现AVAudioPlayerDelegate 协议。

KVO

大多数MVC的实现都依赖控制器及时提供有模型到视图的更新,反之亦然。另一种方式是周期性检查模型和询问模型是否有变更,如果有,然后就向视图提供信息。不过,如果模型不是经常变更,OS X和iOS应用多数情况下模型就不怎么变更,这种方法就很低效了。为了迅速响应,故此Cocoa 实现了所谓的KVO模式。

在这个模式中,对象可能注册到其他对象成为观察者。当被观察者发生变更时,就会通知到那些观察者。

在分离视图和模块对象中,KVO起到重要作用。更多细节,后续章节会讨论。