绝大多数 iOS 开发者在学习 runtime 时都阅读过 runtime.h 文件中的这段代码:

struct objc_class {Class isa  OBJC_ISA_AVAILABILITY;#if !__OBJC2__Class super_class                                        OBJC2_UNAVAILABLE;const char *name                                         OBJC2_UNAVAILABLE;long version                                             OBJC2_UNAVAILABLE;long info                                                OBJC2_UNAVAILABLE;long instance_size                                       OBJC2_UNAVAILABLE;struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;struct objc_cache *cache                                 OBJC2_UNAVAILABLE;struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif} OBJC2_UNAVAILABLE;复制代码

可以看到其中保存了类的实例变量,方法列表等信息。

不知道有多少读者思考过 OBJC2_UNAVAILABLE 意味着什么。其实早在 2006 年,苹果在 WWDC 大会上就发布了 Objective-C 2.0,其中的改动包括 Max OS X 平台上的垃圾回收机制(现已废弃),runtime 性能优化等。

这意味着上述代码,以及任何带有 OBJC2_UNAVAILABLE 标记的内容,都已经在 2006 年就永远的告别了我们,只停留在历史的文档中。

Category 的原理

虽然上述代码已经过时,但仍具备一定的参考意义,比如 methodLists 作为一个二级指针,其中每个元素都是一个数组,数组中的每个元素则是一个方法。

接下来就介绍一下 category 的工作原理,在美团的技术博客 深入理解Objective-C:Category 中已经有了非常详细的解释,然而可能由于时间问题,其中的不少内容已经过时,我根据目前最新的版本(objc-680) 做一些简单的分析,为了便于阅读,在不影响代码逻辑的前提下有可能删除部分无关紧要的内容。

概述

首先 runtime 依赖于 dyld 动态加载,在 objc-os.mm 文件中可以找到入口,它的调用栈简单整理如下:

void _objc_init(void)
└──const char *map_2_images(...)└──const char *map_images_nolock(...)└──void _read_images(header_info **hList, uint32_t hCount)复制代码

以上四个方法可以理解为 runtime 的初始化过程,我们暂且不深究。在 _read_images 方法中有如下代码:

if (cat->classMethods  ||  cat->protocols  /* ||  cat->classProperties */) {addUnattachedCategoryForClass(cat, cls->ISA(), hi);if (cls->ISA()->isRealized()) {remethodizeClass(cls->ISA());}
}复制代码

根据注释可见苹果曾经计划利用 category 来添加属性。在 addUnattachedCategoryForClass 方法中会找到当前类的所有 category,然后在 remethodizeClass 真正的去做处理。不过到目前为止还没有接触到相关的 category 处理,我们继续沿着调用栈向下走:

void _read_images(header_info **hList, uint32_t hCount)
└──static void remethodizeClass(Class cls)└──static void attachCategories(Class cls, category_list *cats, bool flush_caches)复制代码

这里的 attachCategories 就是处理 category 的核心所在,不过在阅读这段代码之前,我们有必要先熟悉一下相关的数据结构。

Category 相关的数据结构

首先来了解一下一个 Category 是如何存储的,在 objc-runtime-new.h 中可以看到如下定义,我只列出了其中成员变量:

struct category_t {const char *name;classref_t cls;struct method_list_t *instanceMethods;struct method_list_t *classMethods;struct protocol_list_t *protocols;struct property_list_t *instanceProperties;
};复制代码

可见一个 category 持有了一个 method_list_t 类型的数组,method_list_t 又继承自 entsize_list_tt,这是一种泛型容器:

struct method_list_t : entsize_list_tt<method_t, method_list_t, 0x3> {// 成员变量和方法
};template <typename Element, typename List, uint32_t FlagMask>
struct entsize_list_tt {uint32_t entsizeAndFlags;uint32_t count;Element first;
};复制代码

这里的 entsize_list_tt 可以理解为一个容器,拥有自己的迭代器用于遍历所有元素。 Element 表示元素类型,List 用于指定容器类型,最后一个参数为标记位。

虽然这段代码实现比较复杂,但仍可了解到 method_list_t 是一个存储 method_t 类型元素的容器。method_t 结构体的定义如下:

struct method_t {SEL name;const char *types;IMP imp;
};复制代码

最后,我们还有一个结构体 category_list 用来存储所有的 category,它的定义如下:

struct locstamped_category_list_t {uint32_t count;locstamped_category_t list[0];
};
struct locstamped_category_t {category_t *cat;struct header_info *hi;
};
typedef locstamped_category_list_t category_list;复制代码

除了标记存储的 category 的数量外,locstamped_category_list_t 结构体还声明了一个长度为零的数组,这其实是 C99 中的一种写法,允许我们在运行期动态的申请内存。

以上就是相关的数据结构,只要了解到这个程度就可以继续读源码了。

处理 Category

对 Category 中方法的解析并不复杂,首先来看一下 attachCategories 的简化版代码:

static void attachCategories(Class cls, category_list *cats, bool flush_caches) {if (!cats) return;bool isMeta = cls->isMetaClass();method_list_t **mlists = (method_list_t **)malloc(cats->count * sizeof(*mlists));// Count backwards through cats to get newest categories firstint mcount = 0;int i = cats->count;while (i--) {auto& entry = cats->list[i];method_list_t *mlist = entry.cat->methodsForMeta(isMeta);if (mlist) {mlists[mcount++] = mlist;}}auto rw = cls->data();prepareMethodLists(cls, mlists, mcount, NO, fromBundle);rw->methods.attachLists(mlists, mcount);free(mlists);if (flush_caches  &&  mcount > 0) flushCaches(cls);
}复制代码

首先,通过 while 循环,我们遍历所有的 category,也就是参数 cats 中的 list 属性。对于每一个 category,得到它的方法列表 mlist 并存入 mlists 中。

换句话说,我们将所有 category 中的方法拼接到了一个大的二维数组中,数组的每一个元素都是装有一个 category 所有方法的容器。这句话比较绕,但你可以把 mlists 理解为文章开头所说,旧版本的 objc_method_list **methodLists

在 while 循环外,我们得到了拼接成的方法,此时需要与类原来的方法合并:

auto rw = cls->data();
rw->methods.attachLists(mlists, mcount);复制代码

这两行代码读不懂是必然的,因为在 Objective-C 2.0 时代,对象的内存布局已经发生了一些变化。我们需要先了解对象的布局模型才能理解这段代码。

Objective-C 2.0 对象布局模型

objc_class

相信读到这里的大部分读者都学习过文章开头所说的对象布局模型,因此在这一部分,我们采用类比的方法,来看看 Objective-C 2.0 下发生了哪些改变。

首先,Classid 指针的定义并没有发生改变,他们一个指向类对应的结构体,一个指向对象对应的结构体:

// objc.h
typedef struct objc_class *Class;
typedef struct objc_object *id;复制代码

比较有意思的一点是,objc_class 结构体是继承自 objc_object 的:

struct objc_object {Class isa  OBJC_ISA_AVAILABILITY;
};struct objc_class : objc_object {Class superclass;cache_t cache;             // formerly cache pointer and vtableclass_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flagsclass_rw_t *data() { return bits.data();}
};复制代码

这一点也很容易理解,早在 Objective-C 1.0 时代,我们就知道一个对象的结构体只有 isa 指针,指向它所属的类。而类的结构体也有 isa 指针指向它的元类。因此让类结构体继承自对象结构体就很容易理解了。

可见 Objective-C 1.0 的布局模型中,cachesuper_class 被原封不动的移过来了,而剩下的属性则似乎消失不见。取而代之的是一个 bits 属性,以及 data() 方法,这个方法调用的其实是 bits 属性的 data() 方法,并返回了一个 class_rw_t 类型的结构体指针。

class_data_bits_t

以下是简化版 class_data_bits_t 结构体的定义:

struct class_data_bits_t {uintptr_t bits;
public:class_rw_t* data() {return (class_rw_t *)(bits & FAST_DATA_MASK);}
}复制代码

可见这个结构体只有一个 64 位的 bits 成员,存储了一个指向 class_rw_t 结构体的指针和三个标志位。它实际上由三部分组成。首先由于 Mac OS X 只使用 47 位内存地址,所以前 17 位空余出来,提供给 retain/release 和 alloc/dealloc 方法使用,做一些优化。其次,由于内存对齐,指针地址的后三位都是 0,因此可以用来做标志位:

// class is a Swift class
#define FAST_IS_SWIFT           (1UL<<0)
// class or superclass has default retain/release/autorelease/retainCount/
//   _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
#define FAST_HAS_DEFAULT_RR     (1UL<<1)
// class's instances requires raw isa
#define FAST_REQUIRES_RAW_ISA   (1UL<<2)
// data pointer
#define FAST_DATA_MASK          0x00007ffffffffff8UL复制代码

如果计算一下就会发现,FAST_DATA_MASK 这个 16 进制常量的二进制表示恰好后三位为0,且长度为47位: 11111111111111111111111111111111111111111111000,我们通过这个掩码做按位与运算即可取出正确的指针地址。

引用 Draveness 在 深入解析 ObjC 中方法的结构 中的图片做一个总结:

class_rw_t

bits 中包含了一个指向 class_rw_t 结构体的指针,它的定义如下:

struct class_rw_t {uint32_t flags;uint32_t version;const class_ro_t *ro;method_array_t methods;property_array_t properties;protocol_array_t protocols;
}复制代码

注意到有一个名字很类似的结构体 class_ro_t,这里的 'rw' 和 ro' 分别表示 'readwrite' 和 'readonly'。因为 class_ro_t 存储了一些由编译器生成的常量。

These are emitted by the compiler and are part of the ABI.

正是由于 class_ro_t 中的两个属性 instanceStartinstanceSize 的存在,保证了 Objective-C2.0 的 ABI 稳定性。因为即使父类增加方法,子类也可以在运行时重新计算 ivar 的偏移量,从而避免重新编译。

关于 ABI 稳定性的问题,本文不做赘述,读者可以参考 Non Fragile ivars。

如果阅读 class_ro_t 结构体的定义就会发现,旧版本实现中类结构体中的大部分成员变量现在都定义在 class_ro_tclass_rw_t 这两个结构体中了。感兴趣的读者可以自行对比,本文不再赘述。

class_rw_t 结构体中还有一个 methods 成员变量,它的类型是 method_array_t,继承自 list_array_tt

list_array_tt 是一个泛型结构体,用于存储一些元数据,而它实际上是元数据的二维数组:

template <typename Element, typename List>{struct array_t {uint32_t count;List* lists[0];};
}
class method_array_t : public list_array_tt<method_t, method_list_t>复制代码

其中 Element 表示元数据的类型,比如 method_t,而 List 则表示用于存储元数据的一维数组,比如 method_list_t

list_array_tt 有三种状态:

  1. 自身为空,可以类比为 [[]]
  2. 它只有一个指针,指向一个元数据的集合,可以类比为 [[1, 2]]
  3. 它有多个指针,指向多个元数据的集合,可以类比为 [[1, 2], [3, 4]]

当一个类刚创建时,它可能处于状态 1 或 2,但如果使用 class_addMethod 或者 category 来添加方法,就会进入状态 3,而且一旦进入状态 3 就再也不可能回到其他状态,即使新增的方法后来又被移除掉。

方法合并

掌握了这些 runtime 的基础只是以后就可以继续钻研剩下的 category 的代码了:

auto rw = cls->data();
rw->methods.attachLists(mlists, mcount);复制代码

这是刚刚卡住的地方,现在来看,rw 是一个 class_rw_t 类型的结构体指针。根据 runtime 中的数据结构,它有一个 methods 结构体成员,并从父类继承了 attachLists 方法,用来合并 category 中的方法:

void attachLists(List* const * addedLists, uint32_t addedCount) {if (addedCount == 0) return;uint32_t oldCount = array()->count;uint32_t newCount = oldCount + addedCount;setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));array()->count = newCount;memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0]));memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0]));
}复制代码

这段代码很简单,其实就是先调用 realloc() 函数将原来的空间拓展,然后把原来的数组复制到后面,最后再把新数组复制到前面。

在实际代码中,比上面略复杂一些。因为为了提高性能,苹果做了一些优化,比如当 List 处于第二种状态(只有一个指针,指向一个元数据的集合)时,其实并不需要在原地扩容空间,而是只要重新申请一块内存,并将最后一个位置留给原来的集合即可。

这样只多花费了很少的内存空间,也就是原来二维数组占用的内存空间,但是 malloc() 的性能优势会更加明显,这其实是一个空间换时间的权衡问题。

需要注意的是,无论执行哪种逻辑,参数列表中的方法都会被添加到二维数组的前面。而我们简单的看一下 runtime 在查找方法时的逻辑:

static method_t *getMethodNoSuper_nolock(Class cls, SEL sel){for (auto mlists = cls->data()->methods.beginLists(), end = cls->data()->methods.endLists(); mlists != end;++mlists) {method_t *m = search_method_list(*mlists, sel);if (m) return m;}return nil;
}static method_t *search_method_list(const method_list_t *mlist, SEL sel) {for (auto& meth : *mlist) {if (meth.name == sel) return &meth;}
}复制代码

可见搜索的过程是按照从前向后的顺序进行的,一旦找到了就会停止循环。因此 category 中定义的同名方法不会替换类中原有的方法,但是对原方法的调用实际上会调用 category 中的方法。

总结

读完本文后,你应该对以下内容有比较深刻的理解,排名不分先后:

  1. 定义在 runtime.h 中的数据结构,如果有 OBJC2_UNAVAILABLE 标记则表示已经废弃。
  2. Objective-C 2.0 中,类结构体的结构层次: objc_class -> class_data_bits_t -> class_rw_t -> method_array_t
  3. class_ro_t 结构体的作用,与 class_rw_t 的区别,以及和 ABI 稳定性的关系。
  4. category 解析过程的调用栈以及基本的流程。
  5. method_array_t 为什么要设计成一种类似于二维数组的数据结构,以及它的三种状态之间的关系。

参考资料

  1. 深入理解Objective-C:Category
  2. 从源代码看 ObjC 中消息的发送
  3. 深入解析 ObjC 中方法的结构
  4. Whats is methodLists attribute of the structure objc_class for?
  5. Objc与C(C++)之亲缘关系(一) Class
  6. Objective-C Runtime

结合 category 工作原理分析 OC2.0 中的 runtime相关推荐

  1. 【高通SDM660平台 Android 10.0】(19) --- Camera_focus、Camera_snapshot、volume_up 按键工作原理分析

    [高通SDM660平台 Android 10.0]19 --- Camera_focus.Camera_snapshot.volume_up 按键工作原理分析 一. DTS代码配置 二. Kernel ...

  2. 原理剖析(第 009 篇)ReentrantReadWriteLock工作原理分析

    2019独角兽企业重金招聘Python工程师标准>>> 原理剖析(第 009 篇)ReentrantReadWriteLock工作原理分析 一.大致介绍 1.在前面章节了解了AQS和 ...

  3. 匿名突破网络限制 (Tor工作原理分析)

    转载自:http://blog.163.com/mike_gz/blog/static/247532200672932800/ 网管顾名思义,就是天天管着网络.想尽各种手段限制我们上网的人.在网络中订 ...

  4. ZVS振荡电路工作原理分析

    简 介: 本文基于 LTspice 仿真,分析了 ZVS 振荡器的工作原理以及相关的参数设计. 关键词: ZVS,LTspice,振荡器 #mermaid-svg-Zw0iqmYO7IX6nMqy { ...

  5. 二极管温度补偿电路工作原理分析

    众所周知,PN结导通后有一个约为0.6V(指硅材料PN结)的压降,同时PN结还有一个与温度相关的特性:PN结导通后的压降基本不变,但不是不变,PN结两端的压降随温度升高而略有下降,温度愈高其下降的量愈 ...

  6. RxJs map operator 工作原理分析

    使用一个例子来研究 map 操作符的工作原理. 推荐阅读本文之前,先浏览这篇文章RxJs fromEvent 工作原理分析以了解相关知识. 源代码: import { Component, OnIni ...

  7. Hadoop生态圈-Zookeeper的工作原理分析

    Hadoop生态圈-Zookeeper的工作原理分析 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任.   无论是是Kafka集群,还是producer和consumer都依赖于Zoo ...

  8. 船型开关工作原理分析

    船型开关工作原理分析 由于疫情,这几个月一直无聊的待在家里,就把家里翻出来的旧电热锅简单的修理了一下,关于电热锅的修理博客之后有时间再写吧,这次就总结下在修过程中让我一直很头疼的船型开关的工作原理. ...

  9. 全自动升降柱工作原理分析

    全自动升降柱工作原理分析 全自动升降柱使用范围比较广泛,在城市交通安全中提供非常便捷的交通舒缓措施,可控制车辆行驶确保安全,全自动升降柱外观设计精美, 具有高的防撞击效果,当电动升降柱升起可在地面形成 ...

最新文章

  1. (13)处理静态资源(自定义资源映射)【从零开始学Spring Boot】
  2. 博科300交换机不中断(non-disruptive)固件升级
  3. LeetCode 股票买卖问题
  4. #20175201 实验五 网络编程与安全
  5. 1037C. Equalize
  6. 【Elasticsearch】 Elasticsearch中数据是如何存储的
  7. ssh远程连接不上linux
  8. SQL语句如何判断某字段是以字母开头而不是汉字开头
  9. netdev_priv() 函数
  10. runnerw.exe: CreateProcess failed with error 193: %1 问题定位-idea
  11. Ubuntu18.04连蓝牙键盘后,搜狗输入法导致输入卡顿问题
  12. c语言中f1(a 25),C语言程序设计A 200901-201707历年考试选择题(全)doc.docx
  13. 有人一起用沙雕情侣头像吗?
  14. UE4 Ultra Dynamic Sky 参数翻译及功能概述
  15. Hbase跨集群数据同步验证
  16. fullPage的简单使用
  17. 第九周 练习判断闰年和平年
  18. gird布局之容器属性justify-items与align-items
  19. 给定一个不多于5位的正整数,判断它是几位数,并输出。 输入
  20. highcharts制作3D饼图渐变效果

热门文章

  1. java获取IP地址:
  2. leetcode1103. 分糖果 II 该模拟就模拟,别老想着优化
  3. leetcode71. 简化路径 Unix 风格
  4. leetcode414. 第三大的数
  5. 密码学专题 序列号文件
  6. 使用多线程的方式调用chineseocr_API
  7. java 输出三位数和n位数的每一位的数
  8. 从工具的奴隶到工具的主人
  9. H.264/AVC视频压缩编码标准的新进展
  10. 最简单的 post 请求发起方式、调用其它系统接口