本页所使用的objc runtime 756.2,来自 Apple 开源文档
类的加载探寻系列:
1、【类的加载】-(1)类的启动
1、objc_init 流程
类的加载,初始化来自dyld 中的objc_init 这个步骤,查看一下源码,得到
void _objc_init(void) |
序号 | 源代码 | 解释 |
---|---|---|
1 | environ_init() | 环境变量、帮助文档配置 |
2 | tls_init() | 线程key 的绑定 |
3 | static_init | C++ 静态构造函数 |
4 | lock_init() | 锁配置 |
5 | exception_init() | 异常初始化 |
6 | _dyld_objc_notify_register | Dyld 通知注册 |
1.1 environ_init
在函数
void environ_init(void)
里,读取环境变量env
以及帮助注释help
,
相关代码如下:
for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) { |
运行代码,在控制台可以得到环境变量参数表:
objc[27076]: OBJC_PRINT_IMAGES: log image and library names as they are loaded |
1.2 tls_init()
对线程的Key 的绑定。
这部分不展开讨论
void tls_init(void) |
1.3 static_init
实现系统级别的 C++ 静态构造函数。
相关源码:
static void static_init() |
1.4 lock_init
锁的初始化,这里并没有进行实现,意味着可以进行重写,自定义一些操作。
相关源码:
/*********************************************************************** |
1.5 exception_init
异常的初始化:libobjc 的异常回调系统初始化,由map_images 调用。
即注册监听回调。
/*********************************************************************** |
这个c++ 函数 set_terminate
会初始化静态的 异常回调。在OC 环境下,所有的异常抛出,都会到(*old_terminate)();
这行代码里。
相关源码如下:
static void (*old_terminate)(void) = nil; |
在业务代码里执行一个报错的方法:
类声明与实现
@interface Dog : NSObject
- (void)bark;
@end
@implementation Dog
@end使用类执行为实现的方法
Dog *d = [Dog alloc];
[d bark];异常抛出如下图所示:
1.6 _dyld_objc_notify_register(核心)
- objc 特有方法
- 注册回调。当镜像文件被映射(mapped)、未被映射(unmapped)、初始化(initialized) 使调用。
- 该方法由dyld 声明。
- 镜像文件加载完后,dyld 会回调”映射”函数,内容为一个包含objc-image-info 数据段的数组。
注册方法源码如下:
_dyld_objc_notify_register(&map_images, load_images, unmap_image); |
这里主要关注 map_images
的这个对象,其实现代码为:
void |
读取镜像文件
继续探索 map_images_nolock
这个函数,读取镜像文件的关键行为
if (hCount > 0) { |
至此,这一部分到此就结束了,关于images 的流程,另外单独分析。
2、read_images - 读镜像文件
2.1 初次进来: 创建容器哈希表
根据源码,得知读取镜像的主要函数为read_image
,其函数具体为:
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses){} |
读取镜像后,最先需要做的是,为这些景象里的内容找一个家——容器,也就是开辟表结构。
在源码里,这里分别通过NXCreateMapTable
、NXCreateHashTable
开辟了2个表,主要存储以下的内容:
未共享缓存的类
gdb_objc_realized_classes
gdb_objc_realized_classes =
NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);开辟内存空间类 -
allocatedClasses
allocatedClasses = NXCreateHashTable(NXPtrPrototype, 0, nil);
2.2 类的处理
从编译后的类列表中,取出所有的类,并进行下一步操作。
for (EACH_HEADER) { |
取出系统类
Class cls = (Class)classlist[i];
读取类
Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
懒加载相关,条件为
newCls != cls && newCls
, 即两个类不相等时初始化懒加载所需内存空间
resolvedFutureClasses = (Class *)
realloc(resolvedFutureClasses,
(resolvedFutureClassCount+1) * sizeof(Class));懒加载的类添加到数组中来
resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
2.3 方法编号处理
把未注册的方法名进行注册到一张HASH表里,形成方法名与方法的映射
static size_t UnfixedSelectors; |
注册到的表名为namedSelectors
, 具体的实现在这里
NXMapInsert(namedSelectors, sel_getName(result), result); |
2.4 协议处理
找到类遵守的协议,修复协议的引用。
关键字: readProtocol 、remapProtocolRef
相关源码,如下所示:
找到协议:
for (EACH_HEADER) { |
修复协议引用(重映射):
for (EACH_HEADER) { |
2.5 非懒加载处理
初始化所有非懒加载类,进行
rw
ro
操作
for (EACH_HEADER) { |
2.6 待处理的类
找到最新特征的类,以备CoreFoundation 需要操作它
if (resolvedFutureClasses) { |
2.7 分类处理
对非类进行处理,包括
Class
的和metaClass
的
for (EACH_HEADER) { |
3、类方法重点
3.1 类的读取readClasses
3.1.1 判断是否是未来处理的类 (popFutureNamedClass
)
如果是,就读取data,设置rw 和 ro
if (Class newCls = popFutureNamedClass(mangledName)) { |
3.1.2 添加命名过的类
addNamedClass(cls, mangledName, replacing); |
3.1.3 把类插入到包含所有类的表里
addClassTableEntry(cls); |
具体的实现为如下:
static void addClassTableEntry(Class cls, bool addMeta = true) { |
3.2 realizeClassWithoutSwift
实现类的初次初始化,包括开辟rw数据的内存空间。
3.2.1 取出类里的 ro 部分(data)
ro = (const class_ro_t *)cls->data(); |
3.2.2 开辟rw 内存空间
// Normal class. Allocate writeable class data. |
最终通过cls->setData(rw)
, 将rw 作为data赋值给类 cls。
但是到这里,cls 仅仅是对数据部分做了初始化,里面的方法都没有写入。
如何验证?
通过lldb 来进行验证吧:
- 通过p/x 获取cls指针
- 偏移16位获得bits 的指针
- 将bits 强转为
class_data_bits_t
的类型$2 - 读取$2点data() 方法。
过程可见下图,methods 属性下的list 为空,并没有方法列表的写入。
可见该过程,仅仅对cls
下rw
的属性的ro
、flags
进行了赋值,而rw
并没有得到赋值
3.2.3 实现父类和元类关联
3.2.3.1 子类链接父类
上面的流程都是在初始化类的各种属性后,而类的结构最重要的一点是它与父类的关联,以及与元类的isa 指向。
所以下面的操作是
- 通过
remapClass(cls->superclass)
对父类进行映射查找 - 通过
remapClass(cls->ISA)
对元类进行查找
然后通过realizeClassWithoutSwift
进行递归,逐步得到相应的supercls
以及metacls
的指针。
步骤依然是:类——元类——根元类——NSObject——nil(跳出)
supercls = realizeClassWithoutSwift(remapClass(cls->superclass)); |
这两个失踪人口进行搜寻成功后,就可以将其指针对类进行关联
// Update superclass and metaclass in case of remapping |
3.2.3.2 父类链接子类
如果找到父类,在父类添加双向链表的引用,将子类引用添加到父类的子类列表里,最终实现父类和子类建立关联
// Connect this class to its superclass's subclass lists |
结论是:子类里添加父类链接,父类里也添加子类链接,你中有我,我中有你。
接下来看一下父类链接的具体实现:
static void addSubclass(Class supercls, Class subcls) |
实现为:
父类的第一个子类,赋值给子类的下一个兄弟类:
subcls->data()->nextSiblingClass = supercls->data()->firstSubclass; |
子类赋值给父类的第一个子类:
supercls->data()->firstSubclass = subcls; |
同时,子类继承父类的各种能力
- hasCxxCtor
- hasCxxDtor
- hasCustomRR
- hasCustomAWZ
- instancesRequireRawIsa(适用NONPOINTER_ISA)
3.2.4 methodizeClass - rw 内容的填充
对之前创建的rw 进行方法、属性,协议的填充实现
对分类的实现
方法的填充实现
method_list_t *list = ro->baseMethods();
if (list) {
prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
rw->methods.attachLists(&list, 1);
}属性填充实现
property_list_t *proplist = ro->baseProperties;
if (proplist) {
rw->properties.attachLists(&proplist, 1);
}协议填充实现
protocol_list_t *protolist = ro->baseProtocols;
if (protolist) {
rw->protocols.attachLists(&protolist, 1);
}根元类额外的实现
if (cls->isRootMetaclass()) {
// root metaclass
addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
}添加分类
// Attach categories.
category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
attachCategories(cls, cats, false /*don't flush caches*/);
提一下上面的方法实现中添加的机制,究竟是如何的?继续看源码,可以看到添加情况有3种
多对多 - 方法原已有多个,在需要添加多个
if (hasArray()) {
// many lists -> many lists
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]));
}这里经历了3个步骤:
- 获取原方法数:
array()->count
- 获取原方法数+新方法的总方法数:
oldCount + addedCount
- 内存扩容:
realloc
- 字节移动:内存中原方法平移:
memmove
- 字节移动:新方法拷贝进入:
memcpy
- 获取原方法数:
0 对多 - 方法原没有,在需要添加多个
list = addedLists[0];
1 对多
// 1 list -> many lists
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
另外,可以观察到无论是方法methods
、属性properties
、协议protocols
,在添加时,使用的同样的方法attachLists
, 这是因为作为同为class_rw_t
类型下的属性,他们拥有同样的数据结构:二维数组。
其中方法列表和属性列表的根类型,均为entsize_list_tt
方法列表结构:
class method_array_t : |
属性列表结构:
class property_array_t : public list_array_tt<property_t, property_list_t> |
协议列表结构:
class protocol_array_t : |
方法列表设计成二维数组,方便分类方法的批量插入:
4、小结
类的加载,流程分为环境的启动配置、加载镜像、读取类方法、对类内存空间初始化、子类-父类建立连接,以及最终rw填充方法,并对分类方法加入进行实现。