从 ObjC Runtime 源码分析一个对象创建的过程

引言

最近闲来无事,研究研究 runtime。借助 runtime,ObjC 基本具备了动态语言的主要特性,下面这段代码便是动态创建一个类:

从 ObjC Runtime 源码分析一个对象创建的过程_第1张图片

不知道大家发现问题了没有,这段代码在运行时 runtime 其实会触发一个 SIGKILL 的自杀信号来终止程序,我们来看看错误是什么:

额,据我们所知 doesNotRecognizeSelector: 是在一个无效 selector 被发出并一直没有被动态解析成功最后的一步,这个方法执行后你的程序也就挂掉了,但是我们这里得到的错误是,这个方法竟然也没有被实现!

这差点让我陷入困扰,知道我想起 Foo 类根本没有父类,所以 Foo 类除了我添加的 sayHello: 方法以外,别无其他任何方法。我们知道在 ObjC 中,任何类都要继承自 NSObject 这一基类,不然编译时就会出错,可以说 NSObject 是 ObjC 的灵魂,诸如 KVOKVC 等特性都是这个基类实现的。这么说 alloc 类方法也是 NSObject 实现的咯。那必然是,为了一探究竟,我下载了 ObjC Runtime 的全部源码,下面拿来分析一下。

Getting Started

首先,我们看一下工程目录:


从 ObjC Runtime 源码分析一个对象创建的过程_第2张图片

P.S. 太长了,我的 15' RMBP 看起来也头疼

这里我们直奔主题,找到 NSObject.mm 这个文件,这是一个 C++ / OC 混写的源文件。这个文件代码特别长,我们通过导航菜单快速定位到 alloc 方法的位置:

从 ObjC Runtime 源码分析一个对象创建的过程_第3张图片

按住 ⌘ 一路点击跟踪,直到这里:

从 ObjC Runtime 源码分析一个对象创建的过程_第4张图片

这是一个纯 C 静态内联函数,可以看到在 ObjC 2.0 版本之后新增了一种自定义的快捷构造方式,我们不用管它,事实上它们最终都要调用 class_createInstance 这个方法,我们来看看:
从 ObjC Runtime 源码分析一个对象创建的过程_第5张图片

这里有个分叉口,就是判断编译时是否使用 GC,至于这点其实我们不用过于纠结,iOS 上是不能使用 GC 的,这是苹果不知道什么时候为 Mac 设计的,应该没什么用。而且那个貌似经常出现的迷之 NSZone 类也是用来辅助 GC 来做内存管理的。这都是历史遗留问题了,我们直接走 _alloc 函数那个分支。这个 _alloc 指针指向了一个名为 _class_createInstance 的静态函数,但这个函数最终还是要调用 _class_createInstanceFromZone 函数,只不过这里 zone 参数传了 nil

继续跟踪,我们来到了 _class_createInstanceFromZone 函数:

从 ObjC Runtime 源码分析一个对象创建的过程_第6张图片

在这里,runtime 主要做了有关内存对齐的一些计算,然后由于 zonenil,因此这里直接用 calloc 申请了一块内存。 callocmalloc 的区别是, calloc 一次可以申请 n * size 字节大小的内存,并且申请后自动置零。紧接着,我们再看看最后一步 objc_constructInstance 函数:
从 ObjC Runtime 源码分析一个对象创建的过程_第7张图片

这一步其实就做了一件事,那就是初始化对象的 isa 指针。我们在开发时用到的 objc/runtime.h 头文件中也有声明 id 就是 objc_object 这个结构体的指针,但是 objc_object 是一个我们称之为 Opaque Type 的东西,也就是说它对于开发者来说不需要理解其结构,只要拿来当一个“句柄”即可。但是现在我们有源码,所以我们就可以一探究竟!

先看看 objc_object 到底是什么:

从 ObjC Runtime 源码分析一个对象创建的过程_第8张图片

这家伙其实就是一个 C++ 结构体,有权限控制,有成员函数。然后我们看看刚才提到的 objc_object::initIsa 函数:


恩,就是把对象的 isa 指针指向这个类的元信息 Class

What's Next?

知道全部过程之后我们其实就可以为我们的 Foo 类写一个 alloc 方法了。

且慢!我们似乎最后一步很难办到,objc_object 对于开发者而言并不能接触到,我们有必要通过直接修改内存的方式去修改其 isa 变量。那么,回到源码,我们看看 isa 那个 isa_t 类型究竟是什么:

从 ObjC Runtime 源码分析一个对象创建的过程_第9张图片

原来是个联合体,鉴于我们从源码中看到的,它在初始化时直接被当做 uintptr_t 对待了,而这家伙又是 unsigned longtypedef,所以我们最终的代码可以顺理成章地写出来:
从 ObjC Runtime 源码分析一个对象创建的过程_第10张图片

当然,这是最简化的代码了,但它和 runtime 的功能是一致的,我们没有考虑其他情况,仅仅对于 Foo 类是足够的了。

有一点需要注意的是,我们全程都不能使用 ARC,因为 ARC 模式下从 void * 转换到 id 是需要有一个 bridge 的过程的,而这个过程仍然依赖于 NSObject 来完成,所以我们又会陷入一个需要 NSObject 的死循环。

下面我们把上面实现的 alloc 方法添加到我们的类中,然后用平常的 [Foo alloc] 初始化一个实例对象,再执行。

仿佛又遇到困难了:

从 ObjC Runtime 源码分析一个对象创建的过程_第11张图片

每个方法执行时 runtime 都会发出图中的警告,并且企图使用 abort() 函数杀死程序。我通过 step in 的方式使 abort() 函数被系统调用绕过,发现其实这整个流程都是可以 work 的。

原因出在哪了呢?可能是 runtime 在执行 objc_msgSend 的时候检查了这两个方法?

没办法,继续看 objc_msgSend 的实现,它的实现是由汇编语言写的,看样子像是宏汇编,反正我汇编很弱,将就看吧。我在一堆汇编代码里一顿通读以后发现问题可能出在 Cache 检查上。

从 ObjC Runtime 源码分析一个对象创建的过程_第12张图片

可以发现,runtime 在检查 Cache 的时候也会执行 forwarding,然而我们没有实现相关方法,因此会触发 MESSENGER_END_FAST 子过程,我们的程序也就挂了。

既然这样,我只好把这两个函数简单做一个空实现了:


从 ObjC Runtime 源码分析一个对象创建的过程_第13张图片

为了找出究竟哪个 selector 不能识别,我用 NSLog 打印出了这个 selector。运行结果让我恍然大悟:

我忘了实现 initialize 了。。。。。。。。。。。。。。。
这个方法在某个类第一次被初始化时调用,行了,加上 initialize 的空实现,没有父类的 Foo 类圆满了。一脸辛酸 ing。笔者写到这此时已是凌晨两点。。不说了,给大家看下最后的成果:

从 ObjC Runtime 源码分析一个对象创建的过程_第14张图片

Wrap Up

本文结束了。最后想总结两句:

  • 手拿源码,走遍天下都不怕;码中自有黄金屋。
  • 闲的没事别瞎折腾,从十点一直搞到凌晨两点,辛酸......

你可能感兴趣的:(从 ObjC Runtime 源码分析一个对象创建的过程)