第二章 对象,消息,运行期

用OC等面向对象语言编程时,“对象”就是“基本构造单元”,开发者可以通过对象来存储并传递数据。在对象之间传递数据并执行任务的过程就叫做“消息传递”。

第六条:理解“属性”这一概念

属性“是OC的一项特性,用于封装对象中的数据,OC对象通常会把所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”来访问。其中,“获取方法”(getter)用于读取变量值,而“设置方法”(setter)用于写入变量值。

1.编译器是怎样访问实例变量的?

对于OC语言,对象布局在编译器就已经固定了,只要访问到实例变量的代码,编译器就把其替换为“偏移量”,这个偏移量是“硬编码”,表示该变量距离存放对象的内存区域的起始地址有多远。
如果代码使用了编译器计算出来的偏移量,那么在修改类定义之后必须重新编译否者就会出错,OC的做法是,把实例变量当作一种存储偏移量所用的“特殊变量”,交给“类对象”保管。偏移量会在运行期查找,如果类的定义变了,那么存储的偏移量也就变了,这样,无论何时访问实例变量,总能找到正确的偏移量。

2.引入属性解决上述问题

上述问题还有一个解决办法,就是尽量不要直接访问实例变量,而应该通过存取方法去做。
在对象接口的定义中,可以使用属性,能够访问封装在对象里的数据。编译器会自动写出一套存取方法(用@propert语法),用以访问给定类型中具有给定名称的变量。例如下面这个类:

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

对于该类的使用者来说,上述代码写出来的类与下面这种写法等效:

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

要访问属性,可以使用“点语法”。编译器会把“点语法”转换为对存取方法的调用,使用“点语法”的效果与直接调用存取方法相同。 如果使用了属性的话,那么编译器就会自动编写访问这些属性所需的方法,此过程叫做“自动合成”(autosynthesis)。
除了生成方法代码之外,编译器还要自动向类中添加适当类型的实例变量面加下列线,以此作为实例变量的名字。在前面,会生成两个实侧变量,其名称分别为**-firstName与-lastNames**,也可以在类的实现代码里通过synthesire语法来指定实例变量的名字:

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

前述语法会将生成的实例变量命名为_myFirstName与_myLastName,而不再使用默认的名字。一般情况下无须修改默认的实例变量。
还有一种办法能阻止编译器自动合成存取方法,就是使用**@dynamic关键字**,它会告诉编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。而且,在编译访问属性的代码时,即使编译器发现没有定义存取方法,也不会报错,它相信这些方法能在运行期找到。

属性特质

使用属性时还有一个问题要注意,就是其各种特质(attribute)设定也会影响编译器所生成的存取方法。
原子性
在默认情况下,由编译器所合成的方法会通过锁定机制确保其原子性(atomicity)。如果属性具备nonatomic特质,则不使用同步锁。
读/写权限
具备readwrite(读写)特质的属性拥有“获取方法”(getter)与“设置方法”(setter),若该属性由@synthesize实现,则编译器会自动生成这两个方法。
具备readonly(只读)特质的属性仅拥有获取方法,只有当该属性由**@synthesize实现时,编译器才会为其合成获取方法**

内存管理语义

属性用于封装数据,而数据则要有“具体的所有权语义”(concrete ownership semantic)。下面这一组特质仅会影响“设置方法”。编译器在合成存取方法时,要根据此特质来决定所生成的代码。

  1. aassign“设置方法”只会执行针对“纯量类型”(scalar type,例如CGFloat或NSInteger等)的简单赋值操作。
  2. strong此特质表明该属性定义了一种“拥有关系”(owning relationship)。为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后再将新值设置上去。
  3. weak此特质表明该属性定义了一种“非拥有关系”(nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似,然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。
  4. unsafe-unretained此特质的语义和assign相同,但是它适用于“对象类型”(objecttype),该特质表达一种“非拥有关系”(“不保留”unretained),当目标对象遭到摧毁时,属性值不会自动清空(“不安全”,unsafe),这一点与weak有区别。
  5. copy 此特质所表达的所属关系与strong类似。然而设置方法并不保留新值,而是将其“拷贝”(copy)。当属性类型为NSString 时,经常用此特质来保护其封装性,因为传递给设置方法的新值有可能指向一个NSMutableString类的实例。这个类是NSSring的子类,表示一种可以修改其值的字符串,此时若是不拷贝字符串,那么没置完属性之后,字符串的值就可能会在对象不知情的情况下遭人更改。所以,这时就要拷贝一份“不可变”(inmutable)的字符串,确保对象中的字符串值不会无意间变动。只要实现属性所用的对象是“可变的”(mutable),就应该在设置新属性值时拷贝一份。
- (id) initWithFirstName:(NSString*)firstName
lastName:(NSString*)lastName{
   if ((self =[super init]){
    _firstName =[firstName copy];
    _lastName =[lastName copy];
 }
    return self;
}

方法名

可通过如下特质来指定存取方法的方法名:
getter=指定“获取方法”的方法名。如果某属性是Boolean型,而你想为其获取方法加上**“is”前缀**,那么就可以用这个办法来指定。

@property (nonatomic, getter=ison) BOOL on;

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

通过上述特质,可以微调编译器所合成的存取方法。不过需要注意:若是自己来实现这些存取方法,那么应该保证其具备相关属性所声明的特质。

atomic与nonatomic的区别是什么呢?

如果开发过iOS程序,你就会发现,其中所有属性都声明为nonatomic。这样做的历史原因是:在iOS中使用同步锁的开销较大,这会带来性能问题。一般情况下并不要求属性必须是“原子的”,因为这并不能保证“线程安全”(thread safety),若要实现“线程安全”的操作,还需采用更为深层的锁定机制才行。

要点:

  1. 可以通过@property语法来定义对象中所封装的数据。
  2. 通过“特质”来指定存储数据所需的正确语义。
  3. 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
  4. 开发iOS程序时应该使用nonatomic属性,因为atomic属性会严重影响性能。

第7条:在对象内部尽量直接访问实例变量

在对象之外访问实例变量时,总是应该通过属性来做,然而在对象内部又该如何呢?我觉得应该:在读取实例变量的时候采用直接访问的形式,而在设置实例变量的时候通过属性来做

直接访问和属性访问的区别:

  1. 由于不经过Objective-C的“方法派发”(method dispatch,参见第11条)步骤,所以真接访问实例变量的速度当然比较快。
  2. 真接访问实例变量时,不会调用其“设置方法”,这就绕过了为相关属性所定义的“内存管理语义”。比方说,如果在ARC下直接访问一个声明为copy的属性,那么并不会拷贝该属性,只会保留新值并释放旧值。
  3. 如果真接访问实例变量,那么不会触发“键值观测”(Key-Value Observing,KVO)通知。
  4. 通过属性来访问有助于排查与之相关的错误,因为可以给“获取方法”和/或“设置方法”中新增“断点”(breakpoint),监控该属性的调用者及其访问时机。

有一种合理的折中方案,那就是:在写入实例变量时,通过其“设置方法”来做,而在读取实例变量时,则直接访问之。此办法既能提高读取操作的速度,又能控制对属性的写入操作。之所以要通过“设置方法”来写入实例变量,这样做能够确保相关属性的“内存管理语义”得以贯彻。

惰性初始化

- (ECOBrain*)brian {
	if (!_brain) {
		_brain = [[Brain alloc] init];
	}
	return _brain;
}

也叫做“延迟初始化”(懒加载)。在惰性初始化的情况下,必须通过“获取方法”来访问属性,否则,实例变量就永远不会初始化。
一般用于:一个属性不常用,而且创建该属性的成本较高的情况。

要点:

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

第8条:理解“对象等同性”这一概念

根据“等同性”(equality)来比较对象是一个非常有用的功能。不过,按照==操作符比较出来的结果未必是我们想要的,因为该操作比较的是两个指针本身,而不是其所指的对象。应该使用NSObject协议中声明的“isEqual”:方法来判断两个对象的等同性。一般来说,两个类型不同的对象总是不相等的(unequal)。

NSObject协议中有两个用于判断等同性的关键方法

-(BOOL)isEqual:(id)object;

-(NSUInteger)hash;

NSObject类对这两个方法的默认实现是:当且仅当其“指针值”(pointer value)完全相等时,这两个对象才相等。如果“isEqual:”方法判定两个对象相等,那么其hash方法也必须返回同一个值。但是,如果两个对象的hash方法返回同一个值,那么“isEqual:”方法未必会认为两者相等。

interface EOCPerson: NSObject
@property(nonatomic, copy) NSString* firstName;
eproperty (nonatomic, copy) NSString *lastName;
@property(nonatomic, assign) NSUInteger age;
@end

我们认为,如果两个EOCPerson的所有字段均相等,那么这两个对象就相等。于是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 (![_lastNameisEqualToString:otherPerson.lastName])       
      return NO;
   if (_age != otherPerson.age)
      return NO;
   return YES;
}

首先,直接判断两个指针是否相等。若相等,则其均指向同一对象,所以受测的对象也必定相等。接下来,比较两对象所属的类。若不属于同一个类,则两对象不相等。不过,一个EOCPerson实例可以与其子类(比如EOCSmithPerson)实例相等。所以实现“isEqual:”方法时要考虑到这种情况。最后,检测每个属性是否相等。只要其中有不相等的属性,就判定两对象不等,否则两对象相等。
接下来该实现hash方法,根据等同性约定,若两对象相等,则其哈希码(hash)也相等,但是,两个哈希码相同的对象未必相等。这是能否正确覆写“isEqual:”方法的关键所在

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

这样能够保持较高效率,又能使生成的哈希码至少位于一定的范围内,而不会过于频繁的重复。把这种对象添加到collection中时,不会产生性能问题。而且哈希码的返回速度够快。

特定类所具有的等同性判定方法

除了刚才提到的NSString之外,NSArray与NSDictionary类也具有特殊的等同性判定方法,前者名为“isEqualToArray:”,后者名为“isEqualToDictionary:”。如果和其相比较的对象不是数组或字典,那么这两个方法会各自抛出异常。由于Objective-C在编译期不做强类型检查(strong type checking),因此开发者应该保证所传对象的类型是正确的。
在编写判定方法时,也应一并覆写“isEqual:”方法。后者的常见实现方式为:如果受测的参数与接收该消息的对象都属于同一个类,那么就调用自已编写的判定方法,否则就交由超类来判断。
例如,在EOCPerson类中可以实现如下两个方法:

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

等同性判定的执行深度

创建等同性判定方法时,需要决定是根据整个对象来判断等同性,还是仅根据其中几个字段来判断。NSArray的检测方式为先看两个数组所含的对象个数是否相同,若相同,则在每个对应位置的两个对象身上调用其“isEqual:”方法。若对应位置的对象均相等,那么两个数组相等,这叫做“深度等同判定”。不过有时候无须将所有的数据逐个比较,只需根据部分数据即可判明二者是否相同。
比如,假设EOCPerson类的实例是依据数据库里的数据创建而来,那么其中就可能会含有另外一个属性,此属性为“唯一标识符”(unique identifier),在数据库中作“主键”(primary key):

@property NSUInteger identifier;

在这种情况下,我们也许只会根据标识符来判断等同性,尤其是该属性为readonly时。就无须比较每一条数据了。

容器中可变类的等同性

有一种情况要特别注意,就是在容器放入可变对象的时候。把某个对象放入collection后,就不应该再改变其哈希码了。collection会把各个对象按照其哈希码分装到不同的“箱子数组”中。如果“入箱”后哈希码又变了,那么它现在所处的箱子对它来说就是“错误”的。所以,要解决这个问题,需要确保哈希码不是根据对象的“可变部分”(mutable portion)计算出来的,或者是保证放入collection后就不再改变对象内容了
先把一个数组加入set中:

NSMutableSet *set = [[NSMutableSet alloc] init];
NSMutableArray *arrayA = [@[@1, @2] mutableCopy];
[set addObject:arrayA];
NSLog(@"set = %@", set); 
 
//set = {((1,2))}

现在set里含有一个数组对象,数组中包含两个对象。再向set中加入一个数组,此数组与前一个数组所含对象相同,顺序也相同,于是,待加入的数组与set中已有的数组是相等的:

NSMutableArray *arrayB = [@[@1, @2] mutableCopy];
[set addObject:arrayB];
NSLog(@"set = %@", set);

//set = {((1,2))}

加入一个和已有数组对象相等的数组对象相等的数组对象,set不会改变。下面添加一个和set中已有对象不同的数组:

NSMutableArray *arrayC = [@[@1] mutableCopy];
[set addObject:arrayC];
NSLog(@"set = %@", set);

//set = {((1),(1,2))}

加入一个和已有数组对象相等的数组对象不相等的数组对象,set会改变。最后,改变下arrayC的内容,令其和最早加入的数组相等:

[arrayC addObject:@2];
NSLog(@"set = %@", set);

//set = {((1,2),(1,2))}

set中竟然包含了两个彼此相等的数组!根据set的语义是不允许这样的。若是去拷贝set,那就更糟糕了:

NSSet *setB = [set copy];
NSLog(@"setB = %@", setB);

//setB = {((1,2))}

复制过的set中又只剩一个对象了,此set好像是一个空set开始,通过逐个向其中添加新对象而创建出来的。这个例子就是提示大家要注意把可变对象放入collection后响应带来的后果。

要点:

  1. 若想检测对象的等同性,请提供“isEqual:”与hash方法。
  2. 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同。
  3. 不要盲目地逐个检测每条属性,而是应该依照具体需求来制定检测方案。
  4. 编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。

第九条:以“类族模式”隐藏实现细节

“类族”(class cluster)是一种很有用的模式,可以隐藏“抽象基类”(abstract base class)背后的实现细节。OC系统框架普遍使用此模式。使用“类族模式”,该模式可以灵活应对多个类,将它们的实现细节隐藏在抽象基类后面,以保持接口简洁。用户无须创建子类实例,只需要调用基类方法来创建即可。

创建类族

假设有一个处理雇员的类,每个雇员都有“名字”和“薪水”两个属性,管理者可以命令其执行日常工作。但是,各种雇员的工作内容不同,经理无须关心每个人如何完成其工作,仅需指示开工即可。
首先定义抽象基类:

//EOCEmployee.h
typedef NS_ENUM(NSUInteger, EOCEmployeeType) {
    EOCEmployeeTypeDeveloper,
    EOCEmployeeTypeDesigner,
    EOCEmployeeTypeFinance,
};

@interface EOCEmployee : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic) NSUInteger salary;

+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type;

- (void)doADaysWork;

@end
//EOCEmployee.m
#import "EOCEmployee.h"
#import "EOCEmployeeDeveloper.h"
#import "EOCEmployeeDesigner.h"
#import "EOCEmployeeFinance.h"

@implementation EOCEmployee

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

- (void)doADaysWork {
   //子类实现这个方法
}

@end

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

@interface EOCEmployeeDeveloper:EOCEmployee
@end
//EOCEmployeeDeveloper.m
#import "EOCEmployeeDeveloper.h"

@implementation EOCEmployeeDeveloper

- (void)doADaysWork {
    NSLog(@"Develop");
}

@end
EOCEmployee *employee1 = [EOCEmployee employeeWithType:EOCEmployeeTypeDesigner];
[employee1 doADaysWork];
        
EOCEmployee *employee2 = [EOCEmployee employeeWithType:EOCEmployeeTypeDeveloper];
[employee2 doADaysWork];
        
EOCEmployee *employee3 = [EOCEmployee employeeWithType:EOCEmployeeTypeFinance];
[employee3 doADaysWork];

则会输出

Design
Develop
Finance

本例中,基类实现一个“类方法”,**该方法根据待创建的雇员类别分配好对应的雇员类实例。**这种“工厂模式”(Factory pattern)是创建类族的办法之一。

可惜,OC这门语言没有办法指明某个基类是“抽象的”(abstract)。于是,开发者通常会在文档中写明用法。这种情况下,基类接口一般没有名为init的成员方法,暗示该类的实例也许不应该由用户直接创建。

注意,如果对象所属的类位于某个类族中,那么在查询其类型信息时就要当心了。你可能觉得自己创建了某个类的实例,然而实际上创建的是其子类的实例。上面的例子[employee1 isMemberOfClass:[EOCEmployee class]]返回NO,因为employee并非Employee类的实例,而是其子类的实例。

Cocoa里的类族

系统框架中有许多类族。大部分collection类都是类族,例如NSArray与其可变版本NSMutableArray。这样看来,实际上有两个抽象基类,一个用于不可变数组,另一个用于可变数组。尽管具备公共接口的类有两个,但仍然可以和起来算做一个类族。不可变的类定义了对所有数组都通用的方法,而可变的类则定义了那些只适用于可变类型的方法。两个类属于同一个类族,这意味着二者在实现各自类型的数组可以共用实现代码。
像NSArray这样的类的背后是个类族(对于大部分collection类都是),下面给出错误示例:

id maybeAnArray = /*...*/;
if ([maybeAnArray class] == [NSArray class]) {
	//永远无法执行
}

因为NSArray是个类族。if语句永不为真。因为初始化方法所返回的那个歌实例隐藏在类族公共接口(public facade)后面的某个内部类型(internal type)。
要想判断某对象是否位于类族中,不要直接检测两个“类对象”是否相等。我们可以改用信息类型查询方法(introspection method)。例:

id maybeAnArray = /*...*/;
if ([maybeAnArray isKindOfClass:[NSArray class]]) {
	//会被执行
}

我们常要向类族中新增实体子类,需要遵守几条规则:

  1. 子类应该继承自类族中的抽象基类。
  2. 子类应该定义自己的数据存储方式。
  3. 子类应当覆写超类文档指明要覆写的方法。

要点:

  1. 类族模式可以把实现细节隐藏在一套简单的公共接口后面。
  2. 系统框架中经常使用类族。
  3. 从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。

第10条:在既有类中使用关联对象存放自定义数据

有时需要在对象中存放相关信息,这时我们通常会从对象所属的类中继承一个子类,然后改用这个子类对象。然而并非所有情况下都能这么做。OC中有一项强大的特性可以解决此问题,这就是“关联对象”(Associated Object)。
可以给某对象关联许多其他的对象,这些对象通过“键”来区分。存储对象值的时候,可以指明“存储策略”(storage policy),用来维护“内存管理语义”。存储策略由名为objc_AssociationPolicy的枚举所定义,下表给出了枚举的取值以及与之等效的@property属性:假如关联对象成为了属性,那么它就具备对应的语义
在这里插入图片描述

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

  • 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:value forKey: key]与[object objectForKey:key]方法。然而两者之间有个重要差别:设置关联对象时用的键(key)是个“不透明的指针”(opaque pointer)。如果在两个键上调用“isEqual:”方法的返回值为YES,那么NSDictionary就认为二者相等;然而在设置关联对象值时,若想令两个键匹配到同一个值,则二者必须是完全相同的指针才行。鉴于此,在设置关联对象值时,通常使用静态全局变量做键。

要点:

  1. 可以通过“关联对象”机制来把两个对象连起来。
  2. 定义关联对象时可指定内存管理语义,用以模仿定义属性时所采用的“拥有关系”与“非拥有关系”。
  3. 只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难于查找的bug。

第11条:理解objc_msgSend的作用

在对象上调用方法是OC中经常使用的功能。用OC的术语来说,这叫做“传递消息”。消息有“名称”或“选择子”,可以接受参数,而且可能还有返回值。
我们之前用C语言写出来的函数就是“静态绑定”的函数,就是说,他在编译期就能决定运行时所调用的函数。

#import <stdio.h>

void printHello() {
	printf("Hello, world!\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 (*func)();
	if (type == 0) {
		func = printHello();
	} else {
		func = printGoodbye();
	}
	func();
	return 0;
}

但是若是我们使用一个函数指针来实现函数调用的话,不过待调用的函数地址无法硬编码在指令中,而是要运行期读取出来。这时他就成为一个“动态绑定”了,因为所调用的函数直到运行期才能确定

在OC中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有的方法都是普通的C语言函数,然而对象收到消息之后究竟该调用那个方法则完全取决于运行期决定,甚至可以在程序运行时改变
给对象发送消息可以这样来写:

id returnValue = [someObject messageName:parameter];

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

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

这是个“参数可变的函数”(可变参数函数,variadic function),能接受两个或两个以上的参数。第一个参数代表接收者,第二个参数代表选择子(SEL是选择子类型),**后续参数就是消息中的那些参数,其顺序不变。**选择子指的就是方法的名字。“选择子”与“方法”这两个词经常交替使用。编译器会把刚才那个例子中的消息转为如下函数:

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

objc_msgSend函数会依据接受者与选择子的类型来调整适当的方法。**该方法需要在接收者所属的类中搜寻其“方法列表”(list of methods),**如果能找到与选择子名称相符的方法,就跳转实现其代码。若是找不到,那就沿着继承体向上查找,等找到合适的方法时再跳转。如果最终还是找不到相符的方法,那就执行“消息转发”(message forwarding)操作。

前面只描述了部分消息的调用过程,其他“边界情况”则要交由OC运行环境中的另一些函数来处理:

  • objc_msgSend_stret:待发送的消息返回结构体。
  • objc_msgSend_fpret:消息返回浮点数。
  • objc_msgSendSuper:给超类发消息。

前面提到,objc_msgSend等函数一旦找到应该调用的方法实现(可执行的代码)之后,就会“跳转过去”。之所以可以这样,是因为OC对象的每个方法都可以视为简单的C函数,原型如下:

<return_type> Class_selector(id self, SEL _cmd, ...)

真正的函数名和上面可能不太一样。用“类”(class)和“选择子”(selector)演示是为了更好理解。每个类里有一张表格,其中的指针都会指向这种函数,而选择子的名称是查找表时所用的“键”objc_msgSend等函数正是通过这张表格来寻找应该执行的方法并跳至其实现的。注意,原型的样子和objc_msgSend函数很像。这是为了利用“尾调用优化”(尾递归优化,tail-call optimization)技术,

如果某函数的最后一项操作是调用另一个函数,那么就可以运用“尾调用优化”技术编译器会生成调转至另一函数的指令码,而且不会向调用堆栈中推入新的“栈帧”(frame stack)只有当某函数的最后一项操作仅仅是调用其他函数而不会将其返回值另作他用时,才能执行“尾调用优化”。此外,若是不优化,还会过早地发生“栈溢出”(stack overflow)现象。

要点:

  1. 消息由接收者、选择子及参数构成。给某对象“发送消息”也就相当于在该对象上“调用方法”。
  2. 发给某对象的全部消息都要由“动态消息派发系统”来处理,该系统会查出对应的方法,并执行其代码。

第12条:理解消息转发机制

在编译期向类发送了其无法解读的消息并不会报错,因为运行期可以继续向类中添加方法,所以编译期在编译时还无法确知类中到底会不会有某个方法实现。当对象接收到无法解读的消息后,就会启动“消息转发”机制,程序员可经由此过程告诉对象应该如何处理未知消息。像之前我们会经常遇到的:
在这里插入图片描述

这种问题,他就是因为编译器无法识别函数造成的错误,这段错误是由NSObject的“doesNotRecognizeSelector:”方法

消息转发分为两大阶段

第一阶段先征询接收者,所属的类,看其能否动态添加方法,以处理当前这个“未知的选择子”(unknown selector),这叫做“动态方法解析”(dynamic method resolution)。
第二阶段涉及“完整的消息转发机制”(full forwarding mechanism)。

这两个阶段之间又细分为两小步。首先,请接收者看看有没有其他对象能处理这条消息。若有,则运行期系统会把消息转给那个对象,于是消息转发过程结束,一切如常。若没有“备援的接收者”(replacement receiver),则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息。

动态方法解析

对象在收到无法解读的消息后,首先将调用其所属类的下列类方法:

+ (BOOL)resolveInstanceMethod:(SEL)selector

该方法的参数就是那个未知的选择子,其返回值为Boolean类型表示这个类是否能新增一个实例方法用以处理此选择子的方法。在继续往下执行转发机制之前,本类有机会新增一个处理此选择子的方法。如果尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另外一个方法,叫做:“resolveClassMethod:”。

使用这种方法的前提是:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就行了
此方案常用来实现@dynamic属性,比如说,要访问CoreData框架中NSManagedObjects对象的属性时就可以这么做,因为实现这些属性所需的存取方法在编译期就能确定。

id autoDictionaryGetter(id self, SEL _cmd);
void autoDictionarySetter(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,则表示设置方法,否则为获取方法。不管哪种情况,都会把处理该选择子的方法加到类里面。所添加的方法都是用纯C函数实现的。

这里有一个需要特别注意的地方,类方法需要添加到元类里面,OC中所有的类本质上来说都是对象,对象的isa指向本类,类的isa指向元类,元类的isa指向根元类,根元类的isa指向自己,这样的话就形成了一个闭环。要先获取本类的元类,对元类添加需要添加的方法。

备援接收者

当前接收者还有第二次机会能处理未知的选择子,在这一步中,运行期系统会问它:能不能把这条消息转给其他接收者来处理。该步骤对应的处理方法如下:

- (id)forwardingTargetForSelector:(SEL)selector

方法参数代表未知的选择子,若当前接收者能够找到备援对象,则将其返回,若找不到,就返回nil。

完整的消息转发

如果转发算法已经来到这一步的话,那么就只能启用完整的消息转发机制了。首先创建NSInvocation对象,把与尚未处理的那条消息有关的全部细节都封于其中。此对象包含选择子、目标(target)及参数。在触发NSInvocation对象时,“消息派发系统”(message-dispatch system)会亲自出马,把消息指派给目标对象。此步骤将会调用:

- (void)forwardInvocation:(NSInvocation*)invocation

这个方法可以实现得很简单:只需改变调用目标,使消息在新目标上得以调用即可。然而这样实现出来的方式与“备援接收者”方案实现的方法等效,所以很少采用。
比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或是改换选择子等。

实现此方法时,若发现某调用操作不应由本类处理,则需调用超类的同名方法。这样,继承体系中的每个类都有机会处理此调用请求,直至NSObject。如果最后调用了NSObject的方法,那么该方法还会继续调用“doesNotRecognizeSelector:”以抛出异常,此异常表示选择子最终未能得到处理。

消息转发全流程

在这里插入图片描述

以完整的例子演示动态方法解析

假设要编写一个类似于“字典的对象”,它里面可以容纳其它对象,只不过开发者要直接通过属性来存取其中的数据。这个类的设计思路是:由开发者来添加属性定义,并将其声明为@dynamic,而类会自动处理相关属性值的存放与获取的操作。
接口可以写成:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface AutoDictionary : NSObject

@property (nonatomic, strong) NSString *string;
@property (nonatomic, strong) NSNumber *number;
@property (nonatomic, strong) NSDate *date;
@property (nonatomic, strong) id opaqueObject;

@end

NS_ASSUME_NONNULL_END

将属性声明成@dynamic后就不会为其自动生成实例变量以及存取方法了。

#import "AutoDictionary.h"
#import <objc/runtime.h>

@interface AutoDictionary ()

@property (nonatomic, strong) NSMutableDictionary *backingStore;

@end

@implementation AutoDictionary

@dynamic string, number, date, opaqueObject;

- (id)init {
    if (self = [super init]) {
        _backingStore = [[NSMutableDictionary alloc] init];
    }
    return self;
}

//关键在于resolveInstanceMethod:方法的实现
+ (BOOL)resolveInstanceMethod:(SEL)selector {
    NSString *selectorString = NSStringFromSelector(selector);
    if ([selectorString hasPrefix:@"set"]) {
        class_addMethod(self, selector, (IMP)autoDictionarySetter, "v@:@");
    } else {
        class_addMethod(self, selector, (IMP)autoDictionaryGetter, "@@:");
    }
    return YES;
}

@end

if ([selectorString hasPrefix:@“set”])判断前缀是否为set以此分辨是set选择子还是get选择子。不论哪种情况,都要向类中新增一个处理该选择子所用的方法,这两个方法分别以autoDictionarySetter和autoDictionaryGetter函数指针的形式出现。此时就用到了class_addMethod方法,它可以向类中动态地添加方法,用以处理给定的选择子。第三个参数为函数指针,指向待添加的方法。而最后一个参数则表示待添加方法的“类型编码”(type encoding)。
实现:

//Getter:
id autoDictionaryGetter(id self, SEL _cmd) {
    //Get the backing store from the object
    AutoDictionary *typedSelf = (AutoDictionary*)self;
    NSMutableDictionary *backingStore = typedSelf.backingStore;
    //The key is simply the selector name
    NSString *key = NSStringFromSelector(_cmd);
    //Return the value
    return [backingStore objectForKey:key];
}
//Setter:
void autoDictionarySetter(id self, SEL _cmd, id value) {
    //Get the backing store from the object
    AutoDictionary *typeSelf = (AutoDictionary*)self;
    NSMutableDictionary *backingStore = typeSelf.backingStore;
    //The selector will be for example, "setOpaqueObject:".
    //We need to remove the "set", ":" and lowercase the first
    //letter of the remainder
    NSString *selectorString = NSStringFromSelector(_cmd);
    NSMutableString *key = [selectorString mutableCopy];
    
    //Remove the ':' at the end
    [key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];
    //Remove the 'set' prefix
    [key deleteCharactersInRange:NSMakeRange(0, 3)];
    //Lowercase the first character
    NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
    [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
    
    if (value) {
        [backingStore setObject:value forKey:key];
    } else {
        [backingStore removeObjectForKey:key];
    }
}

其用法很简单

AutoDictionary *dict = [[AutoDictionary alloc] init];
dict.date = [NSDate dateWithTimeIntervalSince1970:475372800];
NSLog(@"dict.date = %@", dict.date);
//dict.date = 1985-01-24 00:00:00 +0000

通过动态方法解析使其可以通过点语法设置属性

要点:

  1. 若对象无法响应某个选择子,则进入消息转发流程。
  2. 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中。
  3. 对象可以把其无法解读的某些选择子转交给其他对象来处理。
  4. 经过上述两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制。

第13条:用“方法调配技术”调试“黑盒方法”

OC对象收到消息之后,究竟会调用何种方法需要在运行期才能解析出来。与给定的选择子名称相对应的方法也可以在运行期改变,不需要通过继承子类来覆写方法就能改变这个类本身的功能。这样一来,新功能将在本类的所有实例中生效,而并不仅限于覆写了相关方法的那些子类的实例。此方案经常被称为“方法调配”(method swizzling)。

类的方法列表会把选择子的名称映射到相关的方法实现之上,使得“动态消息派发系统”能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做IMP,其原型如下:

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

NSString类可以响应lowercaseString、uppercaseString、capitalizedString等选择子。这张映射表中的每个选择子都映射到了不同的IMP之上:
在这里插入图片描述

OC运行期系统提供的几个方法都能够用来操作这张表,开发者可以向其中新增选择子,也可以改变某选择子所对应的方法实现,还可以交换两个选择子所映射到的指针
在这里插入图片描述

互换两个已经写好的方法实现:

可以使用下列函数:

void method_exchangeImplementations(Method m1, Method m2)

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

Method class_getInstanceMethod(Class aClass, SEL aSelector)

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

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

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

method_exchangeImplementations(originalMethod, swappedMethod);

像这样直接交换两个已经实现了的方法意义不大。但是,可以通过这一手段来为既有的方法实现增加新功能

为既有的方法实现增加新功能

新方法可以添加到NSString的一个“分类”(category)中:

//  NSString+MyAddition.h
@interface NSString (MyAddition)

- (NSString*)myLowercaseString;

@end
//  NSString+MyAddition.m
@implementation NSString (MyAddition)

- (NSString*)myLowercaseString {
    NSString *lowercase = [self myLowercaseString];
    NSLog(@"%@ => %@", self, lowercase);
    return lowercase;
}

@end

将上述新方法于原有的lowercaseString交换
在这里插入图片描述

在方法实现的代码中,看上去会陷入递归调用的死循环,但已经交换了方法,在运行期,myLowercaseString方法内的myLowercaseString方法执行的是原有的lowercaseString方法。

Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(myLowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
NSString *string = @"ThIsiS tHe StRiNg";
NSString* lowercaseString =[string lowercaseString];
//output:ThIs iS tHe StRiNg => this is the string

通过这个方案,开发者可以为那些“完全不知道其具体实现的”(完全不透明的,completely opaque)黑盒方法增加日志记录功能,这非常有助于程序调试。然而,此做法只在调试程序的时候有用。

要点

  1. 在运行期,可以向类中新增或替换选择子所对应的方法实现。
  2. 使用另一份实现来替换原有的方法实现,这道工序叫做“方法调配”,开发者常用此技术向原有实现中添加新功能。
  3. 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。

第14条:理解“类对象”的用意

对象类型并非在编译期就绑定好了,而是要在运行期查找。而且,还有个特殊的类型叫id,它能指代任意的OC对象类型。一般情况下,应指明消息接收者的具体类型,这样的话,如果向其发送了无法解读的消息,那么编译器就会产生警告信息。而对于id类型的对象,编译器假定它能响应所有消息。

“在运行期检视对象类型”这一操作也叫做“类型信息查询”(introspection,内省),这个强大而又有用的特性内置于Foundation框架的NSObject协议里,凡是由公共根类(common root class,即NSObject与NSProxy)继承而来的对象都要遵从此协议。在程序中不要直接比较对象所属的类,明智的做法是调用“类型信息查询方法”。

看看OC对象的本质是什么。
每个OC对象实例都是指向某块内存数据的指针。所以在声明变量时,类型后面要跟一个“*”字符:

NSString *pointerVariable = @"Some string";

所有OC对象都是如此,如果想把对象所需的内存分配在栈上,编译器则会报错。
对于通用对象类型id,由于其本身已经是指针了,所以我们能这样写:

id genericTypeString = @"Some string";

上面这种定义方法与用NSString *定义的唯一区别是:如果在定义时制定了具体类型,那么在该类实例上调用其所没有的方法时,编译器会发出警告信息。

描述OC对象所用的数据结构的定义在运行期程序库的头文件里,id类型本身也定义在这里:

typedef struct objc_object {
	Class isa;
} *id;

由此可见,每个对象结构体的首个成员是Class类的变量该变量定义了对象所属类,通常称为“isa”指针。例如,刚才的例子中所用的对象“是一个”(is a)NSString,所以其“is a”指针就指向NSString。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 **methodLists;
	struct objc_cache *cache;
	struct objc_protocol_list *protocols;
};

此结构体存放类的“元数据”(metadata),例如类的实例实现了几个方法,具备多少个实例变量等信息。此结构体的首个变量也是isa指针,这说明了Class本身亦为OC对象。结构体里面还有个变量叫做super_class,它定义了本类的超类。类对象所属的类型(也就是isa指针所指向的类型)是另外一个类,叫做“元类”(metaclass)(初步了解元类),用来表述对象本身所具备的元数据“类方法”就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”。

假设有个名为SomeClass的子类从NSObject中继承而来,则其继承体系如图所示
在这里插入图片描述
图中也画出了两个对应“元类”之间的继承关系。

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

在类继承体系中查询类型信息

可以使用类型查询信息方法来检视类继承体系。“isMemberOfClass:”能够判断出对象是否为某个特定类的实例,而“isKindOfClass:”则能够判断出对象是否为某类或其派生类的实例

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

[dict isMemberOfClass:[NSMutableDictionary class]];//NO是为什么呢?因为NSMutableDictionary是一个类簇,dict真正的类应该是NSMutableDictionary的子类。

像这样的类型信息查询方法使用isa指针获取对象所属的类,然后通过super_class指针在继承体系中游走
由于OC使用“动态类型系统”(dynamic typing),所以用于查询对象所属类的类型信息查询功能非常有用。。

此外也可以用比较类对象是否等同的办法来做。若是如此,那就要使用==操作符,而不要用“isEqual:”方法。原因在于类对象是“单例”(singleton),在应用程序的范围内,每个类的Class仅有一个实例。如:

if ([object class] == [SomeClass class])

即使可以这样做,我们也仅应该尽量使用类型信息查询方法,而不应该直接比较两个类对象是否等同,因为前者可以正确处理那些使用了消息传递机制的对象。比方说,某个对象可能会其收到的所有选择子都转发给另一个对象,这样的对象叫做”代理“,注意,对于存在“代理”(proxy)的对象,class方法所返回的类表示发起代理的对象,而非接受代理的对象

  • 21
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值