Android 模块化、组件化、插件化架构

从基础概念到实战应用,系统梳理 Android 架构演进之路


一、基础概念

1.1 为什么需要架构优化?

随着业务迭代,单体 App 会遇到诸多问题:

问题 表现 影响
代码耦合严重 模块间直接 import,循环依赖 难以维护、编译慢
编译效率低 改一行代码全量编译 开发效率下降
团队协作冲突 多人修改同一工程 Git 冲突频繁
无法独立开发 强依赖主工程 无法并行开发、独立测试
复用困难 业务逻辑与 UI 混在一起 跨项目复用成本高

1.2 三种架构模式辨析

1
2
3
4
5
6
7
8
9
10
11
12
┌─────────────────────────────────────────────────────────────────┐
│ 架构演进路径 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 单体架构 ──► 模块化 ──► 组件化 ──► 插件化 │
│ │ │ │ │ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ 按功能拆分 Gradle模块 路由解耦 运行时动态加载 │
│ 包结构划分 build独立 ARouter等 热更新/按需下载 │
│ │
└─────────────────────────────────────────────────────────────────┘

模块化(Modularization)

  • 定义:按业务功能将代码拆分为独立的 Gradle 模块,每个模块有清晰的 build.gradle 和边界
  • 特点:物理隔离、可独立编译,通常仍在同一 Git 仓库内
  • 粒度:中等,如「用户模块」「订单模块」「支付模块」

组件化(Componentization)

  • 定义:在模块化基础上,通过路由/服务发现实现模块间完全解耦,可独立开发、独立运行
  • 特点:依赖倒置、接口隔离、可单模块调试(Application 切换)
  • 粒度:较细,如「登录组件」「分享组件」「埋点组件」

插件化(Pluginization)

  • 定义:插件 APK/Dex 可动态加载、热更新,主 App 与插件解耦到运行时
  • 特点:运行时动态、按需下载、可热修复、减小包体积
  • 粒度:独立 APK/模块

二、模块化原理与实践

2.1 模块化的核心原则

  1. 单一职责:每个模块只负责一块业务
  2. 接口隔离:模块间通过接口/路由通信,不暴露实现细节
  3. 依赖倒置:依赖抽象(interface)而非具体实现

2.2 Gradle 多模块目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
MyApp/
├── app/ # 主工程壳
│ └── build.gradle
├── module_user/ # 用户模块
│ ├── build.gradle
│ └── src/main/
├── module_order/ # 订单模块
│ └── build.gradle
├── module_payment/ # 支付模块
│ └── build.gradle
├── common_base/ # 公共基础库
│ ├── network/
│ ├── utils/
│ └── base/
├── common_router/ # 路由层
│ └── build.gradle
├── build.gradle
└── settings.gradle

2.3 settings.gradle 与模块声明

1
2
3
4
5
6
7
8
// settings.gradle
rootProject.name = "MyApp"
include ':app'
include ':module_user'
include ':module_order'
include ':module_payment'
include ':common_base'
include ':common_router'

2.4 模块间依赖配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// app/build.gradle - 主工程依赖各业务模块
dependencies {
implementation project(':module_user')
implementation project(':module_order')
implementation project(':module_payment')
implementation project(':common_router')
implementation project(':common_base')
}

// module_user/build.gradle - 业务模块只依赖基础库和路由
dependencies {
implementation project(':common_base')
implementation project(':common_router')
// 不要依赖 module_order、module_payment!
}

// common_base/build.gradle - 基础库不依赖任何业务模块
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
}

2.5 模块间通信:接口 + 实现注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// common_router 中定义接口
interface IUserService {
fun getCurrentUser(): User?
fun logout()
}

// module_user 中实现接口
class UserServiceImpl : IUserService {
override fun getCurrentUser(): User? = UserManager.currentUser
override fun logout() { /* ... */ }
}

// 通过 ServiceLocator 或依赖注入框架注入
object ServiceLocator {
var userService: IUserService? = null
}

// module_order 中调用
class OrderActivity : AppCompatActivity() {
private val userService: IUserService? by lazy { ServiceLocator.userService }

fun showUserInfo() {
userService?.getCurrentUser()?.let { user ->
// 显示用户信息
}
}
}

三、组件化原理与实现

3.1 组件化架构图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
                ┌──────────────────┐
│ app (壳) │
│ 主工程/组装 │
└────────┬─────────┘

┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ module_user │ │ module_order│ │module_payment│
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└───────────────┼───────────────┘

┌──────▼──────┐
│ ARouter │
│ 路由中间层 │
└──────┬──────┘

┌──────▼──────┐
│ common_base │
│ 基础组件库 │
└─────────────┘

3.2 路由方案对比

方案 原理 优点 缺点
ARouter 注解 + 编译期生成路由表 阿里出品、生态成熟、支持拦截器 需引入注解处理器
WMRouter 美团方案,分组路由 按需加载、包体积优化 学习成本略高
CC (Component Call) 组件调用框架 同步/异步调用、跨进程 概念较多
手写路由表 Map 映射 简单、无依赖 维护成本高

3.3 单模块独立运行(Application 切换)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// module_user/build.gradle
android {
defaultConfig {
// 作为 Application 运行时使用
if (project.hasProperty('runAsApp') && runAsApp) {
applicationId "com.example.user"
}
}
sourceSets {
main {
manifest.srcFile 'src/main/AndroidManifest.xml'
if (project.hasProperty('runAsApp') && runAsApp) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- module_user/src/main/debug/AndroidManifest.xml - 独立运行时的 Manifest -->
<manifest>
<application
android:name=".UserDebugApplication"
android:label="User Module Debug">
<activity android:name=".UserActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
1
2
# 独立运行用户模块
./gradlew :module_user:installDebug -PrunAsApp=true

3.4 ARouter 使用与原理

3.4.1 添加依赖

1
2
3
4
5
// 各模块 build.gradle
dependencies {
implementation 'com.alibaba:arouter-api:1.5.2'
kapt 'com.alibaba:arouter-compiler:1.5.2'
}

3.4.2 路由配置与跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ========== 组件端:Order 模块 ==========
// 在 OrderListActivity 上添加路由注解
@Route(path = "/order/list")
class OrderListActivity : AppCompatActivity() {
@Autowired
lateinit var userId: String

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ARouter.getInstance().inject(this)
// userId 已自动注入
}
}

// ========== 调用端:任意模块 ==========
ARouter.getInstance()
.build("/order/list")
.withString("userId", "12345")
.navigation()

3.4.3 服务发现(跨模块获取实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义接口(可在 common_base 或单独 api 模块)
interface IOrderService {
fun getOrderCount(userId: String): Int
}

// Order 模块实现并注册
@Route(path = "/service/order")
class OrderServiceImpl : IOrderService {
override fun getOrderCount(userId: String): Int = /* ... */
}

// 调用端获取
val orderService = ARouter.getInstance()
.navigation(IOrderService::class.java)
orderService?.getOrderCount("12345")

3.4.5 ARouter 核心源码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 简化版 ARouter 核心逻辑
// 1. 初始化时扫描 Dex,加载路由表(通过 Gradle 插件编译期生成)
class Warehouse {
// 路由表:path -> RouteMeta
static Map<String, RouteMeta> routes = ConcurrentHashMap()
// 分组缓存
static Map<String, Class<? extends IRouteGroup>> groupsIndex = ConcurrentHashMap()
}

// 2. navigation 流程
fun navigation(path: String): Postcard {
val meta = Warehouse.routes[path] // 从路由表获取
return Postcard(path, meta)
}

fun Postcard.navigation(): Any? {
return _ARouter.navigation(context, this, requestCode, navigationCallback)
}

// 3. 实际跳转
fun _ARouter.navigation(...) {
when (meta.type) {
RouteType.ACTIVITY -> {
val intent = Intent(context, meta.destination)
// 注入参数
context.startActivity(intent)
}
RouteType.ISERVICE -> {
return meta.destination.newInstance() // 服务实现类
}
}
}

// 4. 编译期注解处理器生成路由表
// 生成类似:ARouter$$Group$$order、ARouter$$Providers$$order
// 在 init() 时通过反射加载到 Warehouse

3.5 依赖关系设计原则

1
2
3
4
5
6
7
8
9
业务模块 (user/order/payment)

├── 只依赖 common_base、common_router (arouter-api)

└── 业务模块之间 不直接依赖

app 壳工程

└── 依赖所有业务模块,负责组装和 Application 初始化

四、插件化原理与实现

4.1 插件化的应用场景

  • 热更新:修复线上 Bug 无需发版
  • 按需加载:减小包体积,冷启动只加载核心
  • 动态能力:运营活动插件、A/B 测试模块
  • 多端复用:同一套插件可被主 App、Split APK 加载

4.2 Android 插件化技术选型

方案 说明 适用场景
Dynamic Feature Modules Google 官方,AGP 支持 按需下载、免安装功能模块
VirtualAPK 滴滴开源 完整的插件化框架
RePlugin 360 出品 插件即独立 APK,稳定成熟
Shadow 腾讯开源 支持 Android 10+,零 Hook
Atlas 阿里 大型 App 动态化

4.3 Dynamic Feature Modules(官方方案)

4.3.1 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// settings.gradle
include ':app'
include ':feature:order' // 动态功能模块

// app/build.gradle
dependencies {
implementation project(':common_base')
dynamicFeatures = [':feature:order']
}

// feature/order/build.gradle
apply plugin: 'com.android.dynamic-feature'
android {
defaultConfig {
minSdk 24
}
}
dependencies {
implementation project(':app')
}

4.3.2 按需下载与安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用 Play Core Library 下载动态模块
val splitInstallManager = SplitInstallManagerFactory.create(context)
val request = SplitInstallRequest.newBuilder()
.addModule("order")
.build()

splitInstallManager.startInstall(request)
.addOnSuccessListener {
// 安装成功,可跳转
startActivity(Intent(this, OrderActivity::class.java))
}
.addOnFailureListener {
// 处理失败
}

4.4 RePlugin 核心原理简述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌──────────────────────────────────────────────────────┐
│ Host App │
│ ┌────────────────────────────────────────────────┐ │
│ │ RePluginHostConfig / PluginManager │ │
│ │ - 加载插件 APK │ │
│ │ - 替换 ClassLoader │ │
│ │ - Hook Activity/Service 等组件 │ │
│ └────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘

┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Plugin1 │ │ Plugin2 │ │ Plugin3 │
│ .apk │ │ .apk │ │ .apk │
└──────────┘ └──────────┘ └──────────┘

核心流程

  1. Dex 加载:将插件 APK 的 classes.dex 加入宿主的 DexPathList
  2. 资源加载:创建 Resources,合并插件的 AssetManager
  3. 组件占位:通过 Hook Instrumentation/IActivityManager,用占位 Activity 代理插件 Activity

4.5 手写简易插件加载(Dex 加载)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 简化示例:加载插件 APK 中的类
class PluginLoader(private val context: Context) {

private val pluginClassLoaders = mutableMapOf<String, DexClassLoader>()

fun loadPlugin(apkPath: String): Boolean {
val optimizedDir = File(context.filesDir, "plugin_opt").apply { mkdirs() }
val libDir = File(context.filesDir, "plugin_lib").apply { mkdirs() }

val classLoader = DexClassLoader(
apkPath,
optimizedDir.absolutePath,
null,
context.classLoader
)
pluginClassLoaders[apkPath] = classLoader
return true
}

fun loadClass(apkPath: String, className: String): Class<*>? {
val loader = pluginClassLoaders[apkPath] ?: return null
return try {
loader.loadClass(className)
} catch (e: ClassNotFoundException) {
null
}
}
}

// 加载插件中的 Activity(实际还需 Hook 组件启动流程)
val loader = PluginLoader(context)
loader.loadPlugin("/sdcard/plugin.apk")
val clazz = loader.loadClass("/sdcard/plugin.apk", "com.plugin.MainActivity")
val activity = clazz.newInstance()

4.6 插件化资源加载

1
2
3
4
5
6
7
8
9
10
11
12
// 合并插件资源到宿主
fun createPluginResources(hostResources: Resources, apkPath: String): Resources {
val assets = AssetManager::class.java.newInstance()
val addAssetPath = AssetManager::class.java.getMethod("addAssetPath", String::class.java)
addAssetPath.invoke(assets, apkPath)

return Resources(
assets,
hostResources.displayMetrics,
hostResources.configuration
)
}

五、ARouter 框架源码深度解析

5.1 整体架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌────────────────────────────────────────────────────────┐
│ ARouter │
│ - 对外 API:build、navigation、inject │
└────────────────────────────────────────────────────────┘

┌─────────────────────────┴─────────────────────────────┐
│ _ARouter (内部实现) │
│ - LogisticsCenter:路由表、分组加载 │
│ - Warehouse:路由/服务/拦截器 存储 │
└────────────────────────────────────────────────────────┘

┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 注解处理器 │ │ 拦截器链 │ │ 降级策略 │
│ 编译期生成 │ │ 可扩展 │ │ DegradeService│
└──────────────┘ └──────────────┘ └──────────────┘

5.2 LogisticsCenter 路由表加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 初始化时通过插件扫描 Dex 中的特定类
public static void init(Context context) {
try {
// 获取所有 ARouter 生成的类:ARouter$$Root$$xxx
Set<String> routerMap = ClassUtils.getFileNameByPackageName(
context, ROUTE_ROOT_PACKAGE);
for (String className : routerMap) {
if (className.startsWith(ROUTE_ROOT_PACKAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
((IRouteRoot) Class.forName(className).getConstructor().newInstance())
.loadInto(Warehouse.groupsIndex);
}
}
// 分组按需加载,避免启动时加载全部
} catch (Exception e) {
throw new HandlerException("ARouter init logistics center exception");
}
}

5.3 拦截器机制

1
2
3
4
5
6
7
8
9
10
11
12
@Interceptor(priority = 8)
class LoginInterceptor : IInterceptor {
override fun process(postcard: Postcard, callback: InterceptorCallback) {
if (postcard.extra == NEED_LOGIN && !UserManager.isLoggedIn()) {
// 未登录,拦截并跳转登录
ARouter.getInstance().build("/user/login").navigation()
callback.onInterrupt(null)
} else {
callback.onContinue(postcard)
}
}
}

六、实际项目应用案例

6.1 某电商 App 组件化拆分

1
2
3
4
5
6
7
8
9
10
11
app (壳工程)
├── module_home # 首页
├── module_product # 商品详情
├── module_cart # 购物车
├── module_order # 订单
├── module_user # 用户中心
├── module_payment # 支付
├── module_share # 分享(可复用)
├── module_analytics # 埋点(可复用)
├── common_router # ARouter 封装
└── common_base # 网络/缓存/UI 基础库

收益

  • 编译时间:全量 6min → 单模块 1min
  • 4 个业务线可并行开发,Git 冲突减少 60%
  • ShareModule、AnalyticsModule 复用到多个 App

6.2 路由表设计实战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 统一路由 Path 常量
object RouterPath {
const val ORDER_LIST = "/order/list"
const val ORDER_DETAIL = "/order/detail"
const val USER_LOGIN = "/user/login"
const val USER_PROFILE = "/user/profile"
}

// 封装便捷方法
object Router {
fun toOrderList(userId: String) {
ARouter.getInstance()
.build(RouterPath.ORDER_LIST)
.withString("userId", userId)
.navigation()
}

fun toOrderDetail(orderId: String) {
ARouter.getInstance()
.build(RouterPath.ORDER_DETAIL)
.withString("orderId", orderId)
.navigation()
}
}

6.3 解耦实践:避免循环依赖

错误示例

1
2
module_order -> module_user (获取用户信息)
module_user -> module_order (跳转订单列表)

形成循环依赖,Gradle 编译失败。

正确做法

1
2
module_order -> common_router
module_user -> common_router

需要「用户信息」时,Order 通过 ARouter.getInstance().navigation(IUserService::class.java) 获取;需要「跳转订单」时,User 通过 ARouter.getInstance().build("/order/list").navigation() 跳转。

6.4 模块化 + 插件化组合:Dynamic Feature 实践

某资讯 App 将「小说」「漫画」等非核心功能做成 Dynamic Feature,用户首次进入时提示下载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 检查模块是否已安装
fun isModuleInstalled(moduleName: String): Boolean {
return SplitInstallManagerFactory.create(context)
.installedModules.contains(moduleName)
}

// 进入小说模块前检查
fun openNovelModule() {
if (isModuleInstalled("novel")) {
startActivity(Intent(this, NovelActivity::class.java))
} else {
showDownloadDialog {
installDynamicModule("novel")
}
}
}

七、最佳实践与注意事项

7.1 模块拆分原则

  • 高内聚低耦合:模块内聚度高,模块间依赖少
  • 按业务边界拆分:参考 DDD 的 Bounded Context
  • 基础组件下沉:网络、缓存、日志等抽成 common_base
  • 渐进式演进:先模块化再组件化,避免一步到位导致成本过高

7.2 常见坑与规避

问题 原因 规避
编译顺序错误 模块间隐式依赖 严格检查 build.gradle,用 ./gradlew :module_x:dependencies 检查
路由表膨胀 所有页面都注册路由 仅对外暴露的页面注册,内部页面用 startActivity
启动变慢 初始化时加载全部路由 使用分组按需加载(ARouter 已支持)
R 文件冲突 多模块资源 ID 冲突 在 build.gradle 中设置 resourcePrefix "module_user_"
插件化兼容性 不同厂商 ROM 限制 优先使用 Dynamic Feature,第三方框架需充分测试

7.3 R 文件与资源隔离

1
2
3
4
// 各业务模块 build.gradle 中
android {
resourcePrefix "user_" // 资源必须以 user_ 开头
}

7.4 架构演进路线图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Phase 1: 模块化
├── 拆分为 Gradle 多模块
├── 建立 common_base、common_router
└── 模块间通过接口通信

Phase 2: 组件化
├── 引入 ARouter 路由
├── 服务发现替代直接依赖
└── 支持单模块独立运行调试

Phase 3: 插件化(可选)
├── 非核心功能改为 Dynamic Feature
├── 或引入 RePlugin/Shadow 做完整插件化
└── 按需下载、热更新

八、总结

架构 适用场景 核心手段
模块化 中小型 App、团队 < 10 人 Gradle 多模块、接口、依赖配置
组件化 中大型 App、多业务线并行 ARouter、服务发现、单模块调试
插件化 需要动态化、热更新、包体积优化 Dynamic Feature、RePlugin、Shadow

架构没有银弹,需结合团队规模、业务复杂度、迭代节奏选择合适方案。建议从模块化起步,随着复杂度提升再逐步演进到组件化,插件化则按实际需求谨慎引入。


参考资源

React Hooks

从基本概念到源码实现,全面解析 React Hooks


一、基本概念

1.1 什么是 Hooks?

Hooks 是 React 16.8 引入的新特性,它允许你在不编写 class 的情况下使用 state 以及其他的 React 特性。

简单来说,Hooks 就是让你在函数组件中”钩入” React 特性的函数

1.2 为什么需要 Hooks?

在 Hooks 出现之前,React 存在以下问题:

痛点 描述
状态逻辑难复用 使用 render propsHOC 会导致组件嵌套地狱
复杂组件难理解 生命周期中充斥大量无关逻辑,难以拆分
Class 的学习成本 需要理解 this、绑定事件、生命周期等概念
逻辑分散 相关逻辑散落在 componentDidMountcomponentDidUpdatecomponentWillUnmount

1.3 核心 Hooks 一览

Hook 作用
useState 在函数组件中添加 state
useEffect 执行副作用(订阅、请求、DOM 操作等)
useContext 读取 Context 值
useReducer useState 的替代方案,适合复杂 state 逻辑
useCallback 缓存回调函数
useMemo 缓存计算结果
useRef 引用 DOM 或保存可变值
useLayoutEffect 同步执行副作用,在 DOM 更新后、浏览器绘制前

二、常用 Hooks 详解与示例

2.1 useState

1
2
3
4
5
6
7
8
9
10
11
12
13
function Counter() {
const [count, setCount] = useState(0);
const [user, setUser] = useState({ name: '', age: 0 });

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
{/* 函数式更新,适合依赖前一个 state */}
<button onClick={() => setCount(prev => prev + 1)}>+1 (函数式)</button>
</div>
);
}

关键点:

  • useState 接收初始值,返回 [当前值, 更新函数]
  • 初始值可以是函数(惰性初始化),用于避免每次渲染都执行 expensive 计算
1
const [state, setState] = useState(() => computeExpensiveInitialState());

2.2 useEffect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function DataFetcher({ id }) {
const [data, setData] = useState(null);

useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${id}`, { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(console.error);

// 清理函数:组件卸载或 effect 重新执行前调用
return () => controller.abort();
}, [id]); // 依赖数组:id 变化时重新执行

return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}

依赖数组规则:

  • 不传:每次渲染后都执行
  • []:仅挂载时执行一次
  • [a, b]:a 或 b 变化时执行

2.3 useRef

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function TextInput() {
const inputRef = useRef(null);

const focusInput = () => {
inputRef.current?.focus();
};

return (
<>
<input ref={inputRef} />
<button onClick={focusInput}>聚焦</button>
</>
);
}

// 保存可变值,不触发重渲染
function Timer() {
const countRef = useRef(0);
countRef.current++; // 修改不会触发渲染
return <div>Render count: {countRef.current}</div>;
}

2.4 useCallback 与 useMemo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');

// 缓存函数,避免子组件因引用变化而重渲染
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);

// 缓存计算结果
const expensiveValue = useMemo(() => {
return computeExpensiveValue(count);
}, [count]);

return <MemoChild onClick={handleClick} value={expensiveValue} />;
}

三、Hooks 原理浅析

3.1 核心思想:链表存储

React 在内部用链表来存储 Hooks。每个函数组件对应一个 Fiber 节点,每个 Hook 是链表上的一个节点。

1
2
3
4
5
Fiber 节点
└── memoizedState (第一个 Hook)
├── next -> (第二个 Hook)
│ ├── next -> (第三个 Hook)
│ │ └── ...

为什么是链表?

  • 函数组件没有 this,无法像 Class 那样用实例存储
  • 链表可以按调用顺序保存多个 Hook
  • 每次渲染时按顺序遍历链表,对应到正确的 state

3.2 两个重要的链表

React 内部维护两套 Hook 链表:

  1. current:当前屏幕上显示的 UI 对应的 Hooks
  2. workInProgress:正在构建的新的 Hooks

渲染流程大致为:

1
2
3
mount -> workInProgress 构建新链表
update -> 从 current 复制到 workInProgress,按顺序更新
commit -> workInProgress 替换 current

3.3 为什么 Hooks 必须在顶层调用?

1
2
3
4
5
6
7
// ❌ 错误:条件调用
function Bad() {
if (condition) {
const [a, setA] = useState(0); // 破坏调用顺序!
}
const [b, setB] = useState(0);
}

React 依赖 Hooks 的调用顺序 来将 state 与正确的 Hook 关联。如果顺序变化,会错位导致 bug。


四、源码解读

4.1 简化的 Hook 数据结构

1
2
3
4
5
6
7
8
// React 内部简化版
type Hook = {
memoizedState: any, // 当前 state 值
baseState: any, // 基础 state
baseQueue: Update | null,
queue: UpdateQueue, // 更新队列
next: Hook | null, // 下一个 Hook(链表)
};

4.2 useState 实现思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 极简版 useState 实现(理解原理用)
let hooks = [];
let currentHook = 0;

function useState(initialState) {
const index = currentHook;
hooks[index] = hooks[index] ?? (typeof initialState === 'function' ? initialState() : initialState);

function setState(newState) {
hooks[index] = typeof newState === 'function' ? newState(hooks[index]) : newState;
scheduleReRender();
}

currentHook++;
return [hooks[index], setState];
}

function useMyComponent() {
currentHook = 0;
const [count, setCount] = useState(0);
const [name, setName] = useState(''); // 按顺序压入链表
// ...
}

4.3 useEffect 实现思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function useEffect(callback, deps) {
const index = currentHook;
const prevDeps = hooks[index];

const hasChanged = !prevDeps || deps?.some((d, i) => d !== prevDeps[i]);

if (hasChanged) {
// 先执行上一次的 cleanup
if (typeof prevEffect?.cleanup === 'function') {
prevEffect.cleanup();
}
const cleanup = callback();
hooks[index] = { deps, cleanup };
}
currentHook++;
}

五、自定义 Hook

5.1 封装业务逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// useLocalStorage - 同步 state 到 localStorage
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (e) {
return initialValue;
}
});

const setValue = useCallback((value) => {
setStoredValue(prev => {
const next = typeof value === 'function' ? value(prev) : value;
window.localStorage.setItem(key, JSON.stringify(next));
return next;
});
}, [key]);

return [storedValue, setValue];
}

// 使用
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>{theme}</button>;
}

5.2 useDebounce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);

useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);

return debouncedValue;
}

// 搜索防抖
function SearchInput() {
const [search, setSearch] = useState('');
const debouncedSearch = useDebounce(search, 300);

useEffect(() => {
if (!debouncedSearch) return;
fetchResults(debouncedSearch);
}, [debouncedSearch]);
// ...
}

5.3 useFetch(数据请求)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
setLoading(true);
const controller = new AbortController();

fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));

return () => controller.abort();
}, [url]);

return { data, loading, error };
}

六、实际项目应用案例

6.1 场景一:表单管理(复杂表单)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function useForm(initialValues) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});

const handleChange = useCallback((name) => (e) => {
setValues(prev => ({ ...prev, [name]: e.target.value }));
}, []);

const validate = useCallback(() => {
const newErrors = {};
if (!values.email) newErrors.email = '必填';
if (!values.password) newErrors.password = '必填';
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
}, [values]);

const reset = useCallback(() => {
setValues(initialValues);
setErrors({});
}, []);

return { values, errors, handleChange, validate, reset };
}

// 登录表单
function LoginForm() {
const { values, errors, handleChange, validate } = useForm({ email: '', password: '' });

const onSubmit = (e) => {
e.preventDefault();
if (!validate()) return;
login(values);
};

return (
<form onSubmit={onSubmit}>
<input value={values.email} onChange={handleChange('email')} />
{errors.email && <span>{errors.email}</span>}
<input type="password" value={values.password} onChange={handleChange('password')} />
{errors.password && <span>{errors.password}</span>}
<button type="submit">登录</button>
</form>
);
}

6.2 场景二:无限滚动列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function useInfiniteScroll(callback, hasMore, loading) {
const observerRef = useRef();
const lastElementRef = useCallback(node => {
if (loading) return;
if (observerRef.current) observerRef.current.disconnect();
observerRef.current = new IntersectionObserver(entries => {
if (entries[0].isIntersecting && hasMore) {
callback();
}
});
if (node) observerRef.current.observe(node);
}, [loading, hasMore, callback]);

return lastElementRef;
}

function InfiniteList() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);

const loadMore = useCallback(async () => {
setLoading(true);
const res = await fetchItems(page);
setItems(prev => [...prev, ...res.data]);
setHasMore(res.hasMore);
setPage(p => p + 1);
setLoading(false);
}, [page]);

const lastRef = useInfiniteScroll(loadMore, hasMore, loading);

return (
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
<li ref={lastRef} style={{ height: 1 }} />
</ul>
);
}

6.3 场景三:权限与路由守卫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function useAuth() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
authApi.getCurrentUser()
.then(setUser)
.finally(() => setLoading(false));
}, []);

const hasPermission = useCallback((permission) => {
return user?.permissions?.includes(permission);
}, [user]);

return { user, loading, hasPermission };
}

function ProtectedRoute({ permission, children }) {
const { user, loading, hasPermission } = useAuth();

if (loading) return <Spin />;
if (!user) return <Navigate to="/login" />;
if (permission && !hasPermission(permission)) return <Forbidden />;

return children;
}

6.4 场景四:性能优化(虚拟列表 + useMemo)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function VirtualList({ items, itemHeight }) {
const [scrollTop, setScrollTop] = useState(0);
const containerRef = useRef();

const visibleRange = useMemo(() => {
const start = Math.floor(scrollTop / itemHeight);
const end = start + Math.ceil(containerRef.current?.clientHeight / itemHeight) + 2;
return [Math.max(0, start), Math.min(items.length, end)];
}, [scrollTop, itemHeight, items.length]);

const visibleItems = useMemo(() => {
const [start, end] = visibleRange;
return items.slice(start, end).map((item, i) => ({
...item,
index: start + i,
}));
}, [items, visibleRange]);

return (
<div
ref={containerRef}
onScroll={e => setScrollTop(e.target.scrollTop)}
style={{ height: '100%', overflow: 'auto' }}
>
<div style={{ height: items.length * itemHeight, position: 'relative' }}>
{visibleItems.map(item => (
<div key={item.id} style={{ position: 'absolute', top: item.index * itemHeight }}>
{item.content}
</div>
))}
</div>
</div>
);
}

七、常见陷阱与最佳实践

7.1 闭包陷阱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ❌ 错误:setTimeout 中拿到的是旧 count
function Bad() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // count 永远是 0
}, 1000);
return () => clearInterval(id);
}, []); // 缺少 count 依赖
}

// ✅ 正确:使用函数式更新
function Good() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
}

7.2 依赖数组的完整性

  • 建议使用 eslint-plugin-react-hooksexhaustive-deps 规则
  • 不要随意省略依赖,可能导致 stale closure

7.3 何时用 useCallback / useMemo

  • 需要传递回调给 memo 子组件时用 useCallback
  • 计算成本高且依赖稳定时用 useMemo
  • 不要过度使用,有时重新创建的开销小于记忆化

八、总结

主题 要点
概念 Hooks 让函数组件拥有 state 和副作用能力
原理 链表存储,依赖调用顺序,current/workInProgress 双缓冲
源码 极简实现可帮助理解,真实实现见 React 仓库
自定义 抽取可复用逻辑,命名以 use 开头
实践 避免闭包陷阱,合理使用依赖数组,按需优化

React Hooks 是现代 React 开发的核心,掌握其原理与最佳实践,能显著提升代码质量与开发效率。


参考资料

Android 核心组件与核心设计思想

由浅入深,从四大组件、应用骨架到设计哲学与最佳实践,系统梳理 Android 核心组件体系与架构思想


一、基本概念

1.1 什么是 Android 核心组件?

Android 核心组件是系统提供的、用于构建应用的基本构建块。应用由多个组件组成,组件之间通过 IntentBinder 等机制协作,由系统负责创建、调度和生命周期管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
┌─────────────────────────────────────────────────────────────────────────┐
│ Android 应用 = 组件的组合与协作 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 四大组件 │
│ Activity / Service / BroadcastReceiver / ContentProvider │
│ │ │
│ ▼ │
│ Intent(意图)──► 组件间通信、启动、数据传递 │
│ Context(上下文)──► 访问资源、启动组件、获取系统服务 │
│ Application ──► 进程级单例,应用全局状态与初始化 │
│ │
└─────────────────────────────────────────────────────────────────────────┘

1.2 为什么是「组件化」?

Android 没有「一个 main 入口跑到底」的传统应用模型,而是采用 组件化 + 系统调度

设计点 说明
可复用 任意应用可通过 Intent 启动其他应用的 Activity(如调起相机、地图),无需链接其代码
可替换 同一种组件类型可有多个实现(如多个浏览器),用户或系统可选择
生命周期由系统管 组件由系统创建与销毁,便于在内存紧张时回收、在配置变更时重建
安全与隔离 组件运行在应用进程内,通过权限与 Intent 过滤控制谁能调谁

二、四大核心组件

2.1 Activity:界面与用户交互

Activity 代表一个「界面」或「屏幕」。用户看到的每一个全屏/窗口化界面,通常对应一个 Activity(或 Fragment 承载的视图)。

要点 说明
职责 提供 UI、处理用户输入、与用户交互
生命周期 onCreate → onStart → onResume → (运行中) → onPause → onStop → onDestroy
启动方式 其他 Activity 或应用通过 startActivity(Intent) 启动
任务栈 多个 Activity 组成「返回栈」,Back 键按栈顶依次退出
1
2
3
4
5
6
7
8
9
10
11
12
┌─────────────────────────────────────────────────────────────────────────┐
│ Activity 典型生命周期(简化) │
│ │
│ 创建 ──► onCreate() ──► onStart() ──► onResume() ──► 可见且可交互 │
│ │ │
│ ▼ │
│ 切到后台 / 被遮挡 ──► onPause() ──► onStop() │
│ │ │
│ 再次回到前台 ──► onRestart() ──► onStart() ──► onResume() │
│ │
│ 销毁 ──► onDestroy() │
└─────────────────────────────────────────────────────────────────────────┘
  • 与 Fragment 的关系:一个 Activity 可包含多个 Fragment,Fragment 是「界面片段」,用于在同一个 Activity 内做模块化 UI 与复用。

2.2 Service:后台任务与长期运行

Service 用于在「无界面」的情况下执行长时间运行的任务,或为其他组件提供后台能力(如播放音乐、下载、同步)。

类型 说明 典型场景
Started Service 由 startService() 启动,可长期运行直到 stopSelf() 或 stopService() 音乐播放、下载、上传
Bound Service 由 bindService() 绑定,为其他组件提供 C/S 式接口,无客户端绑定后可由系统回收 本地 SDK、数据服务、AIDL 服务
  • 生命周期:onCreate → onStartCommand(Started)或 onBind(Bound)→ 运行 → onUnbind / onDestroy。
  • 前台服务:若需长时间在后台运行且不被系统轻易杀死,可调用 startForeground() 并显示持久通知,符合系统对「前台服务」的规范。

2.3 BroadcastReceiver:事件广播与响应

BroadcastReceiver 用于接收系统或应用发出的「广播」(如开机完成、网络变化、电量低、自定义事件),并做轻量级响应。

要点 说明
注册方式 静态注册(AndroidManifest.xml)或动态注册(代码中 registerReceiver)
执行 主线程、短时执行;耗时逻辑应交给 Service 或 WorkManager
有序广播 可指定优先级与「截断」传播(abortBroadcast)

常见系统广播:ACTION_BOOT_COMPLETEDACTION_BATTERY_LOWCONNECTIVITY_ACTION 等。

2.4 ContentProvider:数据抽象与跨进程共享

ContentProvider 对数据提供统一的「增删改查」接口(类似小型数据库 API),并支持跨应用、跨进程访问,是 Android 中「数据共享」的标准方式。

要点 说明
职责 封装数据源(SQLite、文件、内存、网络),对外提供 URI 与 Cursor/ContentValues API
跨进程 底层通过 Binder 暴露,其他应用通过 ContentResolver 访问,无需直接依赖实现方
权限 可在 AndroidManifest 中为 Provider 声明读写权限,由系统做权限校验

系统示例:通讯录、媒体库、设置等,均通过 ContentProvider 暴露给其他应用。


三、支撑性核心:Intent、Context、Application

3.1 Intent:意图与组件间通信

Intent 表示「要做的一件事」或「要传递的一包信息」,用于启动 Activity/Service、发送广播,并携带数据与标志。

类型 说明 典型用法
显式 Intent 指定 ComponentName(包名 + 类名),明确启动哪个组件 应用内页面跳转、指定 Service
隐式 Intent 只指定 action、category、data(可选),由系统根据各组件声明的 <intent-filter> 匹配 调起相机、分享、打开链接、跨应用启动
1
2
显式:我知道要启动谁 → setComponent / setClassName
隐式:我只描述「要做什么」→ setAction / addCategory / setData,系统找合适的组件
  • Intent 可携带:Bundle 数据、Extra、Flags(如 FLAG_ACTIVITY_NEW_TASK、FLAG_ACTIVITY_SINGLE_TOP)等,是组件间解耦通信的载体。

3.2 Context:上下文与资源访问

Context 是「当前组件/应用所在环境」的抽象,提供:

  • 访问应用资源(布局、字符串、drawable、主题等)
  • 启动组件(startActivity、startService、sendBroadcast)
  • 获取系统服务(getSystemService:如 LayoutInflater、ActivityManager、LocationManager)
  • 访问应用专属目录与 SharedPreferences 等
常见实现 说明
Activity Activity 本身是 Context,且带有「界面/任务」相关能力(如 startActivityForResult)
Application 进程级单例 Context,生命周期等于进程,适合做全局初始化与单例持有
Service Service 也是 Context,但无界面相关 API

注意:长时间持有 Activity 的 Context 容易导致内存泄漏(如静态变量引用 Activity),在异步回调中应避免;可改用 Application Context 或弱引用。

3.3 Application:应用级单例

Application 在进程启动时由系统创建,且每个进程只有一个实例。常用作:

  • 应用级初始化(SDK、全局配置、数据库等)
  • 全局单例或依赖注入容器的持有
  • 实现 Application 级别的生命周期回调(如 onConfigurationChanged)

在 AndroidManifest.xml 中通过 <application android:name=".MyApplication"> 指定自定义子类。


四、核心设计思想

4.1 组件化与「应用即组件集合」

Android 不强调「一个程序从 main 开始跑」,而是强调:

  • 应用由多个组件构成,每个组件有明确类型(Activity/Service/…)和声明(AndroidManifest)。
  • 入口是「可被系统或其它应用调用的组件」:例如 LAUNCHER Activity、可被隐式 Intent 匹配的 Activity/Service。
  • 组件之间通过 Intent 解耦:调用方只表达「意图」,不直接依赖实现类,便于复用与替换。
1
2
3
4
5
6
7
8
9
10
11
12
┌─────────────────────────────────────────────────────────────────────────┐
│ 组件化思想:应用 = 组件 + 声明 + 意图驱动 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ AndroidManifest.xml 声明组件与 Intent-Filter │
│ │ │
│ ▼ │
│ 系统 / 其他应用 ──► 发出 Intent ──► 系统解析 ──► 创建并启动对应组件 │
│ │
│ 同一「动作」可有多个实现(如多个浏览器),用户或系统选择其一 │
│ │
└─────────────────────────────────────────────────────────────────────────┘

4.2 生命周期由系统托管

组件的创建、暂停、恢复、销毁由 系统 根据用户行为、系统资源、配置变更(如旋转、多窗口、语言切换)统一调度:

  • 开发者实现生命周期回调(如 Activity 的 onPause/onResume),在「合适的时机」保存状态、释放资源、恢复 UI。
  • 系统可在内存紧张时回收后台组件;配置变更时销毁并重建 Activity,通过 onSaveInstanceState / ViewModel 等恢复状态。

这种设计把「何时创建/销毁」交给系统,应用只响应「已经发生」的生命周期事件,便于多任务与资源管理。

4.3 安全与权限模型

  • 进程隔离:每个应用默认运行在独立进程,内存与执行空间隔离。
  • 组件级权限:可在 AndroidManifest 中为 Activity/Service/ContentProvider 等声明 android:permission,调用方需具备相应权限才能启动或访问。
  • 运行时权限:危险权限(如相机、位置、存储)需在运行时向用户申请,用户同意后应用才可使用。
  • Intent 与导出:组件可设为 android:exported="true/false",控制是否允许其他应用通过隐式 Intent 调起。

4.4 多任务与任务栈

  • 任务(Task):通常对应用户概念里的「一个应用的一组界面」,由一组 Activity 的栈组成。
  • 启动模式:Activity 的 launchMode(standard、singleTop、singleTask、singleInstance)以及 Intent 的 Flags 共同决定「新 Activity 如何入栈、是否复用已有实例」,从而影响返回栈行为与多任务表现。

五、从设计思想到最佳实践

5.1 用 Intent 做解耦

  • 应用内跳转:可用显式 Intent,也可用隐式 Intent + 自定义 action,便于后续替换实现或做 Deep Link。
  • 跨应用能力:尽量用系统或公共的 action/data 约定(如 VIEW + http/https),或在自己的 Provider/Activity 上声明清晰的 intent-filter,便于被系统或其他应用发现和调起。

5.2 生命周期与状态保存

  • 短时状态:在 onSaveInstanceState 中保存,在 onCreate/onRestoreInstanceState 中恢复。
  • 界面相关数据:使用 ViewModel + LiveData/StateFlow,在配置变更时保留,避免在 Activity 里堆业务状态。
  • 释放资源:在 onPause/onStop 中暂停耗时操作、释放监听与引用,在 onDestroy 中做最终清理,避免泄漏。

5.3 Service 与后台行为规范

  • 短时任务:优先用 WorkManager协程 + 应用前后台状态,避免长时间占住 Service。
  • 需要长时间运行且用户可感知(如音乐播放、导航):使用 前台 Service,并按规定显示通知。
  • 避免在 BroadcastReceiver 中做重活:应启动 Service 或提交到 WorkManager。

5.4 架构分层与组件角色

  • Activity/Fragment:只做 UI 与用户输入,将业务与数据交给 ViewModel 或 Presenter。
  • ViewModel:持有界面状态与业务逻辑入口,不持有 Context/View,便于测试与复用。
  • Repository / UseCase:封装数据来源(网络、本地、ContentProvider),对上层提供统一接口。
  • ContentProvider:仅在需要「跨应用/跨进程共享数据」时使用;应用内本地数据可用 Room + DAO 等,不必强行上 Provider。

六、小结

主题 要点
四大组件 Activity(界面)、Service(后台)、BroadcastReceiver(广播)、ContentProvider(数据抽象与跨应用共享)
支撑核心 Intent(意图与组件通信)、Context(资源与系统服务)、Application(进程级单例)
设计思想 组件化、系统托管生命周期、Intent 驱动与解耦、安全与权限、任务栈与多任务
实践方向 善用 Intent 解耦、重视生命周期与状态保存、规范使用 Service 与后台、UI 与业务分层

理解「应用即组件的组合」「生命周期由系统托管」「Intent 表达意图、系统负责匹配与调度」,就能更好地把握 Android 核心组件与设计思想,并在此基础上用好 ViewModel、WorkManager、Jetpack 等现代组件与工具。

设计模式全解:原理、UML 与项目实战

系统梳理 GoF 23 种设计模式,结合前端与 Node.js 实战代码,配 UML 类图与时序示意图辅助理解。


目录

  1. 什么是设计模式
  2. 创建型模式(5 种)
  3. 结构型模式(7 种)
  4. 行为型模式(11 种)
  5. 速查总表

一、什么是设计模式

设计模式(Design Pattern) 是软件工程中反复出现问题的通用可复用解决方案。1994 年,Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides(即「四人组 GoF」)在《设计模式:可复用面向对象软件的基础》中总结了 23 种经典模式,按目的分为三类:

1
2
3
创建型(Creational)  ——  解决对象的创建问题
结构型(Structural) —— 解决类/对象的组合问题
行为型(Behavioral) —— 解决对象间职责与通信问题

设计原则(SOLID)

原则 说明
S ingle Responsibility 一个类只做一件事
O pen/Closed 对扩展开放,对修改关闭
L iskov Substitution 子类可替换父类而不破坏程序
I nterface Segregation 接口最小化,不强迫实现不需要的方法
D ependency Inversion 依赖抽象,而非具体实现

二、创建型模式(5 种)

2.1 单例模式(Singleton)

意图:保证一个类只有一个实例,并提供全局访问点。

场景:全局状态管理(Store)、日志记录器、配置管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Store {
static #instance = null;

#state = {};

static getInstance() {
if (!Store.#instance) {
Store.#instance = new Store();
}
return Store.#instance;
}

getState() { return this.#state; }
setState(patch) { Object.assign(this.#state, patch); }
}

const a = Store.getInstance();
const b = Store.getInstance();
console.log(a === b); // true

2.2 工厂方法模式(Factory Method)

意图:定义创建对象的接口,让子类决定实例化哪个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Button {
render() { throw new Error('abstract'); }
}
class PrimaryButton extends Button {
render() { return `<button class="btn-primary">OK</button>`; }
}
class DangerButton extends Button {
render() { return `<button class="btn-danger">Delete</button>`; }
}

function createButton(type) {
const map = { primary: PrimaryButton, danger: DangerButton };
const Ctor = map[type];
if (!Ctor) throw new Error(`Unknown type: ${type}`);
return new Ctor();
}

createButton('primary').render(); // <button class="btn-primary">OK</button>

2.3 抽象工厂模式(Abstract Factory)

意图:提供一个接口,用于创建一系列相关或依赖对象,而不指定具体类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const lightFactory = {
createButton: () => ({ type: 'button', theme: 'light' }),
createInput: () => ({ type: 'input', theme: 'light' }),
};
const darkFactory = {
createButton: () => ({ type: 'button', theme: 'dark' }),
createInput: () => ({ type: 'input', theme: 'dark' }),
};

function buildUI(factory) {
return {
button: factory.createButton(),
input: factory.createInput(),
};
}
buildUI(darkFactory); // { button: {theme:'dark'}, input: {theme:'dark'} }

2.4 建造者模式(Builder)

意图:将复杂对象的构造表示分离,同一构建过程可创建不同表示。

场景:SQL 查询构建、配置对象构造、链式调用 API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class QueryBuilder {
#table = '';
#conditions = [];
#fields = ['*'];
#limit = null;

from(table) { this.#table = table; return this; }
select(...f) { this.#fields = f; return this; }
where(cond) { this.#conditions.push(cond); return this; }
limit(n) { this.#limit = n; return this; }

build() {
let sql = `SELECT ${this.#fields.join(', ')} FROM ${this.#table}`;
if (this.#conditions.length) sql += ` WHERE ${this.#conditions.join(' AND ')}`;
if (this.#limit !== null) sql += ` LIMIT ${this.#limit}`;
return sql;
}
}

new QueryBuilder()
.from('users')
.select('id', 'name')
.where('age > 18')
.where('active = 1')
.limit(10)
.build();
// SELECT id, name FROM users WHERE age > 18 AND active = 1 LIMIT 10

2.5 原型模式(Prototype)

意图:通过复制(克隆)已有实例来创建新对象,而不是通过 new

1
2
3
4
5
6
7
8
9
10
11
12
13
const userProto = {
greet() { return `Hi, I'm ${this.name}`; },
clone() { return Object.create(this); },
};

const alice = Object.create(userProto);
alice.name = 'Alice';

const bob = alice.clone();
bob.name = 'Bob';

alice.greet(); // Hi, I'm Alice
bob.greet(); // Hi, I'm Bob

三、结构型模式(7 种)

3.1 适配器模式(Adapter)

意图:将一个接口转换成调用方期望的另一个接口——解决不兼容问题。

场景:旧接口兼容、第三方 SDK 包装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 旧日志库,接口为 log(msg)
class OldLogger {
log(msg) { console.log('[OLD]', msg); }
}

// 新系统要求 info / warn / error 三个方法
class LoggerAdapter {
constructor(logger) { this.logger = logger; }
info(msg) { this.logger.log(`INFO ${msg}`); }
warn(msg) { this.logger.log(`WARN ${msg}`); }
error(msg) { this.logger.log(`ERROR ${msg}`); }
}

const logger = new LoggerAdapter(new OldLogger());
logger.info('server started'); // [OLD] INFO server started

3.2 桥接模式(Bridge)

意图:将抽象部分与实现部分分离,使二者可以独立变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 实现部分:渲染引擎
const canvasRenderer = { draw: (shape) => `Canvas: draw ${shape}` };
const svgRenderer = { draw: (shape) => `SVG: draw ${shape}` };

// 抽象部分:形状
class Shape {
constructor(renderer) { this.renderer = renderer; }
}
class Circle extends Shape {
draw() { return this.renderer.draw('circle'); }
}
class Square extends Shape {
draw() { return this.renderer.draw('square'); }
}

new Circle(svgRenderer).draw(); // SVG: draw circle
new Square(canvasRenderer).draw(); // Canvas: draw square

3.3 组合模式(Composite)

意图:将对象组织成树形结构,使单个对象和组合对象的使用方式一致。

场景:文件系统、组件树、权限树。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class File {
constructor(name, size) { this.name = name; this.size = size; }
getSize() { return this.size; }
}
class Folder {
constructor(name) { this.name = name; this.children = []; }
add(child) { this.children.push(child); return this; }
getSize() { return this.children.reduce((s, c) => s + c.getSize(), 0); }
}

const root = new Folder('root')
.add(new File('a.js', 10))
.add(
new Folder('src')
.add(new File('index.js', 20))
.add(new File('utils.js', 15))
);

root.getSize(); // 45

3.4 装饰器模式(Decorator)

意图:在不改变原对象的前提下,动态地为其添加功能。

场景:中间件(Koa/Express)、日志增强、缓存包装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function withLogging(fn) {
return function (...args) {
console.log(`[CALL] ${fn.name}(${args})`);
const result = fn.apply(this, args);
console.log(`[RET] ${result}`);
return result;
};
}

function add(a, b) { return a + b; }
const loggedAdd = withLogging(add);
loggedAdd(2, 3);
// [CALL] add(2,3)
// [RET] 5

3.5 外观模式(Facade)

意图:为子系统提供统一的高层接口,隐藏内部复杂性。

场景:SDK 封装、初始化流程统一入口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class AuthService   { login(u, p)    { return { token: 'tok_' + u }; } }
class ProfileService { fetch(token) { return { name: 'Alice' }; } }
class LogService { record(action) { console.log('[LOG]', action); } }

class AppFacade {
#auth = new AuthService();
#profile = new ProfileService();
#log = new LogService();

async signIn(username, password) {
const { token } = this.#auth.login(username, password);
const profile = this.#profile.fetch(token);
this.#log.record(`login:${username}`);
return { token, profile };
}
}

new AppFacade().signIn('alice', 'pw');

3.6 享元模式(Flyweight)

意图:共享细粒度对象以节省内存,将对象内在状态与外在状态分离。

场景:字符渲染、粒子系统、大量相似节点池。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class CharStyle {
constructor(font, size, color) {
this.font = font; this.size = size; this.color = color;
}
}

class CharStylePool {
#pool = new Map();
get(font, size, color) {
const key = `${font}-${size}-${color}`;
if (!this.#pool.has(key)) this.#pool.set(key, new CharStyle(font, size, color));
return this.#pool.get(key);
}
size() { return this.#pool.size; }
}

const pool = new CharStylePool();
// 渲染 1000 个字符,只需要极少的 CharStyle 对象
const chars = Array.from('Hello World x1000').map(ch => ({
char: ch,
style: pool.get('Arial', 14, '#333'),
}));
console.log(pool.size()); // 1(所有字符共享同一样式对象)

3.7 代理模式(Proxy)

意图:为另一个对象提供代理以控制对它的访问。

场景:懒加载、权限控制、缓存、数据校验(ES Proxy)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function createValidator(target, rules) {
return new Proxy(target, {
set(obj, prop, value) {
if (rules[prop] && !rules[prop](value)) {
throw new TypeError(`Invalid value for "${prop}": ${value}`);
}
obj[prop] = value;
return true;
},
});
}

const user = createValidator({}, {
age: v => typeof v === 'number' && v >= 0 && v <= 150,
email: v => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
});

user.age = 25; // OK
user.email = 'a@b.com'; // OK
// user.age = -1; // TypeError: Invalid value for "age": -1

四、行为型模式(11 种)

4.1 责任链模式(Chain of Responsibility)

意图:将请求沿着处理者链传递,直到某个处理者处理它为止。

场景:中间件管道(Koa)、审批流程、事件冒泡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Handler {
setNext(handler) { this.next = handler; return handler; }
handle(req) { return this.next?.handle(req); }
}

class AuthHandler extends Handler {
handle(req) {
if (!req.token) return '401 Unauthorized';
return super.handle(req);
}
}
class RoleHandler extends Handler {
handle(req) {
if (req.role !== 'admin') return '403 Forbidden';
return super.handle(req);
}
}
class BusinessHandler extends Handler {
handle(req) { return `200 OK: Hello ${req.user}`; }
}

const chain = new AuthHandler();
chain.setNext(new RoleHandler()).setNext(new BusinessHandler());

chain.handle({ token: 'tok', role: 'admin', user: 'Alice' }); // 200 OK
chain.handle({ token: 'tok', role: 'guest' }); // 403 Forbidden
chain.handle({}); // 401 Unauthorized

4.2 命令模式(Command)

意图:将操作封装为对象,支持撤销、重做、队列调度。

场景:编辑器操作历史、任务队列、宏录制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class TextEditor {
#text = '';
#history = [];

execute(command) {
this.#text = command.execute(this.#text);
this.#history.push(command);
return this;
}
undo() {
const cmd = this.#history.pop();
if (cmd) this.#text = cmd.undo(this.#text);
return this;
}
value() { return this.#text; }
}

const appendCmd = (str) => ({
execute: (t) => t + str,
undo: (t) => t.slice(0, -str.length),
});

const ed = new TextEditor();
ed.execute(appendCmd('Hello')).execute(appendCmd(', World'));
ed.value(); // Hello, World
ed.undo();
ed.value(); // Hello

4.3 解释器模式(Interpreter)

意图:为语言定义文法,并实现解释器来解释该语言的句子。

场景:模板引擎、DSL 解析、规则引擎。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 极简四则运算解释器
function interpret(expr) {
expr = expr.trim();
const addIdx = expr.lastIndexOf('+');
const subIdx = expr.lastIndexOf('-');
const pivot = Math.max(addIdx, subIdx);
if (pivot > 0) {
const left = interpret(expr.slice(0, pivot));
const right = interpret(expr.slice(pivot + 1));
return expr[pivot] === '+' ? left + right : left - right;
}
return parseFloat(expr);
}

interpret('1 + 2 + 3'); // 6
interpret('10 - 3 + 2'); // 9

4.4 迭代器模式(Iterator)

意图:提供顺序访问集合元素的方法,而不暴露其内部表示。

JavaScript 内置迭代器协议(Symbol.iterator)即此模式的语言级实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Range {
constructor(start, end, step = 1) {
this.start = start; this.end = end; this.step = step;
}
[Symbol.iterator]() {
let cur = this.start;
return {
next: () => cur <= this.end
? { value: cur += this.step, done: false }
: { done: true },
};
}
}

[...new Range(1, 10, 2)]; // [3, 5, 7, 9, 11] — 每步 +2
for (const n of new Range(0, 5)) console.log(n); // 1 2 3 4 5

4.5 中介者模式(Mediator)

意图:用中介对象封装一组对象的交互,减少对象间直接引用。

场景:聊天室、事件总线、机场调度。

1
2
3
4
5
6
7
8
9
10
11
12
13
class EventBus {
#listeners = new Map();
on(event, fn) { (this.#listeners.get(event) ?? this.#listeners.set(event, new Set()).get(event)).add(fn); }
off(event, fn) { this.#listeners.get(event)?.delete(fn); }
emit(event, data) { this.#listeners.get(event)?.forEach(fn => fn(data)); }
}

const bus = new EventBus();
bus.on('login', ({ user }) => console.log(`Welcome, ${user}`));
bus.on('login', ({ user }) => console.log(`Log: ${user} logged in`));
bus.emit('login', { user: 'Alice' });
// Welcome, Alice
// Log: Alice logged in

4.6 备忘录模式(Memento)

意图:在不破坏封装的前提下,捕获并外部化对象的内部状态,以便恢复。

场景:撤销/重做、草稿保存、游戏存档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class FormState {
#snapshots = [];
#current = {};

set(patch) { this.#current = { ...this.#current, ...patch }; }
snapshot() { this.#snapshots.push({ ...this.#current }); }
restore() { if (this.#snapshots.length) this.#current = this.#snapshots.pop(); }
get() { return { ...this.#current }; }
}

const form = new FormState();
form.set({ name: 'Alice' });
form.snapshot();
form.set({ name: 'Bob', age: 30 });
form.get(); // { name: 'Bob', age: 30 }
form.restore();
form.get(); // { name: 'Alice' }

4.7 观察者模式(Observer)

意图:对象间一对多依赖,当一个对象状态变化时,自动通知所有依赖对象。

场景:Vue/React 响应式、DOM 事件、数据绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Subject {
#observers = new Set();
#state;

subscribe(fn) { this.#observers.add(fn); }
unsubscribe(fn) { this.#observers.delete(fn); }

setState(val) {
this.#state = val;
this.#observers.forEach(fn => fn(val));
}
getState() { return this.#state; }
}

const counter = new Subject();
counter.subscribe(v => console.log('A sees:', v));
counter.subscribe(v => console.log('B sees:', v));
counter.setState(42);
// A sees: 42
// B sees: 42

4.8 状态模式(State)

意图:允许对象在内部状态改变时改变其行为,使对象看起来修改了它的类。

场景:订单状态机、红绿灯、播放器状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const states = {
idle: { fetch: (ctx) => ctx.transition('loading') },
loading: {
resolve: (ctx, data) => { ctx.data = data; ctx.transition('success'); },
reject: (ctx, err) => { ctx.error = err; ctx.transition('error'); },
},
success: { reset: (ctx) => ctx.transition('idle') },
error: { reset: (ctx) => ctx.transition('idle') },
};

class AsyncMachine {
#state = 'idle';
data = null;
error = null;

transition(s) { this.#state = s; console.log('→', s); }
dispatch(action, payload) {
const handler = states[this.#state]?.[action];
if (!handler) return console.warn(`No "${action}" in state "${this.#state}"`);
handler(this, payload);
}
getState() { return this.#state; }
}

const m = new AsyncMachine();
m.dispatch('fetch'); // → loading
m.dispatch('resolve', { id: 1 }); // → success
m.dispatch('reset'); // → idle

4.9 策略模式(Strategy)

意图:定义一系列算法,封装每一个并使它们可互换,独立于使用者而变化。

场景:排序算法、支付方式、表单验证、折扣计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const sortStrategies = {
bubble: (arr) => {
const a = [...arr];
for (let i = 0; i < a.length; i++)
for (let j = 0; j < a.length - i - 1; j++)
if (a[j] > a[j+1]) [a[j], a[j+1]] = [a[j+1], a[j]];
return a;
},
quick: (arr) => {
if (arr.length <= 1) return arr;
const pivot = arr[0];
const left = arr.slice(1).filter(x => x <= pivot);
const right = arr.slice(1).filter(x => x > pivot);
return [...sortStrategies.quick(left), pivot, ...sortStrategies.quick(right)];
},
};

class Sorter {
constructor(strategy = 'quick') { this.strategy = strategy; }
sort(arr) { return sortStrategies[this.strategy](arr); }
}

new Sorter('bubble').sort([3, 1, 4, 1, 5]); // [1, 1, 3, 4, 5]
new Sorter('quick').sort([3, 1, 4, 1, 5]); // [1, 1, 3, 4, 5]

4.10 模板方法模式(Template Method)

意图:在基类定义算法骨架,将某些步骤的实现延迟到子类,不改变结构只改变细节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class DataExporter {
// 模板方法:算法骨架
export(data) {
const processed = this.process(data);
const formatted = this.format(processed);
this.save(formatted);
}

process(data) { return data.filter(Boolean); } // 默认实现
format(data) { throw new Error('abstract'); } // 子类实现
save(content) { console.log('saving:', content.slice(0, 40)); }
}

class CSVExporter extends DataExporter {
format(data) { return data.map(r => Object.values(r).join(',')).join('\n'); }
}
class JSONExporter extends DataExporter {
format(data) { return JSON.stringify(data, null, 2); }
}

const rows = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
new CSVExporter().export(rows); // saving: 1,Alice\n2,Bob
new JSONExporter().export(rows); // saving: [\n {\n "id": 1,

4.11 访问者模式(Visitor)

意图:在不改变元素类的前提下,为其定义作用于这些元素的新操作。

场景:AST 遍历、报表统计、编译器优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class NumberNode  { constructor(v)    { this.value = v; } accept(v) { return v.visitNumber(this); } }
class AddNode { constructor(l, r) { this.left = l; this.right = r; } accept(v) { return v.visitAdd(this); } }
class MulNode { constructor(l, r) { this.left = l; this.right = r; } accept(v) { return v.visitMul(this); } }

// 求值访问者
const evalVisitor = {
visitNumber: (n) => n.value,
visitAdd: (n) => n.left.accept(evalVisitor) + n.right.accept(evalVisitor),
visitMul: (n) => n.left.accept(evalVisitor) * n.right.accept(evalVisitor),
};

// 打印访问者
const printVisitor = {
visitNumber: (n) => `${n.value}`,
visitAdd: (n) => `(${n.left.accept(printVisitor)} + ${n.right.accept(printVisitor)})`,
visitMul: (n) => `(${n.left.accept(printVisitor)} * ${n.right.accept(printVisitor)})`,
};

// (2 + 3) * 4
const ast = new MulNode(new AddNode(new NumberNode(2), new NumberNode(3)), new NumberNode(4));
ast.accept(evalVisitor); // 20
ast.accept(printVisitor); // ((2 + 3) * 4)

五、速查总表

分类 模式 核心意图 常见场景
创建型 单例 唯一实例 Store、Logger、Config
创建型 工厂方法 子类决定实例化 组件工厂、解析器
创建型 抽象工厂 相关对象族 主题 UI、跨平台组件
创建型 建造者 分步构建复杂对象 QueryBuilder、配置
创建型 原型 克隆已有实例 对象池、深拷贝
结构型 适配器 接口转换 旧库兼容、SDK 包装
结构型 桥接 抽象与实现分离 渲染引擎、驱动层
结构型 组合 树形统一接口 文件系统、组件树
结构型 装饰器 动态扩展功能 中间件、HOC、缓存
结构型 外观 统一高层接口 SDK 初始化入口
结构型 享元 共享细粒度对象 字符渲染、粒子池
结构型 代理 控制对象访问 懒加载、权限、校验
行为型 责任链 链式处理请求 中间件管道、审批流
行为型 命令 操作对象化 撤销/重做、任务队列
行为型 解释器 语言文法解析 模板引擎、DSL
行为型 迭代器 统一遍历接口 for…of、Generator
行为型 中介者 集中对象交互 EventBus、聊天室
行为型 备忘录 状态快照/恢复 撤销历史、草稿
行为型 观察者 自动通知订阅者 Vue 响应式、事件
行为型 状态 状态驱动行为 订单流、状态机
行为型 策略 算法可互换 支付、排序、折扣
行为型 模板方法 固定骨架/可变步骤 导出流程、生命周期
行为型 访问者 不改类增加操作 AST 遍历、报表

iOS 开发中的热更新

由浅入深,从基本概念到源码解析,带你全面掌握 iOS 热更新的原理、方案与实战理、方案与实战


一、什么是热更新?为什么需要它?

1.1 从「发版周期」说起

传统原生 iOS 开发的痛点:

  • 审核周期长:App Store 审核通常需要 1~3 天,紧急 Bug 无法及时修复
  • 版本割裂:用户更新率有限,线上可能同时存在多个旧版本
  • 迭代成本高:每次改动都要重新打包、提审、等待上线

热更新(Hot Update) 的核心诉求是:在不发新版本、不通过 App Store 审核的前提下,动态更新应用逻辑与资源

1.2 热更新的分类

类型 说明 典型场景
资源热更新 更新图片、配置、文案等非代码资源 活动页、Banner、AB 实验
脚本热更新 更新 JavaScript/Lua 等脚本,在解释器中执行 RN、Weex、游戏 Lua
原生热修复 通过 Runtime 等手段替换/修补 OC 方法 紧急 Bug 修复(受政策限制)

1.3 热更新能解决什么问题?

  • 紧急 Bug 修复:线上崩溃、逻辑错误,可快速下发补丁
  • 业务快速迭代:活动页、营销逻辑,无需等审核
  • 灰度与回滚:小范围验证后全量,出问题可秒级回滚

二、核心原理

2.1 动态执行:热更新的基础

iOS 支持多种「运行时执行代码」的方式,这是热更新的技术前提:

1
2
3
4
5
6
7
┌─────────────────────────────────────────────────────────────┐
│ 热更新技术栈层次 │
├─────────────────────────────────────────────────────────────┤
│ 应用层 │ 下发 JS/Lua/配置 → 解释执行 / 加载资源 │
│ 中间层 │ JavaScriptCore / Lua VM / 自研解释器 │
│ 系统层 │ Runtime、dlopen、performSelector 等 │
└─────────────────────────────────────────────────────────────┘

不同方案对「动态执行」的依赖程度不同,也决定了其合规性。

2.2 两类热更新思路

思路 机制 代表方案 Apple 态度
脚本在容器内执行 使用系统提供的 JS 引擎(如 JavaScriptCore)执行脚本,不直接修改原生代码 React Native OTA、Weex 原则上允许
直接 Hook 原生方法 通过 Runtime、libffi 等替换 OC 方法的 IMP,或动态下发可执行代码 JSPatch、WaxPatch 明确禁止

2.3 Apple 3.3.2 条款与审核边界

Apple 开发者协议 3.3.2 明确规定:

应用不得包含、提供或使用未包含在应用中的可执行代码的下载、安装或执行机制。

被严格禁止的:

  • 下载并执行任意原生代码(如通过 dlopendlsym 动态加载)
  • 使用可调用原生 API 的脚本引擎(如 JSPatch 通过 JS 调用 performSelector:、修改 IMP)
  • 绕过审核、改变应用主要功能或目的的动态能力

允许的例外:

  • 使用 WebKitJavaScriptCore 执行脚本
  • 前提:不改变应用的主要功能或目的,与提交版本及宣传描述相符
  • 典型:React Native 的 JS Bundle 热更新、游戏内 Lua 脚本(在引擎容器内运行)

核心差异:Apple 禁止的是「能绕过审核、调用私有 API、实质改变应用功能」的方案,而非热更新本身。使用系统 JS 引擎、仅更新业务逻辑(不修改原生层)的方案,在合理范围内通常可接受。


三、主流方案概览

3.1 方案对比

方案 类型 合规性 能力边界 适用场景
React Native OTA 脚本热更新 ✅ 合规 只更新 JS 层 RN 项目
CodePush 脚本热更新 ✅ 合规 RN/Weex JS Bundle 需完善 OTA 能力的 RN 项目
JSPatch 原生热修复 ❌ 禁止 可替换任意 OC 方法 已弃用
TTDFKit (TTPatch) 原生热修复 ⚠️ 风险 基于 libffi 动态调用 内测/企业包
BuglyHotfix 原生热修复 ⚠️ 风险 兼容 JSPatch 脚本 企业级、需完整工具链
Lua + 游戏引擎 脚本热更新 ✅ 合规 游戏逻辑在引擎内 游戏项目

3.2 选型建议

项目类型 推荐方案
React Native 应用 CodePush 或自建 OTA 服务
纯原生 + 需热修 优先考虑架构调整(如 RN/Flutter 化),慎用原生热修
游戏 引擎自带 Lua/脚本热更
企业内部分发 可评估 TTDFKit、BuglyHotfix,注意合规

四、React Native OTA 热更新

4.1 基本原理

RN 的 UI 与业务逻辑运行在 JavaScript 中,而 JS 可由 JavaScriptCore(或 Hermes)在运行时执行。因此,只需将新的 JS Bundle 下发到设备,启动时优先加载该 Bundle,即可实现热更新,无需修改原生代码

1
2
3
4
5
6
7
8
┌─────────────────────────────────────────────────────────────┐
│ RN 热更新流程 │
├─────────────────────────────────────────────────────────────┤
│ 1. 服务端:打包 JS Bundle(含业务逻辑) │
│ 2. 下发:通过 HTTP/CDN 将 Bundle 下发到客户端 │
│ 3. 客户端:存储到本地(如 Document 目录) │
│ 4. 启动:RCTBridge 优先加载本地 Bundle,而非打包进 App 的 │
└─────────────────────────────────────────────────────────────┘

4.2 CodePush 架构

CodePush 是微软推出的 RN 热更新方案,由三部分组成:

组件 职责
code-push-server 服务端:身份认证、更新包存储、版本校验、下载、统计
code-push-cli 命令行:登录、打包、部署
react-native-code-push 客户端 SDK:检测更新、下载、安装、上报

版本策略:支持 semver 约束,例如 ^1.2.3 表示 >=1.2.3 <2.0.0 的原生版本可收到该更新。

4.3 集成示例

1
2
3
# 安装
npm install --save react-native-code-push
cd ios && pod install
1
2
3
4
5
6
7
8
9
10
11
// AppDelegate.m - 指定 Bundle 加载路径
#import <CodePush/CodePush.h>

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
#else
return [CodePush bundleURL]; // 生产环境从 CodePush 获取
#endif
}
1
2
3
4
5
6
7
// 检查更新
import codePush from 'react-native-code-push';

codePush.sync({
updateDialog: { title: '发现新版本' },
installMode: codePush.InstallMode.IMMEDIATE,
});

4.4 自建 OTA 简要思路

若不使用 CodePush,可自建:

  1. 服务端:提供接口,根据 appVersionbinaryVersion 返回可用的 Bundle 信息(URL、hash、是否强制)
  2. 客户端:启动时请求接口,若有新版本则下载到本地
  3. 加载:修改 RCTRootViewbridge 初始化,优先从本地路径加载 JS Bundle

五、JSPatch 原理与源码解析(历史与教育意义)

⚠️ JSPatch 已被 Apple 明确禁止,此处仅作原理与源码层面的学习参考。

5.1 核心思想

JSPatch 通过 JavaScript 调用 Objective-C,利用 OC 的 Runtime 动态性,实现:

  1. 用 JS 写「补丁逻辑」
  2. 下发 JS 到客户端
  3. 在 JavaScriptCore 中执行
  4. JS 通过桥接层调用 OC Runtime,替换方法实现(IMP)

5.2 JS 调用 OC 的底层机制

OC 是「消息型」语言,方法调用本质是 objc_msgSend(receiver, selector, ...)。JSPatch 的做法是:

  • JS 侧:调用 require('UIView').alloc().init()
  • 桥接层:将 UIViewallocinit 等字符串传给 OC
  • OC 侧:通过 NSClassFromStringclass_getInstanceMethod 等 Runtime API 获取类和方法,用 objc_msgSendNSInvocation 完成调用
1
2
3
4
5
6
7
8
9
10
11
JS: UIView.alloc().init()


JPEngine: 解析调用链 → 获取 Class、SEL


Runtime: objc_msgSend([UIView class], @selector(alloc))
objc_msgSend(allocResult, @selector(init))


返回 OC 对象给 JS(封装为 JPBoxing 等结构)

5.3 方法替换(热修复)实现

热修复的关键是「替换方法的 IMP」:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 伪代码:JSPatch 方法替换思路
// 原方法:- (void)originalMethod;
// 新实现:在 JS 中定义,通过桥接调用

static void (^jsImplementation)(id, ...) = ...; // 从 JS 传过来的 block

IMP newIMP = imp_implementationWithBlock(^(id self, ...) {
// 调用 JS 定义的逻辑
jsImplementation(self, ...);
});

// 替换
Method m = class_getInstanceMethod(cls, @selector(originalMethod));
method_setImplementation(m, newIMP);

5.4 为何被禁止

JSPatch 能让 JS 间接调用任意 OC 方法,包括:

  • performSelector:
  • class_replaceMethodmethod_setImplementation
  • 私有 API、系统内部方法

这相当于 绕过了 App Store 审核,可动态改变应用行为,因此被 Apple 明确列入违规。


六、TTDFKit / TTPatch 与 libffi

6.1 简介

TTDFKit(原 TTPatch)是基于 libffi 的 iOS 热修复方案,不依赖 JSPatch,但能力类似:

  • 方法替换、动态创建方法
  • 添加属性、支持 Block
  • 支持 JavaScript 脚本下发

6.2 libffi 的作用

libffi(Foreign Function Interface)用于在运行时根据函数签名动态调用 C 函数。OC 方法本质上也是 C 函数(带 self_cmd 等参数),通过 libffi 可以:

  1. 根据 NSMethodSignature 得到参数类型、返回值类型
  2. 构造 ffi_cif(调用约定)
  3. 使用 ffi_call 调用目标 IMP

从而实现「用动态生成的逻辑替换原方法」。

6.3 合规性说明

TTDFKit 同样具备「动态修改原生方法」的能力,在正式上架 App Store 的包中使用的合规风险与 JSPatch 类似,更适用于企业内部或内测分发场景。


七、实战示例:RN 自建 OTA

7.1 服务端接口设计(简化)

1
2
3
4
5
6
7
8
// GET /api/ota/check?platform=ios&version=1.2.3&build=10
{
"hasUpdate": true,
"downloadUrl": "https://cdn.example.com/bundle/1.2.3.10.jsbundle",
"hash": "abc123",
"forceUpdate": false,
"minVersion": "1.2.0"
}

7.2 客户端核心逻辑(示例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// OTAManager.m
- (void)checkUpdate {
NSString *url = [NSString stringWithFormat:
@"https://api.example.com/ota/check?platform=ios&version=%@&build=%@",
appVersion, buildNumber];
// 发起请求,解析 hasUpdate、downloadUrl、forceUpdate
// 若 hasUpdate,则下载到 Document/OTA/bundle.js
}

- (NSURL *)bundleURL {
NSString *otaPath = [self otaBundlePath];
if ([[NSFileManager defaultManager] fileExistsAtPath:otaPath]) {
return [NSURL fileURLWithPath:otaPath];
}
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
}

7.3 启动时加载

1
2
3
4
// AppDelegate.m
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
return [[OTAManager shared] bundleURL];
}

八、实际项目中的应用案例

8.1 电商 App:活动页热更新

场景:大促活动页面布局、规则频繁调整,无法每次发版。

方案:使用 React Native 搭建活动页,通过 CodePush 下发 JS Bundle。活动开始前发布新版本,用户打开即可看到最新逻辑,无需等 App Store 审核。

效果:活动页迭代周期从 1~3 天缩短到几分钟,支持灰度与秒级回滚。


8.2 金融 App:合规前提下的热更新

场景:需要快速修复展示类 Bug(如文案错误、接口字段变更),但不能修改核心交易逻辑。

方案

  • 交易、支付等核心流程保持纯原生,不热更
  • 非核心页面(资讯、活动、设置)采用 RN,通过 OTA 更新
  • 严格控制热更范围,符合「不改变应用主要功能」的审核边界

8.3 游戏:Lua 热更新

场景:Unity / Unreal / Cocos 等引擎,游戏逻辑用 Lua 编写。

方案:Lua 脚本存在服务端,启动时或进入场景时下载到本地,由引擎内嵌的 Lua VM 执行。不涉及原生代码动态替换,符合平台规则。


8.4 企业内包:JSPatch 替代方案

场景:内部分发、不通过 App Store,需要紧急修复原生 Bug。

方案:使用 TTDFKit 或 BuglyHotfix,下发 JS 补丁,替换有问题的 OC 方法。需注意仅用于内部分发场景,避免用于正式上架版本。


九、最佳实践与注意事项

9.1 合规优先

  • 上架 App Store 的应用:优先使用 RN OTA、CodePush 等脚本热更新,避免 JSPatch 类方案
  • 热更范围:尽量限制在业务逻辑、UI 展示,不触及支付、权限等敏感能力
  • 文档与描述:确保应用功能与审核描述一致,避免「隐藏能力」引起拒审

9.2 版本与兼容

  • 使用 语义化版本 约束可更新范围,避免旧版本收到不兼容的 Bundle
  • 做好 灰度:先小流量验证,再全量
  • 设计 回滚:保留上一版本 Bundle,出错时快速切回

9.3 安全与校验

  • 对 Bundle 做 哈希校验,防止篡改
  • 使用 HTTPS 下发,避免中间人攻击
  • 敏感逻辑不宜完全依赖热更,核心安全校验应留在原生

9.4 性能与体验

  • 启动时异步检查更新,不阻塞首屏
  • 大 Bundle 可做 差分更新,减少下载量
  • 明确 强制更新静默更新 策略,平衡体验与覆盖率

十、总结

维度 要点
概念 热更新 = 不发版、动态更新逻辑/资源,分资源、脚本、原生三类
原理 依赖运行时执行(JS 引擎、Runtime、libffi),不同方案能力与合规性不同
政策 Apple 3.3.2 禁止下载执行可执行代码;JS 在 JavaScriptCore 内执行、不改变主要功能的可接受
推荐 RN 项目用 CodePush/自建 OTA;纯原生慎用原生热修,优先考虑架构升级
实践 合规优先、版本约束、灰度回滚、安全校验

热更新能显著提升迭代效率,但必须在 合规、安全、可维护 的前提下使用。理解原理与边界,才能做出正确的技术选型与实现。

nginx

强制重装nginx
sudo apt-get purge nginx nginx-common
sudo apt-get install nginx

重载nginx配置
sudo systemctl reload nginx

iOS 开发中的 RunLoop

由浅入深,从基本概念到源码解析,带你全面理解 iOS 事件循环机制


一、什么是 RunLoop?

1.1 从 Event Loop 说起

通常,一个线程一次只能执行一个任务,任务完成后线程就会退出。但在 GUI 应用中,我们需要一种机制:线程能够随时处理事件或消息,并且在空闲时不会退出。这种机制称为 Event Loop(事件循环)

1
2
3
4
5
6
7
function main
initialize()
while message != quit
message := get_next_message()
process_message(message)
end while
end function

Event Loop 的核心问题是:

  • 如何管理事件/消息
  • 如何让线程在没有任务时休眠,避免 CPU 空转
  • 如何在事件到来时唤醒线程处理

1.2 RunLoop 是什么?

RunLoop 是苹果在 OSX/iOS 上对 Event Loop 的实现。它是一个对象,负责:

  • 管理需要处理的事件/消息
  • 提供入口函数执行「接收消息 → 等待 → 处理」的循环
  • 在没有事件时让线程休眠,有事件时唤醒并分发处理

RunLoop 从 Input SourcesTimer Sources 接收事件,然后在线程中执行对应的 Handler。

1.3 NSRunLoop 与 CFRunLoop

苹果提供了两层 API:

类型 说明 线程安全
NSRunLoop 基于 CFRunLoop 的 OC 封装,面向对象 API
CFRunLoopRef CoreFoundation 的 C 实现

日常开发多用 NSRunLoop,底层和性能相关则直接用 CFRunLoop


二、RunLoop 与线程

2.1 一一对应关系

  • RunLoop 和线程是 一一对应
  • 每个线程(含主线程)都有唯一的 RunLoop
  • RunLoop 在 首次获取 时创建,线程结束时 销毁
  • 只能在 对应线程内部 获取该线程的 RunLoop(主线程除外)
1
2
3
4
5
// 获取当前线程的 RunLoop
let runLoop = RunLoop.current

// 获取主线程 RunLoop(任意线程可调用)
let mainRunLoop = RunLoop.main

2.2 主线程 vs 子线程

  • 主线程:应用启动时,主线程 RunLoop 自动创建并运行
  • 子线程:默认不启动 RunLoop,需要主动调用 run 才会进入事件循环
1
2
3
4
5
6
7
// 主线程 RunLoop 自动运行,无需手动启动
// 子线程 RunLoop 默认不运行
Thread.detachNewThread {
let runLoop = RunLoop.current
runLoop.add(Port(), forMode: .default) // 必须有 source,否则 run 会立即退出
runLoop.run()
}

三、核心组件

3.1 Run Loop Source(事件源)

RunLoop 从两类 Source 接收事件:

类型 名称 特点 典型场景
Source0 Custom Input Source 需手动标记为待处理,不主动唤醒线程 UIEvent、CFSocket、普通回调
Source1 Port-Based Source 基于 Mach Port,可主动唤醒 RunLoop 系统触摸事件、进程间通信
1
2
Source0:用户事件、自定义事件 → 需外部调用 CFRunLoopSourceSignal 标记
Source1:系统 Port 事件 → 内核可主动唤醒线程

3.2 Run Loop Timer(定时器)

Timer 本质是 基于 Port 的 Source,所有 Timer 共用同一个「Mode Timer Port」。常见实现如 NSTimerCADisplayLink

3.3 Run Loop Observer(观察者)

Observer 不处理事件,而是 观察 RunLoop 的状态变化,可监控以下活动:

1
2
3
4
5
6
7
8
9
// CFRunLoopActivity 定义
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入 Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出 Loop
}

四、Run Loop Mode

4.1 什么是 Mode?

Mode 是 RunLoop 的「工作模式」。一个 RunLoop 包含多个 Mode,每个 Mode 下有各自的 Source、Timer、Observer。同一时刻 RunLoop 只运行在一个 Mode 下,只处理该 Mode 里的 Source/Timer/Observer。

这样可以把不同场景的事件隔离,例如:

  • 默认模式下处理普通事件
  • 滑动 ScrollView 时切到 UITrackingRunLoopMode,只处理触摸,保证滑动流畅

4.2 常见 Mode

Mode 说明
NSDefaultRunLoopMode 默认模式,App 空闲时主线程通常在此模式
UITrackingRunLoopMode 界面追踪模式,ScrollView 滑动时切换到此
NSRunLoopCommonModes 占位符,表示「Common 模式集合」

NSRunLoopCommonModes 默认包含 NSDefaultRunLoopModeUITrackingRunLoopMode。把 Timer/Source 加到 CommonModes,会在上述两种模式切换时都得到回调。

4.3 常见问题:Timer 滑动时失效

1
2
3
4
5
6
7
8
// 仅加入 DefaultMode:滑动 UITableView 时 Timer 暂停
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
print("tick")
}
RunLoop.main.add(timer, forMode: .default)

// 加入 CommonModes:滑动时 Timer 继续触发
RunLoop.main.add(timer, forMode: .common)

4.4 数据结构示意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct __CFRunLoopMode {
CFStringRef _name; // Mode 名称,如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Source0 集合
CFMutableSetRef _sources1; // Source1 集合
CFMutableArrayRef _observers; // Observer 数组
CFMutableArrayRef _timers; // Timer 数组
...
};

struct __CFRunLoop {
CFMutableSetRef _commonModes; // 标记为 Common 的 Mode 集合
CFMutableSetRef _commonModeItems; // 加到 Common 的 Source/Timer/Observer
CFRunLoopModeRef _currentMode; // 当前 Mode
CFMutableSetRef _modes; // 所有 Mode
...
};

commonModeItems 会被自动同步到所有 Common Mode 中,这就是把 Timer 加到 .common 能解决滑动暂停的原因。


五、RunLoop 工作流程(源码级)

5.1 整体流程

CFRunLoopRun 的简化调用链:

1
2
3
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}

核心逻辑在 CFRunLoopRunSpecific 里,内部是一个 do-while 循环,大致步骤如下:

5.2 核心源码流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
if (__CFRunLoopModeIsEmpty(currentMode)) return; // Mode 为空则直接返回

// 1. 通知 Observers:即将进入 Loop
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);

__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
do {
// 2. 通知 Observers:即将处理 Timer
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);

// 3. 通知 Observers:即将处理 Source0
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);

// 4. 执行被加入的 Block
__CFRunLoopDoBlocks(runloop, currentMode);

// 5. 处理 Source0
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
__CFRunLoopDoBlocks(runloop, currentMode);

// 6. 若有 Source1 就绪,处理 Source1 消息
if (__Source0DidDispatchPortLastTime) {
if (__CFRunLoopServiceMachPort(dispatchPort, &msg))
goto handle_msg;
}

// 7. 通知 Observers:即将进入休眠
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}

// 8. 调用 mach_msg 等待消息,线程休眠,直到被唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, ...);
// 唤醒条件:Port 事件、Timer 到期、超时、手动唤醒

// 9. 通知 Observers:刚刚被唤醒
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);

handle_msg:
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time());
} else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); // GCD 主队列
} else {
__CFRunLoopDoSource1(runloop, currentMode, source1, msg); // Source1
}

__CFRunLoopDoBlocks(runloop, currentMode);

} while (retVal == 0); // 根据 retVal 决定是否继续循环
}

// 10. 通知 Observers:即将退出 Loop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

5.3 流程简图

1
2
3
4
5
6
Entry → BeforeTimers → BeforeSources → DoBlocks → DoSources0

Exit ← BeforeExit ← AfterWaiting ← mach_msg 等待
↑ ↓
└──────── handle_msg ───────┘
(Timer / Source1 / Dispatch)

六、基于 RunLoop 的系统功能

6.1 AutoreleasePool

主线程 RunLoop 中注册了两个 Observer,回调都是 _wrapRunLoopWithAutoreleasePoolHandler

时机 回调 作用
kCFRunLoopEntry _objc_autoreleasePoolPush() 创建自动释放池
kCFRunLoopBeforeWaiting _objc_autoreleasePoolPop() + Push 释放旧池并创建新池
kCFRunLoopExit _objc_autoreleasePoolPop() 退出时释放池

因此主线程上的事件回调、Timer 回调等都会在 AutoreleasePool 包裹下执行,一般无需手动创建。

6.2 事件响应链路

  1. 硬件事件(触摸、按键等)→ IOKit 生成 IOHIDEvent
  2. SpringBoard 接收,通过 Mach Port 转发给 App
  3. 主线程 RunLoop 的 Source1 回调 __IOHIDEventSystemClientQueueCallback
  4. 内部调用 _UIApplicationHandleEventQueue() 包装成 UIEvent
  5. 经 hitTest、响应链,最终到达对应 Target-Action 或 touches 方法

6.3 手势识别

  • _UIApplicationHandleEventQueue() 识别到手势时,会打断 touches 序列
  • 将 UIGestureRecognizer 标记为待处理
  • kCFRunLoopBeforeWaiting 的 Observer _UIGestureRecognizerUpdateObserver 中统一执行手势回调

6.4 界面更新

  • 调用 setNeedsLayout / setNeedsDisplay 后,视图被标记为待更新
  • kCFRunLoopBeforeWaitingkCFRunLoopExit 的 Observer 中,Core Animation 执行实际布局和绘制

6.5 PerformSelector

performSelector:afterDelay:performSelector:onThread: 内部都会创建 Timer 并加入对应线程的 RunLoop。若该线程没有 RunLoop 或 RunLoop 未运行,这些方法不会生效。


七、实战示例

7.1 常驻线程(如 AFNetworking)

子线程默认不跑 RunLoop,需要手动添加 Source 并 run

1
2
3
4
5
6
7
8
9
10
class NetworkThread: Thread {
override func main() {
autoreleasepool {
self.name = "AFNetworking"
let runLoop = RunLoop.current
runLoop.add(Port(), forMode: .default) // 添加 Port 作为 Source,否则 run 会立即退出
runLoop.run()
}
}
}
1
2
3
4
5
6
7
8
9
// AFNetworking 2.x 的经典实现
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}

7.2 使用 Observer 监控卡顿

通过观察主线程 RunLoop 状态,检测某个阶段耗时是否过长:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import UIKit

class RunLoopMonitor {
private var observer: CFRunLoopObserver?
private var semaphore: DispatchSemaphore
private var timeout = true
private let timeoutCount: Int = 5
private var count = 0

init() {
semaphore = DispatchSemaphore(value: 0)
}

func start() {
let activities: CFRunLoopActivity = [.beforeWaiting, .afterWaiting]
var context = CFRunLoopObserverContext(
version: 0,
info: Unmanaged.passUnretained(self).toOpaque(),
retain: nil,
release: nil,
copyDescription: nil
)

observer = CFRunLoopObserverCreate(
kCFAllocatorDefault,
activities.rawValue,
true,
0,
{ _, activity, info in
guard let info = info else { return }
let monitor = Unmanaged<RunLoopMonitor>.fromOpaque(info).takeUnretainedValue()
monitor.handleObserverCallback(activity)
},
&context
)

CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .commonModes)

DispatchQueue.global().async { [weak self] in
self?.monitor()
}
}

private func handleObserverCallback(_ activity: CFRunLoopActivity) {
if activity == .beforeWaiting || activity == .afterWaiting {
count = 0
timeout = false
}
semaphore.signal()
}

private func monitor() {
while true {
let result = semaphore.wait(timeout: .now() + 1)
if result == .timedOut {
if timeout {
count += 1
if count >= timeoutCount {
print("⚠️ 主线程可能发生卡顿")
}
} else {
timeout = true
count = 0
}
}
}
}
}

7.3 在指定 RunLoop 模式执行任务

1
2
3
4
5
6
7
8
// 仅在 Default 模式执行
RunLoop.current.perform(#selector(doWork), target: self, argument: nil, order: 0, modes: [.default])

// 使用 CFRunLoop 添加 Block(iOS 10+)
CFRunLoopPerformBlock(CFRunLoopGetMain(), CFRunLoopMode.defaultMode.rawValue as CFString) {
print("在 RunLoop 当前迭代中执行")
}
CFRunLoopWakeUp(CFRunLoopGetMain())

7.4 NSTimer 与 RunLoop

1
2
3
4
5
6
7
8
9
10
11
12
13
// scheduledTimer 会自动加入当前 RunLoop 的 Default 模式
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
print("tick")
}

// 若在子线程使用,需确保 RunLoop 在运行
DispatchQueue.global().async {
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
print("tick")
}
RunLoop.current.add(timer, forMode: .default)
RunLoop.current.run() // 必须 run,否则 Timer 不会触发
}

八、常见面试要点

  1. RunLoop 和线程的关系:一一对应,主线程自动运行,子线程需手动启动。
  2. Source0 与 Source1:Source0 需手动标记;Source1 基于 Port,可主动唤醒。
  3. Mode 的作用:隔离不同场景的事件,滑动时切换到 UITrackingRunLoopMode 保证流畅。
  4. CommonModes:把 Timer/Source 加到 CommonModes 可在 Default 和 Tracking 下都收到回调。
  5. AutoreleasePool 与 RunLoop:主线程在 Entry、BeforeWaiting、Exit 时自动 Push/Pop。
  6. 卡顿监控思路:用 CFRunLoopObserver 监听主线程,结合超时判定卡顿。

九、参考

iOS 开发中的 JSPatch

由浅入深,从原理到源码,全面解析 JSPatch 的设计思想、实现机制与关键技术细节


一、JSPatch 是什么?

1.1 定位与目标

JSPatch 是一个 iOS 动态更新框架,由 bang590 开源。其核心能力是:在 App 内引入极小的引擎后,用 JavaScript 调用任意 Objective-C 接口,并可替换原生方法实现,从而实现:

  • 热修复:下发 JS 脚本修复线上 Bug,无需发版、无需审核
  • 动态能力:为项目动态添加模块或替换原生逻辑

1.2 与 Apple 审核政策的关系

⚠️ 重要说明:Apple 开发者协议 3.3.2 明确禁止「下载、安装或执行未包含在应用中的可执行代码」。JSPatch 通过 JS 间接调用 Runtime、替换方法 IMP,被认为绕过审核、改变应用行为,已被 Apple 明确禁止上架使用。
本文仅从原理分析、技术学习与架构设计角度展开,不鼓励在正式上架 App 中接入。

1.3 技术栈位置

1
2
3
4
5
6
7
┌─────────────────────────────────────────────────────────────┐
│ JSPatch 技术栈层次 │
├─────────────────────────────────────────────────────────────┤
│ JS 脚本层 │ require / defineClass / 业务补丁逻辑 │
│ 桥接层 │ __c() 元函数、JPEngine、JPBoxing、类型转换 │
│ 系统层 │ JavaScriptCore、Objective-C Runtime │
└─────────────────────────────────────────────────────────────┘

二、基础原理:为什么 JS 能调用 OC?

2.1 根本原因:Objective-C 的动态性

JSPatch 能通过 JS 调用和改写 OC 方法的根本原因是:Objective-C 是动态语言。在 OC 中,类与方法的查找、调用、替换都在运行时通过 Objective-C Runtime 完成,而不是在编译期写死。

因此可以:

  • 通过类名/方法名字符串反射得到类和方法
  • 替换某个类的方法实现(IMP)
  • 动态注册新类、添加方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 反射调用
Class class = NSClassFromString("UIViewController");
id vc = [[class alloc] init];
SEL sel = NSSelectorFromString("viewDidLoad");
[vc performSelector:sel];

// 替换方法实现
static void newViewDidLoad(id slf, SEL sel) {}
class_replaceMethod(class, @selector(viewDidLoad), (IMP)newViewDidLoad, "v@:");

// 动态注册类
Class cls = objc_allocateClassPair(superCls, "JPObject", 0);
class_addMethod(cls, selector, implement, typedesc);
objc_registerClassPair(cls);

结论:JSPatch 的基本原理就是——JS 把类名、方法名、参数等以字符串/结构化数据传给 OC,OC 通过 Runtime 接口完成「查找类 → 查找方法 → 调用/替换」

2.2 整体数据流

1
2
3
4
5
6
7
8
9
10
11
12
13
JS: require('UIView').alloc().init()


__c() 元函数:解析调用链,得到 类名/方法名/参数/调用者


OC 桥接层:JPEngine 接收参数,类型转换,构造 NSInvocation


Runtime:objc_msgSend / NSInvocation 调用,返回结果


结果经 JPBoxing/包装后回传 JS,继续链式调用或使用

三、方法调用:从 JS 到 OC 的完整链路

下面以一段典型代码为例,拆解「JS 调用 OC」的五个环节:

1
2
3
4
require('UIView')
var view = UIView.alloc().init()
view.setBackgroundColor(require('UIColor').grayColor())
view.setAlpha(0.5)

涉及:require 机制 → JS 接口设计 → 消息传递 → 对象持有/转换 → 类型转换

3.1 require:在 JS 中「引入」OC 类

require('UIView') 的作用是:在 JS 全局作用域 上创建一个同名变量,指向一个表示 OC 类的 JS 对象。该对象用 __clsName 保存类名,并标记「这是 OC 类」。

1
2
3
4
5
6
7
8
var _require = function(clsName) {
if (!global[clsName]) {
global[clsName] = {
__clsName: clsName
}
}
return global[clsName]
}

于是 require('UIView') 之后,全局有:

1
UIView === { __clsName: "UIView" }

后续 UIView.alloc() 等调用,都基于这个对象进行。

3.2 JS 接口设计:如何让 UIView.alloc() 不报错?

3.2.1 问题:JS 没有「未定义方法」的转发机制

在 JS 中,若对象没有 alloc 属性,调用 UIView.alloc() 会直接抛错。不像 OC/Lua/Ruby 有「方法缺失 → 转发」的机制。

早期思路是:在 require 时向 OC 要该类(及父类)的全部方法名,在 JS 对象上为每个方法名挂一个函数,函数内部再调 OC。这样 JS 上就有 allocinit 等「真实存在」的属性。

问题在于:一个类就有几百个方法,还要沿继承链汇总,内存暴涨,且要维护 OC→JS 的方法列表同步,难以接受。

3.2.2 方案:正则替换 + __c() 元函数(关键优化)

不改变「JS 语法」,但在 OC 执行 JS 脚本之前,用正则把所有方法调用统一替换成对 __c() 的调用,从而在 JS 侧实现「任意方法名 → 统一入口」的转发:

1
2
3
4
5
// 替换前
UIView.alloc().init()

// 替换后(示意)
UIView.__c('alloc')().__c('init')()

再给 JS 的 Object.prototype 增加 __c 方法,使任意对象(类对象、实例对象)都能走到同一套逻辑:

1
2
3
4
5
6
7
8
9
10
Object.defineProperty(Object.prototype, '__c', {
value: function(methodName) {
if (!this.__obj && !this.__clsName) return this[methodName].bind(this);
var self = this;
return function() {
var args = Array.prototype.slice.call(arguments);
return _methodFunc(self.__obj, self.__clsName, methodName, args, self.__isSuper);
};
}
});
  • 若调用者是 OC 类(有 __clsName):把类名、方法名、参数传给 OC,由 OC 通过 Runtime 调类方法。
  • 若调用者是 OC 实例(有 __obj):把对象指针、方法名、参数传给 OC,调实例方法。

这样不需要在 JS 上枚举任何 OC 方法,内存占用大幅下降,是 JSPatch 中最重要的一步优化。

3.3 消息传递:JS 与 OC 如何互传数据?

OC 端在启动 JSPatch 时会创建 JavaScriptCoreJSContext,并在 context 上挂载 OC 实现的 Block/方法。JS 调这些方法时,参数和返回值会由 JavaScriptCore 自动在 JS 与 OC 类型之间转换(如 NSArray ↔ Array、NSString ↔ string、NSNumber ↔ number 等)。

因此,_methodFunc 只需把「类名 / 对象 / 方法名 / 参数列表」通过 context 上暴露给 JS 的函数传给 OC;OC 用 Runtime 完成调用后,再把返回值通过同一机制回传给 JS。

3.4 对象持有与转换:OC 对象在 JS 侧的表示

  • 类对象:在 JS 里就是 { __clsName: "UIView" },不涉及 OC 对象生命周期。
  • 实例对象:OC 的 id 若直接以指针形式交给 JS,JS 无法「理解」这个指针,但可以再把它传回 OC。
    为了在 JS 里识别「这是一个 OC 实例」,OC 在把对象返回给 JS 前会做一层包装,例如:
1
2
3
static NSDictionary *_wrapObj(id obj) {
return @{@"__obj": obj};
}

在 JS 侧就变成:

1
{ __obj: [OC 对象指针] }

这样在 __c() 里可以通过「是否有 __obj」判断调用者是 OC 实例,并取出 __obj 与方法名、参数一起传回 OC,完成实例方法调用。

对象生命周期:当 JS 侧有变量引用该包装对象时,OC 对象引用计数 +1;JS 侧释放后 -1,由 OC/JS 共同管理。

3.5 类型转换:参数与返回值的 OC 类型

OC 侧实际调用是通过 NSInvocation 完成的。要正确调用并拿到返回值,需要:

  1. 根据 OC 方法的 NSMethodSignature 得到每个参数的类型,把 JS 传过来的对象(如 NSNumber、NSDictionary)转成对应类型(如 intfloatCGRect 等)再传入。
  2. 根据返回值类型NSInvocation 取出返回值,再包装成 JS 可用的对象(或 JPBoxing 等)传回 JS。

例如 view.setAlpha(0.5):JS 传的是 NSNumber,OC 根据 setAlpha: 的签名得知参数是 float,于是把 NSNumber 转为 float 再调用。


四、方法替换(热修复的核心)

4.1 基础思路:替换 IMP

OC 的类方法列表里,每个方法对应一个 Method(SEL + 类型编码 + IMP)。通过 Runtime 可以:

  • 保留原 IMP:给类新增一个方法(如 ORIGviewDidLoad),其 IMP 指向原来的实现。
  • 替换原方法的 IMP:把 viewDidLoad 的 IMP 改成自定义函数,在自定义函数里调 JS 传入的实现,并在需要时再调 ORIGviewDidLoad

以替换 UIViewControllerviewDidLoad 为例(无参数情况):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void viewDidLoadIMP(id slf, SEL sel) {
// 从 JS 侧取到的函数并调用
JSValue *jsFunction = ...;
[jsFunction callWithArguments:nil];
}

Class cls = NSClassFromString(@"UIViewController");
SEL selector = @selector(viewDidLoad);
Method method = class_getInstanceMethod(cls, selector);
IMP imp = method_getImplementation(method);
char *typeDescription = (char *)method_getTypeEncoding(method);

// 原实现保留到 ORIGviewDidLoad
class_addMethod(cls, @selector(ORIGviewDidLoad), imp, typeDescription);
// viewDidLoad 指向新实现
class_replaceMethod(cls, selector, (IMP)viewDidLoadIMP, typeDescription);

这样,所有对 viewDidLoad 的调用都会走到 viewDidLoadIMP,进而执行 JS 逻辑;JS 里可通过 self.ORIGviewDidLoad() 调回原实现。

4.2 有参数时的问题:通用 IMP 如何拿到所有参数?

需要一个通用 IMP,能对「任意方法、任意参数个数与类型」都拿到参数并传给 JS。这里就出现了 32 位与 64 位 的差异。

4.2.1 32 位:va_list 取参(已不可用于 64 位)

最初用可变参数实现:

1
2
3
4
5
6
7
8
static void commonIMP(id slf, ...) {
va_list args;
va_start(args, slf);
NSMethodSignature *methodSignature = [cls instanceMethodSignatureForSelector:selector];
// 根据 methodSignature 的 typeEncoding 用 va_arg 逐个取出参数,组成 NSArray
// 再 [jsFunction callWithArguments:list];
va_end(args);
}

arm64 上,va_list 的 ABI 与 32 位不同,无法用上述方式正确取参,会 crash,因此 64 位必须换方案。

4.2.2 64 位:利用消息转发与 NSInvocation

OC 在「找不到方法实现」时会走消息转发链路,最终会到 -forwardInvocation:,此时会拿到一个 NSInvocation,其中已经包含了本次调用的 selector、参数类型、参数值、返回值类型。因此可以:

  1. 把要替换的方法的 IMP 改为 _objc_msgForward,这样一旦调用该方法,就会直接进入转发流程,最终进入 -forwardInvocation:
  2. 重写该类的 -forwardInvocation::在实现里判断「若是我们替换的方法」,则从 NSInvocation 里解出所有参数,调用我们新增的 _JPxxx 方法(该方法内部再调 JS);否则调原来的 ORIGforwardInvocation:,保证其他转发逻辑不受影响。
  3. 新增 ORIGviewWillAppear:_JPviewWillAppear::前者指向原 IMP,后者是「从 NSInvocation 取参并调 JS」的桥接实现。

这样在 64 位上就能通用地拿到任意方法的参数并交给 JS,无需依赖 va_list

4.3 返回值是 struct 时的注意点:_objc_msgForward_stret

部分架构下,当方法返回值是「较大的 struct」时,用的是 objc_msgSend_stret 的调用约定(返回值通过内存指针传回),若仍用 _objc_msgForward 会出错。此时需要改用 _objc_msgForward_stret
是否「special struct return」没有公开 API,JSPatch 通过 NSMethodSignaturedebugDescription 是否包含 "is special struct return? YES" 来判断,在非 arm64 上对这类方法使用 _objc_msgForward_stret

4.4 新增方法、Protocol、Property

  • 新增方法:OC 侧通过 class_addMethod 动态添加,参数与返回值类型先统一为 id(因为新增方法主要给 JS 用)。若类声明实现了某 Protocol,则从 Protocol 的方法描述里取类型信息,保证与 Protocol 一致(如 tableView:sectionForSectionIndexTitle:atIndex: 等)。
  • Property:已有属性直接通过 getter/setter 方法在 JS 里按普通方法调用即可。动态新增成员则用 objc_setAssociatedObject / objc_getAssociatedObject 模拟(因为 class_addIvar 只能在类注册前使用,无法给已有类加 ivar)。

4.5 self 与 super

  • self:在 defineClass 的实例方法执行前,把「当前实例」写入一个 JS 全局变量(如 self),方法执行完后清空,这样在 JS 里写的 self 就指向当前 OC 实例。
  • superself.super()__c() 里做特殊处理,返回一个带 __isSuper: 1 标记的对象。OC 侧若发现是 super 调用,则取父类该方法的 IMP,为当前类临时加一个方法(如 SUPER_viewDidLoad)指向该 IMP,再转调该方法,从而模拟 OC 的 super 语义。

五、扩展能力:Struct 与 C 函数

5.1 Struct 支持

JS 与 OC 之间不能直接传 C struct,需要序列化/反序列化。JSPatch 的做法是:

  • 内置:对常用类型如 NSRange、CGRect、CGSize、CGPoint 等做专门转换。
  • 可扩展:在 JS 里通过 defineStruct 声明 struct 的「名字、类型串、字段名」,OC 根据类型串按内存布局逐字段读写,再封装成 NSDictionary 与 JS 互传。这样新增 struct 不需要改 OC 代码,只需在 JS 声明布局即可(依赖当前 ABI 下 struct 内存布局稳定)。

5.2 C 函数支持

C 函数无法通过 Runtime 反射调用,因此采用「在 JSContext 上挂 OC Block 包装」的方式:在 context 上暴露与 C 函数同名的 JS 可调接口,内部转调 C 函数,并做好指针等类型转换。为避免引擎体积和启动时一次性注册过多 C 函数,设计了 JPExtension 机制:通过 +main:(JSContext *)formatJSToOC / formatOCToJS 等接口,让扩展按需注册 C 函数,JS 端通过 require('JPEngine').addExtensions(['JPMemory']) 等方式按需加载。


六、关键实现细节

6.1 JPBoxing:避免可变集合被 JavaScriptCore 自动转换

NSMutableArray / NSMutableDictionary / NSMutableString 从 OC 返回给 JS 时,JavaScriptCore 会强制转成 JS 的 Array / Object / String,导致「回到 OC 时无法再调原生的可变方法」。
解决办法:不直接返回这些对象,而是用 JPBoxing 包装一层(把 OC 对象放在 Boxing 的 property 里),返回 Boxing 实例给 JS。JS 再把这个 Boxing 传回 OC 时,OC 从 Boxing 里取出原对象,即可继续调可变方法。同时,为规则统一,NSArray/NSDictionary/NSString 也采用「默认以指针形式在 JS 侧存在,需要再调 .toJS() 转成纯 JS 类型」的策略。

6.2 nil / NSNull 的区分与链式调用

  • nil 与 NSNull:JS 的 null/undefined 传到 OC 时统一变成 nil;若需要明确表示 NSNull,在 JS 里使用全局变量 nsnull,OC 侧据此区分。
  • 链式调用:OC 里 [[obj returnNil] doSomething] 是安全的(对 nil 发消息不崩溃),但 JS 里 null 没有方法,无法写 require("JPObject").returnNil().hash()。JSPatch 用 false 表示 OC 返回的 nil:在 JS 里 false 也是对象可调方法,同时 if (!obj) 仍可用来判断「是否为 nil」。这样链式调用在 JS 侧也能安全进行。唯一的小坑是:若 OC 参数类型是 NSNumber* 而 JS 传 false,OC 会得到 nil 而非 NSNumber,需要业务侧注意。

6.3 下划线 _ 的歧义

OC 方法名用 : 分隔参数,JSPatch 在 JS 里用 单个下划线 _ 连接多参数方法名,例如:

  • setObject:forKey:setObject_forKey_

若 OC 方法名里本身带下划线(如 set_object:forKey:),就会与「参数分隔符」混淆。约定:OC 方法名中的字面下划线在 JS 里用双下划线 __ 表示,例如 set__object_forKey_。这样 OC 的 _ 与 JSPatch 的「参数分隔」可以区分开。

6.4 内存与 ARC

  • 从 NSInvocation 取参数/返回值:若用 id arg; [invocation getArgument:&arg atIndex:i];,ARC 会在退出作用域时对 arg 做 release,但 getArgument:atIndex: 并不会自动做 retain,容易造成 double release。解决方式是用 __unsafe_unretained__weak,或通过 void * + __bridge 明确所有权。
  • alloc / new / copy / mutableCopy 返回值:按 OC 约定,这些方法返回的对象调用方持有,retainCount 已 +1。从 NSInvocation 取返回值时,若 selector 是这类方法,需用 __bridge_transfer 把所有权交给 ARC,否则会泄漏。

七、核心模块与源码结构

模块 / 文件 职责
JPEngine 初始化 JSContext、注入 require/defineClass 等全局方法,执行脚本入口;提供 OC 侧与 JS 的桥接入口(如接收类名、方法名、参数并调用 Runtime)。
JPBoxing 包装 OC 对象(含 NSMutableArray/Dictionary/String、C 指针、Class 等),避免被 JavaScriptCore 自动转换或无法在 JS 侧标识类型。
JPLoader 负责从网络/本地加载、解密、执行 JS 补丁;版本管理、条件执行等。
JPExtension (JPExtension) 扩展接口:暴露 JSContext 与类型转换方法,供 C 函数、自定义 Struct 等扩展按需注册。
JS 脚本预处理 正则替换方法调用为 __c('methodName') 形式,以及 defineClass 中 self/super 的注入等。

源码阅读顺序建议:JPEngine 初始化与注入 → JS 中的 __c_methodFunc → OC 侧根据类名/对象/方法名调用 Runtime(含 NSInvocation)→ 方法替换(forwardInvocation + ORIG/JP 前缀)→ JPBoxing 与类型转换

关键调用链(OC 侧)

1
2
3
4
5
6
7
JS 调用 UIView.alloc().init()
→ _methodFunc 被调用,参数 [className="UIView", methodName="alloc", args=[]]
→ 通过 JSContext 注册的桥接函数进入 OC(如 callSelector:selectorName:arguments:...)
→ JPEngine 内根据 className 取 Class,根据 selectorName 取 SEL,组装 NSInvocation
→ 设置 target、arguments,invoke
→ 返回值经 formatOCToJS / JPBoxing 包装后回传 JS
→ JS 侧得到包装对象 { __obj: 实例 },再调用 .__c('init')() 继续链式调用

方法替换调用链(64 位)

1
2
3
4
5
6
OC 代码调用 [vc viewWillAppear:YES]
→ viewWillAppear: 的 IMP 已被改为 _objc_msgForward
→ 进入消息转发,最终到 forwardInvocation:
→ 自定义 forwardInvocation 实现中:从 NSInvocation 解出参数,调 _JPviewWillAppear:(BOOL)
→ _JPviewWillAppear: 内部把参数打包,通过 JSContext 调 JS 里 defineClass 定义的 viewWillAppear
→ 若 JS 里调 self.ORIGviewWillAppear(),则 OC 再调 ORIGviewWillAppear:,即原实现

八、设计思想总结

  1. 用字符串与 Runtime 打通 JS 与 OC
    不依赖预编译或代码生成,完全依赖「类名/方法名 + Runtime 反射 + NSInvocation」,使任意 OC 接口都能被 JS 调用和替换。

  2. 用「正则替换 + 元函数」规避 JS 语言限制
    JS 没有「未定义方法转发」,通过脚本预处理把方法调用统一成 __c('methodName'),用一层元函数模拟「消息转发」,避免在 JS 侧枚举海量方法,兼顾内存与实现复杂度。

  3. 区分 32/64 位与返回值类型
    32 位用 va_list 取参,64 位用 forwardInvocation + NSInvocation;对 special struct return 用 _objc_msgForward_stret,体现对 ABI 与底层调用约定的细致处理。

  4. 用包装类型统一「跨引擎对象」
    JPBoxing、__obj/__clsName 等,把「OC 对象/类在 JS 侧的句柄」标准化,便于在 __c() 中统一分支(类方法 / 实例方法 / super)。

  5. 扩展点清晰
    Struct 用类型串 + 键名在 JS 侧声明;C 函数通过 JPExtension 按需注册,既控制体积又保持能力可扩展。


九、合规性与替代方案

维度 说明
Apple 态度 3.3.2 禁止未包含在应用内的可执行代码的下载与执行;JSPatch 通过 JS 调 Runtime 替换方法,被视为违规。
现状 作者已不再维护,新上架 App 不建议使用。
替代思路 热修:RN/Weex/Flutter 等脚本层 OTA;紧急修复:服务端降级、开关、兜底逻辑;架构上减少对「运行时替换原生实现」的依赖。

十、小结

JSPatch 通过 Objective-C Runtime + JavaScriptCore,用「类名/方法名 + 参数」在 JS 与 OC 之间架起桥梁,并用 正则替换 + __c() 元函数 在 JS 侧实现无需枚举方法的调用转发;方法替换在 64 位上依赖 消息转发与 NSInvocation 通用地获取参数。再配合 JPBoxing、nil 用 false 表示、Struct/C 函数扩展 等细节,在技术上演进出一套完整的热修方案。理解其原理有助于掌握 Runtime、消息转发、JS–Native 桥接与 ABI 等知识;在实际项目中则应优先采用符合当前审核政策的热更新与架构方案。


本文基于 JSPatch 官方 Wiki、作者博客及公开技术资料整理,仅用于学习与原理分析。

iOS 开发中的性能优化

由浅入深,从基本概念到源码解析,再到实际项目应用,带你全面掌握 iOS 性能优化之道


一、什么是性能优化?

1.1 为什么性能很重要?

在移动端,性能直接关系到用户体验:

指标 用户感知 业务影响
启动速度 3 秒内无法进入应用,约 77% 用户会放弃 流失、留存下降
界面卡顿 掉帧、滑动不跟手 评价差、卸载
内存占用 应用被系统强杀、白屏 体验中断、投诉
耗电发热 续航变短、设备发烫 用户反感

苹果对 App Store 的审核和推荐也会考虑应用质量,性能是重要维度之一。

1.2 性能优化的核心目标

  • :启动快、响应快、界面流畅
  • :省内存、省电、省流量
  • :不崩溃、不卡死、不白屏

1.3 性能优化的「黄金法则」

先测量,再优化;先瓶颈,再细节。

盲目优化往往事倍功半。正确的做法是:用工具定位瓶颈,再针对性地优化。


二、性能指标与测量工具

2.1 关键指标

指标 说明 理想值
FPS 帧率,60fps 为流畅 ≥ 55fps
主线程耗时 单次任务在主线程的耗时 < 16ms(一帧)
启动时间 冷启动/热启动到首屏可交互 冷启动 < 2s
内存占用 常驻内存、峰值内存 视业务而定,避免持续增长
CPU 占用 主线程 CPU 占比 空闲时尽量低

2.2 官方工具:Instruments

Instruments 是 Xcode 自带的性能分析工具套件:

  • Time Profiler:CPU 耗时分析,定位主线程卡顿
  • Allocations:内存分配追踪
  • Leaks:内存泄漏检测
  • Core Animation:离屏渲染、图层混合检测
  • Energy Log:耗电分析
  • Network:网络请求分析

2.3 第三方工具与库

工具 用途 特点
YYFPSLabel 实时 FPS 显示 开发阶段监控
MLeaksFinder 内存泄漏检测 无侵入、自动化
Matrix(微信) 综合性能监控 线上 APM
DoraemonKit 开发调试面板 多维度自检

2.4 简单 FPS 监控实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 基于 CADisplayLink 的 FPS 监控
class FPSMonitor {
private var displayLink: CADisplayLink?
private var lastTime: CFTimeInterval = 0
private var count: Int = 0
var fpsUpdate: ((Int) -> Void)?

func start() {
displayLink = CADisplayLink(target: self, selector: #selector(tick))
displayLink?.add(to: .main, forMode: .common)
}

@objc private func tick(_ link: CADisplayLink) {
if lastTime == 0 {
lastTime = link.timestamp
return
}
count += 1
let delta = link.timestamp - lastTime
if delta >= 1.0 {
let fps = Int(round(Double(count) / delta))
fpsUpdate?(fps)
count = 0
lastTime = link.timestamp
}
}

func stop() {
displayLink?.invalidate()
displayLink = nil
}
}

三、UI 与渲染优化

3.1 离屏渲染(Offscreen Rendering)

离屏渲染 是指 GPU 在当前屏幕缓冲区之外新开缓冲区进行渲染,再合成到主缓冲区的过程。额外的缓冲区和上下文切换会带来性能开销。

常见触发离屏渲染的属性:

属性 说明
cornerRadius + masksToBounds 圆角裁剪
shadow(阴影) 需要额外 Pass 计算
mask(遮罩) 蒙版合成
group opacity 组透明度
edge antialiasing 抗锯齿

优化方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ❌ 容易触发离屏渲染
imageView.layer.cornerRadius = 10
imageView.layer.masksToBounds = true

// ✅ 方案一:只对需要圆角的内容做裁剪,避免整层
imageView.layer.cornerRadius = 10
imageView.layer.masksToBounds = true
imageView.clipsToBounds = true // 对 UIImageView 而言,用 clipsToBounds 配合 contentMode

// ✅ 方案二:用贝塞尔路径 + CAShapeLayer 做圆角(iOS 9+ 可考虑)
let path = UIBezierPath(roundedRect: bounds, cornerRadius: 10)
let mask = CAShapeLayer()
mask.path = path.cgPath
layer.mask = mask // 仍可能离屏,需实测

// ✅ 方案三:直接用圆角图片(切图或 Core Graphics 绘制)
// 在子线程绘制圆角图片,主线程只做 display

阴影优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ❌ 阴影 + 裁剪容易离屏
view.layer.shadowOpacity = 0.5
view.layer.cornerRadius = 10
view.layer.masksToBounds = true // 与 shadow 冲突

// ✅ 分到两个 layer:容器负责阴影,子 layer 负责圆角
let containerView = UIView()
containerView.layer.shadowOpacity = 0.5
containerView.layer.shadowRadius = 4
containerView.layer.shadowOffset = .zero

let contentView = UIView()
contentView.layer.cornerRadius = 10
contentView.layer.masksToBounds = true
contentView.frame = containerView.bounds
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
containerView.addSubview(contentView)

3.2 图层混合(Layer Blending)

当多个图层叠在一起且存在透明像素时,GPU 需要进行混合计算。减少透明区域和图层数量可以降低开销。

优化建议:

  • 给不透明的视图设置 layer.opaque = true(或 isOpaque = true
  • 避免不必要的半透明叠加
  • 减少视图层级
1
2
3
// 已知不透明时
view.layer.opaque = true
view.backgroundColor = .white // 明确不透明色

3.3 TableView / CollectionView 优化

列表是 App 中最常见的性能瓶颈场景。

核心思路:

  1. Cell 复用:使用 dequeueReusableCell,避免重复创建
  2. 减少主线程工作:图片解码、复杂计算放到子线程
  3. 按需加载:快速滑动时减少或暂停非可见 Cell 的加载
  4. 高度缓存UITableViewAutomaticDimension 会反复计算,可缓存高度

示例:Cell 配置优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ❌ 在主线程做重活
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MyCell
let model = dataSource[indexPath.row]
cell.imageView?.image = UIImage(contentsOfFile: model.imagePath) // 同步读盘 + 解码
cell.label.text = heavyCompute(model) // 复杂计算
return cell
}

// ✅ 异步加载图片 + 计算放子线程
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MyCell
let model = dataSource[indexPath.row]
cell.tag = indexPath.row
cell.label.text = nil
cell.imageView?.image = nil

DispatchQueue.global().async {
let image = self.loadImage(path: model.imagePath)
let text = self.heavyCompute(model)
DispatchQueue.main.async {
if cell.tag == indexPath.row {
cell.imageView?.image = image
cell.label.text = text
}
}
}
return cell
}

预加载与 RunLoop 空闲优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 利用 RunLoop 在空闲时预加载
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = ...
preloadIfNeeded(at: indexPath)
return cell
}

private func preloadIfNeeded(at indexPath: IndexPath) {
let maxIndex = min(indexPath.row + 5, dataSource.count - 1)
for i in (indexPath.row + 1)...maxIndex {
if !imageCache.isCached(for: dataSource[i].imagePath) {
DispatchQueue.global().async {
_ = self.loadImage(path: self.dataSource[i].imagePath)
}
}
}
}

3.4 图片加载与解码优化

图片解码是 CPU 密集型操作,大图在主线程解码会导致卡顿。

1
2
3
4
5
6
7
8
9
10
11
// 在子线程解码
func decodeImage(_ image: UIImage) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(image.size, true, 0)
image.draw(at: .zero)
let decoded = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return decoded
}

// 或使用 ImageIO 指定解码选项
// 对于网络图片,使用 SDWebImage / Kingfisher 等库,它们会在后台解码

四、内存优化

4.1 内存管理基础

  • 引用计数:OC 使用 MRC/ARC,Swift 使用 ARC
  • AutoreleasePool:自动释放池,延迟 release
  • 循环引用:block、delegate、闭包持有 self 未使用 weak 导致

4.2 AutoreleasePool 与 RunLoop

主线程 RunLoop 每次循环会创建并销毁一次 @autoreleasepool,因此临时对象会在一次循环结束释放。子线程若没有 RunLoop,需要手动加 @autoreleasepool,否则临时对象会堆积到线程结束。

1
2
3
4
5
6
7
8
9
10
// 子线程大量创建临时对象时
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@autoreleasepool {
for (int i = 0; i < 10000; i++) {
// 创建大量临时对象
NSString *temp = [NSString stringWithFormat:@"item_%d", i];
[array addObject:temp];
}
}
});

objc4 源码中的 AutoreleasePoolPage 结构(简化):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// objc4 源码简化
class AutoreleasePoolPage {
magic_t const magic;
id *next; // 下一个可存放 autorelease 对象的地址
pthread_t const thread; // 所属线程
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
// ...
static void *operator new(size_t size) {
return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);
}
id *add(id obj) {
// 将 obj 加入当前 page,next 指向下一个空位
// ...
}
static void releaseAll() {
// 从 last 到 next 逆序 release
}
};

4.3 循环引用与 weak/strong

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ❌ block 强引用 self,self 强引用持有 block 的成员
class ViewController: UIViewController {
var onComplete: (() -> Void)?
func setup() {
onComplete = {
self.doSomething() // 强引用 self
}
}
}

// ✅ weak self
onComplete = { [weak self] in
self?.doSomething()
}

// ✅ weak + strong 避免 block 执行期间 self 被释放
onComplete = { [weak self] in
guard let self = self else { return }
self.doSomething()
}

4.4 大对象与图片内存

一张 1000×1000 的 RGBA 图片,解码后约占 约 4MB 内存。使用 UIImage(named:) 会缓存,大图慎用。

1
2
3
4
5
6
7
8
9
// 大图使用 imageWithContentsOfFile 或 UIImage(contentsOfFile:) 避免缓存
let image = UIImage(contentsOfFile: path)

// 或使用 ImageIO 进行缩略图解码,减少内存
let options: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageIfAbsent: true,
kCGImageSourceThumbnailMaxPixelSize: 200
]
// 只解码缩略图尺寸,而非整张大图

五、启动优化

5.1 启动阶段

阶段 说明 可优化点
pre-main dyld 加载、ObjC 初始化、+load、C++ 静态构造 减少 +load、精简动态库
post-main main 到首屏可交互 异步化、延迟加载

5.2 pre-main 优化

1
2
# 测量 pre-main 时间:Edit Scheme → Run → Arguments → Environment Variables
# 添加 DYLD_PRINT_STATISTICS = 1
  • 减少动态库数量:合并动态库,能用静态库则用静态库
  • 减少 +load:把逻辑迁移到 +initialize 或首屏使用再初始化
  • 减少 ObjC 类/方法数量:删除无用代码,用 Swift 替代部分 OC

5.3 post-main 优化

1
2
3
4
5
6
7
8
9
10
11
// 串行改并行
func application(_ application: UIApplication, didFinishLaunchingWithOptions...) -> Bool {
// 可并行的初始化
DispatchQueue.global().async { initAnalytics() }
DispatchQueue.global().async { initCrashReporter() }
DispatchQueue.global().async { initNetworkConfig() }

// 必须主线程且阻塞首屏的,尽量后置或精简
setupWindow()
return true
}

延迟加载:

1
2
3
4
5
6
7
8
// 非首屏必需的模块,等首屏展示后再初始化
DispatchQueue.main.async {
self.window?.rootViewController = MainTabBarController()
DispatchQueue.main.async {
// 首屏渲染完成后再做
initThirdPartySDK()
}
}

六、网络与 I/O 优化

6.1 网络请求优化

  • 合并请求、减少请求次数
  • 使用 HTTP/2 多路复用
  • 合理设置超时与重试
  • 大文件使用断点续传
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 请求合并示例(伪代码)
class RequestMerger {
private var pendingRequests: [String: [CompletionHandler]] = [:]
private var inflight: [String: URLSessionTask] = [:]

func fetch(key: String, completion: @escaping (Data?) -> Void) {
if let task = inflight[key] {
// 合并到同一请求的回调
pendingRequests[key, default: []].append(completion)
return
}
let task = URLSession.shared.dataTask(with: url) { data, _, _ in
let handlers = self.pendingRequests.removeValue(forKey: key) ?? []
DispatchQueue.main.async {
handlers.forEach { $0(data) }
}
}
task.resume()
inflight[key] = task
}
}

6.2 文件 I/O 优化

  • 避免在主线程做大量读写
  • 小文件合并、大文件分片
  • 使用 mmap 映射大文件
  • 合理使用 Data(contentsOf:) 与流式读取
1
2
3
4
5
6
7
8
9
10
11
// 大文件流式读取
if let stream = InputStream(fileAtPath: path) {
stream.open()
defer { stream.close() }
let bufferSize = 1024 * 64
var buffer = [UInt8](repeating: 0, count: bufferSize)
while stream.hasBytesAvailable {
let read = stream.read(&buffer, maxLength: bufferSize)
// 处理 buffer
}
}

七、多线程与 GCD 优化

7.1 主线程减压

任何耗时操作都不应阻塞主线程超过 16ms(约一帧)。

1
2
3
4
5
6
DispatchQueue.global(qos: .userInitiated).async {
let result = expensiveComputation()
DispatchQueue.main.async {
self.updateUI(with: result)
}
}

7.2 线程爆炸与串行化

过多并发会导致线程爆炸,反而不利于性能。可使用串行队列 + 多队列分组:

1
2
3
// 为不同任务类型使用不同队列,避免单一队列过长
let imageQueue = DispatchQueue(label: "com.app.image", qos: .userInitiated)
let dbQueue = DispatchQueue(label: "com.app.db", qos: .utility)

7.3 避免锁竞争

1
2
3
4
5
6
7
8
9
10
11
12
13
// 读多写少场景,可用 dispatch_barrier 优化
class ThreadSafeArray<Element> {
private var array: [Element] = []
private let queue = DispatchQueue(label: "com.app.safe", attributes: .concurrent)

func append(_ element: Element) {
queue.async(flags: .barrier) { self.array.append(element) }
}

var last: Element? {
queue.sync { array.last }
}
}

八、实际项目应用案例

8.1 案例一:电商首页 Feed 列表卡顿

现象:首页信息流快速滑动时明显卡顿,FPS 掉到 40 以下。

排查

  1. Time Profiler 发现 cellForRow 内存在 UIImage(contentsOfFile:) 同步解码
  2. Core Animation 发现 Cell 内圆角 + 阴影组合触发离屏渲染

优化措施

  1. 图片改为异步加载 + 子线程解码,使用 Kingfisher 的 downsamplingImageProcessor
  2. 圆角改为用 UIBezierPath 绘制圆角图,或用 cornerRadius 仅作用在 imageView.layer
  3. 高度缓存,避免 UITableViewAutomaticDimension 反复计算

效果:滑动 FPS 稳定在 58–60。


8.2 案例二:App 冷启动超 3 秒

现象:从点击图标到首屏出现超过 3 秒。

排查

  1. 通过 DYLD_PRINT_STATISTICS 发现 pre-main 约 1.2s
  2. 发现 20+ 个动态库、多个 +load 中做了同步网络请求和大量注册

优化措施

  1. 合并部分动态库,能静态链接的改为静态
  2. 移除 +load 中的网络请求和耗时逻辑,改为首屏展示后异步初始化
  3. 路由注册从「启动全量注册」改为「首次使用时按需注册」

效果:pre-main 降至约 0.6s,整体冷启动约 1.8s。


8.3 案例三:内存持续增长被系统强杀

现象:在某个二级页面反复进出多次后,App 被系统强杀。

排查

  1. Allocations 发现每次进入页面,ViewModelNetworkManager 持续增长
  2. Leaks 未报明显泄漏,但 MLeaksFinder 提示 ViewController 未释放

根因

  • NetworkManager 持有请求的 closureclosure 捕获了 ViewController
  • ViewController 又持有 NetworkManager 的 delegate,形成循环引用

优化措施

  1. 所有回调使用 [weak self],并在回调内 guard let self
  2. NetworkManager 的 delegate 改为 weak
  3. 请求完成后主动置空 completion,避免长生命周期持有

效果:反复进出页面,内存稳定回收,不再被强杀。


九、性能优化清单(自检表)

类别 检查项
UI 是否避免不必要的离屏渲染?图层是否过多?是否在子线程解码图片?
列表 Cell 是否复用?高度是否缓存?是否做了预加载?
内存 是否存在循环引用?大图是否控制解码尺寸?
启动 动态库数量是否可控?+load 是否精简?是否延迟非必要初始化?
网络 是否合并请求?超时和重试是否合理?
线程 耗时操作是否在子线程?是否存在锁竞争或线程爆炸?

十、小结

性能优化是一个持续的过程,需要:

  1. 建立指标体系:用 FPS、启动时间、内存等量化指标
  2. 善用工具:Instruments、APM、自研监控
  3. 由瓶颈入手:先解决主要矛盾,再优化细节
  4. 平衡取舍:在开发成本、可维护性和性能之间找平衡
  5. 回归验证:每次改动后做回归测试,避免引入新问题

掌握原理、熟练使用工具、结合业务实践,才能在真实项目中持续提升 App 的性能与体验。

iOS 开发中的多线程

由浅入深,从基本概念到源码解析,带你全面理解 iOS 并发编程


一、为什么需要多线程?

1.1 单线程的局限

在移动应用中,主线程(Main Thread/UI Thread) 负责:

  • 处理用户交互(点击、滑动等)
  • 更新 UI
  • 处理 RunLoop 事件

如果耗时操作(网络请求、大文件读写、复杂计算)在主线程执行,会导致:

  • 界面卡顿:主线程被阻塞,无法及时响应触摸
  • ANR(Application Not Responding):系统可能强制终止「无响应」的 App
  • 糟糕的用户体验
1
2
3
4
5
6
// ❌ 错误示例:主线程执行网络请求
func loadData() {
let url = URL(string: "https://api.example.com/data")!
let data = try? Data(contentsOf: url) // 阻塞主线程!
self.tableView.reloadData()
}

1.2 多线程的核心思想

将耗时任务放到子线程执行,完成后回到主线程更新 UI:

1
2
3
主线程:响应用户 → 派发任务到子线程 → 继续处理 UI
子线程:执行耗时任务 → 完成 → 通知主线程
主线程:收到结果 → 更新 UI

二、iOS 多线程技术栈

2.1 技术对比

技术 抽象层次 使用场景 学习曲线
Thread 底层,直接操作线程 需要精细控制线程生命周期
GCD 任务队列,无需管理线程 绝大多数异步任务
Operation 面向对象,可取消/依赖/优先级 复杂任务编排 中低

2.2 选择建议

  • 首选 GCD:简单异步、串行/并发队列、延迟执行、一次性执行
  • Operation:需要取消、依赖关系、暂停恢复、进度回调
  • Thread:极少需要,仅当必须直接操作线程时使用

三、GCD(Grand Central Dispatch)详解

3.1 核心概念

GCD 是苹果提供的并发编程框架,基于 C 库 libdispatch。它采用「任务 + 队列」模型:

  • 任务(Block/Closure):要执行的代码块
  • 队列(Queue):存放任务,按规则调度到线程执行
1
2
3
4
5
6
7
8
9
// 基本用法
DispatchQueue.global().async {
// 子线程执行
let result = doHeavyWork()
DispatchQueue.main.async {
// 主线程更新 UI
self.updateUI(result)
}
}

3.2 队列类型

队列 类型 说明
主队列 串行 DispatchQueue.main,主线程执行,用于 UI 更新
全局队列 并发 DispatchQueue.global(qos:),系统管理线程池
自定义串行队列 串行 同一时刻只执行一个任务
自定义并发队列 并发 可同时执行多个任务
1
2
3
4
5
6
7
8
9
// 串行队列:任务按顺序执行
let serialQueue = DispatchQueue(label: "com.app.serial")

// 并发队列:任务可并发执行
let concurrentQueue = DispatchQueue(label: "com.app.concurrent", attributes: .concurrent)

// QoS(服务质量)优先级
DispatchQueue.global(qos: .userInitiated).async { } // 用户发起,需快速响应
DispatchQueue.global(qos: .background).async { } // 后台任务,可慢速

3.3 QoS 优先级

QoS 说明 典型场景
.userInteractive 用户交互,最高优先级 动画、即时反馈
.userInitiated 用户发起,高优先级 加载数据、点击后处理
.default 默认 无特别需求
.utility 实用型 下载、导入
.background 后台 同步、预加载

3.4 常用 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 异步执行
queue.async { }

// 同步执行(会阻塞当前线程直到任务完成)
queue.sync { }

// 延迟执行
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { }

// 只执行一次(如单例)
var token: Int = 0
DispatchQueue.once(&token) {
// 全局只执行一次
}

// DispatchWorkItem:可取消的任务
let workItem = DispatchWorkItem { print("working") }
queue.async(execute: workItem)
workItem.cancel() // 取消(若已开始则无法取消)

3.5 dispatch_barrier:读写锁场景

在自定义并发队列中,barrier 可保证「屏障前的任务完成后,再执行屏障任务,屏障完成后再执行屏障后的任务」:

1
2
3
4
5
6
7
8
9
let concurrentQueue = DispatchQueue(label: "com.app.db", attributes: .concurrent)

// 读:可并发
concurrentQueue.async { self.readFromCache() }

// 写:barrier 保证独占
concurrentQueue.async(flags: .barrier) {
self.writeToCache(newValue)
}

四、NSOperation 与 NSOperationQueue

4.1 为什么需要 Operation?

GCD 虽然强大,但在以下场景不够灵活:

  • 需要取消尚未执行的任务
  • 需要任务依赖(A 完成后再执行 B)
  • 需要暂停/恢复队列
  • 需要进度完成回调

Operation 提供了面向对象的方式解决这些问题。

4.2 基本用法

1
2
3
4
5
6
7
8
9
10
11
// 使用 BlockOperation
let op = BlockOperation {
print("执行任务")
}
op.completionBlock = { print("任务完成") }

let queue = OperationQueue()
queue.addOperation(op)

// 设置最大并发数
queue.maxConcurrentOperationCount = 3

4.3 任务依赖

1
2
3
4
5
6
7
8
let opA = BlockOperation { downloadImage() }
let opB = BlockOperation { resizeImage() }
let opC = BlockOperation { uploadImage() }

opB.addDependency(opA) // B 依赖 A
opC.addDependency(opB) // C 依赖 B

queue.addOperations([opA, opB, opC], waitUntilFinished: false)

4.4 自定义 Operation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ImageLoadOperation: Operation {
let url: URL
var image: UIImage?

init(url: URL) {
self.url = url
super.init()
}

override var isAsynchronous: Bool { true }

override func main() {
guard !isCancelled else { return }
if let data = try? Data(contentsOf: url) {
image = UIImage(data: data)
}
}
}

4.5 取消与暂停

1
2
3
queue.cancelAllOperations()   // 取消所有未执行任务
queue.isSuspended = true // 暂停队列(不执行新任务)
queue.isSuspended = false // 恢复

五、线程同步与线程安全

5.1 为什么需要同步?

多个线程同时访问共享资源(变量、文件、网络连接)时,若未做同步,会出现:

  • 数据竞争:读写交错,结果不可预期
  • 脏读:读到未写入完成的数据
  • 崩溃:如数组在遍历时被另一线程修改
1
2
3
4
5
6
// ❌ 非线程安全
var counter = 0
DispatchQueue.concurrentPerform(iterations: 1000) { _ in
counter += 1 // 多线程同时写,结果可能远小于 1000
}
print(counter) // 可能输出 523、687 等

5.2 锁机制

5.2.1 NSLock

1
2
3
4
5
6
7
8
9
let lock = NSLock()
var counter = 0

DispatchQueue.concurrentPerform(iterations: 1000) { _ in
lock.lock()
defer { lock.unlock() }
counter += 1
}
print(counter) // 1000

5.2.2 os_unfair_lock(高性能,iOS 10+)

1
2
3
4
var unfairLock = os_unfair_lock()
os_unfair_lock_lock(&unfairLock)
// 临界区
os_unfair_lock_unlock(&unfairLock)

5.2.3 NSRecursiveLock(可重入锁)

同一线程可多次加锁,用于递归或嵌套调用:

1
2
3
4
5
6
7
8
let recursiveLock = NSRecursiveLock()
func recursiveMethod(_ n: Int) {
recursiveLock.lock()
defer { recursiveLock.unlock() }
if n > 0 {
recursiveMethod(n - 1)
}
}

5.2.4 @synchronized(Objective-C)

1
2
3
@synchronized(self) {
// 临界区
}

底层使用 objc_sync_enter / objc_sync_exit,基于对象做锁。

5.3 信号量(Semaphore)

控制并发数量或实现生产者-消费者

1
2
3
4
5
6
7
8
9
let semaphore = DispatchSemaphore(value: 3)  // 最多 3 个并发

for i in 0..<10 {
DispatchQueue.global().async {
semaphore.wait() // 资源 -1,若为 0 则等待
defer { semaphore.signal() } // 资源 +1
doWork(i)
}
}

5.4 原子操作(Atomic)

对于简单类型,可使用原子属性。Swift 中常用 NSLock + 属性封装,或使用 objc_setAssociatedObject 的原子选项。atomic 属性只保证 getter/setter 原子,不保证复合操作(如 count++)的原子性。

5.5 避免死锁

死锁:两个或多个线程互相等待对方释放资源。

1
2
3
4
// ❌ 死锁示例:主队列同步执行
DispatchQueue.main.sync {
print("永远不会执行") // 主线程等待自己,死锁
}
1
2
3
4
5
6
7
// ❌ 死锁:串行队列嵌套同步
let queue = DispatchQueue(label: "serial")
queue.async {
queue.sync {
print("死锁") // 外层等内层,内层等外层
}
}

原则:避免在同一串行队列中嵌套 sync 调用。


六、Swift 并发(async/await)

6.1 从回调到 async/await

传统异步代码容易产生「回调地狱」:

1
2
3
4
5
6
7
8
// 回调嵌套
loadUser(id: 1) { user in
loadPosts(userId: user.id) { posts in
loadComments(postId: posts[0].id) { comments in
// 层层嵌套...
}
}
}

Swift 5.5 引入 async/await,写法更清晰:

1
2
3
4
5
6
func loadUserData() async throws {
let user = try await loadUser(id: 1)
let posts = try await loadPosts(userId: user.id)
let comments = try await loadComments(postId: posts[0].id)
await MainActor.run { self.updateUI(comments) }
}

6.2 Task 与 MainActor

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建异步任务
Task {
let result = await fetchData()
await MainActor.run {
self.label.text = result
}
}

// MainActor:保证在主线程执行
@MainActor
class ViewController: UIViewController {
func updateUI() { } // 自动在主线程
}

6.3 与 GCD 的桥接

1
2
3
4
5
6
7
8
9
// GCD 转 async
func withCheckedContinuation() async {
await withCheckedContinuation { continuation in
DispatchQueue.global().async {
let result = doWork()
continuation.resume(returning: result)
}
}
}

七、RunLoop 与线程

每个线程都有唯一的 RunLoop。子线程默认不启动 RunLoop,若使用 performSelector:onThread:NSTimer,需要手动 run

1
2
3
4
5
6
class WorkerThread: Thread {
override func main() {
RunLoop.current.add(Port(), forMode: .default)
RunLoop.current.run() // 进入事件循环
}
}

主线程的 RunLoop 由系统自动运行,通常无需关心。


八、GCD 底层原理(libdispatch 简析)

8.1 队列与线程池

  • GCD 维护全局线程池,根据队列类型和系统负载动态创建/复用线程
  • 串行队列:通常绑定一个线程,任务在该线程顺序执行
  • 并发队列:任务可分发到线程池中的多个线程

8.2 任务提交流程(简化)

1
2
3
4
5
6
7
dispatch_async(queue, block)

├─ 将 block 封装为 dispatch_continuation_t

├─ 将任务加入队列的 FIFO 链表

└─ 若有空闲 worker 线程,则唤醒执行;否则根据需要创建新线程

8.3 主队列特殊性

主队列任务一定在主线程执行,通过 RunLoop 的 Source1(Mach Port)唤醒主线程处理。

8.4 源码参考

libdispatch 开源:https://github.com/apple/swift-corelibs-libdispatch

核心结构体(简化):

1
2
3
4
5
6
7
8
9
struct dispatch_queue_s {
// 队列类型:串行 / 并发
// 目标队列、任务链表等
};

struct dispatch_continuation_s {
void (*dc_func)(void *); // block 的执行函数
void *dc_ctxt; // block 捕获的上下文
};

九、实战示例

9.1 图片异步加载与缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ImageLoader {
private let cache = NSCache<NSString, UIImage>()
private let queue = DispatchQueue(label: "com.app.imageloader", attributes: .concurrent)

func loadImage(url: URL, completion: @escaping (UIImage?) -> Void) {
let key = url.absoluteString as NSString
if let cached = cache.object(forKey: key) {
DispatchQueue.main.async { completion(cached) }
return
}
queue.async {
guard let data = try? Data(contentsOf: url),
let image = UIImage(data: data) else {
DispatchQueue.main.async { completion(nil) }
return
}
self.cache.setObject(image, forKey: key)
DispatchQueue.main.async { completion(image) }
}
}
}

9.2 多接口并发请求,统一回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func loadDashboardData(completion: @escaping (DashboardData?) -> Void) {
let group = DispatchGroup()
var user: User?
var orders: [Order]?
var error: Error?

group.enter()
fetchUser { result in
user = try? result.get()
group.leave()
}

group.enter()
fetchOrders { result in
orders = (try? result.get()) ?? []
group.leave()
}

group.notify(queue: .main) {
if let u = user, let o = orders {
completion(DashboardData(user: u, orders: o))
} else {
completion(nil)
}
}
}

9.3 线程安全的单例

1
2
3
4
5
6
7
8
final class DataManager {
static let shared: DataManager = {
let instance = DataManager()
return instance
}()

private init() { }
}

若需「懒加载 + 线程安全」,可使用 dispatch_once 的 Swift 封装,或在 Swift 中依赖 static let 的天然懒加载与线程安全。


十、实际项目中的应用案例

10.1 列表图片预加载

UITableView / UICollectionView 滚动时,预加载即将出现的 cell 所需图片,放到后台队列解码,再回主线程赋值,避免主线程卡顿。

1
2
3
4
5
6
7
8
9
10
11
func prefetchImage(at indexPath: IndexPath) {
let url = urls[indexPath.row]
DispatchQueue.global(qos: .utility).async {
guard let data = try? Data(contentsOf: url),
let image = UIImage(data: data) else { return }
DispatchQueue.main.async {
self.imageCache.setObject(image, forKey: url.absoluteString as NSString)
self.collectionView.reloadItems(at: [indexPath])
}
}
}

10.2 大文件分片上传

将大文件切分为多个 chunk,通过 OperationQueue 控制并发数,每个 Operation 上传一个 chunk,全部完成后组装结果。

1
2
3
4
5
6
7
8
let uploadQueue = OperationQueue()
uploadQueue.maxConcurrentOperationCount = 3
for chunk in chunks {
let op = BlockOperation { uploadChunk(chunk) }
uploadQueue.addOperation(op)
}
uploadQueue.waitUntilAllOperationsAreFinished()
mergeChunks()

10.3 搜索防抖

用户输入时,取消上一次未完成的搜索任务,延迟 300ms 再发起新请求:

1
2
3
4
5
6
7
8
9
var searchWorkItem: DispatchWorkItem?
func searchTextDidChange(_ text: String) {
searchWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
self?.performSearch(text)
}
searchWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: workItem)
}

10.4 后台数据同步

App 进入后台时,使用 beginBackgroundTask 申请有限时间,在后台队列执行同步逻辑,完成后结束 task:

1
2
3
4
5
6
7
8
9
10
11
12
func syncInBackground() {
var taskID: UIBackgroundTaskIdentifier = .invalid
taskID = UIApplication.shared.beginBackgroundTask {
UIApplication.shared.endBackgroundTask(taskID)
}
DispatchQueue.global(qos: .utility).async {
performSync()
DispatchQueue.main.async {
UIApplication.shared.endBackgroundTask(taskID)
}
}
}

十一、常见问题与最佳实践

11.1 主线程检查

开发阶段可用断言检查 UI 是否在主线程更新:

1
assert(Thread.isMainThread, "UI must be updated on main thread")

11.2 避免循环引用

在闭包中使用 self 时注意 [weak self]

1
2
3
4
5
6
7
DispatchQueue.global().async { [weak self] in
guard let self = self else { return }
let result = self.doWork()
DispatchQueue.main.async { [weak self] in
self?.updateUI(result)
}
}

11.3 合理选择 QoS

不要滥用 .userInteractive,避免后台任务抢占用户交互资源。

11.4 优先使用 Swift 并发

新项目优先考虑 async/await + Task,结构更清晰,可组合性更好。


十二、总结

场景 推荐方案
简单异步、延迟执行 GCD
复杂任务依赖、取消、暂停 NSOperation
新项目、网络/IO 密集型 async/await
控制并发数 信号量 或 OperationQueue.maxConcurrentOperationCount
读写分离、缓存 dispatch_barrier
线程安全访问共享资源 NSLock / os_unfair_lock

多线程能提升体验,但也会带来复杂度和潜在问题。理解原理、选对工具、注意同步与线程安全,是写出高质量并发代码的关键。