前面探究了方法在类中的缓存,那么方法的本质是什么呢?方法调用在底层作了什么呢?今天咱们来探索一下:api
看一下一段代码: 先定义一个LGPerson
类,而后定义sayNB
对象方法,而后在main
函数中调用缓存
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
[person sayNB];
}
return 0;
}
复制代码
而后经过clang
生成cpp
文件,在底层编译的cpp
文件中查看main
函数以下:sass
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));imp - 函数
}
return 0;
}
复制代码
由此:咱们能够简单得出,方法
的本质是经过objc_msgSend
发送消息,第一个参数为id
消息接受者,第二个参数为sel
方法编号。bash
那么咱们定义的函数会调用objc_msgSend
发送消息吗? 咱们定义下面函数,并在main
中调用,多线程
void run(){
NSLog(@"%s",__func__);
}
复制代码
经过clang
查看cpp
文件,发现函数不须要调用objc_msgSend
,函数能够直接经过函数名
(指针),找到函数的实现,不须要像方法
经过sel
,找到ipm
,再找到方法的实现。函数
向父类
发送消息(对象方法):性能
struct objc_super lgSuper;
lgSuper.receiver = s;
lgSuper.super_class = [LGPerson class];
objc_msgSendSuper(&lgSuper, @selector(sayHello));
复制代码
向父类
发送消息(类方法):测试
struct objc_super myClassSuper;
myClassSuper.receiver = [s class];
myClassSuper.super_class = class_getSuperclass(object_getClass([s class]));// 元类
objc_msgSendSuper(&myClassSuper, sel_registerName("sayNB"));
复制代码
objc_super
源码:ui
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 */
};
#endif
复制代码
所以,在调用Runtim api
向父类
发送消息时,须要设置receiver
和super_class
。spa
问题:在测试中,不要严格识别参数,须要以下设置:
objc_msgSend
汇编分析在objc
源码中断点
Debug -> Debug Workflow ->always Show Disassembly
进行汇编分析:
在objc_msgSend
处断点,
经过control + in
,查看libobjc.A.dylib objc_msgSend
,发现objc_msgSend
底层是用汇编
实现的。
补充:
为何`objc_msgSend`用汇编实现呢?
1. 在性能方面,`汇编`更容易被机器识别
2. 在发送消息时,有不少未知的参数,c 语言中不能经过写一个函数来保留未知的参数而且跳转到一个任意的
函数指针,c语言没有知足作这件事的必要特性。
汇编寄存器
arm64下有31位通用寄存器,x0 - x7,是参数,返回值会放到 x0中
复制代码
objc_msgSend
的汇编分析:
首先,在objc
源码中全局搜索objc_msgSend
找到汇编源码,
具体分析:
1. cmp p0, #0 // nil check and tagged pointer check
先对比当前0号寄存器是否为空,为空,当前没有接收者
2. 判断 SUPPORT_TAGGED_POINTERS
直接执行 LNilOrTagged 或者 LReturnZero
3. 当有消息接收者,正常状况下,拿到 p13 // p13 = isa
4. GetClassFromIsa_p16 p13 经过p13(isa),获取 Class ;
GetClassFromIsa_p16 先平移,取值 shiftcls,而后获得 Class, 或者 isa & Mask 直接获取 Class
复制代码
LGetIsaDone
源码:
5. LGetIsaDone 查找isa完毕 开始正常查找 CacheLookup NORMAL
5.1 ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
先平移16字节,得到cache,找到缓存方法的 buckets 和 occupied
5.2 and w12, w1, w11 // x12 = _cmd & mask
经过 _cmd & mask 获取哈希的下标,
5.3 循环查找 bucket add p12, p10, p12, LSL
5.4 ldp p17, p9, [x12] 经过 sel 找到 bucket 中的cmd 对比,相等直接返回 CacheHit $0, 找不到,直接走 b.ne 2f 即:CheckMiss
cmp p9, p1 // if (bucket->sel != _cmd)
6. CheckMiss 中找到后,b.eq 3f,进入步骤三,平移哈希,将方法缓存到 bucket中一份,
若是没有找到则 {imp, sel} = *--bucket,循环递归查找。
而后会在查找一遍 5.4 流程(
防止多线程,缓存更新),找不到缓存,则 JumpMiss $0
复制代码
CheckMiss
代码:
NORMAL
形式时,进入
__objc_msgSend_uncached
,以下:
MethodTableLookup
源码:
_class_lookupMethodAndLoadCache3
方法:
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
复制代码
问题:
为何从汇编调用 C 方法?
_class_lookupMethodAndLoadCache3 是一系列慢速方法查找,没有必要使用汇编
复制代码
总结:
1. ENTRY _objc_msgSend 进入
2. TAGGED_POINTERS 判断
3. GetClassFromIsa_p16 p13 经过 isa 获取 Class
4. 缓存查找 CacheLookup
5. cache_t 处理,处理哈希,查找 buckrt,找到返回{imp,sel} = *buckrt->imp,找不到 JumpMiss
6. 缓存中找不到方法 进入 __objc_msgSend_uncached
7. STATIC_ENTRY __objc_msgSend_uncached
8. MethodTableLookup 调用__class_lookupMethodAndLoadCache3
复制代码
最后一个遗留问题,调用_class_lookupMethodAndLoadCache3
中是怎么慢速查找的呢?下一篇接着探索。