KVO底层原理

基础概念 KVO(Key-Value Observing,键值观察)是 Apple 基于 KVC 实现的一种观察者模式,允许对象监听另一个对象特定属性的变化。当被观察对象的属性值发生改变时,观察者会收到通知。 // 注册观察 [person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil]; // 接收通知 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey, id> *)change context:(void *)context { if ([keyPath isEqualToString:@"name"]) { NSLog(@"name changed: %@ → %@", change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]); } } // 移除观察 - (void)dealloc { [person removeObserver:self forKeyPath:@"name"]; } KVO 的底层实现原理:isa-swizzling KVO 的核心实现机制是 isa-swizzling(isa 指针替换)。当对象被添加观察者时,Runtime 会在运行时动态创建该对象所属类的一个子类,并将对象的 isa 指针指向这个新的子类。 完整流程 flowchart TD A["addObserver:forKeyPath:"] --> B["Runtime 动态创建子类NSKVONotifying_ClassName"] B --> C["重写被观察属性的 setter 方法"] B --> D["重写 class 方法返回原始类"] B --> E["重写 dealloc 方法"] B --> F["重写 _isKVOA 方法返回 YES"] C --> G["将对象的 isa 指向NSKVONotifying_ClassName"] G --> H["当属性值改变时"] H --> I["调用 willChangeValueForKey:"] I --> J["调用原始 setter 方法"] J --> K["调用 didChangeValueForKey:"] K --> L["通知所有观察者"] 第一步:动态创建子类 当首次对某个类的实例调用 addObserver:forKeyPath:options:context: 时,Runtime 会: ...

May 2, 2026

RxSwift源码导读

RxSwift 是 ReactiveX 在 Swift 上的实现,提供了一套完整的响应式编程框架。它通过 Observable 序列和操作符组合的方式,简化了异步编程、事件处理和数据流管理。本文基于 v6.10.2(2026年3月发布)源码进行分析。 一、整体架构 RxSwift 项目采用模块化设计,包含五个独立模块: graph TB subgraph "核心层" A["RxSwift核心响应式框架"] end subgraph "扩展层" B["RxCocoaUIKit/AppKit绑定"] C["RxRelayRelay封装"] end subgraph "工具层" D["RxBlocking阻塞操作"] E["RxTest测试工具"] end B --> A C --> A D --> A E --> A 源码目录结构: RxSwift/ ├── Concurrency/ # 并发工具(AsyncLock等) ├── Disposables/ # 资源释放(DisposeBag、CompositeDisposable等) ├── Extensions/ # Swift扩展 ├── Observables/ # Observable实现(Create、Map、Filter、Merge等) ├── Observers/ # Observer实现(AnonymousObserver、ObserverBase等) ├── Platform/ # 平台适配(原子操作、锁等) ├── Schedulers/ # 调度器(MainScheduler、SerialDispatchQueueScheduler等) ├── Subjects/ # Subject实现(PublishSubject、BehaviorSubject等) ├── SwiftSupport/ # Swift语言支持 ├── Traits/ # 特化序列(Single、Completable、Maybe、Infallible) ├── Observable.swift # Observable基类 ├── ObservableType.swift # Observable协议 ├── ObserverType.swift # Observer协议 ├── Event.swift # 事件枚举 ├── Disposable.swift # Disposable协议 ├── Binder.swift # UI绑定 ├── AnyObserver.swift # 类型擦除Observer └── Reactive.swift # .rx命名空间 语言构成: 纯 Swift 实现,100% Swift 代码。 ...

May 2, 2026

TCA(The Composable Architecture)详解

什么是TCA TCA(The Composable Architecture)是由Point-Free团队开发的Swift架构框架,受Elm和Redux启发,为Apple平台提供了一套完整的应用构建工具,涵盖状态管理、副作用处理、模块化组合以及测试支持。 GitHub仓库:swift-composable-architecture 适用平台:iOS、macOS、iPadOS、visionOS、tvOS、watchOS 适用UI框架:SwiftUI、UIKit TCA的核心理念是:将应用状态、变更动作、副作用和依赖显式化,构建可组合、可测试的单向数据流架构。整体架构与Swift的语言特性(值语义、async/await、Observation、SwiftUI)紧密结合,强调小而纯的Reducer组合与明确的副作用管理。 为什么需要TCA 在日常iOS开发中,TCA解决了以下核心问题: 状态管理:使用简单的值类型管理应用状态,支持在多个屏幕间共享状态,使一个屏幕的修改能立即反映到另一个屏幕 组合性:将大型功能拆分成可独立开发、独立测试的小组件,提取到独立模块后可轻松组合回完整功能 副作用处理:以最可测试和可理解的方式让应用的某些部分与外部世界通信 测试能力:不仅能测试单个功能,还能编写由多个部分组成的功能的集成测试,以及端到端测试来验证副作用如何影响应用 人机工程学:用尽可能少的概念和组成部分,通过简单的API实现以上所有目标 TCA的架构渊源 TCA的设计灵感主要来自Elm Architecture和Redux,同时与MVI架构思想有诸多相似之处,但在Swift生态中进行了大量增强: 特性 Elm/Redux/MVI TCA 核心思想 Action -> State -> View 单向数据流 同样的单向数据流,但深度融合Swift特性 副作用处理 自定义Middleware或自行实现 内置Effect系统(基于async/await) 依赖注入 需手动实现 内置@Dependency系统 测试支持 需自己构建 提供TestStore,支持穷尽式断言 组合能力 基础支持 Scope、forEach等丰富的Reducer组合手段 导航管理 需自己实现 内置状态化导航系统 状态共享 需自己实现 内置@Shared系统 TCA的核心概念 TCA包含以下核心概念: Feature:一个功能模块的整体单元,由@Reducer标注,内含State、Action和Reducer State:描述功能所需的数据 Action:功能中可能发生的所有事件 Reducer:接收State和Action,更新State并产生副作用(Effect) Store:持有State、驱动Reducer执行的运行时 Scope:从父Store中切出子Store,实现Feature的模块化组合 Effect:封装异步操作的副作用 Dependency:可替换的外部依赖 它们之间的协作关系如下: flowchart LR View -->|"send(Action)"| Store Store -->|"传递 State + Action"| Reducer Reducer -->|"更新后的 State"| Store Reducer -->|"返回 Effect"| Store Store -->|"State 变化驱动"| View Store -.->|"scope"| ChildStore["子Store"] State(状态) State是描述功能模块在某一时刻全部数据的值类型结构体: ...

May 2, 2026

启动优化-load方法

+load方法是ObjC运行时在类被加载时自动调用的方法,所有+load方法在main函数之前同步执行,会阻塞启动。 问题分析 +load方法的特点: 特性 说明 调用时机 main函数之前,ObjC Runtime初始化时 调用顺序 父类 → 子类 → Category 线程 主线程,同步执行 影响 直接阻塞启动 // 问题代码:+load中执行耗时操作 @implementation HeavyModule + (void)load { // 耗时操作会阻塞启动 [self setupDatabase]; [self preloadResources]; [self registerServices]; } @end +load与+initialize的区别 特性 +load +initialize 调用时机 main之前 类首次使用时 调用次数 只调用一次 可能多次(子类触发) 是否阻塞启动 是 否 线程安全 是 是 调用顺序 父类→子类→Category 父类→子类 更详细的对比请参考:+load与+initialize的区别 优化方案 方案1:使用+initialize替代 将+load中的逻辑迁移到+initialize,延迟到类首次使用时执行: @implementation HeavyModule // 优化前 + (void)load { [self setupDatabase]; } // 优化后 + (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 延迟到类首次使用时执行 [self setupDatabase]; }); } @end 注意:+initialize可能被子类触发多次调用,需要使用dispatch_once保证只执行一次。 ...

May 2, 2026

编译优化-Xcode构建系统

Xcode 的构建系统由多个组件协同完成,从 Cmd+B 到生成 .app 经历了完整的任务图构建、依赖分析、并行调度流程。理解这套体系是后续一切优化的基础。 构建系统组成 Xcode 10 之后默认采用 Swift Build System(基于 llbuild),整体架构如下: flowchart TD A[Xcode IDE] --> B[XCBBuildServiceDaemon] B --> C[Build Description任务图 JSON] C --> D[llbuild调度内核] D --> E1[Swift Driver / swiftc] D --> E2[clang] D --> E3[ld / ld-prime] D --> E4[actool / ibtool / 脚本] E1 & E2 & E3 & E4 --> F[产物缓存] 组件 职责 XCBBuildService 独立进程,为 Xcode / xcodebuild 提供构建 RPC 服务 Build Description 根据 Scheme、Target、Build Settings 生成的任务图 llbuild 通用构建引擎,执行任务图、做增量与并行调度 swift-driver Swift 编译驱动,生成 frontend job 并驱动 swiftc clang / swift-frontend 真正的编译器前端 ld / ld-prime 链接器 XCBBuildService Xcode 和 xcodebuild 都是客户端,真正的构建逻辑在 XCBBuildService 这个独立进程。它通过 XPC 接收请求,生成并执行任务。社区工具 XCBBuildServiceProxy 利用这一架构在中间插一层代理,实现自定义构建(Bazel、远程执行等)。字节 BitSky、Tuist Cloud 都基于此类模式。 ...

May 2, 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

启动优化-二进制重排

二进制重排是一种通过重新排列二进制文件中函数顺序来减少启动时Page Fault的优化技术。 基本原理 Page Fault问题 iOS使用虚拟内存管理,讨论Page Fault时需要先区分两层“内存”: 虚拟地址空间:进程看到的是一段连续的虚拟地址。App启动时,dyld会把Mach-O的__TEXT、__DATA等段映射到进程的虚拟地址空间里。这里的“映射”只是建立虚拟地址和文件偏移的关系,不代表整段二进制代码已经全部进入物理内存。 物理内存:CPU真正执行代码时,需要对应虚拟页背后有可用的物理页。代码页通常是文件映射页,第一次访问某个尚未驻留在物理内存中的代码页时,会触发Page Fault。内核再从App二进制文件中读取这个页的内容,填充到物理页,并更新页表。 所以,更准确地说:程序代码按页映射到虚拟地址空间;启动过程中实际执行到某个函数时,才会按需把该函数所在的代码页调入物理内存。Page Fault本身不是异常崩溃,而是虚拟内存按需调页的正常机制,只是冷启动时如果触发太多文件读取,会增加启动耗时。 默认情况下,编译器按照链接顺序排列函数,导致启动时调用的函数可能分散在不同的代码页中: 优化前(启动函数分散在不同代码页): ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ Page 1 │ │ Page 2 │ │ Page 3 │ │ Page 4 │ │ func_A │ │ func_X │ │ func_B │ │ func_C │ │ ... │ │ ... │ │ ... │ │ ... │ └────────┘ └────────┘ └────────┘ └────────┘ 启动调用 A→B→C 会触达 Page 1, 3, 4。 如果这些代码页还没有驻留在物理内存中,就可能产生3次Page Fault。 二进制重排的效果 通过重排,将启动时调用的函数集中排列在相邻的代码页中: ...

May 8, 2026

APM-数据模型与上报

APM 的上报系统不只是“把 JSON 发到服务端”。它要解决三件事:数据如何建模、事件如何关联、移动端如何可靠且低成本地上传。这一层设计不好,后面的服务端聚合、B 端下钻和告警都会变成补丁工程。 一、设计目标 数据模型与上报层要同时满足: 目标 说明 可关联 页面、操作、网络、错误、性能现场能被 Session 串起来 可聚合 同类问题能按 fingerprint、版本、机型、页面、接口聚合 可重试 弱网、后台、进程退出后数据不轻易丢 可去重 重试不会造成重复统计 可采样 高频数据不会压垮端侧、网络和服务端 可脱敏 敏感数据在端侧就被过滤或掩码 可演进 Schema 版本升级后新旧 SDK 可以共存 二、RUM 对象模型 推荐用 RUM 层级组织端侧事件: Session View Action Resource Error LongTask / Freeze Custom Event 对象 含义 示例 Session 一次用户前台使用会话 打开 App 到退后台 View 一个页面实例 首页、详情页、支付页 Action 一次用户操作 点击下单、搜索、提交表单 Resource 一次资源请求 HTTP 请求、图片资源、WebView 资源 Error 一次错误 Crash、业务错误、网络错误、JS 错误 LongTask / Freeze 一次长任务或卡顿 主线程阻塞 800ms RUM 模型的价值是把“点状指标”变成“用户故事”: ...

May 7, 2026

iOS无障碍树详解

前言 打开 iPhone 的"设置 → 辅助功能 → 旁白(VoiceOver)",再用三指在屏幕上滑动,iPhone 就会逐个读出当前界面上的每个控件:"登录按钮,双击以激活"、"用户名,文本框,已填写 ancheng"、"价格,¥99"。这背后支撑 VoiceOver、开关控制(Switch Control)、语音控制(Voice Control)、全键盘访问(Full Keyboard Access)、Xcode UITest、AI Agent 识屏 等一系列能力的,是一棵并行于 UIView 视图树的 无障碍树(Accessibility Tree,AX Tree)。 对大多数 iOS 工程师来说,无障碍树是"黑盒":设了 isAccessibilityElement = true、填了 accessibilityLabel,然后就不管了。但一旦遇到以下问题,就不得不深入理解它的工作机制: 为什么 UITest 里用 XCUIElement 定位不到某个按钮? 为什么打开 VoiceOver 后 App 明显变卡? 为什么一个自绘的 CALayer 按钮在 VoiceOver 下无法被朗读? 为什么 UILabel 嵌在 UIStackView 里焦点顺序是乱的? SwiftUI 的 .accessibilityElement(children: .combine) 到底合并了什么? Accessibility Inspector 里看到的树结构从哪儿来? AI Agent(如 iOS 上的 Claude、ChatGPT Mac 客户端)是如何"看到"屏幕的? 本文从操作系统层的 AX 架构讲起,逐层拆解:AX Server 守护进程、UIKit 中的 UIAccessibility 协议、UIAccessibilityElement 类、UIAccessibilityContainer 协议、焦点排序算法、SwiftUI 的 AX 实现、辅助技术的工作原理、性能优化、UITest 与 AI 识屏原理、调试工具与最佳实践。 ...

May 2, 2026

Mach-O的链接、装载与库

本文深入介绍iOS/macOS开发中的Mach-O文件格式、动态库与静态库,帮助理解程序的链接、装载过程,以及Rebase/Bind的底层原理。 一、基础概念 在深入学习之前,先了解几个核心概念。 1.0 从源代码到可执行文件 一段源代码要变成可执行文件,需要经历以下几个阶段: flowchart TB A["源代码 (.m/.swift)"] --> B["预处理展开宏、处理#import/#include"] B --> C["编译源代码 → 汇编代码"] C --> D["汇编汇编代码 → 目标文件(.o)"] D --> E["链接目标文件 + 库 → 可执行文件"] E --> F["可执行文件"] 1.1 什么是符号(Symbol) 符号是程序中函数、变量、类等实体的名称标识。当你写下一个函数调用时,编译器需要知道这个函数在哪里,符号就是用来定位它的。 // 这段代码中包含多个符号 void myFunction(void) { // 定义了符号 _myFunction NSLog(@"Hello"); // 引用了外部符号 _NSLog } int globalVar = 10; // 定义了符号 _globalVar 符号分为两类: 已定义符号:在当前编译单元(即当前源文件编译生成的.o文件)中有具体代码或数据定义的符号(如上例中的 _myFunction 和 _globalVar) 未定义符号:当前编译单元只是引用,实际定义在其他编译单元或库中的符号(如上例中的 _NSLog),需要在链接时解析 1.2 什么是链接(Linking) 链接是将多个编译后的目标文件(.o)组合成一个可执行文件的过程。链接器的核心工作是符号解析——把所有"未定义符号"与其他编译单元中的"已定义符号"关联起来。 flowchart LR subgraph 输入 A["main.o已定义: _main未定义: _foo"] B["foo.o已定义: _foo未定义: _NSLog"] C["Foundation已定义: _NSLog"] end A --> D["链接器符号解析"] B --> D C --> D D --> E["可执行文件所有符号已解析"] 符号解析的过程 链接器会遍历所有输入的目标文件和库,执行以下步骤: ...

May 2, 2026