Objective-C 回顾【二】之 对象、消息、运行期

用 Objective-C 等面向对象语言编程时,“对象”(object)就是“基本构造单元”(building block),开发者可以通过对象来存储并传递数据。在对象之间传递数据并执行任务的过程就叫做“消息传递”(Messaging)。若想编写出高效且易维护的代码,就一定要熟悉这两个特性的工作原理。

当应用程序运行起来以后,为其提供相关支持的代码叫做“Objective-C 运行期环境”(Objective-C runtime),它提供了一些使得对象之间能够传递消息的重要函数,并且包含创建类实例所用的全部逻辑。在理解了运行期环境中各个部分协同工作的原理之后,你的开发水平将会进一步提升。

理解“属性”这一概念


“属性”(property)是 Objective-C 的一项特性,用于封装对象中的数据。Objective-C 对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”(access method)来访问,也就是 gettersetter 方法。

1
2
3
4
5
6
7
8
@interface EOCPerson: NSOjbect {
@public
NSString *_firstName;
NSString *_lastName;
@private
NSString *_someInternalData;
}
@end

上面的代码对于写过 Java 和 C++ 程序的人来说比较熟悉,但是 Objective-C 中却很少这么做。这种写法的问题是:对象布局在编译期已经固定了,只要碰到方位 _firstName 变量的代码,编译器就把其替换为“偏移量”(offset),这个偏移量是“硬编码”(hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。这样做目前看来没什么问题,但是如果又加了一个实例变量,那就麻烦了。

1
2
3
4
5
6
7
8
9
@interface EOCPerson: NSOjbect {
@public
NSDate *_dateOfBirth;
NSString *_firstName;
NSString *_lastName;
@private
NSString *_someInternalData;
}
@end

原来表示 _firstName 的偏移量现在却指向 _dateOfBirth 了。把偏移量硬编码于其中的那些代码都会读取到错误的值。如下图所示:

如果代码使用了编译期计算出来的偏移量,那么在修改类定义之后必须重新编译,否则就会出错。例如,某个代码库中的代码使用了一份旧的类定义。如果和其相链接的代码使用了新的类定义,那么运行时就会出现不兼容的现象。

Objective-C 的解决方式:

  • 稳固的“应用程序二进制接口”(Application Binary Interface,ABI)。
    • 将实例变量当做一种存储偏移量所用的“特殊变量”,交由“类对象”保管。偏移量会在运行时期查找,如果类的定义变了,那么存储的偏移量也就变了,这样的话,无论何时访问实例变量,总能使用正确的偏移量。甚至可以在运行期向类中新增实例变量。
  • 尽量不要直接访问实例变量(在对象之外),而是通过存取方法来做。

属性的优势:

  • 使用“点语法”相当于调用存取方法。
  • 自动生成存取方法(前提是没有用 @dynamic 做限制)。
    • @dynamic 关键字会告诉编译器:不要自动创建属性所用的实例变量,也不要为其创建存取方法。而且,在编译访问属性代码时,即使编译器发现没有定义存取方法,也不会报错,它相信这些方法能在运行期找到。比方说,如果从 CoreData 框架中的 NSManagedObject 类里继承了一个子类,那么就需要在运行期动态创建存取方法。继承 NSManagedObject 时之所以要这样做,是因为子类的某些属性不是实例变量,其数据来自后端的数据库。
  • 自动生成加下划线的实例变量。
  • 我们可以用 @synthesize 来指定实例变量的名字。(不推荐)

属性特质

  • 原子性(atomic/nonatomic)
  • 读/写权限(readwrite/readonly)
  • 内存管理语义
    • assign:针对于“纯量类型”(scalar type,例如:CGFloatNSInteger等)的简单赋值操作。
    • strong:定义了一种“拥有关系”。为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后再将新值设置上去。
    • weak:定义了一种“非拥有关系”。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同 assign 类似,然后在属性所指的对象遭到摧毁时,属性值也会清空。
    • unsafe_unretained:此特质的语义和 assign 相同,但是它适用于“对象类型”,该特质表达一种“非拥有关系”,当目标对象遭到摧毁时,属性值不会自动清空(“不安全”,unsafe),这一点与 weak 有区别。
    • copy:此特质所表达的所属关系与 strong 类似。然而设置方法并不保留新值,而是将其“拷贝”(copy)。当属性类型为 NSString* 时,经常用此特质来保护其封装性。
  • 方法名
    • getter=<name> 指定“获取方法”的方法名。
    • setter=<name> 指定“设置方法”的方法名。这种用法不太常见。

要点:

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

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


在读取实例变量的时候采用直接访问的形式,而在设置实例变量的时候通过属性来做。此办法既能提高读取操作的速度,又能控制对属性的写入操作。之所以要通过“设置方法”来写入实例变量,其首要原因在于,这样做能够确保相关属性的“内存管理语义”得以贯彻。选用这种做法时,需要注意两点:

  • 在初始化方法中应该如何设置属性值。这种情况下总是应该直接访问实例变量,因为子类可能会“覆写”设置方法。
  • “惰性初始化”:这种情况下必须通过“获取方法”来访问属性,否则,实例变量就永远不会初始化。

直接访问实例变量和通过属性访问有几点区别:

  • 由于不经过 Objective-C 的“方法派发”步骤,所以直接访问实例变量的速度当然比较快。在这种情况下,编译器所生成的代码会直接访问保存对象实例变量的那块内存。
  • 直接访问实例变量时,不会调用其“设置方法”,这就绕过了为相关属性所定义的“内存管理语义”。比方说,如果在 ARC 下直接访问一个声明为 copy 的属性,那么并不会拷贝该属性,只会保留新值并释放旧值。
  • 如果直接访问实例变量,那么不会触发“键值观测”(KVO)通知。这样做是否会产生问题,还取决于具体的对象行为。
  • 通过属性来访问有助于排查与之相关的错误,因为可以给“获取方法”和/或“设置方法”中新增“断点”,监控该属性的调用者及其访问时机。

要点:

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

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


要点:

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

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


“关联对象”(Associated Object)可以给某对象关联许多其他对象,这些对象通过“键”来区分。存储对象值的时候,可以指明“存储策略”(storage policy),用以维护相应的“内存管理语义”。

关联类型 等效的@property属性
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic, retain
OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic, copy
OBJC_ASSOCIATION_RETAIN retain
OBJC_ASSOCIATION_COPY copy

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

1
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

此方法以给定的键和策略为某对象设置关联对象值。

1
id objc_getAssociatedObject(id object, const void *key);

此方法根据给定的键从某对象中获取相应的关联对象值。

1
void objc_removeAssociatedObjects(id object);

此方法移除指定对象的全部关联对象。

这种做法很有用,但是只应该在其他办法行不通时才去考虑用它。若是滥用,则很快就会令代码失控,使其难于调试。“保留环”产生的原因很难查明,因为关联对象之间的关系并没有正式的定义,其内存管理语义是在关联的时候才定义的,而不是在接口中预先定好的。使用这种写法时要小心,不能仅仅因为某处可以用该写法就一定要用它。

要点:

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

理解 objc_msgSend 的作用


在对象上调用方法是 Objective-C 中经常使用的功能。用 Objective-C 的术语来说,这叫做消息传递。消息有“名称”(name)或“选择器”(selector),可以接受参数,而且可能还有返回值。

在 Objective-C 中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的 C 语言函数,然而对象收到消息后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变,这些特性使得 Objective-C 成为一门真正的动态语言。

给对象发消息可以这样写:

1
id returnValue = [someObject messageName: parameter];

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

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

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

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

objc_msgSend 函数会依据接收者与选择器的类型来调用适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜寻其方法列表,如果能找到与选择器名称相符的方法,就跳至实现代码。若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行消息转发操作。

这么说来,想调用一个方法似乎需要很多步骤。所幸 objc_msgSend 会将匹配结果缓存在“快速映射表”(fast map)里面,每个类都有这样一块缓存,若是稍后还向该类发送与选择器相同的消息,那么执行起来就很快了。当然啦,这种“快速执行路径”(fast path)还是不如“静态绑定的函数调用操作”(statically bound function call)那样迅速,不过只要把选择器缓存起来了,也就不会慢很多,实际上,消息派发(message dispatch)并非应用程序的瓶颈所在。

前面讲的这部分内容只描述了部分消息的调用过程,其他“边界情况”(edge case)则需要交由 Objective-C 运行环境中的另一些函数来处理

  • objc_msgSend_stret:如果待发送的消息要返回结构体,那么可交由此函数处理。只有当 CPU 的寄存器能够容纳得下消息返回类型时,这个函数才能处理此消息。若是返回值无法容纳于 CPU 寄存器中(比如说返回的结构体太大了),那么就由另一个函数执行派发。此时,那个函数会通过分配在栈上的某个变量来处理消息所返回的结构体。
  • objc_msgSend_fpret:如果消息返回的是浮点数,那么可交由此函数处理。在某些架构的 CPU 中调用函数时,需要对浮点数寄存器做特殊处理,也就是说,通常所用的 objc_msgSend 在这种情况下并不合适。这个函数是为了处理 x86 等架构 CPU 中某些令人稍觉惊讶的奇怪状况。
  • objc_msgSendSuper:如果要给超类发消息,例如 [super message: parameter],那么就交由此函数处理。也有另外两个与 objc_msgSend_stretobjc_msgSend_fpret 等效的函数,用于处理发给 super 的相应消息。

刚才曾提到,objc_msgSend 等函数一旦找到应该调用的方法实现之后,就会跳转过去。之所以能这样做,是因为 Objective-C 对象的每个方法都可以视为简单的 C 函数,其原型如下

1
<retum_type> Class_selector(id self, SEL _cmd, ...)

真正的函数名和上面写的可能不太一样,笔者用类和选择器来命名是想解释其工作原理。每个类里都有一张表格,其中的指针都会指向这种函数,而选择器的名称则是査表时所用的“键”。objc_msgSend 等函数正是通过这张表格来寻找应该执行的方法并跳至其实现的。请注意,原型的样子和 objc_msgSend 函数很像。这不是巧合,而是为了利用尾调用优化(尾递归优化)技术。令跳至方法实现这一操作变得更简单些。

如果某函数的最后一项操作是调用另外一个函数,那么就可以运用尾调用优化技术。编译器会生成调转至另一函数所需的指令码,而且不会向调用堆栈中推人新的“栈帧”(frame stack)。只有当某函数的最后一个操作仅仅是调用其他函数而不会将其返回值另作他用时,才能执行尾调用优化。这项优化对 objc_msgSend 非常关健,如果不这么做的话,那么每次调用 Objective-C 方法之前,都需要为调用 objc_msgSend 函数准备栈帧。,大家在“栈踪迹”(stack trace)中可以看到这种“栈帧”。此外,若是不优化,还会过早地发生“栈溢出”现象。

要点:

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

理解消息转发机制


若想令类能理解某条消息,我们必须用代码实现出对应的方法才行。但是,在编译期向类发送了其无法解读的消息并不会报错,因为在运行期可以继续向类中添加方法,所以编译器在编译时还无法确知类中到底会不会有某个方法实现。当对象接收到无法解读的消息
后,就会启动消息转发机制,程序员可经由此过程告诉对象应该如何处理未知消息。

消息转发分为两大阶段:

  • 第一阶段先征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个未知的选择器,这叫做动态方法解析
  • 第二阶段涉及完整的消息转发机制。如果运行期系统已经把第一阶段执行完了,那么接收者自己就无法再以动态新增方法的手段来响应包含该选择器的消息了。此时,运行期系统会请求接收者以其他手段来处理与消息相关的方法调用。这又细分为两小步。
    • 首先,请接收者看看有没有其他对象能处理这条消息。若有,则运行期系统会把消息转给那个对象,于是消息转发过程结束,一切如常。
    • 若没有备援的接收者,则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封装到 NSInvocation 对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息。

动态方法解析


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

1
+ (BOOL)resolveInstanceMethod:(SEL)sel

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

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

下列代码演示了如何用 resolveInstanceMethod 来实现 @dynamic 属性:

1
2
3
4
5
6
7
8
9
10
11
12
id dynmaicGetMethod(id self, SEL _cmd);
void dynmaicSetMethod(id self, SEL _cmd, id value);

+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString hasPrefix:@"set"]) {
class_addMethod(self, sel, (IMP)dynmaicSetMethod, "v@:@");
} else {
class_addMethod(self, sel, (IMP)dynmaicGetMethod, "@@:");
}
return [super resolveInstanceMethod:sel];
}

备援接收者

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

1
- (id)forwardingTargetForSelector:(SEL)aSelector

方法参数代表未知的选择器,若当前接收者能找到备援对象,则将其返回,若找不到,就返回 nil。通过此方案,我们可以用“组合”(composition)来模拟出多重继承的某些特性。在一个对象内部,可能还有一系列其他对象,该对象可经由此方法将能够处理某选择器的相关内部对象返回,这样的话,在外界看来,好像是该对象亲自处理了这些消息似的。

请注意,我们无法操作经由这一步所转发的消息。若是想在发送给备援接收者之前先修改消息内容,那就得通过完整的消息转发机制来做了。

完整的消息转发

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

此步骤会调用下列方法来转发消息:

1
- (void)forwardInvocation:(NSInvocation *)anInvocation

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

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

一个完整的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
@interface DemoHelper: NSObject

- (void)myPrint:(NSString *)message;

@end

@implementation DemoHelper

- (void)myPrint:(NSString *)message {
NSLog(@"DemoHelper: %@", message);
}

@end

@interface Demo ()

{
id helper;
}

@property (nonatomic, strong) NSMutableDictionary *propertyDictionary;

@end

@implementation Demo

@dynamic name, sex;

- (instancetype)init
{
self = [super init];
if (self) {
helper = [[DemoHelper alloc] init];
_propertyDictionary = [[NSMutableDictionary alloc] init];
}
return self;
}

id dynmaicGetMethod(id self, SEL _cmd) {
NSLog(@"%s", __FUNCTION__);
NSString *key = NSStringFromSelector(_cmd);
Demo *typedSelf = (Demo *)self;
return [typedSelf.propertyDictionary objectForKey:key];
}

void dynmaicSetMethod(id self, SEL _cmd, id value) {
NSLog(@"%s", __FUNCTION__);
NSString *selectorName = NSStringFromSelector(_cmd);
NSMutableString *key = [selectorName mutableCopy];
[key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];
[key deleteCharactersInRange:NSMakeRange(0, 3)];
NSString *firstCharacter = [[key substringToIndex:1] lowercaseString];
[key replaceCharactersInRange:NSMakeRange(0, 1) withString:firstCharacter];
//处理字符串,比如吧setName:处理为name
Demo *typedSelf = (Demo *)self;
if (value) {
typedSelf.propertyDictionary[key] = value;
} else{
[typedSelf.propertyDictionary removeObjectForKey:key];
}
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"%s", __FUNCTION__);
if (sel == @selector(setName:) || sel == @selector(setSex:)) {
//是Set方法
class_addMethod(self, sel, (IMP)dynmaicSetMethod, "v@:@");
} else if (sel == @selector(name) || sel == @selector(sex)) {
//是get方法
class_addMethod(self, sel, (IMP)dynmaicGetMethod, "@:@");
}
return [super resolveInstanceMethod:sel];
}


// 这里注销 helper 返回值是为了消息继续向下传递,调用 forwardInvocation: 方法
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s", __FUNCTION__);
// if (aSelector == @selector(myPrint:)) {
// return helper;
// }
return [super forwardingTargetForSelector:aSelector];
}

// 调用 forwardInvocation: 方法前需要先调用此方法生成方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s", __FUNCTION__);
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
//生成方法签名
signature = [helper methodSignatureForSelector:aSelector];
}
return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"%s", __FUNCTION__);
if (!helper) {
[self doesNotRecognizeSelector: [anInvocation selector]];
}
// 修改参数
NSString *message = @"This is a message";
[anInvocation setArgument:&message atIndex:2];
[anInvocation invokeWithTarget:helper];
}

@end

① 中所说的第四个参数是 types:描述方法参数类型的字符数组。有关可能的值,请参阅Objective-C Runtime Programming Guide > Type Encodings。由于函数至少需要两个参数 self_cmd,所以第二个和第三个字符必须是“@:”(第一个字符是返回类型)。

举个例子:

“v@:”

  • v:返回值为 void
  • @:参数 self
  • “:”:选择器 SEL(_cmd)
  • 无参数

“i@:@”

  • i:返回值类型为 int
  • @:同上
  • “:”:同上
  • @:参数(id)

我们运行代码:

1
2
3
4
5
6
Demo *demo = [[Demo alloc] init];
demo.name = @"Tom";
demo.sex = @"Male";
NSLog(@"name: %@, sex: %@", demo.name, demo.sex);

[demo myPrint:@"demo print"];

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
+[Demo resolveInstanceMethod:]
dynmaicSetMethod
+[Demo resolveInstanceMethod:]
dynmaicSetMethod
+[Demo resolveInstanceMethod:]
dynmaicGetMethod
+[Demo resolveInstanceMethod:]
dynmaicGetMethod
name: Tom, sex: Male
+[Demo resolveInstanceMethod:]
-[Demo forwardingTargetForSelector:]
-[Demo methodSignatureForSelector:]
+[Demo resolveInstanceMethod:]
+[Demo resolveInstanceMethod:]
-[Demo forwardInvocation:]
DemoHelper: This is a message

要点:

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

用方法调配(method swizzling)技术调试“黑盒方法”


与给定的选择器名称相对应的方法也是可以在运行期改变的。我们既不需要源代码,也不需要通过继承子类型来覆写方法就能改变这个类本身的功能。这样一来,新功能将在本类的所有实例中生效,而不是仅限于覆写了相关方法的那些子类实例。此方案经常称为“方法调配”(method swizzling)。

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

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

例子:

Demo 中有一个 lowercase 方法,现在我们在 Demo 的类别 SubDemo 中交换两个方法的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@implementation Demo (SubDemo)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];

SEL originalSelector = @selector(lowercase);
SEL swizzledSelector = @selector(uppercase);

Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);


/**
* 我们在这里使用 class_addMethod() 函数对 originalMethod 做了一层验证,如果 self 没有实现被交换的方法,会导致失败。
* 而且 self 没有交换的方法实现,但是父类有这个方法,这样就会调用父类的方法,结果就不是我们想要的结果了。
* 所以我们在这里通过 class_addMethod() 的验证,如果 self 实现了这个方法,class_addMethod() 函数将会返回 NO,我们就可以对其进行交换了。
*/
Boolean didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_addMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}

- (void)uppercase {
// 这时候调用的其实是 Demo 类的 lowercase 方法
[self uppercase];
NSLog(@"SubDemo uppercase");
}

@end

输出如下:

1
2
Demo lowercase
SubDemo uppercase

要点:

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

理解“类对象”的用意


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

1
2
3
typedef struct objc_object {
Class isa;
} *id;

由此可见,每个对象结构体的首个成员是 Class 类的变量。该变量定义了对象所属的类,通常称为“is a”指针。例如:

1
NSString *str = @"Some string";

这里的对象“是一个”(is a)NSString,所以其“is a”指针就指向 NSStringClass 对象也定义在运行期程序库的头文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 本身亦为 Objective-C 对象。结构体里还有个变量叫做 super_class,它定义了本类的超类。类对象所属的类型(也就是 isa 指针所指向的类型)是另外一个类,叫做“元类”(metaclass),用来表述类对象本身所具备的元数据。“类方法”就定义与此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”。

假设有个名为 SomeClass 的子类从 NSObject 中继承而来,则其继承体系如图所示:

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

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

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

1
2
3
4
5
Demo *demo = [[Demo alloc] init];
NSLog(@"%d", [demo isMemberOfClass:[Demo class]]); // YES
NSLog(@"%d", [demo isMemberOfClass:[SubForDemo class]]); // NO
NSLog(@"%d", [demo isKindOfClass:[Demo class]]); // YES
NSLog(@"%d", [demo isKindOfClass:[NSString class]]); // NO

要点:

  • 每个实例都有一个指向 Class 对象的指针,用以表明其类型,而这些 Class 对象则构成了类的继承体系。
  • 如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法来探知。
  • 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。