目录
前言
_objc_init方法
environ_init
tis->init方法
static_init方法
💡 _objc_init 是由 libc 调用的,目的是:
❗️“必须自己实现” 是什么意思?
runtime_init
exception_init
cache_t::init
_imp_implementationWithBlock_init
_dyld_objc_notify_register
dyld与Objc的关联
map_images的调用时机
dyld与Objc关联
map_images
_read_images
创建表
修复预编译阶段的@selector的混乱问题
错误混乱的类处理
修复重映射一些没有被镜像文件加载进来的类
修复一些消息
当类里面有协议时:readProtocol 读取协议
修复没被加载的协议
分类处理
类的加载处理
没有被处理的类,优化那些被侵犯的类
关键步骤
readClass
realizeClassWithoutSwift
读取 data 数据,并设置 ro、rw
递归调用 realizeClassWithoutSwift 完善 继承链
通过 methodizeClass 方法化类
methodizeClass
attachToClass方法
attachCategories方法
attachLists方法
分类
分类的本质
分类的加载
分类加载时机
load_images
prepare_load_methods实现
schedule_class_load方法
add_category_to_loadable_list
call_load_methods
initalize分析
前言
其实在之前分析dyld的加载流程的时候已经有涉及到一些有关类和分类加载的过程了,这篇文章来探索一下类和分类的加载过程在底层的实现,研究的起点是_objc_init。
_objc_init方法
可以看出来_objc_init的实现中主要是调用了许多的init方法。
environ_init
environ_init()方法是初始化一系列环境变量,并读取影响运行时的环境变量。
常用的环境变量有以下这些:
-
DYLD_PRINT_STATISTICS:设置 DYLD_PRINT_STATISTICS 为YES,控制台就会打印 App 的加载时长,包括整体加载时长和动态库加载时长,即main函数之前的启动时间(查看pre-main耗时),可以通过设置了解其耗时部分,并对其进行启动优化
-
OBJC_DISABLE_NONPOINTER_ISA:杜绝生成相应的nonpointer isa(nonpointer isa指针地址 末尾为1 ),生成的都是普通的isa
-
OBJC_PRINT_LOAD_METHODS:打印 Class 及 Category 的 + (void)load 方法的调用信息
-
NSDoubleLocalizedStrings:项目做国际化本地化(Localized)的时候是一个挺耗时的工作,想要检测国际化翻译好的语言文字UI会变成什么样子,可以指定这个启动项.可以设置 NSDoubleLocalizedStrings 为YES
-
NSShowNonLocalizedStrings:在完成国际化的时候,偶尔会有一些字符串没有做本地化,这时就可以设置NSShowNonLocalizedStrings 为YES,所有没有被本地化的字符串全都会变成大写
tis->init方法
tls->init()方法是关于线程key的绑定,主要是本地线程池的初始化以及析构。
static_init方法
static_init()方法注释中提到该方法会运行C++静态构造函数(只会运行系统级别的构造函数) 在dyld调用静态构造函数之前,libc会调用_objc_init,所以必须自己去实现。
为什么要在static_init之前调用_objc_init:
💡
_objc_init
是由 libc 调用的,目的是:在所有 ObjC 相关代码执行之前,先初始化 ObjC Runtime(注册类、创建基本元类、初始化 TLS、Hook、AutoreleasePool 等)。
这意味着:
dyld
还没执行 C++ 静态构造函数而我们已经通过
_objc_init
把 ObjC 的 runtime 环境搭建好了这样之后无论运行什么构造函数,如果里面使用了 ObjC 的对象/类/方法,都不会崩溃
❗️“必须自己实现” 是什么意思?
这是指 Objective-C Runtime 不能依赖 dyld 的
static_init()
去自动初始化自己(因为它太晚了),所以:ObjC Runtime 必须自己注册一个早期初始化入口 ——
_objc_init
,并让 libc 来调用它。这个初始化包括:
注册 TLS
初始化 runtime 状态(
runtime_init()
)hook 一些系统函数(如
malloc
,pthread
等)初始化 autorelease pool
加载 image 等
runtime_init
运行时初始化,主要分为两部分:分类初始化、类的表初始化。
exception_init
exception_init()负责初始化libobjc的异常处理系统,注册异常处理的回调,从而监控异常的处理
cache_t::init
负责缓存初始化
_imp_implementationWithBlock_init
启动回调机制
_dyld_objc_notify_register
这个方法就是注册dyld,具体实现之前已经在分析dyld加载流程的时候分析过了。
从_dyld_objc_notify_register方法的注释中可以得出:
-
仅供objc运行时使用
-
注册处理程序,以便在映射、取消映射和初始化objc图像时调用
-
dyld将会通过一个包含objc-image-info的镜像文件的数组回调mapped函数
_dyld_objc_notify_register中的三个参数含义如下:
-
map_images:dyld将image(镜像文件)加载进内存时,会触发该函数
-
load_image:dyld初始化image会触发该函数
-
unmap_image:dyld将image移除时,会触发该函数
dyld与Objc的关联
dyld源码中_dyld_objc_notify_register的实现
libobjc源码中_dyld_objc_notify_register的调用
结合这两段代码可以得出:
-
mapped
等价于map_images
-
init
等价于load_images
-
unmapped
等价于unmap_image
再在dyld源码中查看registerObjCNotifiers的实现:
可以看到作为参数传进去的三个函数一一被用来进行了赋值操作,所以会存在以下等价关系:
-
sNotifyObjCMapped == mapped == map_images
-
sNotifyObjCInit == init == load_images
-
sNotifyObjCUnmapped == unmapped == unmap_image
map_images的调用时机
关于load_images的调用时机在讲述dyld的加载流程时已经讲解过了,在notifySingle方法中,通过sNotifyObjCInit来调用。接下来我们分析一下map_images的调用时机
在dyld中全局搜索sNotifyObjCMapped,可以发现是在notifyBatchPartial方法中调用的
搜索notifyBatchPartial,可以看到它是在registerObjCNotifiers方法中调用的
到这里,我们再来梳理一遍dyld的流程:
-
在recursiveInitialization方法中会调用bool hasInitializers = this->doInitialization(context);这个方法是用来判断image是否已加载。
-
同时doInitialization这个方法会调用doImageInit和doModInitFunctions(context),这两个方法会进入libSystem框架里调用libSystem_initializer方法,最后就会调用_objc_init方法
-
_objc_init会调用_dyld_objc_notify_register将map_images、load_images、unmap_image传入dyld方法registerObjCNotifiers
-
在registerObjCNotifiers方法中,我们把_dyld_objc_notify_register传入的map_images赋值给sNotifyObjCMapped,将load_images赋值给sNotifyObjCInit,将unmap_image赋值给sNotifyObjCUnmapped
-
在registerObjCNotifiers方法中,我们将传参赋值后就开始调用notifyBatchPartial()
-
notifyBatchPartial方法中会调用(*sNotifyObjCMapped)(objcImageCount, paths, mhs);触发map_images方法
-
dyld的recursiveInitialization方法在调用完bool hasInitializers = this->doInitialization(context)方法后,会调用notifySingle()方法
-
在notifySingle()中会调用(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());上面我们将load_images赋值给了sNotifyObjCInit,所以此时就会触发load_images方法
-
sNotifyObjCUnmapped会在removeImage方法里触发,字面理解就是删除Image(映射的镜像文件)
所以有以下结论:map_images是先于load_images调用,即先map_images,再load_images。
dyld与Objc关联
结合以上分析和之前对dyld的分析,可以总结出dyld与Objc的关联就是通过回调完成的:
-
在
dyld中
注册回调函数,可以理解为添加观察者
-
在
objc
中dyld
注册,可以理解为发送通知
-
触发回调
,可以理解为执行通知selector
下面我们来看看map_images、load_images、unmap_image都做了什么
-
map_images
:主要是管理文件中和动态库中
所有的符号
,即class、protocol、selector、category
等 -
load_images
:加载执行load方法
-
unmap_image
: 卸载移除数据
map_images
先说明为什么map_images有&,而load_images没有:
-
map_images是 引用类型,外界变了,跟着变
-
load_images是值类型,不传递值
map_images的作用是将Mach-O中的类信息加载到内存,map_images调用map_images_nolock,map_images_nolock调用_read_images来加载镜像文件
_read_images
_read_images这个函数的作用主要是加载类信息,即类、分类、协议等
先大致说一下_read_images的流程:
-
条件控制进行的一次加载——创建表(类的哈希表)
-
修复预编译阶段的@selector的混乱问题
-
错误混乱的类处理
-
修复重映射一些没有被镜像文件加载进来的类
-
修复一些消息
-
当类里面有协议时:readProtocol读取协议
-
修复没有被加载的协议
-
分类处理
-
类的加载处理
-
没有被处理的类,优化那些被侵犯的类
现在看不懂没关系,接下来我们来一个部分一个部分讲清楚这个函数做了些什么
创建表
这个部分只有在第一次进入函数时会执行,他会创建一个用来存放类的哈希表,这个表里存放的类是那些不在共享缓存且已命名的类,无论类是否实现,容量是类数量的4/3。
修复预编译阶段的@selector的混乱问题
这段代码的作用简单地说可以用一句话概括:把编译时生成的“假的 selector 地址”,统一替换成运行时真正注册过的“合法 selector 地址”。
为什么会混乱?
@selector(foo) 编译器生成的是静态字符串指针(在 __objc_selrefs 中)。
但在 runtime 调用方法时,必须保证 SEL 是统一注册过、地址唯一的。
如果不统一注册,那么同样的 @selector(foo) 在多个模块中可能是不同地址,会导致消息发送、isEqual, NSStringFromSelector 等出现 bug。
打一个通俗的比方:
你可以把 Selector (@selector(...)
) 想象成你给朋友写的一封信的收件人地址。
-
编译器阶段写的是:“张三,某市某区某号”,但这是你随手写的,可能不是张三真正的地址。
-
运行时系统需要确认:“张三”真实住在哪里?(selector 实际对应哪块内存)
-
如果你不校正这个地址,信可能寄不到正确的人手中,或者你认为两个“张三”其实是不同的人。
所以这个方法做的事就是:
-
翻出所有你写的“张三地址”(selector 指针)
-
拿去“户籍局”查一查(通过
sel_registerNameNoLock
注册) -
如果地址不对,系统会换成正确的地址
错误混乱的类处理
这里其实就是把Mach-O文件中的所有类都取出来,再遍历进行处理。
在readClass之前,cls只是一个地址,在执行完readClass之后,原始的类地址才能被解析为一个有效的类对象。
除此之外,这里还会检查共享缓存中的类是否已被覆盖,如果覆盖就需要重新处理。
对于未来类,如果解析后的类地址发生变化(即if (newCls != cls && newCls)
),就记录到resolvedFutureClasses数组,这些类需要后续非懒加载初始化(立即分配可读写数据class_rw_t
)。
这段代码的作用通俗来说,可以想象成你在整理一个杂乱的工具箱,确保所有工具都放在正确的位置,并且能正常使用。具体来说:
检查是否有“外人改动” 先看一眼工具箱,确认有没有人偷偷替换了里面的工具(比如通过插件或特殊配置修改了系统默认的类)。
逐个翻找工具箱的每个分区 打开每一个小格子(每个程序依赖的库或框架),看看里面装的是什么。
判断是否需要整理 如果某个分区已经是整理好的(预优化过的类),就跳过不管;如果被改动过,才需要手动处理。
拿出所有工具的“设计图纸” 从分区的某个固定位置(Mach-O文件的
__objc_classlist
段)掏出一叠设计图,这些图纸对应着程序里所有定义好的类。核对每张设计图 把图纸一张张展开检查:
如果是正常的图纸(普通类),直接贴上标签(类名),放到对应位置。
如果发现某张图纸写的是“临时占位符”(未来类),比如之前不知道这个类具体长什么样,现在终于找到真正的图纸了,就立刻替换掉占位符,并记录下来:“这几个类需要马上组装好,不能偷懒”。
标记需要立刻组装的工具 把那些替换过占位符的类单独记在小本本上(
resolvedFutureClasses
数组),后续要立刻把它们拼装成完整的工具(分配内存、关联方法等),而不是等到第一次用的时候才临时拼装(懒加载)。
修复重映射一些没有被镜像文件加载进来的类
主要是将未映射的Class
和Super Class
进行重映射,也就是将编译时的引用地址依赖的未确定的镜像基地址修正为运行时实际的地址,确保所有类引用指向正确内存位置
修复一些消息
这个部分是在处理一些历史遗留的特殊消息发送机制,确保它们能在新系统中正常工作
当类里面有协议时:readProtocol 读取协议
从源码和注释中我们可以看出来,大致分为三步:
第一步,通过NXMapTable *protocol_map = protocols();创建protocol哈希表,表的名称为protocol_map
第二步,通过_getObjc2ProtocolList 获取到Mach-O中的静态段__objc_protolist协议列表,即从编译器中读取并初始化protocol
循环遍历协议列表,通过readProtocol方法将协议添加到protocol_map哈希表中
修复没被加载的协议
在 Objective-C 的运行时中,协议 @protocol 也会在编译阶段生成引用,在 Mach-O 文件中它们会被放到一个叫 _objc_protorefs (与_objc_protolist不同)的段里。
在运行时加载镜像(Mach-O 文件)时:某些协议可能在预编译优化(preoptimized)阶段就已经被指向了共享缓存(dyld shared cache)中的协议定义;但是如果镜像是后来才加载的,比如动态库或插件(Bundle),这些协议引用可能仍然指向一个未修正的地址(stub 或未来协议)。为了保证协议指针指向真正的定义,就要在这里修正它们。
这段话看起来复杂,可以理解成一句话:「在协议的真实定义还没有被加载之前,其他地方就已经引用了它,所以在协议真正加载之后,必须把原来的引用修正为真实地址。」
分类处理
这段代码主要是处理分类,需要在分类初始化并将数据加载到类后才执行,对于运行时出现的分类,将分类的发现推迟到对_dyld_objc_notify_register的调用完成后的第一个load_images调用为止。
这是为了解决启动时加载过早的问题:
-
load_images()
是 Runtime 加载新 Mach-O 镜像(如动态库、插件)时的回调; -
didInitialAttachCategories
是一个标记,代表初次附加分类是否完成; -
Runtime 只有在收到
_dyld_objc_notify_register()
的通知后,才开始做真正的分类附加; -
因为某些分类定义可能在系统框架里过早加载,如果这时就处理分类可能错过关联主类(主类还没加载)。
所以:
👉 为了避免“分类加载时主类还没准备好”的问题,分类的附加操作被延后到:
-
_dyld_objc_notify_register()
调用完; -
并且是在 第一个 load_images() 时执行;
-
保证主类和分类都已在内存中,分类才能正确附加上。
总结成一句话就是:Runtime 为了保证分类附加的时机正确,会延迟处理一些分类,直到确保主类已经加载,分类数据也加载完成之后,再统一合并附加。
那么问题来了:既然这段代码是在map_images()这个函数里的,那怎么会在load_images()之后执行呢?
-
答案是当调用load_images()时,系统底层会调用 load_images() → 然后再次调用 map_images(),加载这个新镜像。
那是不是意味着第一次调用map_images时,对于分类没有进行任何操作呢?
-
答案是确实如此:分类是 编译时写入 Mach-O 文件的静态结构,但只有在运行时才会“附加到主类上”,这个过程我们称为 分类的附加(attach)。
-
map_images() 第一次执行时,不处理分类;只有在 Runtime 初始化完成后,再次调用 map_images() 时,才会在内部调用 load_categories_nolock() 去处理分类。
类的加载处理
这一段就是实现类的加载处理,实现非懒加载类
-
通过_getObjc2NonlazyClassList获取Mach-O的静态段__objc_nlclslist非懒加载类表(这是编译器标记的那些类,它们要在程序启动时就立刻初始化)
-
通过addClassTableEntry将非懒加载类插入类表,存储到内存,如果已经添加就不会载添加,需要确保整个结构都被添加
-
通过realizeClassWithoutSwift实现当前的类,因为前面 ③中的readClass读取到内存的仅仅只有地址+名称,类的data数据并没有加载出来
-
⚙️ 真正实现这个类,realizeClassWithoutSwift这一步很关键:
-
把类的元信息(ro -> rw 等)建立起来;
-
设置方法列表、属性、协议;
-
准备好实例大小、布局;
-
做好准备以便可以创建对象;
-
⚠️ 如果类实现了
+load
,此时load
方法也会被调用。(错误,load在load_images阶段调用)
-
关于懒加载类和非懒加载类:
如果实现了+load方法,就是非懒加载类,否则就是懒加载类
为什么实现load方法就会变成非懒加载类?:
因为
+load
是在类加载后立即执行的,如果类没有先实现(realize),就无法安全执行+load
,也就可能错过一些初始化逻辑,比如方法交换(swizzling)等。(load
方法会在load_images
调用)懒加载类在什么时候调用?:
只有在第一次使用(例如
alloc/init
)时才会加载。节省启动性能。
没有被处理的类,优化那些被侵犯的类
这一步负责处理在运行时动态解析的“未来类”(Future Classes),并确保它们被正确初始化
初始化未来类,如果是调试模式,强制初始化所有懒加载类(即使未被使用)
这里有一个误区:懒加载类VS未来类
未来类与懒加载类是两个完全独立的概念,并且未来类不可能是懒加载类,懒加载类不可能是未来类。
关键步骤
在上述流程中,有两个函数非常重要,分别是 readClass和realizeClassWithoutSwift
readClass
首先是readClass,readClass主要是读取类,在未调用该方法前,cls只是一个地址,执行该方法后,cls是类的名称
Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{const char *mangledName = cls->nonlazyMangledName();//获取类的名字
// printf("%s -哎呦不错!- %s \n",__func__,mangledName);// tcj 玩的 ----如果想进入自定义的类,自己加一个判断
// const char *TCJPersonName = "TCJPerson";
// if (strcmp(mangledName, TCJPersonName) == 0) {
// auto cj_ro = (const class_ro_t *)cls->data();
// printf("%s -- 哎呦不错!--%s\n", __func__,mangledName);
// }//当前类的父类中若有丢失的weak-linked类,则返回nilif (missingWeakSuperclass(cls)) {// No superclass (probably weak-linked). // Disavow any knowledge of this subclass.if (PrintConnecting) {_objc_inform("CLASS: IGNORING class '%s' with ""missing weak-linked superclass", cls->nameForLogging());}addRemappedClass(cls, nil);cls->setSuperclass(nil);return nil;}cls->fixupBackwardDeployingStableSwift();//判断是不是后期要处理的类//正常情况下,不会走到popFutureNamedClass,因为这是专门针对未来待处理的类的操作//通过断点调试,不会走到if流程里面,因此也不会对ro、rw进行操作Class replacing = nil;if (mangledName != nullptr) {if (Class newCls = popFutureNamedClass(mangledName)) {// This name was previously allocated as a future class.// Copy objc_class to future class's struct.// Preserve future's rw data block.if (newCls->isAnySwift()) {_objc_fatal("Can't complete future class request for '%s' ""because the real class is too big.",cls->nameForLogging());}//读取mach-o的data,设置ro、rw//经过调试,并不会走到这里class_rw_t *rw = newCls->data();const class_ro_t *old_ro = rw->ro();memcpy(newCls, cls, sizeof(objc_class));// Manually set address-discriminated ptrauthed fields// so that newCls gets the correct signatures.newCls->setSuperclass(cls->getSuperclass());newCls->initIsa(cls->getIsa());rw->set_ro((class_ro_t *)newCls->data());newCls->setData(rw);freeIfMutable((char *)old_ro->getName());free((void *)old_ro);addRemappedClass(cls, newCls);replacing = cls;cls = newCls;}}//判断是否类是否已经加载到内存if (headerIsPreoptimized && !replacing) {// class list built in shared cache// fixme strict assert doesn't work because of duplicates// ASSERT(cls == getClass(name));ASSERT(mangledName == nullptr || getClassExceptSomeSwift(mangledName));} else {if (mangledName) { //some Swift generic classes can lazily generate their namesaddNamedClass(cls, mangledName, replacing);//加载共享缓存中的类} else {Class meta = cls->ISA();const class_ro_t *metaRO = meta->bits.safe_ro();ASSERT(metaRO->getNonMetaclass() && "Metaclass with lazy name must have a pointer to the corresponding nonmetaclass.");ASSERT(metaRO->getNonMetaclass() == cls && "Metaclass nonmetaclass pointer must equal the original class.");}addClassTableEntry(cls);//插入表,即相当于从mach-O文件 读取到 内存 中}// for future reference: shared cache never contains MH_BUNDLEsif (headerIsBundle) {cls->data()->flags |= RO_FROM_BUNDLE;cls->ISA()->data()->flags |= RO_FROM_BUNDLE;}return cls;
}
readClass的流程主要分为以下几步:
① 通过mangledName获取类的名字
② 当前类的父类中若有丢失的weak-linked类(weak-linked 类(弱链接类)是一种特殊的类引用方式,允许应用在编译时引用某个类,但在运行时才检查该类是否实际存在),则返回nil
③ 通过addNamedClass将当前类添加到已经创建好的gdb_objc_realized_classes哈希表(名称映射表),该表用于存放所有类
④ 通过addClassTableEntry,将已经可用的类结构(已读入内存,已具备基本结构(isa、ro/rw 等))添加到allocatedClasses表(类哈希表),这个表在_objc_init中的runtime_init就初始化创建了
综上所述,readClass的主要作用就是将Mach-O中的类读取到内存,即插入表中(将 Mach-O 文件中解析出的 class 地址转换为真正可用的类对象,并放入类表中,为后续使用做好准备),但是目前的类仅有两个信息:地址以及名称,而mach-O的其中的data数据还未读取出来
realizeClassWithoutSwift
realizeClassWithoutSwift方法主要作用是实现类,将类的data数据加载到内存中,主要有以下几部分操作:
-
① 读取data数据,并设置ro、rw
-
② 递归调用realizeClassWithoutSwift完善继承链
-
③ 通过methodizeClass方法化类
读取 data 数据,并设置 ro、rw
这一步负责读取class的data数据,并将其强转为ro,以及rw初始化和ro拷贝一份到rw中的ro
-
ro 表示 readOnly,即只读,其在编译时就已经确定了内存,包含类名称、方法、协议和实例变量的信息,由于是只读的,所以属于Clean Memory,而Clean Memory是指加载后不会发生更改的内存
-
rw 表示 readWrite,即可读可写,由于其动态性,可能会往类中添加属性、方法、添加协议,但其实在rw中只有10%的类真正的更改了它们的方法,所以有了rwe,即类的额外信息。对于那些确实需要额外信息的类,分配一块 rwe(这是一个rw的可选扩展字段,不需要额外信息的类就不会分配 rwe),并将其滑入类中供其使用。其中rw就属于dirty memory,而 dirty memory是指在进程运行时会发生更改的内存,类结构一经使用就会变成 ditry memory,因为运行时会向它写入新数据,例如创建一个新的方法缓存,并从类中指向它
递归调用 realizeClassWithoutSwift 完善 继承链
递归调用realizeClassWithoutSwift完善继承链,并设置当前类、父类、元类的rw
-
递归调用 realizeClassWithoutSwift设置父类、元类
-
设置父类和元类的isa指向
-
通过addSubclass 和 addRootClass设置父子的双向链表指向关系,即父类中可以找到子类,子类中可以找到父类
在realizeClassWithoutSwift递归调用时,isa找到根元类之后,根元类的isa是指向自己,并不会返回nil,所以有以下递归终止条件,其目的是保证类只加载一次:
-
如果类不存在,则返回nil
-
如果类已经实现,则直接返回cls
通过 methodizeClass 方法化类
该方法会:
-
从
ro
中提取类本身的方法、属性、协议,存入rw
的动态列表(如methods
、properties
、protocols
)。 -
从运行时获取分类的方法、属性、协议,合并到
rw
的对应列表中。 -
确保
rw
包含所有可访问的成员,供运行时动态查询。
🙋问题来了:之前已经用ro给rw赋过值了,为什么还要再给rw写入方法、属性和协议呢?
🔑原因是当我们对rw赋值后:
此时
rw
仅包含ro
的只读数据副本,但未处理动态数据(如分类)。
rw
的方法列表、属性列表等动态数据字段(如methods
、properties
、protocols
)尚未填充。
methodizeClass
关于methodizeClass,它的实现源码如下:
static void methodizeClass(Class cls, Class previously)
{runtimeLock.assertLocked();
bool isMeta = cls->isMetaClass();auto rw = cls->data();auto ro = rw->ro();auto rwe = rw->ext();
// Methodizing for the first timeif (PrintConnecting) {_objc_inform("CLASS: methodizing class '%s' %s", cls->nameForLogging(), isMeta ? "(meta)" : "");}
// Install methods and properties that the class implements itself.method_list_t *list = ro->baseMethods();if (list) {prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls), nullptr);if (rwe) rwe->methods.attachLists(&list, 1);}
property_list_t *proplist = ro->baseProperties;if (rwe && proplist) {rwe->properties.attachLists(&proplist, 1);}
protocol_list_t *protolist = ro->baseProtocols;if (rwe && protolist) {rwe->protocols.attachLists(&protolist, 1);}
// Root classes get bonus method implementations if they don't have // them already. These apply before category replacements.if (cls->isRootMetaclass()) {// root metaclassaddMethod(cls, @selector(initialize), (IMP)&objc_noop_imp, "", NO);}
// Attach categories.if (previously) {if (isMeta) {objc::unattachedCategories.attachToClass(cls, previously,ATTACH_METACLASS);} else {// When a class relocates, categories with class methods// may be registered on the class itself rather than on// the metaclass. Tell attachToClass to look for those.objc::unattachedCategories.attachToClass(cls, previously,ATTACH_CLASS_AND_METACLASS);}}objc::unattachedCategories.attachToClass(cls, cls,isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
#if DEBUG// Debug: sanity-check all SELs; log method list contentsfor (const auto& meth : rw->methods()) {if (PrintConnecting) {_objc_inform("METHOD %c[%s %s]", isMeta ? '+' : '-', cls->nameForLogging(), sel_getName(meth.name()));}ASSERT(sel_registerName(sel_getName(meth.name())) == meth.name());}
#endif
}
就像之前所说,methodizeClass主要就是两步:
-
将
属性列表、方法列表、协议列表
等贴到rwe
中 -
附加
分类
中的方法
rwe的逻辑:
方法列表
加入rwe
的逻辑如下:
-
获取
ro
的baseMethods
-
通过
prepareMethodLists
方法排序 -
对
rwe
进行处理即通过attachLists
插入
prepareMethodLists
内部通过fixupMethodList
方法排序,排序的逻辑是根据selector address
排序。
attachToClass方法
在方法化类的方法methodizeClass中,还用到了几个比较重要的方法,比如attachToClass方法,它进行的操作是将分类添加到主类中
attachToClass中的外部循环是找到一个分类就会进到attachCategories一次,即找一个就循环一次,在这个方法里可以确定分类和对应主类,在attachCategories方法中会进行数据、协议、方法的添加
attachCategories方法
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,int flags)
{if (slowpath(PrintReplacedMethods)) {printReplacements(cls, cats_list, cats_count);}if (slowpath(PrintConnecting)) {_objc_inform("CLASS: attaching %d categories to%s class '%s'%s",cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");}
/** Only a few classes have more than 64 categories during launch.* This uses a little stack, and avoids malloc.** Categories must be added in the proper order, which is back* to front. To do that with the chunking, we iterate cats_list* from front to back, build up the local buffers backwards,* and call attachLists on the chunks. attachLists prepends the* lists, so the final result is in the expected order.*/constexpr uint32_t ATTACH_BUFSIZ = 64;method_list_t *mlists[ATTACH_BUFSIZ];property_list_t *proplists[ATTACH_BUFSIZ];protocol_list_t *protolists[ATTACH_BUFSIZ];
uint32_t mcount = 0;uint32_t propcount = 0;uint32_t protocount = 0;bool fromBundle = NO;bool isMeta = (flags & ATTACH_METACLASS);auto rwe = cls->data()->extAllocIfNeeded();
for (uint32_t i = 0; i < cats_count; i++) {auto& entry = cats_list[i];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);if (mlist) {if (mcount == ATTACH_BUFSIZ) {prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);rwe->methods.attachLists(mlists, mcount);mcount = 0;}mlists[ATTACH_BUFSIZ - ++mcount] = mlist;fromBundle |= entry.hi->isBundle();}
property_list_t *proplist =entry.cat->propertiesForMeta(isMeta, entry.hi);if (proplist) {if (propcount == ATTACH_BUFSIZ) {rwe->properties.attachLists(proplists, propcount);propcount = 0;}proplists[ATTACH_BUFSIZ - ++propcount] = proplist;}
protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);if (protolist) {if (protocount == ATTACH_BUFSIZ) {rwe->protocols.attachLists(protolists, protocount);protocount = 0;}protolists[ATTACH_BUFSIZ - ++protocount] = protolist;}}
if (mcount > 0) {prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,NO, fromBundle, __func__);rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);if (flags & ATTACH_EXISTING) {flushCaches(cls, __func__, [](Class c){// constant caches have been dealt with in prepareMethodLists// if the class still is constant here, it's fine to keepreturn !c->cache.isConstantOptimizedCache();});}}
rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
attachCategories
函数负责将分类(Category)中的方法、属性和协议合并到目标类中,确保分类内容在运行时生效。
auto rwe = cls->data()->extAllocIfNeeded();这一行是第一步,负责进行rwe的创建(因为现在要往本类
中添加属性、方法、协议
等,即对原来的 clean memory
要进行处理了,所以需要先对rwe进行初始化)
进入extAllocIfNeeded
方法的源码实现,判断rwe
是否存在,如果存在则直接获取,如果不存在则开辟
进入extAlloc
源码实现,即对rwe 0-1
的过程,在此过程中,就将本类的data
数据加载进去了
需要注意的几个点是:
-
逆序存储正序附加:通过倒序填充缓冲区,正序附加,确保后加载的分类内容优先生效。
-
分批处理:每满64个分类处理一次,平衡性能与内存占用。
-
方法覆盖:分类方法插入到类方法列表头部,实现“后编译的分类覆盖先编译的类或分类方法”。
attachLists方法
注意到在添加分类的方法时,是通过attachLists方法插入数据的,并且不止方法,属性和协议都是通过attachLists方法插入数据的。
其实,方法列表和属性列表都继承自entsize_list_tt,协议则是类似entsize_list_tt实现,都是二维数组。
那么我们来看看attachLists方法的实现:
从源码中可以看见,插入表存在三种情况:
-
①
多对多
: 如果当前调用attachLists
的list_array_tt
二维数组中有多个一维数组
-
通过
malloc
根据新的容量大小,开辟一个数组,类型是 array_t,通过array()获取 -
倒序遍历把原来的数据移动到容器的末尾
-
遍历新的数据移动到容器的起始位置
-
-
②
0对1
: 如果调用attachLists
的list_array_tt
二维数组为空且新增大小数目为 1
-
直接赋值
addedList
的第一个list
-
-
③
1对多
: 如果当前调用attachLists
的list_array_tt
二维数组只有一个一维数组
-
通过malloc开辟一个容量和大小的集合,类型是 array_t,即创建一个数组,放到array中,通过array()获取
-
由于只有一个一维数组,所以直接赋值到新
Array
的最后一个位置 -
循环遍历从数组起始位置存入新的list,其中array()->lists 表示首位元素位置
-
这就是为什么子类可以重写父类的方法,也是为什么分类可以重写类的方法,要加一个newlist
的目的是由于要使用这个newlist
中的方法,这个newlist
对于用户的价值要高,即优先调用
总结一下:attachLists
方法主要是将类
和分类
的数据加载到rwe
中
-
首先
加载本类的data数据
,此时的rwe没有数据为空
,走0对1
流程 -
当
加入一个分类
时,此时的rwe仅有一个list
,即本类的list
,走1对多
流程 -
再
加入一个分类
时,此时的rwe中有两个list
,即本类+分类的list
,走多对多
流程
分类
从上面的内容中,我们已经知道怎么把分类加到主类上了,接下来我们从分类的角度来分析一下分类
分类的本质
首先探索一下分类的本质。给TCJPerson定义分类TCJ
用Clang进行反编译得到C++代码,可以看到分类是存储在MachO
文件的__DATA
段的__objc_catlist
中,还可以看到TCJPerson
分类的结构。
可以发现TCJPerson改为_CATEGORY_TCJPerson_,并且被_category_t修饰,可以看到_category_t的结构
可以看见_category_t是个结构体,里面保存有名称(类的名字)、cls、对象方法列表、类方法列表、协议、属性
为什么分类的方法要将实例方法和类方法分开存呢?
分类有两个方法列表是因为分类是没有元分类的,分类的方法是在
运行时
通过attachToClass
插入到class
的
查看方法列表的反编译代码
可以看到有三个对象方法和一个类方法,格式为:sel+签名+地址
,和method_t
结构体一样
再看看属性
发现存在属性的变量名但是没有对应的set和get方法
分类的加载
在之前的部分提到了类有懒加载类和非懒加载类,二者的加载时机不同,那么如果涉及到分类,又是何时进行加载呢?
首先先回顾一下分类是如何进行加载的:
-
分类数据在attachCategories方法中加载,分类的加载遵循这样一个规则——越晚加进来,越在前面
-
在methodizeClass中,通过attatchToClass方法将分类数据添加到主类。methodizeClass方法中类的数据和分类数据分开处理,因为编译阶段已经确定好了
方法的归属位置
(即实例方法
存储在类
中,类方法
存储在元类
中),而分类
是后面才加进来的。
分类加载时机
关于分类的加载时机,有一条规律:只要有一个分类是非懒加载分类,那么所有的分类都会被标记位非懒加载分类。
因为加载一个分类,意味着类已经开辟了rwe,那么就不会再次懒加载,重新去处理主类了。
根据类和分类是否实现+load方法,我们可以得到4种情况:
-
非懒加载类+非懒加载分类:类在read_images中加载,而分类数据如之前所述在第一次进入read_images时,不会加载分类数据,所以此时无法合并分类。接着会再调用一次_load_images,随后再次调用map_images,这时再运行到read_images时就可以成功methodizeClass,合并分类。
-
非懒加载类与懒加载分类:类会在read_images中加载,而分类不会像非懒加载分类一样在read_images时合并到主类,而是会被暂时加入
_unattachedCategories
列表中,等待后续时机触发合并 -
懒加载类与懒加载分类:第一次发送消息给类时,类会实现并且分类会合并到主类上
-
懒加载类与非懒加载分类:懒加载类 + 非懒加载分类的数据加载,只要分类实现了load,会迫使主类提前加载,即主类强行转换为非懒加载类样式
在这四种情况中,分类的数据都是在load_images调用map_images时read_class()来加载到内存的,区别只在于类何时实现以及分类何时附加到主类
load_images
load_images方法的主要作用是加载镜像文件,其中有两个比较重要的方法:prepare_load_methods
(加载) 和 call_load_methods
(调用)
Load_images源码如下:
这里的加载所有分类,其实是在分类表中遍历,检查分类的主类是否已经实现,如果已经实现就把分类合并上去,否则放到 _unattachedCategories
中,这一步其实正是之前提到的对懒加载分类的处理。懒加载类实现了之后,在这里就会把分类合并上去
prepare_load_methods实现
如图所示,这个方法会把类及其父类和分类的load方法都放到数组中。
这里有两个方法:schedule_class_load、add_category_to_loadable_list
schedule_class_load方法
关于schedule_class_load方法:
这个方法根据类的继承链递归调用获取load,直到cls不存在才结束递归,这样做的目的是为了确保父类的load优先加载
add_class_to_loadable_list:
在schedule_class_load方法中调用了这个方法,此方法主要是将load方法
和cls类名
一起加到loadable_classes
表中
getLoadMethod:
在add_class_to_loadable_list方法中调用了这个方法
这个方法主要就是用来获取方法列表中sel为load的方法
add_category_to_loadable_list
主要是获取所有的非懒加载分类中的load方法
,将分类名+load方法
加入表loadable_categories
call_load_methods
这个方法有三步操作:
-
反复调用
类的+load
,直到不再有 -
调用一次
分类的+load
-
如果有类或更多未尝试的分类,则运行更多的
+load
方法的实现中主要就是两个方法:call_class_loads和 call_category_loads
call_class_loads主要加载类的load方法,而call_category_loads主要是加载一次分类的load方法
initalize分析
关于initialize,它通常是在某个类接收到第一条消息之前调用,它的调用链是lookUpImpOrForward
->realizeAndInitializeIfNeeded_locked
->initializeAndLeaveLocked
->initializeAndMaybeRelock
->initializeNonMetaClass
在initializeNonMetaClass
递归调用父类initialize
,然后调用callInitialize
void initializeNonMetaClass(Class cls)
{ASSERT(!cls->isMetaClass());
Class supercls;bool reallyInitialize = NO;
// Make sure super is done initializing BEFORE beginning to initialize cls.// See note about deadlock above.supercls = cls->getSuperclass();if (supercls && !supercls->isInitialized()) {initializeNonMetaClass(supercls);}// Try to atomically set CLS_INITIALIZING.SmallVector<_objc_willInitializeClassCallback, 1> localWillInitializeFuncs;{monitor_locker_t lock(classInitLock);if (!cls->isInitialized() && !cls->isInitializing()) {cls->setInitializing();reallyInitialize = YES;
// Grab a copy of the will-initialize funcs with the lock held.localWillInitializeFuncs.initFrom(willInitializeFuncs);}}if (reallyInitialize) {// We successfully set the CLS_INITIALIZING bit. Initialize the class.// Record that we're initializing this class so we can message it._setThisThreadIsInitializingClass(cls);
if (MultithreadedForkChild) {// LOL JK we don't really call +initialize methods after fork().performForkChildInitialize(cls, supercls);return;}for (auto callback : localWillInitializeFuncs)callback.f(callback.context, cls);
// Send the +initialize message.// Note that +initialize is sent to the superclass (again) if // this class doesn't implement +initialize. 2157218if (PrintInitializing) {_objc_inform("INITIALIZE: thread %p: calling +[%s initialize]",objc_thread_self(), cls->nameForLogging());}
// Exceptions: A +initialize call that throws an exception // is deemed to be a complete and successful +initialize.//// Only __OBJC2__ adds these handlers. !__OBJC2__ has a// bootstrapping problem of this versus CF's call to// objc_exception_set_functions().
#if __OBJC2__@try
#endif{callInitialize(cls);
if (PrintInitializing) {_objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",objc_thread_self(), cls->nameForLogging());}}
#if __OBJC2__@catch (...) {if (PrintInitializing) {_objc_inform("INITIALIZE: thread %p: +[%s initialize] ""threw an exception",objc_thread_self(), cls->nameForLogging());}@throw;}@finally
#endif{// Done initializing.lockAndFinishInitializing(cls, supercls);}return;}else if (cls->isInitializing()) {// We couldn't set INITIALIZING because INITIALIZING was already set.// If this thread set it earlier, continue normally.// If some other thread set it, block until initialize is done.// It's ok if INITIALIZING changes to INITIALIZED while we're here, // because we safely check for INITIALIZED inside the lock // before blocking.if (_thisThreadIsInitializingClass(cls)) {return;} else if (!MultithreadedForkChild) {waitForInitializeToComplete(cls);return;} else {// We're on the child side of fork(), facing a class that// was initializing by some other thread when fork() was called._setThisThreadIsInitializingClass(cls);performForkChildInitialize(cls, supercls);}}else if (cls->isInitialized()) {// Set CLS_INITIALIZING failed because someone else already // initialized the class. Continue normally.// NOTE this check must come AFTER the ISINITIALIZING case.// Otherwise: Another thread is initializing this class. ISINITIALIZED // is false. Skip this clause. Then the other thread finishes // initialization and sets INITIALIZING=no and INITIALIZED=yes. // Skip the ISINITIALIZING clause. Die horribly.return;}else {// We shouldn't be here. _objc_fatal("thread-safe class init in objc runtime is buggy!");}
}
callInitialize是一个普通的消息发送
关于initialize:
-
initialize
在类或者其子类的第一个方法被调用前(发送消息前)调用 -
只在类中添加
initialize
但不使用的情况下,是不会调用initialize
-
父类的
initialize
方法会比子类先执行 -
当子类未实现
initialize
方法时,会调用父类initialize
方法;子类实现initialize
方法时,会覆盖父类initialize
方法 -
当有多个分类都实现了
initialize
方法,会覆盖类中的方法,只执行一个(会执行最后被加载到内存中的分类的方法)