iOS底层——runtime系列iOS基础知识

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

2019-05-08  本文已影响287人  我只不过是出来写写代码

在整理复习 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_rw_t 不同于 class_ro_t,在运行时动态将类的分类信息加入对应数组中,为类提供了很好的扩展能力,这也印证了 Objective-C 动态语言的特性。


3. cache_t cache

 发送消息时若每次从方法列表中去查找,性能会发生损耗,并且类存在继承关系时,方法查找链会更长,损耗更严重,而 cache_t cache; 正是为了解决方法查找所引发的性能问题。通过散列表形式缓存调用过的方法函数,大幅提高访问速度。

cache_t结构体

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);
    }

 在上述流程执行前,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 博客

上一篇下一篇

猜你喜欢

热点阅读