在整理复习 runtime 知识点的过程中,发现不得不巩固 runtime 关于数据结构方面的知识,所以单独开篇关于 NSObject 文章
目录
准备:runtime 源码
1. Class superclass
2. class_data_bits_t bits
(1). class_data_bits_t bits 掩码取值
(2). class_rw_t
(3). class_ro_t
3. cache_t cache
4. realizeClass
正文
在使用 Objective-C 语言中创建的所有类基类,绝大部分都是继承自 NSObject(NSProxy除外,上文已经有过说明,runtime的那些事(一)——runtime基础介绍。因此想要深入学习 iOS 底层知识,NSObject 类拿来开刀再合适不过了(一脸正经:哈哈哈(ಡωಡ)hiahiahia)
首先,进入查看 NSObject 类结构
代码语言:javascript复制@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
过滤掉 clang 命令的忽略警告代码,其作用为忽略不推荐使用接口中的实例变量声明
(关于 clang diagnostic 处理警告用法,可查询clang.llvm.org提供的文档说明,发现 NSObject 类只有只有一个实例变量Class isa
,而Class
定义为typedef struct objc_class *Class;
,作用为指向objc_class
的指针。
runtime 源码准备
如果继续深入关于objc_class
的数据结构,就不能仅仅通过 Xcode 查看,因为在 Xcode 中提供给我们的 runtime API,是已经被废弃的 Legacy 版本,若是想要查看现行使用的 Modern 版本,则可以从 Apple开源项目链接 查看下载最新版本,写此文章时,runtime 最新版本为 objc4-750.1
。但直接下载的 runtime 源码是无法在 Xcode 编译通过。关于可编译runtime源码,直接从该链接下载最新Runtime源码objc4-750编译
回到正题,有了 runtime 的源码,就可以看到现行 Objective-C 2.0 版本关于objc_class 结构体组成
在结构体里,objc_class
继承自objc_object
,意味着 class 本身在 runtime 中被作为对象来处理。而且objc_object
本身也是一个 struct 结构体。objc_class 结构体的完整声明函数占据了300行代码。其中有几个最基础、最关键的属性Class superclass;
、cache_t cache;
、class_data_bits_t bits;
、class_rw_t *data() { return bits.data(); }
、void setData(class_rw_t *newData) { bits.setData(newData); }
结构体声明截图
1. Class superclass
Class superclass;
,此处就是消息执行流程向父类传递最重要的实现属性,代表着作为当前类的父类
2. class_data_bits_t bits
class_data_bits_t bits;
,objc_class
结构体的核心,用于存储类的属性、方法、遵循的协议等各种信息。其本质是一个可被 Mask 标记的指针类型,根据不同 Mask,取出对应不同值。
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
在该结构体声明 bits 的右侧,runtime 注释了 bits 相当于 class_rw_t
结构体加上 rr/alloc 的flag标记
class_data_bits_t 结构体声明
bits 只有一个成员 uintptr_t bits;
,此处 bits 不仅包含了指针,也记录了Class
本身各种异或flag,用于声明 Class
的属性。将上述类的各种信息仅用一个 uint 指针复合到一起表示,可以理解成是一个复合指针
。
当按需取出各类不同那个信息时,通过以FAST_
前缀开头的 flag 掩码对 bits 进行按位与操作。
在写文章过程中不断出现早已变陌生的知识点,自己看着也是头晕,决定一步一步消化掉
(1). 如何通过一个 uint 指针获取类中各种不同信息?
runtime 中已经声明 class_data_bits_t bits
对于 data 数据读取维护,基于 class_rw_t *
的结构体数据进行。执行 class_data_bits_t bits
结构体或者 objc_class
中的 data()
方法,会返回同一个 class_rw_t *
指针。
首先,要了解 class_data_bits_t bits
在内存中不同系统架构存在不同的位排列方式:
32位
0 | 1 | 2-31 |
---|---|---|
FAST_IS_SWIFT | FAST_HAS_DEFAULT_RR | FAST_DATA_MASK |
64位兼容
0 | 1 | 2 | 3-46 | 47-63 |
---|---|---|---|---|
FAST_IS_SWIFT | FAST_HAS_DEFAULT_RR | FAST_REQUIRES_RAW_ISA | FAST_DATA_MASK | 空闲 |
64位不兼容
0 | 1 | 2 | 3-46 | 47 |
---|---|---|---|---|
FAST_IS_SWIFT | FAST_REQUIRES_RAW_ISA | FAST_HAS_CXX_DTOR | FAST_DATA_MASK | FAST_HAS_CXX_CTOR |
48 | 49 | 50 | 51 | 52-63 |
FAST_HAS_DEFAULT_AWZ | FAST_HAS_DEFAULT_RR | FAST_ALLOC | FAST_SHIFTED_SIZE_SHIFT | 空闲 |
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
当通过 data()
方法读取 class_rw_t *
指针数据时,runtime 代码会添加一个 FAST_DATA_MASK
宏定义判断,为啥要加这个宏定义?FAST_DATA_MASK
的宏定义如下
// data pointer
#define FAST_DATA_MASK 0x00007ffffffffff8UL
使用MacOS自带的计算器,将上述十六进制转换成二进制后:
转换结果
可以发现,class_rw_t
指针在 class_data_bits_t
结构体中真正存储的位是 从第3位至46位
,这样也能正好验证了在64位兼容与不兼容的系统架构下,FAST_DATA_MASK
的位范围是 3-46。
关于在 32 位与 64 位不同系统架构下的其它宏定义,有兴趣的话,可以通过计算器一一验证 runtime 中掩码宏定义列表中的位数。
关于其它的掩码宏定义,可去 runtime 源码中 objc-runtime-new.h
类文件的 372 - 525 行代码查看。
(2). class_rw_t
接下来,继续深入,刚才已经得知 class_data_bits_t *bits
结构体中真正存储类信息的是 class_rw_t
,看下其中的数据结构
class_rw_t数据结构
可以看到,类中的属性、方法、遵循的协议都以 二维数组
的形式存储,都是可读写属性,其中包含了类的初始信息(来源于 class_ro_t
类型的常量指针)、以及分类的信息。设置成可写属性,为的是在运行时将该类的多个分类信息(包括属性、方法、协议等)合并至类对应的二维数组中。
还有两个 Class
类的成员变量,分别代表着第一个子类、下一个分类,还有一个使用 const
修饰的 class_ro_t
常量指针(下面会介绍)
(3). class_ro_t
关于内部结构,直接贴代码
class_ro_t
发现该结构体和 class_rw_t
非常相似,但作用却不同。在编译期完成类的原始信息存储,并用 const
修饰代表常量,不可再进行写入修改。
class_ro_t
在编译期具体做了什么事?
- 类的结构体
class_data_bits_t
指向了class_ro_t
指针; - 类的属性、方法、遵循协议数组都是在编译期就已经确定(不包括分类信息),为只读属性,存储于
class_ro_t
; - 类定义的实例化方法会添加至
class_ro_t
的baseMethodList
中
换句话说,class_rw_t
不同于 class_ro_t
,在运行时动态将类的分类信息加入对应数组中,为类提供了很好的扩展能力,这也印证了 Objective-C 动态语言的特性。
3. cache_t cache
发送消息时若每次从方法列表中去查找,性能会发生损耗,并且类存在继承关系时,方法查找链会更长,损耗更严重,而 cache_t cache;
正是为了解决方法查找所引发的性能问题。通过散列表形式缓存调用过的方法函数,大幅提高访问速度。
cache_t结构体
struct bucket_t *_buckets;
,是其核心部分,通过散列表来实现,并以key
与对应IMP
来存储的缓存节点mask_t _mask;
,代表用来分配缓存bucket
总数-1mask_t _occupied;
,代表当前已实际占用的缓存bucket
数量 此处又碰到了一个mask_t
的类型声明,查看后发现是一个通过 typedef 定义的数据类型,uint32_t
代表32位无符号类型的数据,uint64_t
代表64位无符号类型的数据。
mask_t声明
接下来就看下bucket_t
类型的组成
bucket_t声明
cache_key_t _key
代表@selector
的方法名称
IMP _imp
代表函数的存储地址
在public
中,可以发现对key
与对应IMP
的存储过程,此处通过C 代码分别实现了Key
与IMP
的 set 与 get 方法,并通过void bucket_t::set(cache_key_t newKey, IMP newImp)
函数方法完成赋值。
void bucket_t::set(cache_key_t newKey, IMP newImp)方法实现
在该实现方法中,我理解的赋值流程是,
1. 当_key
值为0或者_key
内容(即selector方法名称)与传参newKey
相同时,不再进行下一步操作、
2. newImp
直接赋值给_imp
3. 当_key
与newKey
内容不相等时,会将newKey
赋值给_key
。
在第3步执行前,先去执行了mega_barrier()
宏定义,为什么要先执行该函数再去赋值_key
?
习惯性的点进了mega_barrier()
宏定义声明,然后是一脸懵。。。
mega_barrier()声明
但我不甘心就此止步,于是 Google 了半天,最后在早已关注的欧阳大哥简书深入解构objc_msgSend函数的实现文章找到了答案。
原来此处使用了编译内存屏障(Compiler Memory Barrier)技术,使用的原因是:因为程序在运行时内存实际的访问顺序与程序代码编写访问顺序不保证一致,即内存乱序访问(内存乱序访问的初衷是为了提升程序运行时性能),因此添加 mega_barrier()
确保内存访问顺序与代码编写访问顺序一致。此处若不添加mega_barrier()
函数,则可能会造成先执行了_key
的赋值,再执行_imp
的赋值问题。
cache 查找过程:(以对象方法为例)
(1). 通过isa
查找到指定 class
(2). 从 cache 中查找,若存在缓存,则直接调用
(3). 若缓存中不存在方法,则在自己的 class 里 bits 的 rw 中查找方法
(4). 若找到该方法则调用,并将方法缓存至cache中
(5). 若没有找到,则通过 superclass 找到父类,继续从父类class里 bits 的 rw 中查找方法
(6). 若在父类中找到,则直接调用,并将方法缓存至自己 class 中;若找不到,则一直向上查找
内部 cache 原理因篇幅限制,会再开一篇新文章分析。
4. realizeClass
这里单独把 realizeClass
提溜出来,主要是用于类首次初始化流程,其重要性不言而喻。
相对于在运行时,对于类信息的处理,主要依靠于 realizeClass
函数来实现。这里仅仅是介绍下 realizeClass
函数内部实现,关于类的初始化流程放在后续文章中。
附上结构体源代码
realizeClass函数部分代码
在源代码中有这样一段注释,翻译过来就是:
realizeClass
,核心作用是对类进行首次初始化,其中包括分配读写数据内存空间,返回类的实际类结构。还有最后一句:锁定状态,runtimeLock必须由调用方进行写入锁定
其中的主要作用代码:
代码语言:javascript复制 ro = (const class_ro_t *)cls->data();
if (ro->flags & RO_FUTURE) {
// This was a future class. rw data is already allocated.
rw = cls->data();
ro = cls->data()->ro;
cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
// Normal class. Allocate writeable class data.
rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);
}
- 通过
data()
方法获取到class_rw_t
类型指针,并强制转换成class_ro_t
类型指针赋值给ro
。 - 判断若是普通的类,rw数据已经
allocated
分配了空间,则初始化一个class_rw_t
类型的结构体rw
。 - 对
rw
中ro
属性进行指向第一步中被强制转换的ro
指针操作, 并对flags
属性进行位移操作,此处位移作用:表明当前类已开始实现但未完成或已完成实现。 - 最终将经过修改的
rw
设置为class_data_bits_t *bits
的 data 值,即objc_class
中最终完整的类结构数据。
在上述流程执行前,realizeClass 执行了 runtimeLock.assertWriting();
代码,我个人理解的代码作用,是对数据的写入进行了线程保护,并且由调用方(即函数的入参Class对象)进行写入锁定操作,保障数据写入安全。
runtime 类的运行逻辑:在编译时,类的方法、属性、协议等信息都存在于常量 class_ro_t
中,且无法再进行更改,这时class_data_bits_t
中通过 data()
方法获取数据指向的是 class_ro_t
。到了运行时,类就能够动态创建 class_rw_t
指针并将 class_ro_t
中的信息存储,同时会将类的分类信息(包括:分类中的方法、属性、协议等)一并存储。通过二维数组进行排序,将分类信息放入数组前端,class_ro_t
中已有类信息放入数组后端。此时,class_data_bits_t
通过 data()
方法指针由 class_ro_t
变成了指向 class_rw_t
。以上的操作,是通过 realizeClass
函数来实现的。
上面所写的,是对 NSObject 类的结构分析,文章初衷是计划把 IMP 、NSInvocation、以及 NSObject 类初始化流程等 runtime 知识点都囊括,作为一个总结。但 runtime 的内容真的不是一两篇就可以写完的,写作过程中发现仅仅是 NSObject 的数据结构介绍就占用了这么多篇幅。下一篇准备写下 NSObject 类在初始化流程。
该文章首次发表在 简书:我只不过是出来写写代码 博客,并自动同步至 腾讯云:我只不过是出来写写iOS 博客