从 ObjC Runtime 源码分析一个对象创建的过程
引言
最近闲来无事,研究研究 runtime。借助 runtime,ObjC 基本具备了动态语言的主要特性,下面这段代码便是动态创建一个类:
不知道大家发现问题了没有,这段代码在运行时 runtime 其实会触发一个
SIGKILL
的自杀信号来终止程序,我们来看看错误是什么:额,据我们所知
doesNotRecognizeSelector:
是在一个无效 selector
被发出并一直没有被动态解析成功最后的一步,这个方法执行后你的程序也就挂掉了,但是我们这里得到的错误是,这个方法竟然也没有被实现!
这差点让我陷入困扰,知道我想起 Foo
类根本没有父类,所以 Foo
类除了我添加的 sayHello:
方法以外,别无其他任何方法。我们知道在 ObjC 中,任何类都要继承自 NSObject
这一基类,不然编译时就会出错,可以说 NSObject
是 ObjC 的灵魂,诸如 KVO、KVC 等特性都是这个基类实现的。这么说 alloc
类方法也是 NSObject
实现的咯。那必然是,为了一探究竟,我下载了 ObjC Runtime 的全部源码,下面拿来分析一下。
Getting Started
首先,我们看一下工程目录:
P.S. 太长了,我的 15' RMBP 看起来也头疼
这里我们直奔主题,找到 NSObject.mm 这个文件,这是一个 C++ / OC 混写的源文件。这个文件代码特别长,我们通过导航菜单快速定位到 alloc
方法的位置:
按住 ⌘ 一路点击跟踪,直到这里:
这是一个纯 C 静态内联函数,可以看到在 ObjC 2.0 版本之后新增了一种自定义的快捷构造方式,我们不用管它,事实上它们最终都要调用
class_createInstance
这个方法,我们来看看:这里有个分叉口,就是判断编译时是否使用 GC,至于这点其实我们不用过于纠结,iOS 上是不能使用 GC 的,这是苹果不知道什么时候为 Mac 设计的,应该没什么用。而且那个貌似经常出现的迷之
NSZone
类也是用来辅助 GC 来做内存管理的。这都是历史遗留问题了,我们直接走 _alloc
函数那个分支。这个 _alloc
指针指向了一个名为 _class_createInstance
的静态函数,但这个函数最终还是要调用 _class_createInstanceFromZone
函数,只不过这里 zone
参数传了 nil
。
继续跟踪,我们来到了 _class_createInstanceFromZone
函数:
在这里,runtime 主要做了有关内存对齐的一些计算,然后由于
zone
是 nil
,因此这里直接用 calloc
申请了一块内存。calloc
与 malloc
的区别是,calloc
一次可以申请 n * size
字节大小的内存,并且申请后自动置零。紧接着,我们再看看最后一步 objc_constructInstance
函数:这一步其实就做了一件事,那就是初始化对象的
isa
指针。我们在开发时用到的 objc/runtime.h
头文件中也有声明 id
就是 objc_object
这个结构体的指针,但是 objc_object
是一个我们称之为 Opaque Type 的东西,也就是说它对于开发者来说不需要理解其结构,只要拿来当一个“句柄”即可。但是现在我们有源码,所以我们就可以一探究竟!
先看看 objc_object
到底是什么:
这家伙其实就是一个 C++ 结构体,有权限控制,有成员函数。然后我们看看刚才提到的 objc_object::initIsa
函数:
恩,就是把对象的
isa
指针指向这个类的元信息 Class
。
What's Next?
知道全部过程之后我们其实就可以为我们的 Foo
类写一个 alloc
方法了。
且慢!我们似乎最后一步很难办到,objc_object
对于开发者而言并不能接触到,我们有必要通过直接修改内存的方式去修改其 isa
变量。那么,回到源码,我们看看 isa
那个 isa_t
类型究竟是什么:
原来是个联合体,鉴于我们从源码中看到的,它在初始化时直接被当做
uintptr_t
对待了,而这家伙又是 unsigned long
的 typedef,所以我们最终的代码可以顺理成章地写出来:当然,这是最简化的代码了,但它和 runtime 的功能是一致的,我们没有考虑其他情况,仅仅对于 Foo
类是足够的了。
有一点需要注意的是,我们全程都不能使用 ARC,因为 ARC 模式下从 void *
转换到 id
是需要有一个 bridge 的过程的,而这个过程仍然依赖于 NSObject
来完成,所以我们又会陷入一个需要 NSObject
的死循环。
下面我们把上面实现的 alloc
方法添加到我们的类中,然后用平常的 [Foo alloc]
初始化一个实例对象,再执行。
仿佛又遇到困难了:
每个方法执行时 runtime 都会发出图中的警告,并且企图使用
abort()
函数杀死程序。我通过 step in 的方式使 abort()
函数被系统调用绕过,发现其实这整个流程都是可以 work 的。
原因出在哪了呢?可能是 runtime 在执行 objc_msgSend
的时候检查了这两个方法?
没办法,继续看 objc_msgSend
的实现,它的实现是由汇编语言写的,看样子像是宏汇编,反正我汇编很弱,将就看吧。我在一堆汇编代码里一顿通读以后发现问题可能出在 Cache 检查上。
可以发现,runtime 在检查 Cache 的时候也会执行 forwarding,然而我们没有实现相关方法,因此会触发
MESSENGER_END_FAST
子过程,我们的程序也就挂了。
既然这样,我只好把这两个函数简单做一个空实现了:
为了找出究竟哪个 selector 不能识别,我用 NSLog
打印出了这个 selector。运行结果让我恍然大悟:
我忘了实现 initialize
了。。。。。。。。。。。。。。。
这个方法在某个类第一次被初始化时调用,行了,加上 initialize
的空实现,没有父类的 Foo
类圆满了。一脸辛酸 ing。笔者写到这此时已是凌晨两点。。不说了,给大家看下最后的成果:
Wrap Up
本文结束了。最后想总结两句:
- 手拿源码,走遍天下都不怕;码中自有黄金屋。
- 闲的没事别瞎折腾,从十点一直搞到凌晨两点,辛酸......