当前位置: 首页 > news >正文

【iOS】类和分类的加载过程

目录

前言

_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的流程:

  1. 在recursiveInitialization方法中会调用bool hasInitializers = this->doInitialization(context);这个方法是用来判断image是否已加载。

  2. 同时doInitialization这个方法会调用doImageInit和doModInitFunctions(context),这两个方法会进入libSystem框架里调用libSystem_initializer方法,最后就会调用_objc_init方法

  3. _objc_init会调用_dyld_objc_notify_register将map_images、load_images、unmap_image传入dyld方法registerObjCNotifiers

  4. 在registerObjCNotifiers方法中,我们把_dyld_objc_notify_register传入的map_images赋值给sNotifyObjCMapped,将load_images赋值给sNotifyObjCInit,将unmap_image赋值给sNotifyObjCUnmapped

  5. 在registerObjCNotifiers方法中,我们将传参赋值后就开始调用notifyBatchPartial()

  6. notifyBatchPartial方法中会调用(*sNotifyObjCMapped)(objcImageCount, paths, mhs);触发map_images方法

  7. dyld的recursiveInitialization方法在调用完bool hasInitializers = this->doInitialization(context)方法后,会调用notifySingle()方法

  8. 在notifySingle()中会调用(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());上面我们将load_images赋值给了sNotifyObjCInit,所以此时就会触发load_images方法

  9. sNotifyObjCUnmapped会在removeImage方法里触发,字面理解就是删除Image(映射的镜像文件)

所以有以下结论:map_images是先于load_images调用,即先map_images,再load_images

dyld与Objc关联

结合以上分析和之前对dyld的分析,可以总结出dyld与Objc的关联就是通过回调完成的:

  • dyld中注册回调函数,可以理解为 添加观察者

  • objcdyld注册,可以理解为发送通知

  • 触发回调,可以理解为执行通知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的流程:

  1. 条件控制进行的一次加载——创建表(类的哈希表)

  2. 修复预编译阶段的@selector的混乱问题

  3. 错误混乱的类处理

  4. 修复重映射一些没有被镜像文件加载进来的类

  5. 修复一些消息

  6. 当类里面有协议时:readProtocol读取协议

  7. 修复没有被加载的协议

  8. 分类处理

  9. 类的加载处理

  10. 没有被处理的类,优化那些被侵犯的类

现在看不懂没关系,接下来我们来一个部分一个部分讲清楚这个函数做了些什么

创建表

这个部分只有在第一次进入函数时会执行,他会创建一个用来存放类的哈希表,这个表里存放的类是那些不在共享缓存且已命名的类,无论类是否实现,容量是类数量的4/3。

修复预编译阶段的@selector的混乱问题

这段代码的作用简单地说可以用一句话概括:把编译时生成的“假的 selector 地址”,统一替换成运行时真正注册过的“合法 selector 地址”。

为什么会混乱?

@selector(foo) 编译器生成的是静态字符串指针(在 __objc_selrefs 中)。

但在 runtime 调用方法时,必须保证 SEL 是统一注册过、地址唯一的。

如果不统一注册,那么同样的 @selector(foo) 在多个模块中可能是不同地址,会导致消息发送、isEqual, NSStringFromSelector 等出现 bug。

打一个通俗的比方:

你可以把 Selector (@selector(...)) 想象成你给朋友写的一封信的收件人地址

  • 编译器阶段写的是:“张三,某市某区某号”,但这是你随手写的,可能不是张三真正的地址。

  • 运行时系统需要确认:“张三”真实住在哪里?(selector 实际对应哪块内存)

  • 如果你不校正这个地址,信可能寄不到正确的人手中,或者你认为两个“张三”其实是不同的人。

所以这个方法做的事就是:

  1. 翻出所有你写的“张三地址”(selector 指针)

  2. 拿去“户籍局”查一查(通过 sel_registerNameNoLock 注册)

  3. 如果地址不对,系统会换成正确的地址

错误混乱的类处理

这里其实就是把Mach-O文件中的所有类都取出来,再遍历进行处理。

在readClass之前,cls只是一个地址,在执行完readClass之后,原始的类地址才能被解析为一个有效的类对象。

除此之外,这里还会检查共享缓存中的类是否已被覆盖,如果覆盖就需要重新处理。

对于未来类,如果解析后的类地址发生变化(即if (newCls != cls && newCls)),就记录到resolvedFutureClasses数组,这些类需要后续非懒加载初始化(立即分配可读写数据class_rw_t)。

这段代码的作用通俗来说,可以想象成你在整理一个杂乱的工具箱,确保所有工具都放在正确的位置,并且能正常使用。具体来说:

  1. 检查是否有“外人改动” 先看一眼工具箱,确认有没有人偷偷替换了里面的工具(比如通过插件或特殊配置修改了系统默认的类)。

  2. 逐个翻找工具箱的每个分区 打开每一个小格子(每个程序依赖的库或框架),看看里面装的是什么。

  3. 判断是否需要整理 如果某个分区已经是整理好的(预优化过的类),就跳过不管;如果被改动过,才需要手动处理。

  4. 拿出所有工具的“设计图纸” 从分区的某个固定位置(Mach-O文件的__objc_classlist段)掏出一叠设计图,这些图纸对应着程序里所有定义好的类。

  5. 核对每张设计图 把图纸一张张展开检查:

    • 如果是正常的图纸(普通类),直接贴上标签(类名),放到对应位置。

    • 如果发现某张图纸写的是“临时占位符”(未来类),比如之前不知道这个类具体长什么样,现在终于找到真正的图纸了,就立刻替换掉占位符,并记录下来:“这几个类需要马上组装好,不能偷懒”。

  6. 标记需要立刻组装的工具 把那些替换过占位符的类单独记在小本本上(resolvedFutureClasses数组),后续要立刻把它们拼装成完整的工具(分配内存、关联方法等),而不是等到第一次用的时候才临时拼装(懒加载)。

修复重映射一些没有被镜像文件加载进来的类

主要是将未映射的ClassSuper 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

这一步负责读取classdata数据,并将其强转为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指向

  • 通过addSubclassaddRootClass设置父子的双向链表指向关系,即父类中可以找到子类,子类中可以找到父类

realizeClassWithoutSwift递归调用时,isa找到根元类之后,根元类的isa是指向自己,并不会返回nil,所以有以下递归终止条件,其目的是保证类只加载一次

  • 如果类不存在,则返回nil

  • 如果类已经实现,则直接返回cls

通过 methodizeClass 方法化类

该方法会:

  1. ro 中提取类本身的方法、属性、协议,存入 rw 的动态列表(如 methodspropertiesprotocols)。

  2. 从运行时获取分类的方法、属性、协议,合并到 rw 的对应列表中。

  3. 确保 rw 包含所有可访问的成员,供运行时动态查询。

🙋问题来了:之前已经用ro给rw赋过值了,为什么还要再给rw写入方法、属性和协议呢?

🔑原因是当我们对rw赋值后:

  • 此时 rw 仅包含 ro只读数据副本,但未处理动态数据(如分类)。

  • rw 的方法列表、属性列表等动态数据字段(如 methodspropertiesprotocols)尚未填充。

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的逻辑如下:

  • 获取robaseMethods

  • 通过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方法的实现:

从源码中可以看见,插入表存在三种情况:

  • 多对多: 如果当前调用attachListslist_array_tt二维数组中有多个一维数组

    • 通过malloc根据新的容量大小,开辟一个数组,类型是 array_t,通过array()获取

    • 倒序遍历把原来的数据移动到容器的末尾

    • 遍历新的数据移动到容器的起始位置

  • 0对1: 如果调用attachListslist_array_tt二维数组为空且新增大小数目为 1

    • 直接赋值addedList的第一个list

  • 1对多: 如果当前调用attachListslist_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方法,会覆盖类中的方法,只执行一个(会执行最后被加载到内存中的分类的方法)

http://www.lryc.cn/news/603065.html

相关文章:

  • LNMP架构+wordpress实现动静分离
  • Cacti RCE漏洞复现
  • 四、计算机组成原理——第1章:计算机系统概述
  • 可扩展架构模式——微服务架构最佳实践应该如何去做(方法篇)
  • 《汇编语言:基于X86处理器》第10章 结构和宏(2)
  • linux命令grep的实际应用
  • 在虚拟机ubuntu上修改framebuffer桌面不能显示图像
  • 1.vue体验
  • Android 媒体播放开发完全指南
  • Ansible提权sudo后执行报错
  • 电脑开机不显示网卡的原因
  • selenium 特殊场景处理
  • 刘润探展科大讯飞WAIC,讯飞医疗AI该咋看?
  • CSP-J 2022_第三题逻辑表达式
  • 技术工具箱 |五、一个避免头文件重复引用的 Python 脚本
  • 嵌入式Linux:注册线程清理处理函数
  • Zynq SOC FPGA嵌入式裸机设计和开发教程自学笔记:硬件编程原理、基于SDK库函数编程、软件固化
  • 第五章:进入Redis的Hash核心
  • 设计模式实战:自定义SpringIOC(亲手实践)
  • 深度研究——OpenAI Researcher Agent(使用OpenAI Agents SDK)
  • EAP(基于事件的异步编程模式)
  • 如何解决pip安装报错ModuleNotFoundError: No module named ‘papermill’问题
  • 时间数字转换器TDC的FPGA方案及核心代码
  • 将 NI Ettus USRP X410 的文件系统恢复出厂设置
  • C#:基于 EF Core Expression 的高性能动态查询构建实战 —— 面向大数据量环境的查询优化方案(全是干货,建议收藏)
  • Day22-二叉树的迭代遍历
  • 代码随想录Day32:动态规划(斐波那契数、爬楼梯、使用最小花费爬楼梯)
  • 10:00开始面试,10:06就出来了,问的问题有点变态。。。
  • Jmeter 性能测试监控之ServerAgent
  • AT89C 系列单片机知识点总结