iOS OC 方法的本质
- 前言:
- 1. 方法本质初探
- 2. `objc_msgSend`汇编分析
前言:
前面探究了方法在类中的缓存,那么方法的本质是什么呢?方法调用在底层做了什么呢?今天我们来探索一下:
1. 方法本质初探
看一下一段代码:
先定义一个
LGPerson
类,然后定义
sayNB
对象方法,然后在
main
函数中调用
int main(int argc, const char * argv[]) {@autoreleasepool {LGPerson *person = [LGPerson alloc];[person sayNB];}return 0;}
然后通过
clang
生成
cpp
文件,在底层编译的
cpp
文件中查看
main
函数如下:
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
方法编号。
那么我们定义的函数会调用
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
源码:
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
。
问题:在测试中,不要严格识别参数,需要如下设置:
2.
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 或者 LReturnZero3. 当有消息接收者,正常情况下,拿到 p13 // p13 = isa4. GetClassFromIsa_p16 p13 通过p13(isa),获取 Class ;GetClassFromIsa_p16 先平移,取值 shiftcls,然后得到 Class, 或者 isa & Mask 直接获取 Class
LGetIsaDone
源码:
5. LGetIsaDone 查找isa完毕 开始正常查找 CacheLookup NORMAL5.1 ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask先平移16字节,获得cache,找到缓存方法的 buckets 和 occupied5.2 and w12, w1, w11 // x12 = _cmd & mask通过 _cmd & mask 获取哈希的下标,5.3 循环查找 bucket add p12, p10, p12, LSL5.4 ldp p17, p9, [x12] 通过 sel 找到 bucket 中的cmd 对比,相等直接返回 CacheHit $0, 找不到,直接走 b.ne 2f 即:CheckMisscmp 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 获取 Class4. 缓存查找 CacheLookup5. cache_t 处理,处理哈希,查找 buckrt,找到返回{imp,sel} = *buckrt->imp,找不到 JumpMiss6. 缓存中找不到方法 进入 __objc_msgSend_uncached7. STATIC_ENTRY __objc_msgSend_uncached8. MethodTableLookup 调用__class_lookupMethodAndLoadCache3
最后一个遗留问题,调用
_class_lookupMethodAndLoadCache3
中是怎么慢速查找的呢?下一篇接着探索。
- 点赞1
- 收藏
- 分享
- 文章举报
亮亮不想说话发布了18 篇原创文章 · 获赞 10 · 访问量 266私信关注