对象、消息、运行期

一、对象

对于OC等面向对象编程语言,“对象”就是“基本构造单元”,开发者可以通过对象来存储并传输数据。在对象间传递数据并执行任务的过程叫做“消息传递”。
当应用程序运行起来以后,为其提供相关支持的代码叫做“OC运行期环境”,他提供了一些使得对象之间能够传递消息的重要函数,并且包含创建类实例所用的全部逻辑。
属性 是OC的一项特性,用于封装对象中的数据。OC对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”来访问。其中“获取方法”用于读取变量值,而“设置方法”用于写入变量值。切此特性引入了一种新的“点语法”,使开发者可以更为容易地依照类对象来访问存放于其中的数据。
例如:我们在类接口的public区段中声明一些实例变量:

@interface EOCPerson: NSObject {
@public 
	NSString *_firstName;
	NSString *_lastName;
@private
	NSString *_someInternalData;
}
@end

但是如果使用此方法去声明实例变量的话,会有以下问题:
例如,我们对其第一个位置在添加一个实例变量:

@interface EOCPerson: NSObject {
@public 
	NSString *_dateOfBirth;
	NSString *_firstName;
	NSString *_lastName;
@private
	NSString *_someInternalData;
}
@end

这样的话,我们新添加的实例变量就会代替原第一个位置实例变量的偏移量。
这样的话,如果代码使用了编译器计算出来的偏移量,那么在修改类定义之后必须重新编译,否则就会出错。例如:某个代码库中的代码使用了一份旧的类定义。如果和其相链接的代码使用了新的类定义,那么运行时就会出现不兼容现象。对此类问题,OC的解决方法是,把实例变量当做一种存储偏移量所用的“特殊变量”,交由“类对象”保管。偏移量会在运行期查找,如果类的定义变了,那么存储的偏移量也就变了,这样的话,无论何时访问实例变量,总能使用正确的偏移量。这就是稳固的“应用程序二进制接口”
这个问题还有一种解决方法,就是尽量不要直接访问实例变量,而应该通过存取方法来做。这时@property语法就派上用场了。这种规范的命名方法OC会自动创建出存取方法。

如下方的代码:

@interface EOCPerson: NSObject
@property NSString *firstName;
@property NSString *lastName;
@end

对于上方的代码,下述代码与其效果一样:

@interface EOCPerson: NSObject
- (NSString *)firstName;
- (void)setFirstName: (NSString *)firstName;
- (NSString *)lastName;
- (void)setLastName: (NSString *)lastName;
- @end

其中,编译器自动编写访问这些属性所需的方法的过程叫做:“自动合成”。
这个过程由编译器在编译期执行,所有编译器例看不到这些“合成方法”的源代码。另外,编译器还会自动向类中添加适当类型的实例变量,并且在属性名前加下划线,以此作为实例变量的名字。还有,也可以在类的实现代码里通过@synthesize语法来指定实例变量的名字:

@implementation EOCPerson
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end

修改后将不再使用原来默认的名字。
若不想编译器自动合成存取方法,则可以自己实现。如果你只实现了其中一个存取方法,那么另外一个还是会由编译器来合成。还有一种方法就是使用@dynamic关键字,它会告诉 编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。而且,在编译访问属性的代码时,即使编译器发现没有定义存取方法,也不会报错。

属性特质:
使用属性时还有一个问题要注意,就是各种特质设定也会影响编译器所生成的存取方法。比如下面这个属性就指定了三项特性:
(1)原子性:
默认情况下,有编译器所合成的方法会通过锁定机制确保其原子性。如果属性具备nonatomic特质,则不使用同步锁。如果不具备,那它就是“原子的”,但是仍然可以在属性特质中写明这一点,编译器不会报错。如果是自己定义存取的方法,那么就应该遵从与属性特质相符的原子性。
(2)读/写权限

  1. 具备readwrite(读写)特质的属性拥有“获取方法”与“设置方法”。若该属性由@synthesize实现,则编译器会自动生成这两个方法。
  2. 具备readonly(只读)特质的属性仅拥有获取方法,只有当该属性由@synthesize实现时,编译器才会为其合成获取方法。你可以用此特质把某个属性对外公开为只读属性,然后在 分类 中将其重新定义为读写属性。
    (3)内存管理语义
    属性用于封装数据,而数据则要有“具体的所有权语义”,如果自己编写存取方法,就必须同有关属性所具备的特质相符。
    · assign "设置方法"只会执行针对“纯量类型”的简单赋值操作。
    · strong 此特质表明该属性定义了一种 拥有关系。为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后在将新值设置上去。
    · weak 此特质表明该属性定义了一种“非拥有关系”。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似,然而在属性所指的对象遭到摧毁时,属性值也会清空。
    · unsafe_unretained 此特质的语义和assign相同,但是它适用于“对象类型”,该特质表达一种“非拥有关系”,当目标对象遭到摧毁时,属性值不会自动清空,这一点与weak有区别。
    · copy 此特质所表达的所属关系与strong类似。然而设置方法并不保留新值,而是将其“拷贝”。
    方法名
    可通过如下特质来指定存取方法的方法名:
    getter=指定“获取方法”的方法名。如果某属性时BOOL型,而你想为其获取方法加上“is”前缀,那么就可以用这个办法来指定。如:
@property (nonatomic, getter=isOn) BOOL on;

setter=指定“设置方法”的方法名,这种用法不太常见。

nonatomic 与atomic的区别是什么呢。具备atomic特质的获取方法会通过锁定机制来确保其操作的原子性。也就是说,如果两个线程读写同一属性,无论何时,总能看到有效的属性值。而不加锁的话 (使用nonatomic),当另一个线程在改写某属性值的时候,另外一个线程也许会突然闯入,把尚未修改好的属性值读取出来 。发生这种情况时,线程读到的属性值可能不对。
然而,现在的iOS开发中,由于使用同步锁的开销较大,这会带来性能问题。而且原子性也并不能保证“线程安全”,若要实现“线程安全”的操作 ,还需采用更为深层的锁定机制才行。而开发Mac OS X 程序时,使用atomic属性通常都不会有性能瓶颈。
要点:
(1)可以用@property语法来定义对象中所封装的数据。
(2)通过“特质”来指定存储数据所需的正确语义。
(3)在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
(4)开发iOS程序时应该使用nonatomic属性,因为atomic属性会严重影响性能。

二、在对象内部尽量直接访问实例变量

在读取实例变量的时候采用直接访问的形式,而在设置实例变量的时候通过属性来做
一般我们使用点语法,通过使用其存取方法来访问香关实例变量,如下:

//我们假设这个类声明了一个firstName的NSString类型的属性
self.firstName = @"张三";

如果我们直接访问实例变量的时候就是这样:

//通过下划线加属性名来直接访问实例变量
_firstName = @"张三";

这两种写法有几个区别:
· 由于不经过哦OC的“方法派发”步骤,所以直接访问实例变量的速度当然比较快。这种情况下,编译器所生成的代码会直接访问保存对象实例变量的那块内存。
· 直接访问实例变量时,不会调用其“设置方法”,这就饶过了为相关属性所定义的“内存管理语义”。
· 如果直接访问实例变量,那么就不会触发“键值观测”通知。这样做是否会产生问题,还取决于具体的对象行为。
· 通过属性访问有助于排查与之相关的错误,因为可以给“获取方法”和/或“设置方法”中新增“断点”,监控该属性的调用者及其访问时机。

另外,有一个折中的方法,那就是:在写入实例变量是,通过其“设置方法”来做,而在读取实例变量时,则直接访问之。
然而,选用这种做法是,需要注意几个问题:

  1. 在初始化方法中设置属性值时,子类可能会覆写设置方法。
  2. 在“惰性初始化”的情况下,必须通过“获取方法”来访问属性,否则,变量就永远不会初始化。
    比如某个属性,我们创建它的成本较高,所以我们可能就会在“获取方法”中对其执行惰性初始化:
- (EOCBrain *) brain {
	if (!_brain) {
		_brain = [Brain new];
	}
	return _brain;
}

要点:
· 在对象内部读取数据时 ,应该直接通过实例变量来读,而写入数据时,则应通过属性来写。
· 在初始化方法及dealloc方法中,总是应该直接通过实例变量来读写数据。
· 有时会使用惰性初始化技术配置某份数据,这种情况下,需要通过属性来读取数据。

三、理解“对象等同性”这一概念

等同性 来 比较对象是一个非常有用的功能。不过,按照==操作比较出来的结果未必是我们想要的,因为该操作比较的是两个指针本身,而不是其所指的对象。应该使用NSObject协议中声明的“isEqual”:方法来判断两个对象的等同性。一般来说,两个类型不同的对象总是不相等的。如果知道两个受测对象属于同一个类,那么就可以使用这种方法。以如下代码为例:

NSString *foo = @"Badger 123";
NSString *bar = [NSString stringWithFormat:@"Badger %i", 123];
BOOL equalA = (foo == bar);//NO
BOOL equalB = [foo isEqual:bar];//YES
BoOL equalC = [foo isEqualToString:bar];//YES

从上方可以清晰的看到==与等同性判断方法之间的差别。
有一个特例,NSString 类实现了一个自己独有的等同性判断方法,名叫isEqualToString:。传递给该方法的对象必须是NSString,否则结果未定义。调用该方法比调用isEqual:方法快,因为后者还要执行额外的步骤,因为它不知道受测对象的类型。NSObject类对这两个方法的默认实现是:当且仅当其“指针值”完全相等时,这两个对象才相等。但其实也可以在自定义对象中覆写该方法,比如我们可以将其写为如果两个对象的所有字段都相等,那么这两个对象就相等。于是isEqual:方法可以写成:

- (BOOL) isEqual:(id)object {
	if (self == object) return YES;
	if ([self class] != [object class]) return NO;
	
	EOCPerson *otherPerson = (EOCPerson *) object; 	
	if (![_firstName isEqualToString:otherPerson.firstName]) 
		return NO;
	if (![_lastName isEqualToString:otherPerson.lastName])
		return NO;
	if (_age != otherPerson.age)
		return NO;
	return YES;
}

接下来该实现hash方法了。根据等同性约定:若两个对象相等,则其哈希码也相等。但是两个哈希码相同的对象却未必相等。这也是能否正确覆写isEqual:方法的关键所在。下面这种写法非常高效:

- (NSUInteger) hash {
	NSUInterger firstNameHash = [_firstName hash];
	NSUInterger lastNameHash = [_lastName hash];
	NSUInter ageHash = _age;
	return firstNameHash ^lastNameHash ^ageHash;
}

这种做法既能保持较高效率,又能使生成的哈希码至少位于一定范围之内,而不会过于频繁的重复。
编写hash方法时,应该用当前的对象做做实验,以便在减少碰撞频度与降低运算复杂程度之间取舍。

除了刚才提到的NSString之外,NSArray与NSDictionary类也具有特殊的等同性判定方法,前者为:isEqualToArray:,后者名为:isEqualToDictionary:。如果和其相比较的对象不是数组或字典,那么这两个方法会各自抛出异常。
另外。我们可以自己创建所需要的等同性判定方法,因为无需检测参数类型,所以能大大提升检测速度。
在新类中,若需要判定的两个对象共属同一个类,那我们就调用自己所覆写的方法,若不同属同一个类,那我们就通过使用super关键字调用超类的方法来判断,如:[super isEqual:object];
等同性判定的执行深度:
创建等同性判断方法时,需要决定是根据整个对象来判断等同性,还是仅根据其中几个字段来判断。NSArray的检测方式为先看两个数组所含的对象个数是否相同,若相同,则在每个对应位置的两个对象身上调用其isEqual:方法,如果对应位置上的对象均相等,那么这两个数组就相等,这叫做“深度等同性判定”。
容器中可变类的等同性:
比如,我们向NSMutableSet中添加几个NSMutableArray对象进行测试:

NSMutableSet *set = [NSMutableSet new];

NSMutableArray *arrayA = [@[@1, @2] mutablecopy];
[set addObject:arrayA];
NSLog(@"set = %@", set);//output: set = {((1,2))}

由于set中是不能添加相同元素的,但是如果我们向里添加一个内容元素为1的可变数组作为set元素,然后再该可变数组修改为(1,2),然后再打印set的值,就会得到结果://output: set = {((1,2)(1,2))}
这显然不符合规定,然后我们再创建一个容器去拷贝set:

NSSet *setB = [set copy];
NSLog(@"setB = %@", setB);
//Output:setB = {((1,2))} 

会发现所复制的结果又只有一个对象了,就是由于其结果的难以预料,所以可能会导致程序中这样那样的问题。
要点:
· 若想检测对象的等同性,请提供isEqual:与hash方法。
· 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同。
· 不要盲目地逐个检测每条属性,而是应该依照具体需求来制定检测方案。
· 编写hash方法时,应该使用计算速度快而哈希码碰撞几率低的算法。

以“类族模式”隐藏实现细节:
“类族”是一种很有用的模式,可以隐藏“抽象基类”背后的实现细节。OC的系统框架中普遍使用此模式。比如OC的系统UI界面框架UIKit中就有一个名为UIButton的类。想创建按钮,需要调用下面这个“类方法”:

+ (UIButton *)buttonWithType:(UIButtonType)type;

该方法所返回的对象,其类型取决于传入的按钮类型。然而不管返回的是什么类型的对象,它们都继承自同一个基类:UIButton。这么做的意义在于:UIButton类的使用者无须关心创建出来的按钮具体属于哪个子类。也不用考虑按钮的绘制方式等实现细节。使用者只需明白如何创建按钮,如何设置像“标题”这样的属性,如何增加触摸动作的目标对象等问题就好。
创建类族:
现在举例来演示如何创建类族。假设有一个处理雇员的类,每个雇员都有“名字”和“薪水”这两个属性,管理者可以命令其执行日常工作。但是,各种雇员的工作内容却不同。经理在带领雇员做项目时,无须关心每个人如何完成其工作,仅需指示其开工即可。
首先要定义抽象基类:

typedef NS_ENUM (NSUInterger, EOCEmployeeType) {
	EOCEmploeeTypeDeveloper,
	EOCEmploeeTypeDesigner,
	EOCEmploeeTypeFinance,
};

@interface EOCEmployee: NSObject 
@property (copy) NSString *name;
@property NSUInteger salary;

//Helper for creating Employee objects
+ (EOCEmployee *)employeeWithType:(EOCEmployeeType)type;

//Make Employees do their respective day's work
- (void) doADaysWork;
@end

@implementation EOCEmployee

+ (EOCEmployee *)employeeWithType: (EOCEmployeeType) type {
	switch (type) {
		case EOCEmployeeTypeDeveloper:
			return [EOCEmployeeDeveloper new];
			break;
		case EOCEmployeeTypeDesigner:
			return [EOCEmployeeDesigner new];
			break;
		case EOCEmployeeTypeFinance:
			return [EOCEmployeeFinance new];
			break;
	}
}

- (void) doADaysWork {
	//Subclasses implement this.
}

@end

每个“实体子类”都从基类继承而来。例如:

@interface EOCEmployeeDeveloper: EOCEmployee
@end

@implementation EOCEmployeeDeveloper 

- (void) doADaysWork {
	[self writeCode];
}
 
@end 

在本例中,基类实现了一个“类方法”,该方法能根据待创建的雇员类别分配好对应的雇员类实例。这种“工厂模式”是创建类族的办法之一。
如果对象所属的类位于某个类族中,那么在查询其类型信息时就要当心了。你可能觉得自己创建了某个类的实例,然而实际上创建的却是其子类的实例。在Employee这个例子中,[employee isMemberOfClass:[EOCEmployee class]]似乎会返回YES,但实际上返回的却是NO,因为employee并非Employee类的实例,而是其某个子类的实例。
Cocoa里的类族:
系统框架中有许多类族。大部分collection类都是类族,例如NSArray与其可变版本NSMutableArray。这样看来,实际上有两个抽象基类,一个用于不可变数组,另一个用于可变数组。
在使用NSArray的alloc方法来获取实例时,该方法首先会分配一个属于某类的实例,此实例充当“占位数组”。该数组稍后会转为另一个类的实例,而那个类则是NSArray的实体子类。这个过程稍显复杂。
像NSArray这样的类的背后其实是个类族(对于大部分collection类而言都是这样),明白这一点很重要,否则就可能会写出下面这种代码:

id maybeAnArray = /* ... */;
if ([maybeAnArray class] == [NSArray class]) {
	//Will never be hit
}

这样子的写法其中的if语句永远都不可能为真。[maybeAnArray class]所返回的类绝不可能是NSArray类本身,因为由NSArray的初始化方法所返回的那个实例其类型是隐藏在类族公共接口后面的某个内部类型。
不过,要是想判断出某个实例所属的类是否位于类族之中。我们可以使用下列代码:

id maybeAnArray = /* ... */
if ([maybeAnArray isKindOfClass:[NSArray class]]) {
	//Will be hit
}

对于Cocoa中NSArray这样的类族来说,还是有办法新增子类的,但是需要遵守几条规则,这几条规则如下:
· 子类应该继承自类族中的抽象基类。
· 子类应该定义自己的数据存储方式。
· 子类应当覆写超类文档中指明需要覆写的方法。
· 类族模式可以把实现细节隐藏在一套简单的公共接口后面。
· 系统框架中经常使用类族。
· 从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。

四、在既有类中使用关联对象存放自定义数据

有时需要在对象中存放相关信息。这时我们通常会从对象所属的类中继承一个子类,然后改用这个子类对象。然而有时候类的实例可能是有某种机制所创建的,而开发者无法令这种机制创建出自己所写的子类实例。OC中有一项强大的特性可以解决此类问题,这就是关联对象。
可以给某对象关联许多其他对象,这些对象通过“键”来区分。存储对象值的时候,可以指明“存储策略”,用以维护相应的“内存管理语义”。存储策略有名为objcj_AssociationPolicy的枚举所定义,表2-1列出了该枚举的取值,同时还列出了与之等效的@property属性:假如关联对象成为了属性,那么它就会具备对应的语义。
对象关联类型

关联类型等效的@property属性
OBJC_ASSOCIATION_ASSIGNassign
OBJC_ASSOCIATION_RETAIN_NONATOMICnonatomic, retain
OBJC_ASSOCIATION_COPY_NONATOMICnonatomic, copy
OBJC_ASSOCIATION_RETAINretain
OBJC_ASSOCIATION_COPYcopy

下列方法可以管理关联对象:

 void objc_setAssociatedObject (id object, void *key, id value, objc_AssociationPolicy policy)
//此方法以给定的键和策略为某对象设置关联对象值。
 id objc_getAssociatedObject(id object, void *key)
//此方法根据给定的键从某对象中获取相应的关联对象值。
 void objc_removeAssociatedObjects(id object)
//此方法移除指定对象的全部关联对象。

我们可以把某对象想象成NSDictionary,把关联到该对象的值理解为字典中的条目,于是,存取关联对象的值就相当于在NSDictionary对象上调用[object setObject:valueforKey:key]与[object objectForKey:key]方法,然而两者之间的重要差别就是:设置关联对象时用的键是个“不透明的指针”。由于字典的话,若调用isEqual:方法,如果两个key的比较结果返回的是YES,那么就证明这两个元素对像相等,而关联对象的话,两者必须是完全相同的指针才行。在设置关联对象值时,通常使用静态全局变量做键。
关联对象用法举例:
例如,如果想在同一个类里处理多个警告信息视图,那么代码就会变得更为复杂,我们必须在delegate方法中检查传入的alertView参数,并据此选用相应的逻辑。要是能在创建警告视图的时候直接把处理每个按钮的逻辑都写好,那就简单多了。这可以通过关联对象来做。创建完警告视图之后,设定一个与之关联的“快”,等到执行delegate方法时再将其读出来。此方案的实现代码如下:

#import <objc/runtime.h>

static void *EOCMyAlertViewKey = "EOCMyAlertViewKey";

- (void) askUserAQuestion {
	UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question" message:@"What do you want to do?" delegate: self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil];

	void (^block) (NSInteger) = ^(NSInteger buttonIndex) {
		if (buttonIndex == 0) {
			[self doCancel];
		} else {
			[self doContinue];
		}
	};
	objc_setAssociatedObject(alert, EOCMyAlertViewKey, block, OBJC_ASSOCIATION_COPY);
	[alert show];
}

//UIAlertViewDelegate protocol method
- (void) alertView:(UIAleretView *)alertView clickedButtonAtIndex:(NSInteger) buttonIndex {
	void (^block)(NSInteger) = objc_getAssociatedObject(alertView, EOCMyAlertViewKey);
	block(buttonIndex);
}

这样我们就将创建警告视图与处理操作结果的代码都放在一起了,这样比原来更易读懂,因为我们无须在两部分代码之间来回游走,即可明白警告视图的用处。但这也许会造成“保留环”。
这样的做法很有用,但是只应该在其他办法行不通时才去考虑用它,若滥用,则很快就会令代码失控,使其难于调试。
要点:
· 可以通过“关联对象”机制来把两个对象连起来。
· 定义关联对象时可指定内存管理语义,用以模仿定义属性时所采用的“拥有关系”与“非拥有关系”
· 只有在其他做法不可行时才应该选用关联对象,因为这种做法通常会引入难于查找的bug。

五、理解objc_msgSend的作用

在对象上调用方法是OC中经常使用的功能。用OC的术语来说,这叫“传递消息”。消息有“名称”或“选择子”,可以接受参数,而且可能还有返回值。
C语言静态绑定的方法如下:

#import <stdio.h>

void printHello () {
	printf("Hello, workd!\n");
}

void printGoodbye () {
	printf("Goodbye, world!\n");
}

void doTheThing(int type) {
	if (type == 0) {
		printHello();
	} else {
		printGoodbye();
	}
	return 0;
}

如果不考虑“内联”,那么编译器在编译代码的时候就已经知道程序中有printHello与printGoodbye这两个函数了,于是会直接生成调用这些函数的指令。而函数地址实际上是硬编码在指令之中的。若是将刚才那段代码下成下面这样:

#import <stdio.h>

void printHello() {
	printf("Hello, world!\n");
}

void printGoodbye() {
	printf("Goodbye, world!\n");
}

void doTheThing(int type) {
	void (*fnc) ();
	if (type == 0) {
		fnc = printHello;
	} else {
		fnc = printGoodbye;
	}
	fnc();
	return 0;
}

这时就得使用“动态绑定”了,因为所要调用的函数直到运行期才能确定。在OC中,如果想某对象传递消息,那就会使用动态绑定机制类决定需要调用的方法。
给对象发消息可以这样写:

id returnVlaue = [someObject messageName:parameter];

其中,someObject叫做“接收者”,messageName叫做“选择子”。选择子与参数合起来称为“消息”。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃事消息传递机制中的核心函数,叫做objc_msgSend,其“原型”如下:

void objc_msgSend(id self, SEL cmd, ...);

这是个“参数个数可变的函数”,能接受两个或两个以上的参数。第一个参数代表接收者,第二个参数代表选择子,后续参数就是消息中的那些参数,其顺序不变。选择子就是方法的名字,编译器会把刚才的那个例子中的消息转换为如下函数:

id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);

objc_msgSend函数会依据接收者与选择子的类型来调用适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜寻其“方法列表”,如果能找到与选择子名称相符的方法,就跳至其实现代码。若找不到,就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行“消息转发”操作。
虽然调用一个方法的步骤很多,所幸objc_msgSend会将匹配结果缓存在“快速映射表”里面,每个类都有这样一块缓存,若是稍后还向该类发送与选择子相同的消息,那么执行起来会很快。

处部分消息的调用过程,其他“边界情况”则需要交由OC运行环境中的另一些函数来处理:
· objc_msgSend_stret。如果待发送的消息要返回结构体,那么可交由此函数处理。
· objc_msgSend_fpret。如果消息返回的是浮点数,那么可交由此函数处理。
· objc_msgSendSuper。如果要给超类发消息,例如[super message:parameter],那么就交由此函数处理。
如果某函数的最后一项操作是调用另外一个函数,那么就可以运用“尾调用优化”技术,编译器会生成调转至另一函数所需的指令码,而且不会向调用堆栈中推入新的“栈帧”。只有当某函数的最后一个操作仅仅是调用其他函数而不会将其返回值另作他用时,才能执行“尾调用优化”。
要点总结:
· 消息有接收者、选择子集参数构成。给某对象“发送消息”也就相当于在该对象上“调用方法”。
· 发给某对象的全部消息都要由“动态消息派发系统”来处理,该系统会查出对应的方法,并执行其代码。

六、理解消息转发机制

在对象收到无法解读的消息之后就会发生消息转发机制。
对象在收到无法解读的消息后,首先将调用其所属类的下列类方法:

+ (BOOL)resolveInstanceMethod:(SEL)selector

该方法的参数就是那个未知的选择子,其返回值为BOOlean类型,表示这个类是否能新增一个实例方法用以处理此选择子。再继续往下执行转发机制之前,本类有机会新增一个处理此选择子的方法。假如尚未实现的方法不是实例方法二是类方法,那么运行期系统就会调用另外一个方法。该方法与“resolveInstanceMethod:”类似,叫做“resolveClassMethod:”。
使用这种办法的前提是:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就可以了。此方案常用来实现@dynamic属性。
下面演示如何用“resolveInstanceMethod:”来实现@dynamic属性:

id autoDictionaryGetter (id self, SEL _cmd);
void autoDictoinarySetter(id self, SEL _cmd, id value);

+ (BOOL)resolveInstanceMethod:(SEL)selector {
	NSString *selectorString = NSStringFromSelector(selector);
	if (/* selector is from a @dynamic property */) {
		if ([selectorString hasPrefix:@"set"]) {
			class_addMethod(self, selector, (IMP)autoDictionarySetter, "v@:@");
		} else {
			class_addMethod(self, selector, (IMP)autoDictionaryGetter, "@@:");
		}
		return YES;
	}
	return [super resolveInstanceMethod:selector];
}

首先将选择子化为字符串,然后检测其是否表示设置方法。若前缀为set,则表示设置方法,否则就是获取方法。
备援接收者:
当前接收者还有第二次机会能处理未知的选择子,能不能把这条消息转给其他接收者来处理。与该步骤对应的处理方法如下:

- (id)forwardingTargetForSelector:(SEL)selector

方法参数代表未知的选择子,若当前接收者能找到备援对象,则将其返回,若找不到就返回nil。通过此方案,我们可以用“组合”来模拟出“多重继承的某些特性。”在一个对象内部,可能还有一系列其他对象,该对象可经由此方法将能够处理某选择子的相关内部对象返回,这样的话,在外界看来,好像是该对象亲自处理了这些消息似的。
我们无法操作经由这一步所转发的消息。若是想在发送给备援接收者之前先修改消息内容,那就得通过完整的消息转发机制来做了。
完整的消息转发:
如果转发算法已经来到了这一步的话,那么唯一能做的恶就是启用完整的消息转发机制了。首先创建NSInvocation对象,把与尚未处理的那条消息有关的全部细节都封与其中。此对象包含选择子、目标及参数。在触发NSInvocation对象时,“消息派发系统”将亲自出马,把消息指派给目标对象。
此步骤会调用下列方法来转发消息:

- (void) forwardInvocation: (NSInvocation *)invocation

这个方法可以实现得很简单:只需改变调用目标,是消息在新目标上得以调用即可。
实现此方法时,若发现某调用操作不应由本类处理,则需要用超类的同名方法。这样的话,继承体系中的每个类都有机会处理此调用请求,直至NSObject。如果最后调用了NSObject类的方法,那么该方法还会继而调用“doesNotRecognizeSelector:”以抛出异常,此异常表明选择子最终未能得到处理。
消息转发全流程如下图所示:
在这里插入图片描述
要点总结:
· 若对象无法响应某个选择子,则进入消息转发流程。
· 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中。
· 对象可以吧其无法解读的某些选择子转交给其他对象来处理。
· 经过上述两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制。
用“方法调配技术”调试“黑盒方法”
OC对象收到消息之后,究竟会调用何种方法需要在运行期才能解析出来。在OC中,与给定的选择子名称相对应的方法也可以在运行期改变。若善用此特性,则可发挥出巨大优势,因为我们既不需要源代码,也不需要通过 继承子类来覆写方法就能改变这个类本身的功能。这样一来,新功能将在本类的所有实例中生效,而不是仅限于覆写了相关方法的那些子类实例。此方法经常称为“方法调配”。
类的方法列表会把选择子的名称映射到相关的方法实现之上,使得“动态消息派发系统”能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做“IMP”。其原型如下:

id (*IMP)(id, SEL, ...)

NSString类可以响应lowercaseString、uppercaseString、capitalizedString等选择子。这张映射表中的每个选择子都映射到了不同的IMP之上,如图所示:

在这里插入图片描述
OC运行期系统提供的几个方法都能够用来操作这张表。开发者可以向其中新增选择子,也可以改变某选择子所对应的方法实现,还可以交换两个选择子所映射到的指针。经过几次操作之后,类的方法表就会变成下图的样子:
在这里插入图片描述
在新的映射表中,多了一个名为newSelestor的选择子,capitalizedString的实现也变了,而lowercaseString与uppercaseString的实现则互换了。上述修改均无须编写子类,只要修改了方法表的布局,就会反映到程序中所有的NSString实例之上。
本条将会说到如何互换两个方法实现。通过此操作,可以为已有方法添加新功能。不过在讲解怎样添加新功能之前,我们先来看看怎样互换两个已经写好的方法实现。 想交换方法实现,可用下列函数:

void method_exchangeImplementations(Method m1, Method m2)

此函数的两个参数表示待交换的两个方法实现,而方法实现则可通过下列函数获得:

Method class_getInstanceMethod(Class aClass, SEL aSelector)

此函数根据给定的选择从类中取出与之相关的方法。执行下列代码,即可交换前面提到的两个方法实现:

Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));

Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString));

method_exchangeImplementations(originalMethod, swappedMethod);

现在开始,如果在NSString实例上调用lowercaseString,那么执行的将是uppercaseString的原有实现,反之亦然。
但是这两个方法都是实现好的,交换的意义并不大。但是,我们可以通过这一手段来为既有的方法实现增添新功能。比方说,我们想要对某个已经实现好的方法添加某些操作,我们就可以新编写一个方法,在此方法中添加想要的附加功能,并调用原有实现(在这个新函数中调用自己,此时的自己其实是原有函数),写好新函数后与原函数交换函数实现,即可,到时候外面调用的时候调用原有函数名即可实现附加操作。
要点总结:
· 在运行期,可以向类中新增或替换选择子所对应的方法实现。
· 使用另一份实现来替换原有的方法实现,这道工序叫做“方法调配”,开发者常用此技术向原有实现中添加新功能。
· 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。

七、理解“类对象”的用意

对象类型并非在编译器就绑定好了,而是要在运行期查找。还有个特殊的类型叫做id,它能指代任意的OC对象类型。一般情况下,一个指明消息接收者的具体类型。这样的话,如果向其发送了无法解读的消息,那么编译器就会产生警告信息。而类型为id的对象则不然,编译器假定它能响应所有消息。
描述OC对象所用的数据结构定义在运行期程序库的头文件里,id类型本身也定义在这里:

typedef struct objc_object {
	Class isa;
} *id;

由此可见,每个对象结构体的首个成员是Class类的变量,该变变量定义了对象所属的类,通常称为“is a”指针。Class对象也定义在运行期程序库的头文件中:

typedef struct objc_class *Class;
struct objc_class {
	Class isa;
	Class super_class;
	const char *name;
	long version;
	long info;
	long instance_size;
	struct objc_ivar_list *ivars;
	struct objc_method_list *ivars;
	struct objc_cache *cache;	
	struct objc_protocol_list *protocols;
}

此结构体存放类的“元数据”,例如类的实例实现了几个方法,具备多少个实例变量等信息。此结构体的首个变量也是isa指针,这说明Class本身亦为OC对象。结构体里还有个变量叫做super_class,它定义了本类的超类。类对象所属的类型是另外一个类,叫做“元类”,用来表述类对象本身所具备的元数据。“类方法”就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”。假设又个名为SomeClass的子类从NSObject中继承而来,则其继承体系如下图:

请添加图片描述

super_class指针确立了继承关系,而isa指针描述了实例所属的类。通过这张布局关系图即可执行“类型信息查询”。我们可以查出对象是否能响应某个选择子,是否遵从某项协议,并且能看出此对象位于“类继承体系”的那部分。

其中,每个类的实例的isa指向其类,其类指向其元类。
在类继承体系中查询类型信息:
isMemberOfClass:能够判断出对象是否为某个特定类的实例,而isKindOfClass:则能够判断出对象是否为某类或其派生类的实例,例如:

NSMutableDictionary *dict = [NSMutableDictionary new];
[dict isMemberOfClass:[NSDictionary class]];//NO
[dict isMemberOfClass:[NSMutableDictionary class]];//YES
[dict isKindOfClass:[NSDictionary class]];//YES
[dict isKindOfClass:[NSArray class]];//NO

像这样的类型信息查询方法使用isa指针获取对象所属的类,然后通过super_class指针在继承体系中游走。由于对象是动态的,所以此特性显得极为重要。在OC中,必须查询类型信息,方能完全了解对象的真实类型。
由于OC使用“动态类型系统”,所以用于查询对象所属类的类型信息查询功能非常有用。
另外,由于类对象是“单例”,在应用程序范围内,每个类的Class仅有一个实例。也就是说,另外一种可以精确判断出对象是否为某类实例的办法是:

id object = /* ... */;
if ([object class] == [EOCSomeClass class]) {
	// 'object' is an instance of EOCSomeClass
}

即便能这样做,我们也应该尽量使用类型信息查询方法,而不应该直接比较两个类对象是否相同,因为前者可以正确处理那些使用了消息传递机制的对象。比方说,某个对象可能会把其收到的所有选择子都转发给另一个对象。这样的对象叫做“代理”,此中对象均以NSProxy为根类。

通常情况下,如果此中代理对象上调用class方法,那么返回的是代理对象本身,而非接受的代理的对象所属的类。然而,若是改用isKindOfClass:这样的类型信息查询方法,那么代理对象就会把这条消息转给“接受代理的对象”。也就是说,这条消息的返回值与直接在接收代理的对象上面查询其类型所得的结果相同。因此,这样查出来的类对象与通过class方法所返回的那个类对象不同,class方法所返回的类表示发起代理的对象,而非接收代理的对象。
要点总结:
· 每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系。
· 如果对象类型无法在编译器确定,那么就应该使用类型信息查询方法来探知。
· 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值