objc4 类加载、read_images 与类实现化

objc4 类加载、read_images 与类实现化 本文解释 Objective-C runtime 从 dyld 通知镜像映射,到读取类、处理 future class、realize class、methodize class、附着 category 的主干流程。 重点源码来自 runtime/objc-runtime-new.mm、objc-runtime-new.h、objc-os.mm、objc-class.mm 及相关测试。 作用 核心结构 关键流程 测试视角 一、这条链路的作用 把 Mach-O 中的静态元数据变成运行时可用的类 编译器把类、元类、方法列表、协议列表、属性列表和 ivar 布局写入 Mach-O 的 Objective-C sections。 这些数据起初主要是只读的 class_ro_t。runtime 加载镜像时要登记类名、修正 selector/protocol/class 引用,并在必要时分配可写的 class_rw_t。 延迟成本,同时保证消息发送能看到正确结构 大量类可以保持未 realized 状态,直到非懒加载、+load、消息发送、Swift 桥接或显式 API 需要它们。 realize 时才连接父类和元类、修正 ivar 偏移、初始化 cache、复制运行时标志,并把方法列表整理成可查找的形态。 简化理解:read_images 是“发现并登记”;readClass 是“读一个类声明并处理 future/remap”;realizeClassWithoutSwift 是“把类接入运行时继承图”;methodizeClass 是“准备方法/协议/属性并合并 category”。 二、核心类/结构和关键字段 结构 关键字段 作用 objc_class superclass、cache、bits 类对象本体。bits 初始可指向 class_ro_t,realize 后指向 class_rw_t 并夹带 fast flags。 class_ro_t flags、instanceStart、instanceSize、name、baseMethods、ivars 编译期只读描述。包含类名、方法、协议、属性、ivar 布局和 root/meta/future/realized 等 ABI 标志。 class_rw_t flags、ro_or_rw_ext、firstSubclass、nextSiblingClass 运行时可写状态。保存 realized/initialized/constructing 等状态,并把类接入子类链。 class_rw_ext_t ro、methods、properties、protocols 当类需要扩展列表时分配。category 附着后,方法/属性/协议列表通常进入这里。 method_list_t entsizeAndFlags、count、方法条目 方法列表。加载时会 uniquing selector,必要时排序并标记 fixed-up。 category_t cls、instanceMethods、classMethods、protocols、instanceProperties 分类元数据。目标类已 realized 时立即附着,否则暂存到 unattached categories。 header_info classlist、nlclslist、catlist、selrefs、protocolrefs 一个 Mach-O 镜像的 ObjC 元数据索引。objc-os.mm 中 addHeader 负责创建或取出它。 类数据从 ro 切换到 rw struct objc_class : objc_object { Class superclass; cache_t cache; class_data_bits_t bits; // 未实现时指向 class_ro_t,实现后指向 class_rw_t }; struct class_rw_t { uint32_t flags; // RW_REALIZED、RW_FUTURE、RW_INITIALIZED 等 explicit_atomic<uintptr_t> ro_or_rw_ext; Class firstSubclass; Class nextSiblingClass; }; class_data_bits_t::safe_ro() 允许在并发 realize 场景下安全取出 ro。 setData() 则在 realization 或构造阶段把 objc_class::bits 改成 rw 指针,并设置 FAST_IS_RW_POINTER。 ...

June 1, 2026

Objective-C 消息发送 objc_msgSend 与慢速查找入口

objc4 runtime dispatch Objective-C 消息发送:objc_msgSend 与慢速查找入口 Objective-C 的方法调用本质上是一次以 receiver 和 SEL 为键的动态派发。 在 arm64 上,objc_msgSend 先用少量汇编完成 nil/tagged pointer 判断和方法缓存查找; 只有缓存未命中时,才进入 C++ 慢路径 lookUpImpOrForward,沿类层级查找、触发动态解析、填充缓存或进入消息转发。 入口:runtime/Messengers.subproj/objc-msg-arm64.s 慢路径:runtime/objc-runtime-new.mm 缓存:runtime/objc-cache.mm 测试:test/msgSend.m 目录 作用 实现原理总览 核心结构与约定 快速路径 慢速查找入口 消息转发 测试观察点 作用 objc_msgSend(id self, SEL _cmd, ...) 是 Objective-C 实例方法和类方法调用的公共派发入口。 编译器把 [obj method:arg] 形式的调用降低为对 objc_msgSend 的调用;真正执行哪个 IMP 由运行时根据接收者的实际类、选择子和当前方法缓存决定。 动态派发 同一个 SEL 可以在不同类上命中不同 IMP,体现多态。 缓存加速 大多数热路径只扫描类的 cache_t,命中后直接尾调用 IMP。 语义兼容 nil 接收者返回零值;tagged pointer 通过标签映射到伪类继续派发。 扩展入口 未找到实现时支持 +resolve... 和完整的 forwarding 机制。 ...

June 1, 2026

objc4 方法缓存 cache_t / bucket_t 技术讲解

objc4 方法缓存 cache_t / bucket_t 技术讲解 本文基于当前仓库中的 runtime/objc-cache.mm、 runtime/objc-runtime-new.h、runtime/objc-runtime-new.mm 以及 cache flush 相关测试,说明 Objective-C runtime 如何把一次慢速方法查找变成后续的快速 objc_msgSend cache hit。 一、方法缓存的作用 Objective-C 消息发送以 (Class, SEL) 为核心输入。完整方法查找需要处理类实现、父类链、 动态方法解析、转发和分类变更等逻辑,代价高于一次普通函数调用。cache_t 是挂在每个 objc_class 上的 IMP cache,用 SEL 作为 key,缓存最终应该调用的 IMP。 加速热路径命中时 objc_msgSend 不进入 runtime 慢路径,直接由 bucket 中的 IMP 跳转。 缓存继承结果子类未实现某 selector 时,也可以把父类找到的 IMP 缓存在子类 cache 中。 支持动态变更分类加载、添加方法、交换实现等操作会触发 cache flush,避免继续调用旧 IMP。 cache 不是方法列表本身,也不是权威数据源。它是一个可丢弃、可重建的性能结构;扩容时甚至不会搬迁旧条目, 而是让后续消息重新填充热点 selector。 二、核心结构 bucket_t:一个 selector 到 IMP 的槽位 字段 _sel 保存 selector,_imp 保存编码后的 IMP。arm64 上 IMP 在前,其他架构通常 SEL 在前,以贴合汇编 fast path 和指针认证需求。 sel() 以 relaxed atomic 读取 selector。空槽的 selector 为 0。 imp(base, cls) 读取并解码 IMP。arm64e 可使用 ptrauth,部分配置使用 class 指针 XOR,未编码配置则直接返回。 set<Atomic, Encoded>() 写入一个 bucket。写入顺序被精心安排,保证无锁读取者不会看到“新 SEL + 旧 IMP”的错误组合。 cache_t:每个类上的哈希表 _bucketsAndMaybeMask 保存 buckets 指针,有些 64 位配置还把 mask 打包进高位;preoptimized cache 也用低位 marker 复用这个字段。 _mask / 内联 mask bucket 数量始终为 2 的幂,mask = capacity - 1,哈希后用按位与得到起始槽。 _occupied 已占用动态 bucket 数。插入成功后递增;换表时清零。 _flags 保存若干快速路径标记,例如 metaclass、C++ ctor/dtor、默认 alloc 或 RR 等,具体位定义在 objc-runtime-new.h。 关键方法 insert、eraseNolock、destroy、copyCacheNolock、maybeConvertToPreoptimized、preoptFallbackClass。 preopt_cache_t:dyld shared cache 预构建的常量 cache fallback_class_offset 预优化查找未覆盖时继续查找的类,相对当前 class 地址保存。 shift / mask 用于计算预优化 entries 下标。capacity() 返回 mask + 1。 occupied 预优化 cache 中的有效条目数量。 has_inlines 标记是否存在内联 selector;方法列表变更时需要更谨慎地禁用这类 cache。 entries[] 每项保存 selector offset 和 IMP offset,而不是直接保存完整指针。 三、实现原理 动态 cache 是一个开放寻址哈希表。cache_hash(sel, mask) 用 selector 地址和 mask 得到起始槽; 冲突时调用 cache_next 继续探测。不同架构的探测方向不同:部分架构递增并使用 end marker, arm64 递减并通过 mask 回绕。 ...

June 1, 2026

Objective-C Category 装载与附加机制

objc4 runtime Category 装载与方法、协议、属性附加 Category 的本质不是“修改类结构体”,而是在镜像加载和类实现化时,把分类携带的方法列表、协议列表、属性列表合并到目标类或元类的可变列表视图中。objc4 同时要处理未实现类、stub class、dyld 预附加列表、方法缓存失效和 +load 顺序。 runtime/objc-runtime-new.mm runtime/objc-runtime-new.h runtime/objc-loadmethod.mm test/category.m 等测试 目录 作用 实现原理总览 核心结构和字段 关键流程 带注释核心代码 测试揭示的行为 作用 扩展类行为 分类把实例方法挂到类,把类方法挂到元类,让现有类获得新 selector 或覆盖既有 selector。 扩展反射信息 分类的协议和属性参与 class_copyProtocolList、class_getProperty 等运行时查询。 保持加载顺序 后加载的分类应优先被方法查找看到,所以列表附加采用“前插”。 支持动态镜像 bundle、共享缓存、stub class 和未实现类都能在不同时间点接收分类。 关键结论:分类附加不是把方法逐个复制进类定义,而是把 method_list_t、protocol_list_t、property_list_t 这类“列表”挂到 class_rw_ext_t 的数组视图前端。 实现原理总览 Mach-O 镜像 __objc_catlist / __objc_catlist2 中保存 category_t *。runtime 通过 header_info 知道分类来自哪个镜像。 → 分类分流 目标类未实现则暂存到 unattachedCategories;目标类已实现则立即 attachCategories。 → 列表前插 attachLists 把新增列表放在旧列表前面,方法查找和反射会先看到分类列表。 依据:load_categories_nolock 分流逻辑见 runtime/objc-runtime-new.mm:3585-3717;列表前插见 runtime/objc-runtime-new.h:2021-2105。 ...

June 1, 2026

Swift 面试:编译流程、Runtime 与元数据源码解析

Swift 面试:编译流程、Runtime 与元数据源码解析 Swift 面试里问编译流程和 Runtime,通常不是想听一串名词,而是想看你能不能把 源码 -> AST -> SIL -> 优化 -> IRGen -> LLVM -> Runtime Metadata 串起来。 这篇文章按面试题展开,把答案落到 Swift 编译器源码里的 SILGen、SILOptimizer、IRGen、Metadata、Value Witness Table、Protocol Witness Table 和 HeapObject。 面试高频问题 Swift 源码从 .swift 到机器码经历哪些阶段? AST、SIL、LLVM IR 分别负责什么? 为什么 Swift 需要 SIL,而不是直接生成 LLVM IR? SILGen 做什么?SILOptimizer 做什么? IRGen 为什么要生成 Metadata 和 Witness Table? Swift Runtime Metadata 保存了哪些信息? Value Witness Table 是什么? Protocol Witness Table 和 class vtable 有什么区别? class、struct、enum 在 Runtime 表示上有什么差异? HeapObject 和 Metadata 有什么关系? 30 秒回答版 Swift 编译流程可以简化为: ...

June 16, 2026

Objective-C +load 调度:load_images 与 call_load_methods

objc4 runtime +load 调度:从 load_images 到 call_load_methods 这页解释 objc4 如何在 dyld 映射镜像后发现、排队并调用 Objective-C 的 +load。 重点是类与分类的顺序、父类优先、递归和重入处理,以及 runtimeLock 与 loadMethodLock 的边界。 入口:dyld callback 队列:loadable_classes / loadable_categories 调度:call_load_methods 测试:load*.m 目录 作用 实现原理 核心结构和关键函数 关键流程 带注释代码片段 测试体现的语义 作用 +load 是 Objective-C 运行时在类或分类被装入进程时主动调用的类方法。 它不依赖消息发送触发,也早于普通的 +initialize。objc4 的任务不是简单遍历所有方法并调用, 而是在 dyld 映射镜像、runtime 完成类注册和分类附着后,按照语言语义和装载依赖稳定地调用。 装载时机 dyld 通过 _dyld_objc_register_callbacks 注册 runtime 回调。镜像映射后先走 map_images,随后 load_images 处理该镜像中的非懒加载类和分类。 顺序语义 类 +load 必须父类优先;所有当前可调用的类 +load 先于分类 +load;分类必须等宿主类已经完成自己的 +load。 重入安全 +load 内部可能 dlopen 新镜像,再次进入 load_images。objc4 让内层调用只排队,真正调用由最外层 call_load_methods 收尾。 实现原理 objc4 将 +load 分成“发现”和“调用”两阶段。发现阶段需要持有 runtimeLock, 因为它要读取和实现类、解析分类、查找元类方法列表。调用阶段释放 runtimeLock, 只持有递归的 loadMethodLock,因为用户代码可能执行任意 Objective-C 行为并重新映射镜像。 ...

June 1, 2026

+initialize 懒初始化与并发状态机

objc4 runtime 懒初始化 并发等待 fork safety +initialize 懒初始化与并发状态机 +initialize 是 Objective-C 运行时在类第一次被真正使用时触发的类级初始化钩子。 objc4 的实现重点不是“调用一个方法”,而是用每个类的递归初始化锁、元类上的状态位、线程本地的初始化列表和 fork 子进程保护规则,保证父类优先、单线程执行、并发可等待、重入不死锁。 目录 作用 实现原理 核心状态字段 入口与触发 并发状态机 关键流程 测试依据 作用 +initialize 的语义是:在某个类或其子类第一次收到会触发初始化的消息前,运行时先给该类发送一次 +initialize。 这让类可以延迟建立全局状态、注册方法、准备缓存或执行只依赖本类的初始化逻辑。 懒触发 objc_getClass() 只查找类,不触发初始化;普通消息发送、部分运行时查询、autorelease 返回值路径和弱引用相关路径可能需要先完成初始化。 父类优先 子类开始初始化前,运行时递归确保父类已经初始化,避免类层次中间被并发线程插入造成死锁。 一次完成 同一类只有一个线程真正发送 +initialize;其他线程要么等待,要么在同一初始化线程重入时直接放行。 实现原理 objc4 把 +initialize 做成一个按需进入的并发状态机:消息查找路径发现目标类未初始化时,先把类 realize,再释放 runtimeLock,进入 initializeNonMetaClass()。 后者用父类递归、每类递归锁、元类状态位和线程本地初始化列表协调所有线程。真正执行用户代码的线程设置 INITIALIZING,发送 +initialize,最后在 @finally 中切换到 INITIALIZED;其他线程通过同一把类锁等待状态稳定。 这个设计的核心约束是单调性:类状态只从“未初始化”前进到“初始化中”,再前进到“已初始化”。即使 +initialize 抛异常,状态机也会完成收尾;即使发生重入,只有当前初始化线程能绕过等待;即使 fork 发生在多线程初始化期间,子进程也只允许 trivial 初始化继续,其他自定义初始化会主动终止。 核心类/结构状态字段 初始化状态存在类对象的元类标志位里。源码注释明确约束:初始时 RW_INITIALIZING 和 RW_INITIALIZED 都未设置;初始化中只设置前者;完成后清除前者并设置后者;两者永不同时为真,且 RW_INITIALIZED 一旦设置就不会清除。 ...

June 1, 2026

objc4 Selector / SEL 注册、唯一化与方法名查找

Selector / SEL 注册、唯一化与方法名查找 在 objc4 中,SEL 的关键价值不是承载一段复杂对象,而是把方法名字符串 “驻留”为全进程唯一的指针。注册、镜像加载 fixup、方法列表排序、方法查找和消息缓存都建立在这个事实上: 同名 selector 最终拥有同一个地址,因此比较 selector 可以退化为一次指针比较。 作用 统一方法名身份 编译器、Mach-O 镜像、动态注册 API 和运行时内部方法列表都可能产生方法名。selector 唯一化后,同一个方法名只对应一个 SEL。 让查找足够便宜 方法列表和缓存查找不需要反复比较字符串。命中条件基本都是 method.name == sel 或 bucket.sel == _cmd。 支撑 dyld 预优化 共享缓存中的 selector 可以由 dyld 提供内建唯一值;未优化镜像在 _read_images 中被 runtime 修正到唯一值。 对外头文件把 SEL 声明为不透明类型:typedef struct objc_selector *SEL;。 但在当前实现中,runtime 会把 selector 当成方法名 C 字符串指针使用,sel_getName() 直接把 SEL 转回 const char *。 核心结构、函数与字段 SEL 公开为不透明 selector 指针,实际唯一值是某个方法名字符串的地址。来源:runtime/objc.h。 namedSelectors ExplicitInitDenseSet<const char *>,保存非 dyld 内建 selector 的唯一字符串指针。来源:runtime/objc-sel.mm。 selLock 保护 selector 表。sel_registerName() 自行加锁;镜像加载和方法列表 fixup 常在外层持锁后调用 sel_registerNameNoLock()。 _sel_searchBuiltins() 在支持预优化时向 dyld 查询共享缓存内建 selector,优先返回 dyld 已唯一化的值。 sel_registerName() 注册并返回 selector;若已存在则返回既有唯一值;会复制传入字符串。 sel_lookUpByName() 只查找,不创建。未注册返回 NULL,测试 test/sel.m 明确覆盖该行为。 method_t::name 方法条目的 selector 字段。方法列表 fixup 后,这个字段应是唯一化的 SEL。 cache_t / bucket_t 消息缓存以 SEL 为 key,桶保存 SEL + IMP,hash 直接来自 selector 地址。 实现原理 selector interning 的核心是“先按名字查找唯一槽,再把槽里的字符串地址作为 SEL 返回”。 对 dyld 共享缓存内建 selector,runtime 先问 dyld;对普通 selector,runtime 使用 namedSelectors 保存唯一字符串。 ...

June 1, 2026

objc4 Protocol 结构、注册与查询

目录 1. Protocol 的作用 2. 核心结构 3. read_images 读取协议 4. remap 与重复协议 5. 查询与 conforms 判断 6. 动态创建与注册 7. 测试体现的行为 8. 总结 1. Protocol 的作用 在 objc4 中,Protocol * 并不是一个只有名字的轻量句柄,而是运行时管理的一类 Objective-C 对象。 它承载协议名、继承的协议列表、必需/可选方法列表、实例/类属性列表等元数据。类、分类和协议自身都可以引用这些对象。 对外 API 位于 runtime/runtime.h 的 “Working with Protocols” 区域,例如 objc_getProtocol、objc_copyProtocolList、protocol_conformsToProtocol、 protocol_getMethodDescription、objc_allocateProtocol 与 objc_registerProtocol。 runtime/Protocol.mm 中的 @implementation Protocol 则把老式 Objective-C 消息转接到这些 C API。 关键心智模型:编译器把协议写进镜像的 __objc_protolist 等段;runtime 在 _read_images 中发现并安装协议; 后续所有查询都尽量通过“协议名 => 胜出的协议对象”来回到唯一的、当前有效的定义。 2. 核心结构 当前实现中的底层结构定义在 runtime/objc-runtime-new.h。公开的 Protocol * 在实现里经常通过 newprotocol(p) 转成 protocol_t * 使用。 // runtime/objc-runtime-new.h,保留关键字段并加注释 typedef uintptr_t protocol_ref_t; // 尚未 remap 的 protocol_t * struct protocol_t : objc_object { const char *mangledName; // 协议的运行时名字;Swift v1 可能是 mangled 名 struct protocol_list_t *protocols; // 该协议继承/组合的其他协议 method_list_t *instanceMethods; // @required 实例方法 method_list_t *classMethods; // @required 类方法 method_list_t *optionalInstanceMethods; // @optional 实例方法 method_list_t *optionalClassMethods; // @optional 类方法 property_list_t *instanceProperties; // required instance properties uint32_t size; // 磁盘结构大小;用于判断尾部字段是否存在 uint32_t flags; // fixed-up、canonical 等运行时标志 // 以下字段不是所有磁盘协议结构都有,访问前必须看 size。 const char **_extendedMethodTypes; // 扩展类型编码 const char *_demangledName; // Swift demangled 名称缓存 property_list_t *_classProperties; // 类属性列表 }; struct protocol_list_t { uintptr_t count; // 历史原因:count 是指针宽度 protocol_ref_t list[0]; // 变长数组;元素可能需要 remap }; 关键字段与方法 mangledName 全局协议表的 key。objc_getProtocol 先按原名查,再查 dyld 预优化表,再尝试 Swift v1 mangled 等价名。 protocols 协议继承关系。protocol_conformsToProtocol 会递归遍历它;protocol_copyProtocolList 只复制直接继承项。 四个方法列表 按 required/optional 与 instance/class 分成四组。protocol_copyMethodDescriptionList 只返回当前协议直接声明的方法,不包含父协议。 size 兼容小协议或旧 ABI 的关键字段。protocolSmall.m 手工构造较小结构,验证访问尾部字段前必须判定字段是否存在。 flags 高位用于 PROTOCOL_FIXED_UP_* 与 PROTOCOL_IS_CANONICAL,标记是否已修正以及共享缓存中的 canonical 定义。 Protocol 类 runtime/Protocol.h 只暴露不可直接使用的 @interface Protocol; Protocol.mm 中 -conformsTo:、-name、-isEqual: 分别调用 protocol_conformsToProtocol、protocol_getName 与 protocol_isEqual。 ...

June 1, 2026

Tagged Pointer 对象实现讲解

objc4 runtime internals Tagged Pointer 对象 Tagged Pointer 是一种“指针即对象”的表示法:对象没有堆内存,类信息和 payload 直接编码在指针位中。objc4 对它提供了创建、取 tag、取 payload、取类、消息发送、禁用和混淆等完整路径。 目录 作用 实现原理 核心宏、结构、函数 关键流程 测试视角 源码依据 作用 减少堆分配 小整数、短字符串、日期、索引路径等小对象可以直接塞进指针。NSNumber 的小整数就是典型例子,test/taggedNSPointers.m 验证了 NSNumber numberWithInt:1234 是 tagged pointer。 保留对象语义 虽然指针不指向对象内存,它仍能接收消息、查询类、作为字典 key/value,并通过 Foundation/CF 桥接。运行时把 tag 映射到真实 Class 后,继续走常规方法缓存查找。 绕开生命周期表 test/taggedPointers.m 验证 retain、release、autorelease、weak store/load 对 tagged pointer 不产生真实引用计数或弱引用表负担。它们本质上是立即值,不需要释放。 **一句话:**Tagged Pointer 把“对象身份”改成“位编码值”。运行时需要做的,是在所有需要对象元数据的地方识别它,并把 tag 翻译成类。 实现原理 objc4 在 64 位平台启用 tagged pointer。runtime/objc-config.h 中 SUPPORT_TAGGED_POINTERS 对 __LP64__ 为 1;非 64 位平台关闭。tag 标记位在大多数平台使用 MSB,macOS x86_64 使用 LSB。arm64 还使用 split tagged pointer 布局。 ...

June 1, 2026