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

在整理复习 runtime 知识点的过程中,发现不得不巩固 runtime 关于数据结构方面的知识,所以单独开篇关于 NSObject 文章

目录


准备:runtime 源码

1. objc_object

2. Class superclass

3. class_data_bits_t bits

 (1). class_data_bits_t bits 掩码取值

 (2). class_rw_t

 (3). class_ro_t

4. cache_t cache

5. 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 编译通过,而且若系统升级到macOS Mojave,则只能使用 obj4_750 版本,旧版本会报错。关于可编译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); }

结构体声明截图

该结构体使用C++代码声明,对C语言本身做了扩展,该结构体中可包含函数声明。


1. objc_object

objc_class 继承自 objc_objectobjc_object 中存在一个 isa 指针,因此 objc_class 也拥有自己的 isa 指针。在 Objective-C 语言中,所有的对象都会拥有一个 isa 指针,指针指向当前对象所属的类,通过 isa 可在运行时当前对象的所属类。关于 isa 指针,这篇 isa的本质 文章个人认为是解释最全面细致的。

objc_object结构体


2. Class superclass

Class superclass;,此处就是消息执行流程向父类传递最重要的实现属性,代表着作为当前类的父类


3. 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 动态语言的特性。


4. 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 原理因篇幅限制,会再开一篇新文章分析。


5. 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 博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,706评论 4 366
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,002评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,462评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,375评论 0 216
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,763评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,849评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,033评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,768评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,490评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,734评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,204评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,566评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,227评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,137评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,934评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,926评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,774评论 2 274

推荐阅读更多精彩内容