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

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

Kingfisher源码导读

Kingfisher 是一款纯 Swift 图片下载和缓存库,支持 iOS、macOS、tvOS、watchOS 和 visionOS 全平台。当前最新版本为 8.8.0(2026年3月),已全面适配 Swift 6 严格并发模型。本文基于 v8.x 源码进行分析。 一、整体架构 Kingfisher 采用协议导向的模块化设计,核心由四大组件构成: graph TB subgraph "UI层" A["UIImageView.kf / KFImage"] end subgraph "管理层" B["KingfisherManager"] end subgraph "功能层" C["ImageDownloader"] D["ImageCache"] E["ImageProcessor"] end subgraph "存储层" F["MemoryStorage.Backend(NSCache)"] G["DiskStorage.Backend(FileManager)"] end A -->|"retrieveImage"| B B -->|"下载"| C B -->|"缓存"| D B -->|"处理"| E D --> F D --> G 源码目录结构: Sources/ ├── General/ # KingfisherManager, KingfisherOptionsInfo, Resource, Source ├── Networking/ # ImageDownloader, SessionDelegate, ImagePrefetcher ├── Cache/ # ImageCache, MemoryStorage, DiskStorage ├── Image/ # ImageProcessor, 图片格式处理, 滤镜 ├── Extensions/ # UIKit/AppKit 扩展 (ImageView+Kingfisher, UIButton+Kingfisher) ├── SwiftUI/ # KFImage, KFAnimatedImage, ImageBinder ├── Views/ # AnimatedImageView └── Utility/ # 辅助工具类 二、命名空间设计 — KingfisherWrapper Kingfisher 使用 kf 命名空间来避免对系统类型的污染,这是 Swift 社区广泛使用的一种模式。 ...

May 2, 2026

KVC底层原理

基础概念 KVC(Key-Value Coding,键值编码)是 Apple 提供的一种通过字符串 key 间接访问对象属性的机制,定义在 NSKeyValueCoding 协议中,NSObject 默认遵循该协议。 // 直接访问 person.name = @"Tom"; NSString *name = person.name; // KVC 访问 [person setValue:@"Tom" forKey:@"name"]; NSString *name = [person valueForKey:@"name"]; KVC 的核心价值在于:通过字符串动态访问属性,不需要在编译期知道属性名。这使得字典转模型、序列化/反序列化、Interface Builder 的 Runtime Attributes 等场景成为可能。 setValue:forKey: 的底层流程 当调用 [obj setValue:value forKey:@"name"] 时,Runtime 按照以下顺序查找: flowchart TD A["setValue:forKey:@'name'"] --> B{"查找 setter 方法"} B -->|"找到"| C["调用 setter"] B -->|"未找到"| D{"accessInstanceVariablesDirectly返回 YES?"} D -->|"YES"| E{"按顺序查找实例变量_name → _isName → name → isName"} D -->|"NO"| F["调用 setValue:forUndefinedKey:默认抛出 NSUndefinedKeyException"] E -->|"找到"| G["直接设置实例变量值"] E -->|"未找到"| F 第一步:查找 setter 方法 Runtime 按以下顺序查找 setter 方法: ...

May 2, 2026

MVVM架构详解

什么是MVVM MVVM(Model-View-ViewModel)是一种通过数据绑定实现View和业务逻辑解耦的架构模式。它最初由Microsoft提出,用于WPF开发,后来被广泛应用于iOS开发。 MVVM的核心特点是: ViewModel不持有View的引用 通过数据绑定实现View和ViewModel的同步 View的状态完全由ViewModel驱动 MVVM的结构 flowchart TB subgraph View["View (ViewController)"] V1["绑定ViewModel的属性"] V2["将用户事件传递给ViewModel"] end subgraph ViewModel["ViewModel"] VM1["暴露可观察的属性"] VM2["展示逻辑 + 业务逻辑"] end subgraph Model["Model"] M1["数据模型"] M2["数据访问 (API/DB)"] end View <-->|"数据绑定 (Binding)"| ViewModel ViewModel -->|"获取/更新数据"| Model MVVM的数据流 MVVM的核心原则是:View不直接访问Model,所有交互都通过ViewModel进行。在iOS开发中,由于UIViewController的特殊地位,MVVM的数据流可以更细化为四个角色: flowchart LR subgraph View["View"] V["纯UI展示用户交互触发"] end subgraph ViewController["ViewController"] VC["数据绑定生命周期管理路由/导航"] end subgraph ViewModel["ViewModel"] VM["业务逻辑数据转换状态管理"] end subgraph Model["Model"] M["数据结构数据存储"] end VC -->|"持有"| V VC -->|"持有"| VM V -->|"1. 用户交互"| VC VC -->|"2. 转发事件"| VM VM -->|"3. 业务处理"| M M -->|"4. 数据变化"| VM VM -->|"5. 状态更新"| VC VC -->|"6. 更新UI"| V 各层职责与持有关系 组件 职责 持有关系 View 纯UI展示、触发用户交互事件 不持有其他层 ViewController 数据绑定、生命周期管理、持有View和ViewModel 持有View和ViewModel ViewModel 业务逻辑、数据转换、状态管理 持有Model/Service Model 数据结构、数据存储 不持有其他层 详细数据流说明 View → ViewController(用户交互) ...

May 2, 2026

WebView离线包

背景 Hybrid 开发模式下,WebView 加载 H5 页面的体验一直是痛点。一个典型的 H5 页面加载流程涉及:初始化 WebView -> DNS 解析 -> 建立连接 -> 下载 HTML -> 解析 HTML -> 下载 CSS/JS/图片 -> 渲染页面。在弱网或首次加载场景下,白屏时间常常达到 2~5 秒,远不如 Native 体验。 离线包的核心思路是:将 H5 的静态资源(HTML、CSS、JS、图片、字体等)预先打包下发到客户端本地,WebView 加载时直接从本地读取资源,跳过网络请求环节,从而大幅缩短页面加载时间。 离线包加载 vs 在线加载 sequenceDiagram participant App participant WebView participant Local as 本地离线包 participant Server as 远程服务器 Note over App, Server: 在线加载流程 App->>WebView: loadURL WebView->>Server: DNS + TCP + TLS + HTTP请求 Server-->>WebView: HTML WebView->>Server: 请求 CSS/JS/图片 Server-->>WebView: 资源文件 WebView->>WebView: 渲染页面 Note over App, Server: 离线包加载流程 App->>WebView: loadURL(被拦截) WebView->>Local: 读取本地 HTML Local-->>WebView: HTML WebView->>Local: 读取本地 CSS/JS/图片 Local-->>WebView: 资源文件 WebView->>WebView: 渲染页面 对比项 在线加载 离线包加载 首屏时间 2~5秒(弱网更久) 0.5~1秒 网络依赖 强依赖 仅更新时需要网络 白屏问题 严重 基本消除 资源新鲜度 实时最新 有一定延迟 流量消耗 每次访问都消耗 仅增量更新消耗 整体架构 一个完整的离线包系统包含三大部分: ...

May 2, 2026

启动优化-didFinishLaunching

didFinishLaunchingWithOptions是main阶段的核心入口,在这里初始化大量SDK和服务会阻塞启动。本文介绍如何优化这个阶段的耗时。 问题分析 典型的问题代码: func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // 所有初始化都在主线程同步执行 CrashReporter.setup() Analytics.setup() PushNotification.setup() NetworkManager.setup() DatabaseManager.setup() ImageCache.setup() AdSDK.setup() SocialSDK.setup() // ... 更多SDK return true } 这种写法的问题: 所有初始化都在主线程同步执行 无论是否需要,所有SDK都在启动时初始化 阻塞首帧渲染 优化方案 方案1:分级启动任务管理 将启动任务按优先级分类,只在启动时执行必要的任务: // 启动任务优先级 enum LaunchTaskPriority { case required // 必须在首帧前完成 case high // 首帧后立即执行 case normal // 首屏稳定后执行 case low // 空闲时执行 } // 启动任务管理器 class LaunchTaskManager { static let shared = LaunchTaskManager() private var tasks: [LaunchTaskPriority: [() -> Void]] = [:] func register(priority: LaunchTaskPriority, task: @escaping () -> Void) { if tasks[priority] == nil { tasks[priority] = [] } tasks[priority]?.append(task) } func executeRequiredTasks() { tasks[.required]?.forEach { $0() } } func executeHighPriorityTasks() { DispatchQueue.main.async { self.tasks[.high]?.forEach { $0() } } } func executeNormalTasks() { DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { self.tasks[.normal]?.forEach { $0() } } } func executeLowPriorityTasks() { // 监听RunLoop空闲时执行 CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopDefaultMode) { self.tasks[.low]?.forEach { $0() } } } } 使用示例 func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let manager = LaunchTaskManager.shared // 必须在首帧前完成 manager.register(priority: .required) { CrashReporter.setup() // 崩溃收集必须最先初始化 } // 首帧后立即执行 manager.register(priority: .high) { Analytics.setup() PushNotification.setup() } // 首屏稳定后执行 manager.register(priority: .normal) { AdSDK.setup() SocialSDK.setup() } // 空闲时执行 manager.register(priority: .low) { PreloadManager.preloadResources() } // 只执行必须的任务 manager.executeRequiredTasks() return true } 任务分级建议 优先级 适用场景 示例 required 崩溃收集、核心功能依赖 Crash SDK、必要的配置加载 high 用户可能很快用到 推送、统计、网络配置 normal 非首屏功能 广告SDK、社交分享 low 预加载、缓存 图片预加载、数据预热 方案2:并行初始化 将无依赖关系的初始化任务并行执行: ...

May 2, 2026

编译优化-Swift编译优化

Swift 的类型推导、泛型约束求解、WMO / CMO 是编译性能的核心影响因素。理解它们能让源码层面的小改动带来显著的编译加速。本文聚焦"代码写法 + 编译开关"两方面的优化。 Swift 编译流程速览 相比 C/OC,Swift 编译多出大量工作: flowchart LR A[Parse] --> B[Sema类型检查] B --> C[SILGen生成 SIL] C --> D[SIL Optimization] D --> E[IRGenLLVM IR] E --> F[LLVM Optimization] F --> G[CodeGen] 其中 Sema(语义分析) 阶段负责类型推导,是 Swift 特有的性能热点。-debug-time-compilation 看到的 “Type checking” 时间基本都集中在这里。 类型推导与 “Too Complex” 错误 约束求解 Swift 的类型系统比大多数语言复杂: 函数重载 + 运算符重载 隐式字面量类型(Int、Double、Float80…) 泛型 + 协议 + 关联类型 闭包参数类型推导 SwiftUI 风格的 ViewBuilder / Result Builders 遇到表达式 let x = a + b * c - d,编译器需要给 + * - 每个运算符枚举所有可能的重载,在多个候选里做约束传播。当候选组合爆炸时,Sema 会放弃并抛出: ...

May 2, 2026

3D球场项目 (五)

3D球场项目 (四) 把服务端推到了线上: 导演 Agent 把每个 Point 切成 SERVE / RALLY / CONCLUSION 三段,配齐了动作、机位、 位置、比分、解说,按 seq 单调下发到客户端。本篇回到客户端,看 Cocos 这一侧拿到 Script 之后,是怎么把它变成屏幕上一拍接一拍的比赛画面的。 整个客户端改造分四层落地,本篇只讲和 3D 球场主干业务直接相关的内容: CocosEngine 层:场景加载架构升级(进度 / 传参 / Delegate1 回调 / JSBridge2 内存语义); MagicDanmaku 层:3D 球场的核心业务代码(数据流、状态机、动画 / 球轨迹 / 相机 / 观众); MagicDanmaku iOS 层:场景加载回调的演化,以及 CocosPlayer 在调用链里的角色; 业务层:腾讯体育 App 的 SwiftUI3 视图把上面三层包成一个标准组件。 每一层和业务流程脱钩的内容(性能优化、稳定性 / crash 修复、构建工程化)统一抽到了 3D球场项目 (六):客户端工程化与优化。 下面这张图先把四层的位置关系和数据流向摆出来,读者可以把它当作整篇文章的导航—— 箭头是数据流方向(服务端 Script 自顶向下灌进引擎、引擎事件自底向上回到 SwiftUI), 每个节点旁标了对应章节: ...

May 30, 2026

APM-指标体系

APM 的第一步不是写代码,而是定义指标。一个 App 的性能好坏不是一句话能说清的,必须把模糊的"快 / 稳 / 省"拆解为可量化、可对比、可报警的数字。本文把业界常用指标按"稳定性 / 流畅性 / 启动 / 资源 / 业务 / 网络"六大族分类,并补充 RUM 的 Session / View / Action / Resource / Error 口径,给出每个指标的定义、计算方式、典型目标值与常见陷阱。 一、指标设计原则 设计指标前先达成共识: 1.1 北极星分层 graph TB L0[用户体验北极星留存率 / NPS] L1[业务指标秒开率 / 转化率 / 关键路径完成率] L2[技术指标FPS / Crash率 / 启动时间] L3[原子指标单帧耗时 / 内存峰值 / 单请求耗时] L0 --> L1 --> L2 --> L3 好的指标体系应该:技术指标能解释业务指标,业务指标能解释北极星。否则就是"为了采集而采集"。 1.2 分位数思维 永远不要只看平均值。同一个启动指标: 设备 平均 P50 P90 P99 iPhone 15 800ms 750ms 900ms 1200ms iPhone SE 2 2000ms 1500ms 3500ms 8000ms 平均值把两者拉成 1400ms,但 iPhone SE 的 P99 用户"实际等了 8 秒"。APM 的所有耗时指标都必须同时提供 P50 / P90 / P99。 ...

May 7, 2026