前言
在上一篇的探究类的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。
- 为什么要先拿到