Object-C动态性
说到Objcet-C的动态性,我们可以从两点来聊。首先是类的结构的动态性,静态语音类的结构确定是在编译时候,而Objcet-C从编译推迟到了运行时。另一方面就不得不提到消息发送机制了,Objcet-C中大多数函数的调用都是通过objc_msgSend
来调用的。那么本文就消息解读objc_msgSend
过程中最重要的一步--方法查找。
objc_msgSend参数
objc_msgSend
在调用的时候有两个默认参数,第一个参数是消息的接收者,第二个参数是方法名。
这一点可以通过oc代码重写成cpp代码来证明。
int object_c_source_m() {
OSTestObject1 *obj1 = [[OSTestObject1 alloc] init];
[obj1 print];
return 0;
}
重写后:
int object_c_source_m() {
OSTestObject1 *obj1 = ((OSTestObject1 *(*)(id, SEL))(void *)objc_msgSend)((id)((OSTestObject1 *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("OSTestObject1"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)obj1, sel_registerName("print"));
return 0;
}
可以看到print
的调用转化成了objc_msgSend
调用并传入 objc1
和 print
。如果方法本身有参数,会把本身的参数拼接到这两个参数后面。
在cache中快速查找
在objc4中搜索 _objc_msgSend
可以看到各个架构版本的
objc_msgSend
的源码。我们主要以arm64为主,所有直接看objc_msg_arm64
中的源码,可以看到都是汇编代码。这里我们可以思考一个问题,苹果为什么objc_msgSend
这部分代码要使用汇编来编写呢?答案很简单--效率。汇编的效率是比c/c++更快的,因为汇编大多是直接对寄存器的读写,相比较对内存的操作更底层,效率也更高(个人愚见,欢迎评论区互喷)。另外苹果在所有的汇编方法命值钱都会用下划线开头,目的是为了防止符号冲突。
言归正传,我们先看汇编源码。
其实我们不需要看源码,只需要看注释就能知道大概流程。
这部分其实是
objc_msgSend
开始到找类对像cache
方法结束的流程。
首先判断receiver
是否存在,以及是否是taggedPointer
类型的指针,如果不是我们取出对象的isa指针(x13寄存器中),通过isa
指针找到类对象(x16寄存器),然后通过CacheLookup
,在类对象的cache
中查找是否有方法缓存,如果有就调用,如果没有走objc_msg_uncached
分支。
其中CacheLookup
的源码如下
可以大致看看注释,不用深究汇编代码逻辑,大概应该是通过类对象内存平移找到
cache
,然后再获取buckets
,然后再查找方法。
注释解释:如果没有找到返回NULL,查找的时候x0存放方法接收者,x1存放方法名,x16存放isa指针。
我们可以通过汇编断点来证实以上流程。 代码:
- (void)viewDidLoad {
[super viewDidLoad];
[self test]; //断点在这!
}
调试过程以及结果:
马上要进入
objc_msgSend
流程。调用objc_msgSend
方法。x0中存放的是objc_msgSend
的第一个参数,x1中存放的是objc_msgSend
的第二个参数。
对象的首地址其实就是
isa
地址,把isa
指针放到x13寄存器中。x13的值就是isa的指针。
和isa_mask
做与运算,获取类对象的指针。先在x16中,再放到x15中。
如果没有找到,直接走
_objc_msgSend_uncached
流程
方法类表中查找
在全局搜索
loolUpImpOrForward
定位到关键代码
这一段代码可以说是非常厉害了,是我们程序员代码的楷模。第一个红框是在
cache
中查找,第二个红框是在方法列表中查处。这里肯定有一个疑问?之前不是站在cache
中查找过了么?为什么还要再查找一次,首先是因为多线程同步问题,还有就是注意在这个循环里面curClass
要继续从superClass
中去找,这样逻辑统一,也需要在superClass
的cache
中查找。
我们这一小节主要看看如何在方法列表中寻找的。
找到getMethodNoSuper_nolock
的实现
继续到
search_method_list_inline
的实现
这里可以看
fastpath
代表的是大概率走这边,然后再看findMethodInSortedMethodList
的这个方法名,关键字InSorted
!!这里面可以大胆从字面翻译一下--从排过序的方法类表中查找方法
。
这里面有2个方法实现,是因为C/C++可以通过参数来区分方法,这两个方法一个是两个参数,一个是三个参数,两个参数的这个方法最终也是调用三个参数的方法的。所以直接看三个参数的实现就可以。
注意看
for
循环中的count
的变化。count>>1
相当与 count = count / 2
。再结合之前的InSorted
,可以看到这是明显的二分法查找
!
缓存找到的方法
接下来我们回到我们loolUpImpOrForward
的实现中。
找到方法然后去了
done
。
再看
log_and_fill_cache
的实现。
这里
cache的insert
是不是上一遍详细讲过。要注意的是cls
传的的是msgSend receiver 的isa指向的类对象/元类对象
。也就是说,即使方法是从父类的类对象/元类对象中找到的,这个方法缓存也是存再自己的cache中
的。
总结
之前忘了再哪收藏了一个不错的流程图,对这一过程总结的十分到位。