runtime的那些事(二)——NSObject数据结构

在整理复习 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 类结构

@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_tbaseMethodList

 换句话说,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 总数-1
  • mask_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++代码分别实现了KeyIMP的 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. 当_keynewKey内容不相等时,会将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必须由调用方进行写入锁定 其中的主要作用代码:

    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
  • rwro 属性进行指向第一步中被强制转换的 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 博客

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券