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

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

3D球场项目 (六):客户端工程化与优化

3D球场项目 (五) 把客户端的核心业务流 讲完了:Native 推送 → DataManager 队列 → ScenePlayer 章节调度 → GameController 状态机 → Handler → 渲染层。功能闭环跑通之后,真正的硬仗是 “工程化"和"性能”——前者保证 项目能稳定上线(编译跑通、退出不崩、Swift 调用顺手),后者保证在中低端机上画面流畅。 第一版上线时遇到了两类问题: 稳定性 / 工程化:modulemap 编译报错、lipo 架构冲突、A11 设备启动 crash、 Cocos 退出期 OpenAL 资源竞争 crash……这些都不是"画面卡",但每一个都能让用户体验 归零; 性能:中端 iPhone 在比赛回合渲染期间,GPU 和 CPU 都偶尔抖到 80%+,帧率掉到 30 fps 出头。 本篇按这两类拆成两部分,所有改动都遵循一条原则:不动玩法,只把工程问题和帧时间 解决掉。 架构改动一览 3D球场项目 (二) 介绍过弹幕引擎的四层 架构(腾讯视频 APP / MagicDanmakuiOS / MagicDanmaku / cocos-engine)。这一阶段 每一层都动了——3D 球场不是单点改造,而是一次自上而下的纵贯。下面这张图是把 PartTwo 那张原始架构图重画一遍,改动的节点用蓝色、新增的节点用绿色, 没动的节点保持白色: ...

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