复杂度高的项目系统,我惯于花时间梳理出大手稿,作为参考,根据这个思路,我花时间对于OC底层的机制进行梳理,产出大手稿若干。
始篇,从对类对象使用的数据结构的静态关联分析开始。也就是从源码中找到他们的定义和关联关系。不做运行的分析等。
[TOC]
思路想法:
- 整体数据结构
- alloc init 实例化源码流程
- dealloc 对象消亡源码流程
- 消息调用机制,方法查找流程
- App? Start! 关于 main 方法之前 dyld 的执行流程
- 所有类的初始化过程,主要 ro rw rwe 的关系
- 应用常见操作原理(例如super原理)
- 关联对象 & methodSwi & isaSwi
IDE 上面默认代了一个参数,禁止了Runtime的代码提示,源码和文档方面也删除了一些解释,这叫做 enable strict checking of objc_msgSend Calls
本篇,完成概要整体结构。
不包括 classdatabitst
到 ro
rw
rwe
的演变以及后续过程。
不包括 objc_class
中对于 cache
的处理(在方法查找流程里写)。
整体代码基于目前官方最近开源的OC底层源码版本objc4_781,主要研究 objc-runtime-new.h
文件,runtime 这部分包:
- OC 的声明后的实现部分开源。
- 底层调用的 C 和 C++ 开源。
- 汇编语言
.mm
,Apple 对于一部分CC++无法实现的功能使用,例如msgSend
整个逻辑关系图如下
1. objc_object 「万物之源」
连类的结构都是基于 objc_object
的,所以说 OC 里面一切皆对象呢。
objc_object
1 | /// Represents an instance of a class. |
1.1 id 和 isa
id
的本质其实就是一个 objc_object *
的结构体指针
1 | /// A pointer to an instance of a class. |
在结构体内部只有一个 Class
结构的 isa
isa
是什么?isa 的本质是 isa_t
,在另外一个文件 objc-private.h
中有 isa_t
的定义,是一个联合体,也叫共用体,可以参考这篇文章讲解共用体。
1 | union isa_t { |
这个里面存储两个,Class cls
和 uintptr_t bits
。因为是联合体,所有实际只能有一个出现,ISA_BITFIELD是真正存储值的东西,它存储于文件 isa.h
,根据平台不同而不同,下面是 arm64
版本的:
ISA_BITFIELD
1 |
|
这个 ISA_MASK 这种的就是一个蒙版,isa 完整的值通过跟他进行与操作,就只留下了class的地址,(Class)(isa.bits & ISA_MASK)
话外一提,objc_object
的结构除了上面这个结构,根据这个结构存的值,提供了一整套的API方法,存储于 objc_private.h
文件中。
那么 Class 是什么呢?看2.0
2. class 结构体:objc_class
Class
是 objc_class *
结构体指针。
objc_class
1 | struct objc_class : objc_object { |
这里先后存储的东西分四个:
- 继承来的
isa
结构的指针,占8字节 - superClass 的 Class 指针,占8字节
- cache类型,本篇跳过。
- class_data_bits_t类型的bits,下段落展开。
3. 类中数据 class_data_bits_t
class_data_bits_t
里存储着当前类的很多信息,通过 class_data_bits_t 进行蒙版运算 (bits & FAST_DATA_MASK)
可以算出 class_rw_t
,简称就是 rw
。
下一段落重点讲解 class_rw_t
。
1 | class_data_bits_t ==> class_rw_t ==> class_ro_t |
这个关系是 2020 年新的变动,作为OC Swift的开发者一点要了解,具体看这段 WWDC Session
class_data_bits_t
1 | struct class_data_bits_t { |
上面代码提到了内存屏障,那么什么是内存屏障,这个概念涉及到代码的乱序执行,扩展里面单独说。这个注释的意思是说,imageLoader的时候,还在实现这个类的过程中,不要读取ro数据,因为还没有构建完成。
ro rw rwe 是相互依存的表述类结构的结构,ro 是在内存中直接读取出来的,只读readonly的,其中包括了我们熟悉的ivar,所以说ivar是不可以动态添加的,就是这个原因。
3.1 类数据演变出:class_ro_t
class_ro_t
1 | struct class_ro_t { |
其中包含 const ivar_list_t * ivars
用来表示 ivar_list_t
和 ivar_t
。
3.1.1ivar_list_t
和 ivar_t
ivar_list_t
通过 entsize_list_tt
存储了 ivar_t
。
entsize_list_tt
是提供遍历器的通用容器,详细见我博客的另一篇文章,Apple源码用到的一些数据结构。
ivar_list_t
1 | struct ivar_list_t : entsize_list_tt<ivar_t, ivar_list_t, 0> { |
ivar_t
1 | struct ivar_t { |
3.2 类数据演变出:class_rw_t
rw 是可读可写的意思,runtime 动态增加方法等就是通过对 rw 进行修改写的操作完成的。
这里先看他的结构定义,对全局有了解。这里后续章节「。。。」详细展开更细致的研究。
class_rw_t
1 | struct class_rw_t { |
3.3 类数据演变出:class_rw_ext_t
具体三者关系这里先不赘述。怎么能最短时间讲清楚三者的演变关系,我粘贴一段类构造过程的源码:如下是目前 2020年11月 最新的版本:
1 | // at file `objc-runtime-new.mm` static void methodizeClass(Class cls, Class previously) |
注意:为什么有的地方说 ro 是从 rw 提取出来的,有的说 rw 是 ro 的扩展。
这里的源码是构造过程,只是说明了ro最开始的初始化确实是从 RW 中来的。
但是 ro 其实才是一直不变的,也叫 clean memory,RW 是跟随runtime去操作变化的,叫做 Dirty Memory。
rwe 是什么其实很简单,2020年苹果出了iOS14,其SDK里面把rw中不常用的一部分砍了出去,变成了RWE,官方说,用 Mac 的 MailApp 做测试,Dirty Memory从80MB节约到了40MB。
砍掉的东西是什么,我后面有空研究:「2020年苹果这个DirtyMemory搞的rwt是什么?」
class_rw_ext_t
1 | struct class_rw_ext_t { |
4. method_array_t、 method_list_t、 method_t
接下来三部分是:
- 类的
rw
rwe
中的method_array_t
- 类的
rw
rwe
中的property_array_t
- 类的
rw
rwe
中的protocol_array_t
这三个的继承数据结构都是:list_array_tt
,详情可见我博客的另一篇文章,Apple源码用到的一些数据结构。
通过 rw
rwe
中的 method_array_t 获取 methods,它的类型定义如下:
method_array_t
1 | class method_array_t : |
其中获取到的一个或多个 method_list_t
,具体原理移步到上面文章中。
method_list_t
1 | // entsize 中的2个字符位用来做 fixup 标记。 |
通过遍历器遍历 method_t
method_t 就是表示一个方法或者函数(包含:函数名、返回值、参数、函数体)
method_t
1 | struct method_t { |
SortBySELAddress()
方法是方便函数进行排序,其实在 methodList中 函数是根据SEL name这个对象的内存地址进行排序的,后续寻找方法的时候,会用二分查找增加查找效率。
这里也是今年的一个变动,获取方法的 MethodListIMP 直接从sel里面内存平移8个字节就可以得到。
4.1 method_t 方法的数据结构
SEL是一个指向objc_selector 结构体的指针:typedef struct objc_selector *SEL;
获取一个SEL的方法
1 | SEL sel1 = @selector(selector); |
IMP 是指向方法实现提的函数指针,调用一个方法,本质上就是一个方法寻址的过程,通过SEL寻找IMP。
method_t,就是在两者之间做映射关系
1 | #if !OBJC_OLD_DISPATCH_PROTOTYPES |
4.2 Type Encodings
Type Encodings
就是一个编码,runtime 中对于 method_t 的返回值和参数,使用了Type Encodings 字符串来进行描述:@encode()
指令可以将类型转换为 Type Encodings
字符串编码:
1 | char *buf1 = @encode(int **); |
OC 方法都有两个隐式参数,方法调用者 (id)self
和方法名 (SEL) _cmd
,所以我们才能在方法中使用 self
和 _cmd
;
如 -(void)test
,它的编码为 “v16@0:8”
,可以简写为 “v@:”
v
:代表返回值类型为 void16
:代表所有参数所占的总字节数@
:代表参数 1 类型为 id0
:代表参数 1 从第几个字节开始存储:
:代表参数 2 类型为 SEL8
:代表参数 2 从第几个字节开始存储
具体这里在官网上有详细解释,See Type Encodings in Objective-C Runtime Programming Guide。
5. property_array_t、 property_list_t、 property_t
property_array_t
1 | class property_array_t : |
property_list_t
1 | struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> { |
property_t
1 | struct property_t { |
6. protocol_array_t、 protocol_list_t、 protocol_t
protocol_array_t
1 | class protocol_array_t : |
protocol_list_t
1 | struct protocol_list_t { |
protocol_t
1 | struct protocol_t : objc_object { |
6.isa 走位图
这个图解释网上太多了,没什么好说的,放这里,跳过不讲了。
拓展研究
内存屏障 memory barrier
上面源码中 class_ro_t *safe_ro()
的注释里提到了一个的 compiler barrier
,编译器屏障,这个是为了解决代码执行问题的。
指令重排是指:导致的代码乱序执行。举例来说:如果第一个语句涉及内存读取,第二个语句只是简单的指令计算,那么CPU为了提高效率,完全可能在第一个语句的内存读取完成之前,先执行完了第二个语句。
上下两条指令没有相关性,就可能发生。如果涉及一个值,就有可能发现。
另外一个概念叫做 as if serial
,重排序的原则是看上去像是顺序执行的,不影响单线程的最终一致性。
但是多线程是不保证一致性的。所以重排序是CPU的公共特性。
然后就出现了,内存屏障 memory barrier
屏障上下的指令不会发生重排序,在汇编层面,通过 lock 汇编指令完成,lock是锁总线,CPU访问内存的总线,等指令访问完了,其他CPU才能去访问。
lock 本身的前面的所有指令全部不能越过 lock 重排序。
拓展阅读:Linux内核同步机制之(三):memory barrier
GCD 里面的 barrier 栅栏函数,等待所有位于栅栏函数之前的操作执行完毕后执行
,跟这个命名类似,有相同也有不同,相同就是都是为了保证因为多线程和并发导致的前后指令/任务的乱序。
内存屏障是指令级别的,在CPU汇编指令级别上的。栅栏函数是GCD级别的,GCD又是iOS系统级别的机制。
References
- objc4_781 官方源码
- 简图记录-android fence机制
- Fence和非原子操作的ordering
- C语言共用体(C语言union用法)详解。
- 「注意这个已经过时了,新版本不是这样的」补充git上找到的一个 o_t c_t ro rw 的连通图,缺点是没有rwe,名字我自己瞎起的,我后面画图会参考这个。
- Post link: http://yangzai360.top/2017/06/16/OCBigManuscript01_StructGlimpse/
- Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.