前言
在上一篇的探究类的cache文章中,我们在查找调用过程时找到了objc_msgSend
,也就是我们常说的Runtime(运行时)
,我们先来了解下Runtime文档,接下来我们去分析Runtime
。
Runtime简介
Runtime
我们知道是运行时
,提到运行时我们会想到还有个编译时
,他们之前的区别是什么呢?
编译时
:编译就是编译器帮你把源代码翻译成机器能识别的代码
,编译时只是对语言进行最基本的检查报错,包括词法分析、语法分析等等。编译通过并不意味着程序就可以成功运行
。可以说编译时是一个静态的阶段
,类型错误很明显可以直接检查出来。运行时
:代码跑起来,被装载到内存中。,这个时候会具体对类型
进行检查,而不仅仅是对代码简单扫描分析,此时若出错,程序会崩溃。运行时则是动态的阶段
,开始具体与运行环境结合起来。 我们可以用三个方式调用Runtime
:-
OC
的方法,例如:[person sayNB]
-
NSObject
的方法,例如:isKindOfClass
-
RuntimeApi
,例如:class_getInstanceSize
调用Runtime
的三种方式:
方法的本质
方法的本质
-
- 代码准备:
定义一个
WSPerson
类,然后调用方法,最后用clang
编译成C++
代码:
- 代码准备:
定义一个
@interface WSPerson : NSObject
- (void)sayNB;
- (void)sayGood:(NSString *)job;
@end
@implementation WSPerson
- (void)sayNB {
NSLog(@"%s", __func__);
}
// 调用:
WSPerson *person = [WSPerson alloc];
[person sayNB];
[person sayGood: @"coder"];
// C++
WSPerson *person = ((WSPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("WSPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)person, sel_registerName("sayGood:"), (NSString *)&__NSConstantStringImpl__var_folders_7t_93mn_rbs0p1d7bkzy4z_f0s40000gn_T_main_e9af44_mi_1);
@end
通过代码分析,我们知道方法的本质是objc_msgSend(消息发送)
,而objc_msgSend
调用需要消息的接收者
和sel+参数
。那么我们直接用模仿C++
中的objc_msgSend
来调用方法能成功吗?接下来我们来验证
- 2.
objc_msgSend
直接调用方法
注意:
1. 导入头文件#import <objc/message.h>
2.关闭objc_msgSend
的检查机制:target
->Build Setting
->搜索msg
->Enable strict Checking of obc_msgSend Calls
改为NO
。 然后模仿调用:
WSPerson *person = [WSPerson alloc];
[person sayNB];
objc_msgSend(person, sel_registerName("sayNB"));
打印结果如下:
- 得出结论:
[person sayNB]
和objc_msgSend(person, sel_registerName("sayNB"))
是等价的。
调用父类方法
- 我们调用父类方法再试试:
@interface WSPerson : NSObject
- (void)sayNB;
- (void)sayGood;
@end
@implementation WSPerson
- (void)sayNB {
NSLog(@"%s", __func__);
}
- (void)sayGood {
NSLog(@"父类的方法:%s", __func__);
}
@end
@interface WSTeacher : WSPerson
@end
@implementation WSTeacher
- (void)sayGood {
[super sayGood];
}
@end
编译成C++
后,发现多了个objc_msgSendSuper
:
static void _I_WSTeacher_sayGood(WSTeacher * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("WSTeacher"))}, sel_registerName("sayGood"));
}
objc_msgSendSuper
应该是给父类发送消息,我们去objc4-818.2
查看源码:
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
objc_msgSendSuper
有两个参数,第二个是SEL
,第一个是个objc_super
结构体,结构如下:
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver; //接受者
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
我们现在用的是__OBJC2__
,所以objc_super
参数只有两个:receiver
和super_class
,这个super_class
有个注释,是第一次查找的类,难道是查找不到会再查找父类?我们去试试。
- 使用
objc_msgSendSuper
去调用父类方法:
struct objc_super ws_objc_super;
ws_objc_super.receiver = person;
ws_objc_super.super_class = teacher.class;
objc_msgSendSuper(&ws_objc_super, @selector(sayGood));
struct objc_super ws_objc_super1;
ws_objc_super1.receiver = person;
ws_objc_super1.super_class = person.class;
objc_msgSendSuper(&ws_objc_super1, @selector(sayGood));
// 结果
// 2021-07-01 10:51:36.953720+0800 RuntimeDemo[1977:2219575] 父类的方法:-[WSPerson sayGood]
// 2021-07-01 10:51:36.953777+0800 RuntimeDemo[1977:2219575] 父类的方法:-[WSPerson sayGood]
- 说明
super_class
无论是teacher.class
还是person.class
,不影响结果。
总结
- 从调用当前类和父类的方法,我们得出结论:方法的本质是消息发送。那么
objc_msgSend
的具体过程是怎样的,接下来我们进行汇编分析
。
objc_msgSend汇编
- 在
objc
源码中搜索objc_msgSend
,然后找到objc-msg-arm64.s
汇编文件
_objc_msgSend
- 首先
objc_msgSend
有两个参数消息的接收者
和sel
cmp
是compare
比较的意思,p0
是消息的接收者,#0
是0
,这里是拿p0
和0
做对比。SUPPORT_TAGGED_POINTERS
是小对象模式,它的定义是:
#if __arm64__
...
#define SUPPORT_TAGGED_POINTERS 1
...
// true arm64
#else
// arm64_32
#define SUPPORT_TAGGED_POINTERS 0
...
#endif
我们现在是模拟器,所以SUPPORT_TAGGED_POINTERS
为0
,所以判断往下走。b.eq LReturnZero
:这个是p0
和0
相等,也就是消息接受者不存在的意思,这时候返回LReturnZero
:
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend
也就是直接返回nil
。
GetClassFromIsa_p16
- 继续往下走,这里
p13=isa
然后再看这个主要的方法GetClassFromIsa_p16
,调用时传入了p13, 1, x0
:
// src=p13=isa, needs_auth=1, auth_address=x0
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
#if SUPPORT_INDEXED_ISA //arm64
// Indexed isa
mov p16, \src // optimistically set dst = src
tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa
// isa in p16 is indexed
adrp x10, _objc_indexed_classes@PAGE
add x10, x10, _objc_indexed_classes@PAGEOFF
ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS // extract index
ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:
#elif __LP64__
.if \needs_auth == 0 // _cache_getImp takes an authed class already
mov p16, \src //将src赋值给p16
.else
// 64-bit packed isa
ExtractISA p16, \src, \auth_address //得到p16
.endif
#else
// 32-bit raw isa
mov p16, \src
#endif
.endmacro
GetClassFromIsa_p16
是一个宏定义,要传入三个参数,由于SUPPORT_INDEXED_ISA
在arm64
中为1
,则直接跳过看 __LP64__
,这里传入的参数needs_auth=1
,所以看ExtractISA
这个方法:
.macro ExtractISA
and $0, $1, #ISA_MASK
.endmacro
ExtractISA
ExtractISA
也是个宏定义,and
是&运算
,这里是将$1 & ISA_MASK
,再将得到的结果赋值给$0
,而$0
就是p16
,所以这是根据isa
与ISA_MASK
拿到class
的过程,然后回到最开始GetClassFromIsa_p16 p13, 1, x0
,这个步骤就是先拿到class
,所以这一行的注释上p16 = class
。
问题
-
- 为什么要先拿到
class
- 首先,我们分析
objc_msgSend
主要是为了寻找cache
,但cache
是在class
里,所以我们必须先拿到class
后才能取寻找class
中的cache
。
- 为什么要先拿到