Objective-C是一门基于对象的动态语言,它里面所有的继承自NSObject
的类本身以及类所实例化的对象都是对象,好像有点儿绕,大意就是这是一门基于对象的语言,就连类自己也是一个对象,程序运行起来以后类本身也会被初始化,也占有内存空间。
在这篇笔记里,我会记录如下的一些信息:
- 消息传递
- runtime如何处理对象
- self 和 super
- Method Swizzling
- 消息转发
消息传递
Objective-C是一门动态的语言,当我们在某个对象上调用方法是,Objective—C
里面叫做消息传递
,和C语言
的函数调用在本质上就不一样,C语言
使用静态绑定
,在编译器就能决定运行时应该调用的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
以上的代码在编译器编译代码的时候就已经知道程序中有 printHello 和 printGoodbye 这两个函数了,于是就直接生成调用这些函数的指令。而函数地址实际上是硬编码在指令之中的。但是,对于Objective—C
来说,如果像下面这样写,情况会完全不一样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
这时就需要使用动态绑定
了,因为实际调用的方法在运行期根据type的值才能确定,在代码一中,if和else里面都有函数调用指令,而在代码二
中,只有一个函数调用指令,而且待调用的函数地址无法硬编码到指令中,而是需要在运行期间读取出来。
在底层,所有的方法都是普通的C语言
函数,当对象接收到消息之后,究竟该调用哪个方法完全是运行期决定的,甚至可以在运行期改变以及交换某些函数的实现,这就是runtime
所体现出来的异于其他编程语言的特点。
当给某个对象传递一个消息:
id returnValue = [someObject messageName:parameter];
在这个消息中,someObject
叫接收者(receiver),messageName
叫做选择子(selector),选择子和参数结合起来叫消息
(message)。当编译器看到此消息后,会将其转换为标准的C语言
函数调用,这是就引出了我们消息传递中最核心的一个函数objc_msgSend
,其原型如下:
id objc_msgSend(id self, SEL op, ...)
它是一个参数可变的函数,可以接受两个及两个以上的参数。有了这个函数,编译器会把刚才的消息转换为如下的函数:
id returnValue = objc_msgSend(someObjetc,
@selector(messageName:),
parameter);
到了运行期间,为了完成此操作,该方法需要到接受者所属的类中搜索其方法列表(后续会有讲到),如果能找到与选择子名称相符的方法,就跳至其实现代码,如果找不到则按照其继承关系向上查找,如果到最终(NSObject)还是没能找到匹配的方法,那就执行消息转发
(后续会讲到)。
除了刚才讲到的objc_msgSend
外,还有其他的几种处理边界情况
的函数:
//处理待发送的消息要返回结构体
objc_msgSend_stret(id self, SEL op, ...)
//处理待发送的消息返回值是浮点数
objc_msgSend_fpret(id self, SEL op, ...)
//处理将消息发送给超类的情况
objc_msgSendSuper(struct objc_super *super, SEL op, ...)
上面讲到了,objc_msgSend
函数会根据选择子去查找应该调用的方法实现,选择子可以理解为一个标记,它其实就是char *
字符串,选择字和方法的实现IMP
之间是一对一的关系,runtime
里面通过结构体objc_method
来存储它们:
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
runtime如何处理对象
Objective-C
是一门基于对象的动态语言,它的动态性是建立在对象上面的,从runtime
源码我们可以看到它是由C语言
编写,这可以保证它执行的高效性,编译器在编译期会将对象转换为它里面定义的结构体:
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
由此可见,每个对象结构体的首个成员变量是Class
类的变量,通过它可以知道对象所属的类,通过isa
指针可以访问到。
比如说我们定义了如下一个类MyClass
:
@interface MyClass : NSObject {
NSString *name;
NSUInteger age;
}
编译器编译MyClass
类以后,它的实例变量布局如下:
1 2 3 4 5 6 7 8 |
|
精简一下,去除struct
的壳:
1 2 3 4 5 |
|
isa
变量的类型是Class
,它的定义在runtime
程序库里可以找到,它的定义如下:
typedef struct objc_class *Class;
所以由此我们知道isa
本身就是一个objc_class
类型的指针,同样我们可以在runtime
程序库里面找到objc_class
的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
这个结构体存放了类的所有元数据
,例如定义了哪些实例变量,定义了哪些方法,超类等。此结构体的首个变量也是isa
指针,说明Class
本身也是一个Objective-C
对象,Class
里面的isa
指向的类就是元类
,用它来定义类对象本身的元数据
。也就是说实例对象的isa
指针指向它唯一的类对象,而类对象的isa
指针指向它唯一的元类。我们知道,类和实例对象都可以发送class
消息,从runtime
的源码中有这样的定义:
1 2 3 4 5 6 7 8 9 10 |
|
由上可以看出实例对象的class就是isa
指向的class对象,而实例对象的类的class就是自己,这也印证了上面的说法。
在Objective-C
中,对象的实例方法存放在它的isa
指向的类对象中,类方法存放于类对象的isa
指向的元类中,某个类在应用程序范围内,类对象以及它所属的元类只有一个,它们都是单例
。
当我们向某个类的实例化对象发送消息时,实例对象通过它的isa
指针找到它所属的类对象,再去类对象的方法列表里面根据选择子
查找,如果没有找到就去它父类的类对象里面查找。如果是向类对象调用类方法,类对象先通过它的isa
指针找到它的元类
,在元类的方法列表里面根据选择子
查找需要调用的方法,如果找到了则跳转到方法实现,若没有找到则向它的父类的元类里面查找。
实例对象的类以及元类的关系如下图(图片来源):
这里需要说明的是:图中类对象Root class,也就是NSObject
,它的父类是nil
,它的元类
是NSObject元类
,而NSObject元类
的元类指向自己,父类指向NSObject
类对象,这样刚好形成一个环,可能是苹果设计的需要,因为NSObject
里面有很多定义好的行为,比如内存管理,空间开辟等,让所有的类对象都收敛于NSObject
,便于统一管理。
好了,说了这么多,下面来做个简单的练习,热热身😃,首先贴出object_getClass
方法的实现:
1 2 3 4 5 6 |
|
也就是说这个方法会返回传入对象的isa
指向。
以下是输出每行对象的内存地址:
1 2 3 4 5 6 7 8 |
|
①⑤的指向相同,②③④指向相同,⑥指向nil。
① 对NSObject
调用class
方法返回NSObject
类对象本身。
② 获取NSObject
类对象的isa
变量,它指向NSObject
类对象的元类。
③ 获取NSObject
元类的isa
变量,它和②的指向一样,说明NSObject元类的isa指向了NSObject类对象自身,和上图中的指向一致。
④ 获取NSObject类对象的元类。
⑤ 获取NSObject实例对象的isa
指向,其实就是NSObject类对象,所以和①的值相同。
⑥ 获取NSObject类对象的父类,打印nil,和上图中标明的一致。
再来一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
如果执行下面的代码会输出什么呢? 😖
1 2 3 |
|
①:对NSObject
发送foo
消息,会到NSObject类对象的元类中查找,理所当然没能找到,然后去到NSObject类对象元类的父类里面查找,它的父类是NSObject
,然后在NSObject类对象
的方法列表里面查找,这时找到了- (void)foo
方法,然后跳到函数入口处执行,输出:
1
|
|
②:首先初始化一个NSObject实例对象,然后发送foo
消息,这是会到NSObject类对象
里面查找,这是找到了- (void)foo
方法,然后跳到函数入口处执行,输出:
1 2 |
|
self和super
- 在实例方法中
self
代表着对象
本身。 - 在类方法中,
self
代表当前类
。
万变不离其宗,记住一句话就行了:self
代表着当前方法的调用者。
上面讲到了,在Objective-C
中对super
发送消息时,编译后会被转换为:
1 2 |
|
接下来我们看看super
指针的类型objc_super
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
通过上面的结构我们可以看出:对super
发送消息,receiver
还是self
,唯一的区别是查找方法时不是从本类开始,而是从super_class
开始。下面我们来看看一个测试:
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 |
|
运行结果都是输出:Son
。由于class
在基类NSObject
里面有实现,所以两个输出都是输出调用者自己所属的类,这里都输出了Son
,而没有输出Father
,所以说super
只是一个标记符,标记消息的查找是从superClass
开始,其他和self
没有任何区别。
Method Swizzling
我们有时候为了调试方便或者想对系统的方法进行自定义的改造,这是我们可以利用Method Swizzling
来进行方法实现的交换。
对方法的交换我们可以在load
或者initialize
里面进行,但是需要根据自己的需要进行选择,它们的区别如下:
方法的交换就是更改选择子(selector)所对应的IMP
,如下:
下面贴上一段Method Swizzling
(方法调配)的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
这里需要说明的是if
条件里面的判断,如果为真则表示currentClass
没有originSelector
的方法,然后进行该方法的添加,如果为假则表示currentClass
里面已经有了originSelector
方法。这里这么做的原因是我们只想对当前类进行方法调配,而不想对父类造成影响。比如当前类的父类里面有一个方法,当前类没有进行覆盖,当我们在当前类里进行方法调配
对这个方法的实现进行重新定义,如果直接进行method_exchangeImplementations
则交换的是父类的方法。结果会与我们预想的不符。
消息转发
前面讲了Objective-C
里面的消息传递
机制,当我们向一个对象传递一个消息,而当程序运行中无法找到该消息对应的实现,这是就会启动消息转发
(message forwarding)机制,这个时候我们可以进行一些操作来防止程序crash。
我们经常遇到程序异常,控制台输出:
1 2 |
|
这就是向对象传递了无法识别的消息报的异常信息。我们在开发中,可以在进入消息转发
后执行预定的逻辑,从而避免奔溃。
消息转发
大致分为三个步奏:
1、动态方法解析
对象在接收到无法识别的消息后,会首先调用所属类的下列方法:
1 2 3 |
|
参数就是无法识别的选择子
,这时我们可以在该方法里面为该选择子添加一个实现,返回YES
,系统会对该方法再进行一次传递,这时就能正常运行了,但它的前提是添加的实现必须是已经定义好的。如果返回NO
,则会就行下一步(下面会说到),这里贴出简单的示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
2、备援接受者
如果步骤一未能处理,运行期系统则会看看能不能把消息转发到其他对象,因为当前类肯定组合
了一些其他的对象,这时调用:
1 2 |
|
若当前对象能找到合适的消息接受者,则将该接受者返回,若找不到则返回nil,消息转发会进行下一步,这里需要注意的是,未知消息到了这里只能将消息的转发给其他对象,无法修改消息的内容。
3、完整的消息转发
如果前两步都没能将未知消息进行有效的处理,则会进入完整的消息转发
,首先需要重写methodSignatureForSelector:
方法来获取未知消息的参数和返回值类型,如果这个方法里返回nil
则消息转发结束,系统发出doesNotRecognizeSelector:
消息,然后就挂掉了,如果返回一个签名函数,runtime
就会创建一个NSInvocation
对象,把未知消息相关的全部细节封装在里面:选择子、target、参数。然后发送-forwardInvocation:
消息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
综合起来如下图所示: