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

APM-业界方案

本文对比目前 iOS APM 领域主流的七大方案,从接入形态、原理、能力深度、适用场景四个维度深入剖析。每个方案都包含核心技术原理与关键源码解读,帮助读者做"自研还是三方、自研该参考哪个"的选型决策。 一、全景对比 quadrantChart title "iOS APM 方案对比" x-axis "低侵入" --> "高侵入" y-axis "浅能力" --> "深能力" quadrant-1 "专业级" quadrant-2 "旗舰级" quadrant-3 "入门级" quadrant-4 "高性价比" MetricKit: [0.05, 0.45] Firebase: [0.15, 0.4] "Xcode Organizer": [0.02, 0.35] Sentry: [0.3, 0.65] Bugly: [0.25, 0.5] "Matrix (微信)": [0.6, 0.88] "Slardar (字节)": [0.65, 0.95] "Hertz (美团)": [0.5, 0.8] "Alita (阿里 mPaaS)": [0.55, 0.85] 二、MetricKit + Xcode Organizer(Apple 官方) 2.1 定位 Apple 自 iOS 13 推出的官方性能监控框架,完全系统侧实现,SDK 零开销,是所有方案的基线。 ...

May 7, 2026

编译优化-Explicit Modules

Apple 从 Xcode 15 开始在 Swift 上引入 Explicit Modules,Xcode 16 在 C/C++/Objective-C 上全面铺开,Xcode 17 进一步默认启用并与细粒度依赖追踪结合。这是近五年 Apple 构建系统最重要的一次变革,直接影响到大部分项目的编译模型。 模块(Module)基础 为什么需要模块 C 家族语言的 #include 机制有两个致命问题: 文本替换:头文件是纯文本替换,每个源文件都要重新解析一遍所有包含的头文件 宏污染:先引入的头文件宏会影响后续头文件的行为,没有隔离 Clang 在 2012 年引入 Clang Modules,用一个预编译的二进制模块(.pcm)替代文本包含,同一 module 在一次构建里只解析一次。Swift 的 .swiftmodule 从设计之初就是模块化的。 flowchart LR A["源文件"] -->|"include 头文件"| B["Foundation.h 文本"]; B --> C["每次都重新解析"]; D["源文件"] -->|"@import Foundation"| E["Foundation.pcm 预编译"]; E --> F["一次解析,多次复用"]; 模块的组成 类型 载体 描述 Clang Module .pcm C/OC 模块的序列化 AST Swift Module .swiftmodule Swift 模块的接口 + SIL Swift Interface .swiftinterface 可被不同 Swift 版本解析的文本接口 Module Map module.modulemap 描述哪些头文件构成某个 module Implicit Modules 的问题 原理 在 Xcode 16 之前,Clang Modules 以 隐式 方式工作: ...

May 5, 2026

IGListKit源码导读

IGListKit 是 Instagram(Meta)开源的一款数据驱动的 UICollectionView 框架,旨在构建快速、灵活的列表。它的核心思想是将每个数据对象映射为独立的 Section Controller,通过高效的 O(n) Diff 算法自动计算数据变化并应用最小化更新,避免手动调用 performBatchUpdates 或 reloadData。本文基于 v5.2.0 源码(2026年2月发布)进行分析。 一、整体架构 IGListKit 采用分层架构,核心由三大模块构成: graph TB subgraph "使用层" A["UIViewController"] end subgraph "适配层" B["IGListAdapter"] C["IGListAdapterDataSource"] end subgraph "控制层" D["IGListSectionController"] E["IGListBindingSectionController"] end subgraph "更新层" F["IGListAdapterUpdater"] G["IGListUpdateCoalescer"] H["IGListUpdateTransaction"] end subgraph "Diff层 (IGListDiffKit)" I["IGListDiffPaul Heckel算法"] J["IGListDiffable协议"] end subgraph "视图层" K["UICollectionView"] end A --> B B --> C B --> D D --> E B --> F F --> G F --> H H --> I I --> J B --> K 源码目录结构: ...

May 2, 2026

MVP架构详解

什么是MVP MVP(Model-View-Presenter)是一种将展示逻辑与视图分离的架构模式,起源于上世纪90年代。MVP通过引入Presenter层来解决MVC中Controller职责过重(Massive ViewController)的问题。MVP的核心思想是让View变得"被动"(Passive View),所有的展示逻辑都由Presenter处理,View只负责UI的展示和事件的转发。 MVP的结构 graph LR subgraph View["View (ViewController)"] V1["- 只负责UI展示- 将事件转发给Presenter- 持有Presenter强引用"] end subgraph Presenter["Presenter"] P1["- 持有View的弱引用(通过协议)- 持有Model的引用- 处理所有业务逻辑- 决定何时更新View"] end subgraph Model["Model"] M1["数据和业务逻辑"] end View -->|"用户事件"| Presenter Presenter -.->|"weak引用调用协议方法更新UI"| View Presenter <-->|"获取/更新数据"| Model MVP的三个组件 Model(模型层) 与MVC中的Model相同,负责数据和业务逻辑。 // Model struct User { let id: Int let name: String let email: String let avatarURL: URL? } // Service protocol UserServiceProtocol { func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) func updateUser(_ user: User, completion: @escaping (Result<Void, Error>) -> Void) } class UserService: UserServiceProtocol { func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) { // 网络请求实现 } func updateUser(_ user: User, completion: @escaping (Result<Void, Error>) -> Void) { // 更新用户实现 } } View(视图层) 在MVP中,View是"被动的"(Passive View),它: ...

May 2, 2026

WebView底层原理

前言 iOS 上的 WebView 目前以 WKWebView 为事实标准。它对外表现得像一个 UIView 子类,但内部其实对接着整个 WebKit 引擎——一个由多个独立进程协作的庞大系统。日常开发中遇到的那些困扰:为什么 Web 页白屏不会把 App 带崩?为什么 Cookie 和 App 自身的 NSHTTPCookieStorage 总是对不上?为什么 JS 调用 Native 永远是异步?为什么 NSURLProtocol 拦不到 WKWebView 的请求?这些问题单看 API 都得不到答案,必须下潜到 WebKit 源码里才能看清楚。 好在 WebKit 是 Apple 官方开源的项目,仓库在 github.com/WebKit/WebKit。本文基于其 Source/ 目录下的公开源码,按"源码地图 → 多进程架构 → 各进程职责 → 全链路串联 → 工程启示"的顺序,把 WKWebView 从创建到渲染一帧的底层机制梳理清楚。 一、先看源码地图 阅读源码前,先在脑子里建立目录结构。以 WebKit/WebKit 仓库的 Source/ 目录为例: Source/ ├── JavaScriptCore/ # JS 引擎(解释器、JIT 编译器、GC) ├── WebCore/ # 渲染引擎核心(DOM、CSSOM、Layout、Paint) ├── WebKit/ # 多进程框架 + Cocoa 层 API │ ├── UIProcess/ # 主进程侧(App 进程) │ │ ├── API/Cocoa/ # WKWebView.mm、WKProcessPool.mm 等 │ │ ├── API/ios/ # WKWebViewIOS.mm │ │ ├── ios/ # WKContentView.mm、WebPageProxyIOS.mm │ │ └── ... │ ├── WebProcess/ # 渲染进程侧(WebContent) │ ├── NetworkProcess/ # 独立网络进程 │ ├── GPUProcess/ # 独立 GPU 进程 │ └── Shared/ # 跨进程共享的数据结构、IPC 消息定义 └── WTF/ # 基础容器、线程、字符串工具库 日常 iOS 开发能看到的 WKWebView,对应的实现就在 Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm。它是一个 Objective-C++ 文件,内部只是持有一个 C++ 的 WebPageProxy 对象,几乎所有实际工作都转发给它。为什么要这样设计?原因就藏在下一节要讲的多进程架构里。 ...

May 2, 2026

启动优化-Rebase与Bind

Rebase和Bind是Pre-main阶段的重要步骤,用于修正程序中的指针地址。ObjC类、方法、协议等元数据越多,需要修复的指针越多,启动时间越长。 基本概念 由于ASLR(Address Space Layout Randomization)技术,App每次启动时加载到内存的地址都是随机的,因此需要进行地址修正。关于Rebase和Bind的底层原理,可以参考Mach-O的链接、装载与库。 Rebase(重定位) 修正指向Mach-O内部的指针,将编译时的地址加上ASLR偏移量。 Bind(绑定) 修正指向Mach-O外部的指针,查找符号表,绑定到正确的外部符号地址。 flowchart LR subgraph compile["编译时 (Mach-O文件)"] A1["内部指针: 0x1000(指向内部数据)"] A2["外部符号: _objc_msgSend(待绑定)"] end subgraph runtime["运行时 (内存中)"] B1["内部指针: 0x1000 + slide(Rebase修正)"] B2["外部符号: 实际地址(Bind绑定)"] end A1 -->|"Rebase"| B1 A2 -->|"Bind"| B2 问题分析 以下因素会增加Rebase/Bind的工作量: 因素 影响 ObjC类数量 每个类都有元数据需要修正 Swift类数量 Swift类在Apple平台也会生成ObjC兼容的元数据,同样需要修正 ObjC方法数量 方法列表中的指针需要修正 Category数量 Category的方法列表需要绑定 C++虚函数 虚函数表中的指针需要绑定 全局变量 指向其他符号的全局变量需要绑定 关于缓存机制: 系统动态库:Rebase/Bind 操作已在 dyld shared cache 中预先完成,不会在 App 启动时执行 App 动态库:dyld 3 的 Launch Closure 会缓存 Rebase/Bind 的元数据信息(哪些指针需要修正、修正方式等),但 Rebase/Bind 操作本身仍需每次启动时执行,因为 ASLR slide 每次启动都不同 因此,减少 ObjC 类、Category、C++ 虚函数等仍然是有效的优化手段。 ...

May 2, 2026

Swift 面试:并发、async/await 与 Actor 源码解析

Swift 面试:并发、async/await 与 Actor 源码解析 Swift Concurrency 面试经常从 async/await 问起,但真正的重点是:Task 是结构化并发的执行单元,await 是显式暂停点,Actor 通过隔离和串行执行器保护状态,Sendable 则约束跨并发边界传递的数据。 这篇文章按面试题展开,把结论落到 Swift 标准库和编译器源码里的 Actor、GlobalActor、MainActor、AsyncLet、TaskGroup、Sendable 和 ActorIsolation。 面试高频问题 async/await 和 GCD 的关系是什么? await 到底表示线程阻塞还是任务暂停? Task、async let、TaskGroup 有什么区别? Actor 如何保证数据隔离? MainActor 是什么,为什么 UI 更新要回到 MainActor? GlobalActor 和普通 Actor 有什么区别? Sendable 解决什么问题? Task.detached 为什么要慎用? Actor 之间调用为什么需要 await? Swift Concurrency 是编译器机制还是运行时机制? 30 秒回答版 Swift Concurrency 是编译器、标准库和运行时共同完成的并发模型。 async/await 不是对 GCD 的简单语法糖。await 表示当前异步任务可能在这里暂停,把执行权交还给调度器;等异步结果可用时,再从 continuation 恢复。它不应该理解成“阻塞当前线程等待”。 Actor 是一种受隔离保护的引用类型。Actor 内部可变状态只能在 actor 隔离域内访问;跨 actor 访问必须异步,编译器会插入隔离检查。运行时通过 executor 保证同一 actor 的隔离状态不会被多个任务同时进入。 ...

June 16, 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

APM-产品与架构设计

APM 系统的产品设计不能从“要采哪些指标”开始,而应该从“谁要用这些数据解决什么问题”开始。iOS APM 的产品形态可以分成 C 端真实用户体验、B 端研发治理平台两面;技术实现则分成 iOS SDK、数据平台、Web 控制台三层。 一、产品边界 APM 的 C 端不是一个给用户看的页面,而是运行在用户设备里的监控能力;APM 的 B 端才是研发、测试、架构、运维、产品、客服使用的控制台。 视角 C 端 B 端 用户 App 真实用户 研发、测试、架构、运维、产品、客服 形态 iOS SDK,无感运行 Web 控制台、告警、工单、报表 核心体验 不打扰、不拖慢、不泄露隐私 快速发现、下钻、归因、分发、验证 主要风险 SDK 自身引发卡顿、崩溃、耗电、流量 指标口径混乱、告警疲劳、无法定位 成功标准 数据真实且采集成本低 问题能被稳定治理,发布劣化能被拦截 因此产品设计要同时回答两类问题: C端:真实用户到底经历了什么? B端:团队如何用这些数据把问题修掉? 二、用户角色 APM B 端不是只给研发看,不同角色需要不同入口。 角色 典型问题 核心视图 客户端研发 我负责的模块有没有新 Crash、卡顿、FOOM 我的 Issue、堆栈详情、会话时间线 后端研发 某接口在真实用户侧是否变慢、失败率是否升高 网络资源详情、Trace 关联、接口维度大盘 测试/QA 灰度版本是否比线上版本变差 版本对比、灰度监控、回归报告 架构/技术负责人 App 整体质量趋势如何,哪个团队拖后腿 质量大盘、模块排行、SLO 产品/业务负责人 性能是否影响转化、留存、下单 页面秒开率、关键路径成功率、业务漏斗 客服/运营 单个用户为什么反馈无法使用 用户查询、Session 轨迹、错误上下文 设计 B 端时不要把所有人塞进同一个大盘。首页可以共用,但下钻路径要按角色分流。 ...

May 7, 2026