VIPER架构详解

什么是VIPER VIPER是一种更加细粒度的架构模式,由Mutual Mobile公司提出。VIPER将应用分为五个层次: View(视图):负责UI展示 Interactor(交互器):负责业务逻辑 Presenter(展示器):负责视图逻辑 Entity(实体):数据模型 Router(路由):负责页面导航 VIPER的核心思想是单一职责原则,每个组件只负责一件事。 VIPER的结构 flowchart TB subgraph Module["Module"] View["View"] Presenter["Presenter"] Interactor["Interactor"] Router["Router"] Entity["Entity"] View -->|用户事件| Presenter Presenter -->|更新UI| View Presenter -->|业务请求| Interactor Interactor -->|业务结果| Presenter Presenter -->|导航请求| Router Interactor -->|读写数据| Entity Router -.->|创建并导航到其他模块| View end 数据流向说明 用户交互流:View → Presenter → Interactor → Presenter → View 导航流:View → Presenter → Router → 新的 View 数据流:Interactor → Entity(数据存储)→ Interactor → Presenter → View(数据展示) VIPER的五个组件 View(视图层) View负责: 展示UI 接收用户输入并传递给Presenter 实现Presenter定义的协议 // View协议 protocol UserListViewProtocol: AnyObject { func showLoading() func showUsers(_ users: [UserViewModel]) func showError(_ message: String) } // ViewController实现 class UserListViewController: UIViewController, UserListViewProtocol { var presenter: UserListPresenterProtocol! private var users: [UserViewModel] = [] override func viewDidLoad() { super.viewDidLoad() presenter.viewDidLoad() } func showUsers(_ users: [UserViewModel]) { self.users = users tableView.reloadData() } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { presenter.didSelectUser(at: indexPath.row) } } Interactor(交互器) Interactor负责: ...

May 2, 2026

SDWebImage源码导读

SDWebImage 是 iOS 生态中历史最悠久、最流行的异步图片下载缓存库,由 Olivier Poitrey 在 2009 年创建,如今由社区维护。截至 2026 年 4 月,GitHub Star 数已超过 25.6k,最新稳定版本为 5.21.7(2026 年 2 月)。5.21 系列引入了 HDR 图片编解码支持,并持续完善线程安全和 iOS 26 的兼容性。本文基于 master 分支源码(Objective-C 实现)进行分析。 一、整体架构 SDWebImage 采用协议导向 + 责任链的模块化设计,核心由五大子系统构成:Manager 协调层、Cache 缓存层、Downloader 下载层、Coder 编解码层和 UI 扩展层。 graph TB subgraph "UI 扩展层" A["UIImageView+WebCacheUIButton+WebCacheUIView+WebCache"] end subgraph "管理层" B["SDWebImageManager"] end subgraph "缓存层 SDImageCache" C1["SDMemoryCache(NSCache + NSMapTable)"] C2["SDDiskCache(NSFileManager)"] end subgraph "下载层" D1["SDWebImageDownloader"] D2["SDWebImageDownloaderOperation(NSOperation)"] end subgraph "编解码层 SDImageCodersManager" E1["SDImageIOCoder"] E2["SDImageGIFCoder"] E3["SDImageAPNGCoder"] E4["SDImageHEICCoderWebP/AVIF (插件)"] end subgraph "辅助层" F["SDImageTransformerSDWebImageCacheKeyFilterSDWebImageOptionsProcessorSDCallbackQueue"] end A -->|sd_setImageWithURL| B B --> C1 B --> C2 B --> D1 D1 --> D2 D2 --> E1 B --> F 源码目录结构: ...

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

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

编译优化-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

启动优化-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

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

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

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

iOS国际化

前言 很多团队对"国际化"的理解停留在"把中文文案翻译成英文",等真正把 App 推向多区域时就会发现远远不够:阿拉伯语用户看到的界面左右颠倒了,用户在纽约和东京看到的"今天"不是同一天,欧洲的小数点变成了逗号,日本用户抱怨价格里的¥符号指向人民币而不是日元,印度用户发现自己的 ₹1,23,456.78 被错误地按千分位显示成 ₹123,456.78,复数规则在俄语里比英语复杂得多,而这些问题单靠"翻译"都解决不了。 国际化(Internationalization,简称 i18n,取首末字母与中间 18 个字符)是架构层面的能力建设,让 App 能够适配不同语言、地区、文字方向、日历、时区、货币、单位制与文化习俗;而本地化(Localization,l10n)是针对某个具体地区的"适配落地"。两者的关系是:“国际化一次,本地化 N 次”。 本文从 NSLocale/Locale 的基础模型开始,把 iOS 国际化的完整工程面梳理清楚——文案本地化、复数/性别规则、RTL 布局、多时区、多币种、单位与数字格式、图片资源、字体、App 内切换语言、测试方法,最后给出研发规范与排查清单。 一、基础概念:Locale、Language 与 Region 1.1 Locale 不等于 Language 大多数工程师第一次接触国际化时会把"语言"和"地区"混为一谈,这在 iOS 下会直接导致格式错误。 Language(语言):zh、en、ar、ja…决定 UI 文字内容。 Region(地区):CN、US、SA、JP…决定格式(日期、数字、货币、时间制 12/24h、起始星期、度量单位)。 Locale(语言+地区):两者合成一个完整的标识,如 zh_CN、en_US、ar_SA、en_IN。 一个美国人移居日本后,完全可能把 iPhone 语言设为英文,但地区设为日本。此时: 场景 期望 UI 文案 英文 日期格式 1月23日(月) 风格?No,应显示 January 23 (Mon),因为语言决定月/星期的名字 数字分隔符 , 作千分位(与日本/美国一致) 货币符号 ¥(JPY) 温度单位 ℃(日本) 首日周 周日(日本区域) 对应的 Locale 是 en_JP——看似奇怪,但在现实中非常普遍。代码里要区分"用哪个语言去查字符串"和"用哪个 Locale 去格式化数据": let preferredLanguage = Locale.preferredLanguages.first ?? "en" let currentLocale = Locale.current let formatter = DateFormatter() formatter.locale = currentLocale formatter.dateStyle = .long print(formatter.string(from: Date())) 不要传 Locale(identifier: "en") 去格式化数字,那会丢掉用户的区域偏好。 ...

May 2, 2026