iOS 开发中的 React Native

由浅入深,从基本概念到源码解析,带你全面掌握 React Native 在 iOS 平台的开发与应用


一、什么是 React Native?为什么选择它?

1.1 从 Hybrid 到 React Native

移动开发经历了从纯原生(Native)到 Hybrid(WebView)再到跨平台框架的演进:

方案 代表 优势 劣势
原生 Swift/ObjC 性能最佳、体验最好 双端重复开发
Hybrid Cordova、WebView 一套 HTML/JS 性能差、体验割裂
跨平台 React Native、Flutter 一套代码、接近原生 学习曲线、生态依赖

React Native (RN) 由 Meta 于 2015 年开源,核心理念是:用 JavaScript 编写逻辑,用原生组件渲染 UI,而不是在 WebView 中渲染。

1
2
传统 Hybrid:    JS/HTML → WebView 渲染 → 间接调用原生 API
React Native: JS/React → 虚拟 DOM → 原生组件(UILabel、UIView 等)直接渲染

1.2 为什么 iOS 开发者要学 React Native?

  • 业务需要:公司采用 RN 做跨端,需要维护/扩展原生能力
  • 原生桥接:RN 依赖大量原生模块(相机、蓝牙、支付等),需要 iOS 侧配合开发
  • 性能优化:理解 RN 与原生通信机制,才能做性能调优和问题排查
  • 新架构:新架构大量使用 C++、JSI,与 iOS 底层结合更紧密

1.3 RN 与 Flutter 的简要对比

维度 React Native Flutter
语言 JavaScript/TypeScript Dart
渲染 原生组件 自绘引擎(Skia)
包体积 相对较小 相对较大
生态 依赖 React、npm 独立生态
与原生交互 通过 Bridge/JSI 通过 Platform Channel

二、核心概念与架构

2.1 三层架构概览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌─────────────────────────────────────────────────────────┐
│ JavaScript 层 │
│ React 组件、业务逻辑、状态管理、事件处理 │
└──────────────────────────┬──────────────────────────────┘
│ Bridge / JSI
┌──────────────────────────▼──────────────────────────────┐
│ C++ 层(新架构) │
│ JSI、Fabric 渲染、TurboModules 调度 │
└──────────────────────────┬──────────────────────────────┘
│ FFI / Objective-C++
┌──────────────────────────▼──────────────────────────────┐
│ Native 层(iOS) │
│ UIKit、系统 API、自定义原生模块 │
└─────────────────────────────────────────────────────────┘

2.2 关键概念

概念 说明
Bridge 旧架构中 JS 与 Native 的异步通信桥梁,数据需序列化
JSI JavaScript Interface,新架构中 JS 可直接持有 C++ 对象引用,同步调用
Fabric 新架构的渲染系统,将布局、绘制逻辑下沉到 C++
TurboModules 新架构的原生模块系统,懒加载、类型安全
Hermes 字节码引擎,替代 JavaScriptCore,提升启动与运行性能

2.3 旧架构 vs 新架构

维度 旧架构 新架构
通信 Bridge 异步、JSON 序列化 JSI 同步、直接引用
渲染 各平台各自实现 Fabric 统一 C++ 渲染管线
原生模块 Native Modules 启动时全量加载 TurboModules 按需懒加载
类型 无强类型约定 通过 Codegen 生成类型

三、环境搭建与项目创建

3.1 环境要求

  • Node.js:建议 LTS 版本(18+)
  • Xcode:最新稳定版
  • CocoaPodsgem install cocoapods
  • Watchman(可选):brew install watchman,用于文件监听

3.2 创建新项目

1
2
3
4
5
6
7
8
9
10
11
# 使用 React Native CLI
npx @react-native-community/cli init MyApp

# 进入 iOS 目录
cd MyApp/ios

# 安装 CocoaPods 依赖
pod install

# 返回根目录,启动 Metro
cd .. && npx react-native start

另开终端运行 iOS:

1
2
3
npx react-native run-ios
# 或指定模拟器
npx react-native run-ios --simulator="iPhone 15"

3.3 项目结构(iOS 侧)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
MyApp/
├── ios/
│ ├── MyApp/ # 原生 iOS 工程
│ │ ├── AppDelegate.mm # 入口,加载 RN 根视图
│ │ ├── Info.plist
│ │ └── ...
│ ├── Podfile # CocoaPods 配置
│ ├── Podfile.lock
│ └── MyApp.xcworkspace # 用此打开工程
├── android/
├── src/ # JS 源码
├── node_modules/
├── package.json
└── metro.config.js

3.4 AppDelegate 与 RN 加载

典型的 AppDelegate.mm 中加载 RN 的流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSURL *jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];

RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"MyApp"
initialProperties:nil
launchOptions:launchOptions];

self.window.rootViewController = [[UIViewController alloc] init];
self.window.rootViewController.view = rootView;
[self.window makeKeyAndVisible];
return YES;
}

RCTRootView 负责加载 JS Bundle、创建 Bridge、挂载 React 组件树。


四、JS 与 Native 通信原理

4.1 旧架构:Bridge 模型

旧架构下,JS 与 Native 通过 异步 Bridge 通信:

1
2
3
4
5
6
7
JS 层发起调用

├─ 将参数序列化为 JSON

├─ 通过 Bridge 发送到 Native 队列

└─ Native 解析 JSON,执行对应模块方法,再序列化结果回传 JS

特点

  • 异步:所有跨端调用都是异步的
  • 序列化:参数和返回值需要 JSON 序列化,有性能开销
  • 全量加载:所有 Native Modules 在启动时注册

4.2 旧架构模块注册流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. 实现 RCTBridgeModule 协议
@interface MyNativeModule : NSObject <RCTBridgeModule>
@end

@implementation MyNativeModule

RCT_EXPORT_MODULE(); // 导出模块名,默认类名

RCT_EXPORT_METHOD(getDeviceId:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
NSString *id = [[UIDevice currentDevice] identifierForVendor].UUIDString;
resolve(id);
}

@end

JS 端调用:

1
2
3
4
import { NativeModules } from 'react-native';
const { MyNativeModule } = NativeModules;

const id = await MyNativeModule.getDeviceId();

4.3 新架构:JSI 直接调用

JSI 允许 JavaScript 直接持有 C++ 对象的引用,无需经过 Bridge 序列化:

1
2
3
4
5
6
7
8
// C++ 侧:通过 JSI 暴露方法
jsi::Function getDeviceId = jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(runtime, "getDeviceId"),
0,
[](jsi::Runtime& rt, const jsi::Value&, const jsi::Value*, size_t) {
return jsi::String::createFromUtf8(rt, getNativeDeviceId());
});

JS 可直接同步调用,无需 Promise 包装。


五、新架构:JSI、Fabric、TurboModules

5.1 JSI(JavaScript Interface)

JSI 是 C++ 实现的薄封装层,让 JS 引擎(Hermes/JSC)能够:

  • 调用 C++ 函数
  • 读取/写入 C++ 对象属性
  • 在 C++ 中执行 JS 回调
1
2
3
4
5
6
7
8
9
// 简化示意:HostObject 暴露给 JS 的对象
class DeviceModule : public jsi::HostObject {
jsi::Value get(jsi::Runtime& rt, const jsi::PropNameID& name) override {
if (name.utf8(rt) == "getDeviceId") {
return jsi::Function::createFromHostFunction(...);
}
return jsi::Value::undefined();
}
};

5.2 Fabric 渲染管线

Fabric 将 React 的渲染逻辑从各平台分别实现,统一到 C++:

1
2
3
4
5
6
7
8
9
10
11
12
13
React 组件树


Shadow Tree(C++ 中的布局树)


布局计算(Yoga)


提交到原生层(Mount)


iOS UIView 创建/更新

优势:减少跨 Bridge 的序列化、支持同步布局、更好的并发与优先级调度。

5.3 TurboModules

TurboModules 的特性:

  • 懒加载:只在首次被 JS 引用时初始化
  • 类型安全:通过 Codegen 从 TypeScript 定义生成 C++ 和 ObjC 代码
  • 同步能力:通过 JSI 可实现同步调用

定义原生模块的规范(新架构):

1
2
3
4
5
6
7
8
9
10
// NativeMyModule.ts (Codegen 规范)
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
getDeviceId(): Promise<string>;
multiply(a: number, b: number): number;
}

export default TurboModuleRegistry.getEnforcing<Spec>('MyNativeModule');

六、原生模块开发(Native Modules)

6.1 旧架构:RCT_EXPORT_MODULE

完整示例:实现一个获取设备信息的原生模块。

Objective-C:

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
// DeviceInfoModule.h
#import <React/RCTBridgeModule.h>

@interface DeviceInfoModule : NSObject <RCTBridgeModule>
@end

// DeviceInfoModule.m
#import "DeviceInfoModule.h"
#import <React/RCTLog.h>
#import <UIKit/UIKit.h>

@implementation DeviceInfoModule

RCT_EXPORT_MODULE(DeviceInfo)

RCT_EXPORT_METHOD(getDeviceInfo:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
dispatch_async(dispatch_get_main_queue(), ^{
NSDictionary *info = @{
@"model": [[UIDevice currentDevice] model],
@"systemVersion": [[UIDevice currentDevice] systemVersion],
@"name": [[UIDevice currentDevice] name],
};
resolve(info);
});
}

@end

JavaScript:

1
2
3
4
5
6
7
8
9
10
11
12
import { NativeModules } from 'react-native';

const { DeviceInfo } = NativeModules;

async function loadDeviceInfo() {
try {
const info = await DeviceInfo.getDeviceInfo();
console.log(info);
} catch (e) {
console.error(e);
}
}

6.2 新架构:TurboModule + Swift

新架构推荐用 Swift 实现业务逻辑,用 ObjC++ 做 JSI 胶水层。

Swift 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// DeviceInfoModule.swift
import Foundation

@objc(DeviceInfoModule)
class DeviceInfoModule: NSObject {
@objc
static func requiresMainQueueSetup() -> Bool {
return false
}

@objc
func getDeviceInfo(_ resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
let info: [String: Any] = [
"model": UIDevice.current.model,
"systemVersion": UIDevice.current.systemVersion
]
resolve(info)
}
}

通过 RCT_EXTERN_MODULE 导出给 ObjC:

1
2
3
4
5
6
7
8
9
// DeviceInfoModule.m(桥接)
#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(DeviceInfoModule, NSObject)

RCT_EXTERN_METHOD(getDeviceInfo:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

@end

6.3 事件发送:从 Native 到 JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 在原生模块中
#import <React/RCTEventEmitter.h>

@interface MyModule : RCTEventEmitter <RCTBridgeModule>
@end

@implementation MyModule

RCT_EXPORT_MODULE()

- (NSArray<NSString *> *)supportedEvents {
return @[@"onScanResult"];
}

- (void)sendScanResult:(NSString *)result {
[self sendEventWithName:@"onScanResult" body:@{@"result": result}];
}

@end
1
2
3
4
5
6
import { NativeEventEmitter, NativeModules } from 'react-native';

const emitter = new NativeEventEmitter(NativeModules.MyModule);
emitter.addListener('onScanResult', (event) => {
console.log(event.result);
});

七、原生 UI 组件(Native UI Components)

7.1 使用 ViewManager 封装 UIView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// MyCustomViewManager.h
#import <React/RCTViewManager.h>

@interface MyCustomViewManager : RCTViewManager
@end

// MyCustomViewManager.m
#import "MyCustomViewManager.h"
#import "MyCustomView.h"

@implementation MyCustomViewManager

RCT_EXPORT_MODULE(MyCustomView)

- (UIView *)view {
return [[MyCustomView alloc] init];
}

RCT_EXPORT_VIEW_PROPERTY(title, NSString)
RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)

@end
1
2
3
4
5
// MyCustomView.h
@interface MyCustomView : UIView
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
@end

7.2 JS 侧使用

1
2
3
4
5
6
7
8
9
10
11
12
import { requireNativeComponent } from 'react-native';

const MyCustomView = requireNativeComponent('MyCustomView');

export default function Screen() {
return (
<MyCustomView
title="Hello"
onPress={(e) => console.log(e.nativeEvent)}
/>
);
}

7.3 新架构:Fabric 组件

新架构下,通过 Codegen 定义 Props 和事件,生成 C++ 与各平台代码,实现类型安全和更好的性能。


八、源码解析

8.1 初始化流程(iOS)

1
2
3
4
5
6
7
8
9
10
11
12
main()
└─ UIApplicationMain
└─ AppDelegate didFinishLaunchingWithOptions
└─ RCTRootView initWithBundleURL:moduleName:...
├─ 创建 RCTBridge
│ ├─ 加载 JavaScript Bundle
│ ├─ 初始化 JS 引擎(Hermes/JSC)
│ ├─ 注册所有 Native Modules
│ └─ 执行 JS 入口(AppRegistry.runApplication)

└─ 创建 RCTRootContentView
└─ 挂载 React 根组件,触发首次渲染

8.2 Bridge 核心结构(旧架构)

1
2
3
4
5
6
7
8
9
10
11
12
// 简化示意
@interface RCTBridge : NSObject
@property (nonatomic, strong) RCTBridge *batchedBridge; // 实际执行 Bridge
@end

// 模块调用流程
// JS: NativeModules.DeviceInfo.getDeviceInfo()
// -> __callFunction(moduleName, methodName, args)
// -> 序列化为 JSON,通过 RCTBridge 发送
// -> Native: RCTModuleData 根据 moduleName 找到模块实例
// -> 反序列化参数,invoke 对应方法
// -> 结果序列化回传 JS

8.3 新架构关键路径

  • JSIReactCommon/jsi/,提供 jsi::RuntimeHostObject
  • FabricReactCommon/react/renderer/,Shadow Tree、Mount、Component 定义
  • TurboModulesReactCommon/react/nativemodule/,模块注册与调用

可参考官方仓库:
https://github.com/facebook/react-native


九、实战示例

9.1 调用系统分享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ShareModule.m
RCT_EXPORT_METHOD(share:(NSDictionary *)options
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *title = options[@"title"] ?: @"";
NSString *url = options[@"url"] ?: @"";

UIActivityViewController *activityVC = [[UIActivityViewController alloc]
initWithActivityItems:@[title, [NSURL URLWithString:url]]
applicationActivities:nil];

UIViewController *rootVC = [UIApplication sharedApplication]
.keyWindow.rootViewController;
[rootVC presentViewController:activityVC animated:YES completion:nil];
resolve(@YES);
});
}

9.2 封装原生 TabBar

在 RN 中嵌入 UITabBarController 的容器,通过 Native Module 控制 Tab 切换,实现与原生 TabBar 一致的外观和动效。

9.3 列表性能优化:FlashList

1
2
3
4
5
6
7
8
9
10
11
12
13
import { FlashList } from '@shopify/flash-list';

function ProductList({ data }) {
const renderItem = ({ item }) => <ProductCard item={item} />;

return (
<FlashList
data={data}
renderItem={renderItem}
estimatedItemSize={100}
/>
);
}

FlashList 使用按需渲染和复用,比 FlatList 更适合长列表场景。


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

10.1 电商 App:商品详情混合栈

  • Native:顶部 Banner 轮播、视频播放、复杂动效
  • RN:评价列表、推荐列表、加购/下单逻辑

通过 RCTRootView 嵌入到 UIViewController 的指定区域,实现「上原生、下 RN」的混合页面。

10.2 金融 App:安全键盘

输入密码时使用 Native 自定义键盘(避免 RN 侧键盘被截屏/录屏),通过 Native Module 将输入结果回传 JS:

1
2
3
4
5
6
7
8
RCT_EXPORT_METHOD(showSecureKeyboard:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
SecureKeyboardViewController *vc = [[SecureKeyboardViewController alloc]
initWithCompletion:^(NSString *pin) {
resolve(pin);
}];
[self presentVC:vc];
}

10.3 地图与 LBS

地图、路径规划、定位等使用 Native SDK,通过 Native UI Component 和 Native Module 暴露给 RN,兼顾性能与功能完整性。

10.4 OTA 热更新

将打包好的 JS Bundle 下发到本地,启动时优先加载本地 Bundle,实现不发版即可更新业务逻辑(需注意各应用市场的合规要求)。


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

11.1 主线程与 UI 更新

原生模块中涉及 UI 的操作必须回到主线程:

1
2
3
dispatch_async(dispatch_get_main_queue(), ^{
[self presentViewController:vc animated:YES completion:nil];
});

11.2 避免内存泄漏

  • 使用 RCTEventEmitter 时正确实现 invalidate
  • Block 中使用 weakSelf 避免循环引用
  • 大对象及时释放,避免长期持有

11.3 调试技巧

1
2
3
4
5
6
7
8
# 查看 Metro 日志
npx react-native start

# 真机调试
npx react-native run-ios --device

# 查看原生日志
# Xcode -> Debug -> Open System Log,或使用 Console.app

11.4 性能建议

  • 长列表使用 FlashList 或优化 FlatListgetItemLayout
  • 复杂动画考虑 react-native-reanimated
  • 新项目尽量启用新架构(Fabric + TurboModules)
  • 图片使用 FastImage 或自定义 Native 图片组件做缓存

11.5 启用新架构

ios/Podfile 中:

1
ENV['RCT_NEW_ARCH_ENABLED'] = '1'

执行 pod install 后重新编译。


十二、总结

场景 建议
新项目 启用新架构(Fabric + TurboModules + Hermes)
原生能力扩展 通过 Native Module 暴露,优先用 Swift + ObjC 桥接
复杂 UI 封装 Native UI Component,或使用成熟第三方组件
性能敏感 长列表用 FlashList,动画用 Reanimated
调试 Metro + Flipper + Xcode 结合使用

React Native 在 iOS 上的核心价值是:用 React 生态统一业务逻辑,用原生能力保证体验与性能。理解 Bridge/JSI、Fabric、TurboModules 的演进,有助于在混合栈项目中做出更合适的架构与实现选择。

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

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


一、基础概念

1.1 为什么需要架构优化?

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

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

1.2 三种架构模式辨析

1
2
3
4
5
6
7
8
9
10
11
12
┌─────────────────────────────────────────────────────────────────┐
│ 架构演进路径 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 单体架构 ──► 模块化 ──► 组件化 ──► 插件化 │
│ │ │ │ │ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ 按功能拆分 物理隔离 独立仓库 运行时动态加载 │
│ 代码目录 组件解耦 CocoaPods 热更新/按需加载 │
│ │
└─────────────────────────────────────────────────────────────────┘

模块化(Modularization)

  • 定义:按业务功能将代码拆分为独立的「模块」,每个模块有清晰的边界
  • 特点:逻辑划分、职责单一,通常仍在同一工程内
  • 粒度:中等,如「用户模块」「订单模块」「支付模块」

组件化(Componentization)

  • 定义:将模块进一步拆分为可独立编译、可复用的「组件」,通过依赖注入解耦
  • 特点:物理隔离、独立仓库、独立编译、协议解耦
  • 粒度: finer,如「登录组件」「分享组件」「埋点组件」

插件化(Pluginization)

  • 定义:组件可动态加载、热更新,主 App 与插件解耦到运行时
  • 特点:运行时动态、按需加载、可热修复
  • 粒度:动态 Bundle/Framework

二、模块化原理与实践

2.1 模块化的核心原则

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

2.2 目录结构示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
MyApp/
├── App/ # 主工程壳
│ ├── AppDelegate
│ └── Main
├── Modules/
│ ├── ModuleA_User/ # 用户模块
│ │ ├── Services/
│ │ ├── Views/
│ │ └── Models/
│ ├── ModuleB_Order/ # 订单模块
│ └── ModuleC_Payment/ # 支付模块
├── Common/ # 公共基础库
│ ├── Network/
│ ├── Utils/
│ └── BaseClasses/
└── Router/ # 路由层(模块间通信枢纽)

2.3 模块间通信:Protocol + 依赖注入

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
// 1. 定义协议(放在公共层或 Protocol 模块)
protocol UserServiceProtocol: AnyObject {
func getCurrentUser() -> User?
func logout()
}

// 2. 模块实现协议
// UserModule/UserService.swift
class UserService: UserServiceProtocol {
func getCurrentUser() -> User? { /* ... */ }
func logout() { /* ... */ }
}

// 3. 依赖注入(App 启动时或 ServiceLocator)
class ServiceLocator {
static let shared = ServiceLocator()
var userService: UserServiceProtocol?
}

// 4. 其他模块通过协议调用
class OrderViewController: UIViewController {
var userService: UserServiceProtocol?

func showUserInfo() {
guard let user = userService?.getCurrentUser() else { return }
// ...
}
}

三、组件化原理与实现

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
24
                ┌──────────────────┐
│ App Shell │
│ (主工程/壳工程) │
└────────┬─────────┘

┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 用户组件 │ │ 订单组件 │ │ 支付组件 │
│ (Pod) │ │ (Pod) │ │ (Pod) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└───────────────┼───────────────┘

┌──────▼──────┐
│ 路由/中间层 │
│ (URL/Mediator)│
└──────┬──────┘

┌──────▼──────┐
│ 基础组件 │
│ (网络/缓存/UI)│
└─────────────┘

3.2 路由方案对比

方案 原理 优点 缺点
URL Router 字符串 URL 映射到 VC 简单、支持 H5 跳转 参数传递不便、类型不安全
Target-Action (Mediator) 通过 Mediator 类反射调用 解耦彻底、类型安全 需维护中间类
Protocol 协议注册 + 实现查找 接口清晰 需集中注册

3.3 Mediator 模式源码实现

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
// CTMediator 核心实现(简化版,源自 Casa 的 CTMediator)
import UIKit

public class CTMediator: NSObject {
public static let shared = CTMediator()

// 缓存 Target 实例,避免重复创建
private var targetCache = [String: NSObject]()

/// 通过 Target-Action 调用
/// - Parameters:
/// - targetName: 目标类名,如 "Order"
/// - actionName: 方法名,如 "orderListViewController"
/// - params: 参数字典
public func perform(target targetName: String,
action actionName: String,
params: [String: Any]? = nil) -> Any? {
let targetClassString = "Target_\(targetName)"
let actionString = "Action_\(actionName):"

// 1. 获取 Target 类
guard let targetClass = NSClassFromString(targetClassString) as? NSObject.Type else {
return nil
}

// 2. 获取或创建 Target 实例
var target = targetCache[targetClassString]
if target == nil {
target = targetClass.init()
targetCache[targetClassString] = target
}

// 3. 通过 NSInvocation 或 perform 调用
let selector = NSSelectorFromString(actionString)
guard (target?.responds(to: selector)) ?? false else {
return nil
}

return target?.perform(selector, with: params)?.takeUnretainedValue()
}
}

// ========== 组件端:Order 模块 ==========
// 在 Order 组件内定义 Target_Order
class Target_Order: NSObject {
@objc func Action_orderListViewController(_ params: [String: Any]?) -> UIViewController {
let userId = params?["userId"] as? String ?? ""
let vc = OrderListViewController(userId: userId)
return vc
}
}

// ========== 调用端:任意模块 ==========
let vc = CTMediator.shared.perform(
target: "Order",
action: "orderListViewController",
params: ["userId": "12345"]
) as? UIViewController
navigationController?.pushViewController(vc!, animated: true)

3.4 依赖关系设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Podfile 示例
target 'MainApp' do
# 业务组件(同层,互不依赖)
pod 'UserModule'
pod 'OrderModule'
pod 'PaymentModule'

# 中间层
pod 'Mediator'
pod 'Mediator/Order' # Category,扩展 Order 的便捷方法
pod 'Mediator/User'
end

# 各业务组件的 Podspec 只依赖基础库
# OrderModule.podspec
Pod::Spec.new do |s|
s.name = 'OrderModule'
s.dependency 'Mediator'
s.dependency 'BaseNetwork'
s.dependency 'BaseUI'
end

关键点:业务组件之间不直接依赖,都通过 Mediator 间接通信。


四、插件化原理与实现

4.1 插件化的应用场景

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

4.2 iOS 插件化技术选型

技术 说明 限制
Dynamic Framework 动态库,可延迟加载 App Store 限制主二进制外的动态库
Bundle + 反射 资源/代码打包成 .bundle,运行时加载 无法绕过沙盒,需提前打包进 App
JavaScript 引擎 React Native / Flutter / JSI 非原生,性能与体验有差异
JSPatch / 热修复 通过 Runtime 动态替换方法 已不可上架 App Store

注意:苹果审核禁止下载执行任意代码,真正「从网络下载插件并执行」的纯插件化在 App Store 场景不可行。实际做法多为:预置多个 Framework/Bundle,运行时按需加载,或通过 JS 引擎 + 离线包 实现业务热更新。

4.3 动态 Framework 加载示例

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
import UIKit

class PluginManager {
static let shared = PluginManager()
private var loadedBundles: [String: Bundle] = [:]

/// 加载动态 Framework(需预置在 App 内)
func loadPlugin(named pluginName: String) -> Bool {
guard let path = Bundle.main.path(
forResource: pluginName,
ofType: "framework",
inDirectory: "Frameworks"
) else { return false }

guard let bundle = Bundle(path: path), bundle.load() else {
return false
}

loadedBundles[pluginName] = bundle
return true
}

/// 通过反射获取插件内的类并调用
func instantiateViewController(fromPlugin pluginName: String,
className: String) -> UIViewController? {
guard let bundle = loadedBundles[pluginName] ?? {
loadPlugin(named: pluginName) ? loadedBundles[pluginName] : nil
}() else { return nil }

guard let pluginClass = bundle.classNamed(className) as? UIViewController.Type else {
return nil
}

return pluginClass.init()
}
}

// 使用
if PluginManager.shared.loadPlugin(named: "ActivityPlugin") {
let vc = PluginManager.shared.instantiateViewController(
fromPlugin: "ActivityPlugin",
className: "ActivityPlugin.ActivityViewController"
)
present(vc!, animated: true)
}

4.4 基于 Runtime 的模块注册

1
2
3
4
5
6
7
8
9
10
11
12
// 插件自注册:通过 +load 在加载时注册到中心
// PluginModule.m
+ (void)load {
[[PluginRegistry shared] registerPlugin:@"Activity"
withClass:[ActivityPlugin class]];
}

// PluginRegistry 管理所有插件
@interface PluginRegistry : NSObject
- (void)registerPlugin:(NSString *)name withClass:(Class)cls;
- (id)createInstanceForPlugin:(NSString *)name;
@end

五、BeeHive 框架源码解析

BeeHive 是阿里开源的 iOS 模块化框架,结合了 Protocol 注册Module 生命周期

5.1 架构概览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌────────────────────────────────────────────────────────┐
│ BHContext (上下文) │
│ - 配置信息、环境变量、共享数据 │
└────────────────────────────────────────────────────────┘

┌─────────────────────────┴─────────────────────────────┐
│ BHModuleManager │
│ - 管理 Module 注册、加载、生命周期 │
└────────────────────────────────────────────────────────┘

┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ BHService │ │ BHModule │ │ BHConfig │
│ (协议-实现) │ │ (模块基类) │ │ (配置) │
└──────────────┘ └──────────────┘ └──────────────┘

5.2 核心类:BHModuleManager

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
// 简化版核心逻辑
@implementation BHModuleManager

- (void)registerDynamicModule:(Class)moduleClass {
if ([moduleClass conformsToProtocol:@protocol(BHModuleProtocol)]) {
BHModuleInfo *info = [[BHModuleInfo alloc] initWithModuleClass:moduleClass];
[self.modules addObject:info];
[self.modules sortUsingComparator:^NSComparisonResult(BHModuleInfo *m1, BHModuleInfo *m2) {
return m1.priority < m2.priority; // 按优先级排序
}];
}
}

- (void)triggerEvent:(NSInteger)eventType {
for (BHModuleInfo *info in self.modules) {
id<BHModuleProtocol> instance = [info moduleInstance];
switch (eventType) {
case BHMSetupEvent:
[instance modSetUp:nil];
break;
case BHMInitEvent:
[instance modInit:nil];
break;
case BHMSplashEvent:
[instance modSplash:nil];
break;
// ... 更多生命周期事件
}
}
}

@end

5.3 Protocol 注册与查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// BHServiceManager:Protocol -> 实现类 映射
- (void)registerService:(Protocol *)protocol implClass:(Class)implClass {
NSString *key = NSStringFromProtocol(protocol);
self.services[key] = implClass;
}

- (id)createService:(Protocol *)protocol {
Class implClass = self.services[NSStringFromProtocol(protocol)];
return [[implClass alloc] init];
}

// 使用
@protocol UserServiceProtocol <NSObject>
- (User *)currentUser;
@end

// 注册
[[BHServiceManager sharedManager] registerService:@protocol(UserServiceProtocol)
implClass:[UserServiceImpl class]];

// 获取
id<UserServiceProtocol> service = [[BHServiceManager sharedManager] createService:@protocol(UserServiceProtocol)];

六、实际项目应用案例

6.1 某电商 App 组件化拆分

1
2
3
4
5
6
7
8
9
10
11
MainApp (壳工程)
├── HomeModule # 首页
├── ProductModule # 商品详情
├── CartModule # 购物车
├── OrderModule # 订单
├── UserModule # 用户中心
├── PaymentModule # 支付
├── ShareModule # 分享(可复用)
├── AnalyticsModule # 埋点(可复用)
├── Mediator # 路由中间层
└── BasePods # 网络/缓存/UI 基础库

收益

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

6.2 路由设计实战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Mediator+Order.swift (Mediator 的 Category)
extension CTMediator {
func orderListViewController(userId: String) -> UIViewController? {
return perform(target: "Order",
action: "orderListViewController",
params: ["userId": userId]) as? UIViewController
}

func orderDetailViewController(orderId: String) -> UIViewController? {
return perform(target: "Order",
action: "orderDetailViewController",
params: ["orderId": orderId]) as? UIViewController
}
}

// 调用处:无需关心 Order 模块内部实现
if let vc = CTMediator.shared.orderListViewController(userId: "123") {
navigationController?.pushViewController(vc, animated: true)
}

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

错误示例

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

形成循环依赖,无法独立编译。

正确做法

1
2
OrderModule -> Mediator
UserModule -> Mediator

需要「用户信息」时,Order 通过 Mediator 获取 UserServiceProtocol;需要「跳转订单」时,User 通过 Mediator 获取 OrderListViewController。Mediator 依赖各模块的 Target 类(可通过 Category 按需引入)。


七、最佳实践与注意事项

7.1 模块拆分原则

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

7.2 常见坑与规避

问题 原因 规避
编译顺序错误 组件间隐式依赖 严格检查 Pod 依赖,用 pod lib lint 校验
协议爆炸 过度抽象 只对跨模块通信定义协议,模块内部保持自由
路由表膨胀 所有页面都走路由 仅对外暴露的页面注册路由
启动变慢 Module 过多,+load 耗时 延迟注册、按需加载、控制 Module 数量

7.3 架构演进路线图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Phase 1: 模块化
├── 按功能拆分目录
├── 引入 Protocol 解耦
└── 建立 ServiceLocator

Phase 2: 组件化
├── 拆分为独立 Pod
├── 引入 Mediator/路由
└── 独立编译、独立开发

Phase 3: 插件化(可选)
├── 非核心模块动态加载
├── 预置多套业务 Bundle
└── 或引入 RN/Flutter 做动态化

八、总结

架构 适用场景 核心手段
模块化 中小型 App、团队 < 10 人 目录拆分、Protocol、依赖注入
组件化 中大型 App、多业务线并行 独立 Pod、Mediator、路由
插件化 需要动态化、热更新 动态 Framework、Bundle、JS 引擎

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


参考资源

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;纯原生慎用原生热修,优先考虑架构升级
实践 合规优先、版本约束、灰度回滚、安全校验

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

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

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

iOS 开发中的 Runtime

由浅入深,从基本概念到源码解析,带你全面理解 Objective-C 运行时机制


一、什么是 Runtime?

1.1 从「面向对象」说起

在 C++ 这类静态语言中,方法的调用在编译期就确定了,编译器会把方法调用翻译成确定的函数地址。而 Objective-C 是一门「消息型」语言,方法调用在编译期只是生成了「发消息」的代码,真正要调用哪个方法,要到运行期才能确定。

这种「推迟到运行时再决定」的能力,就是 Runtime 的精髓。

1.2 Runtime 是什么?

Runtime 是 Objective-C 的运行时系统,是一套用 C 和汇编实现的底层库。它负责:

  • 类与对象的创建、布局
  • 方法的查找、派发(Message Dispatch)
  • 消息传递机制(Message Passing)
  • 动态添加/修改类、方法、属性
  • 方法交换(Method Swizzling)
  • KVO、KVC、Block 等特性的底层支撑

可以理解为:Runtime 是 Objective-C 的「操作系统」,没有它,OC 就无法运行。

1.3 两种版本

版本 说明 适用场景
Legacy Runtime 较早版本 32 位 macOS、老设备
Modern Runtime 当前主流 64 位 macOS、iOS、模拟器

Modern Runtime 支持:非 fragile instance variables、属性自动合成、Objective-C 2.0 语法等。


二、核心概念

2.1 对象(Object)与类(Class)

在 C 中,结构体就是「数据 + 布局」。在 OC 中,对象 本质上是一个指向结构体的指针,结构体第一个成员是指向 类对象(Class) 的指针 isa

1
2
3
4
5
6
7
8
9
10
11
12
13
// 简化理解
struct objc_object {
Class isa; // 指向所属类
};

struct objc_class {
Class isa; // 类对象的 isa 指向元类(metaclass)
Class superclass; // 父类
// ... 方法列表、属性列表等
};

typedef struct objc_object *id;
typedef struct objc_class *Class;
  • 实例对象isa → 类对象
  • 类对象isa → 元类(metaclass)
  • 元类isa → 根元类,superclass → 父类的元类

2.2 消息传递(Message Passing)

OC 中「调用方法」实际上是「发消息」:

1
2
3
[person sayHello];
// 等价于
objc_msgSend(person, @selector(sayHello));

objc_msgSend 是 Runtime 提供的 C 函数,流程大致为:

  1. 检查 receiver 是否为 nil(若为 nil,直接返回,不崩溃)
  2. receiverisa 指向的类中查找方法
  3. 若未找到,沿 superclass 链向上查找
  4. 找到后跳转执行实现(IMP)
  5. 若最终未找到,进入「消息转发」流程

2.3 SEL、IMP、Method

类型 说明 示例
SEL 方法选择器,方法名的唯一标识 @selector(sayHello)
IMP 函数指针,方法的实际实现 void (*)(id, SEL, ...)
Method 方法结构体,包含 SEL 和 IMP struct objc_method
1
2
3
4
5
6
7
typedef struct objc_method *Method;

struct objc_method {
SEL method_name;
char *method_types;
IMP method_imp;
};

三、消息查找与转发

3.1 方法查找流程(快速查找 + 慢速查找)

1
2
3
4
5
6
7
8
9
10
objc_msgSend(receiver, sel)

├─ 1. 检查 receiver 是否为 nil

├─ 2. 从类/父类缓存中查找 (objc_cache) —— 快速路径

└─ 3. 缓存未命中 → 调用 lookUpImpOrForward 慢速查找
├─ 在当前类 method_list 中查找
├─ 沿 superclass 链向上查找
└─ 若仍未找到 → 进入动态方法解析

3.2 动态方法解析(Dynamic Method Resolution)

在找不到方法时,Runtime 会先给类一次「补救」机会:

1
2
3
4
5
6
7
8
9
10
11
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(dynamicMethod)) {
class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}

void dynamicMethodIMP(id self, SEL _cmd) {
NSLog(@"动态添加的方法被调用了");
}

3.3 消息转发(Message Forwarding)

若动态解析也返回 NO,则进入转发流程:

  1. Fast Forwarding- (id)forwardingTargetForSelector:(SEL)aSelector

    • 返回一个能响应该 Selector 的对象,消息将转给该对象
    • 不修改方法签名,性能较好
  2. Normal Forwarding- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector + - (void)forwardInvocation:(NSInvocation *)anInvocation

    • 返回方法签名,再在 forwardInvocation: 中处理
    • 可灵活重定向、修改参数、多对象分发等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Fast Forwarding
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(sayHello)) {
return self.backupObject; // 转给其他对象处理
}
return [super forwardingTargetForSelector:aSelector];
}

// Normal Forwarding
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(sayHello)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
if (anInvocation.selector == @selector(sayHello)) {
[anInvocation invokeWithTarget:self.backupObject];
} else {
[super forwardInvocation:anInvocation];
}
}

3.4 流程小结

1
2
3
4
5
6
7
8
9
10
11
12
13
查找 IMP

├─ 1. 缓存命中 → 直接调用

├─ 2. 当前类及父类 method_list 中找到 → 写入缓存并调用

├─ 3. 未找到 → resolveInstanceMethod / resolveClassMethod

├─ 4. 解析失败 → forwardingTargetForSelector

├─ 5. 返回 nil → methodSignatureForSelector + forwardInvocation

└─ 6. 仍无法处理 → doesNotRecognizeSelector: crash

四、Runtime 源码结构

4.1 主要头文件

1
2
3
4
5
6
7
8
objc4 源码(可在 opensource.apple.com 获取):
├── objc-runtime.mm # 运行时初始化、类加载
├── objc-msg-x86_64.s # objc_msgSend 汇编实现(x86_64)
├── objc-msg-arm64.s # objc_msgSend 汇编实现(ARM64)
├── objc-class.mm # 类结构、方法列表
├── objc-object.mm # 对象、isa、关联对象
├── message.mm # 消息查找、转发
└── runtime.mm # 动态创建类、方法等

4.2 objc_object 与 isa

1
2
3
4
5
6
7
8
9
// objc-private.h (简化)
struct objc_object {
private:
isa_t isa; // 非指针时存储类地址;Tagged Pointer 优化时存更多信息
public:
Class getIsa();
void initIsa(Class cls);
// ...
};

4.3 objc_class 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// objc-runtime-new.h (简化)
struct objc_class : objc_object {
Class superclass;
cache_t cache; // 方法缓存
class_data_bits_t bits; // 方法列表、属性、协议等

class_rw_t *data() const {
return bits.data();
}

const method_array_t methods() const;
const property_array_t properties() const;
const protocol_array_t protocols() const;
};

4.4 方法缓存(cache_t)

为提高查找效率,每个类都有方法缓存。查找顺序:先查缓存,再查方法列表

1
2
3
4
5
6
7
8
9
10
11
// 简化理解
struct cache_t {
bucket_t *buckets; // 哈希表
mask_t mask; // 容量 - 1
mask_t occupied; // 已缓存数量
};

struct bucket_t {
SEL sel;
IMP imp;
};

五、常用 Runtime API

5.1 类与对象

1
2
3
4
5
6
7
8
9
10
11
// 获取类
Class cls = [MyObject class];
Class cls2 = object_getClass(obj);

// 创建实例
id obj = class_createInstance(cls, 0);

// 判断类型
BOOL isMeta = class_isMetaClass(cls);
BOOL isMember = [obj isMemberOfClass:[MyObject class]];
BOOL isKind = [obj isKindOfClass:[NSObject class]];

5.2 方法操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取方法
Method m = class_getInstanceMethod(cls, @selector(sayHello));

// 获取 SEL 和 IMP
SEL sel = method_getName(m);
IMP imp = method_getImplementation(m);

// 交换实现
Method m1 = class_getInstanceMethod(cls, @selector(methodA));
Method m2 = class_getInstanceMethod(cls, @selector(methodB));
method_exchangeImplementations(m1, m2);

// 添加方法
class_addMethod(cls, @selector(newMethod), (IMP)newMethodIMP, "v@:");

5.3 属性与成员变量

1
2
3
4
5
6
7
8
9
10
11
// 获取属性列表
unsigned int count;
objc_property_t *properties = class_copyPropertyList(cls, &count);

// 获取成员变量
Ivar *ivars = class_copyIvarList(cls, &count);
for (unsigned int i = 0; i < count; i++) {
Ivar ivar = ivars[i];
const char *name = ivar_getName(ivar);
const char *type = ivar_getTypeEncoding(ivar);
}

5.4 动态创建类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建新类
Class newClass = objc_allocateClassPair([NSObject class], "MyDynamicClass", 0);

// 添加实例变量(需在 allocate 之后、register 之前)
class_addIvar(newClass, "title", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));

// 添加方法
class_addMethod(newClass, @selector(doSomething), (IMP)doSomethingIMP, "v@:");

// 注册类
objc_registerClassPair(newClass);

// 使用
id instance = [[newClass alloc] init];

六、Method Swizzling(方法交换)

6.1 基本原理

通过 method_exchangeImplementations 交换两个方法的 IMP,使调用 A 时实际执行 B 的实现。常用于:

  • 无侵入式 Hook 系统或第三方方法
  • 统计埋点、日志
  • 修复 Bug、兼容旧版本

6.2 基础写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 交换 viewDidLoad 实现
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = [UIViewController class];
SEL originalSel = @selector(viewDidLoad);
SEL swizzledSel = @selector(swizzled_viewDidLoad);

Method originalMethod = class_getInstanceMethod(cls, originalSel);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSel);

method_exchangeImplementations(originalMethod, swizzledMethod);
});
}

- (void)swizzled_viewDidLoad {
NSLog(@"ViewController viewDidLoad 被调用");
[self swizzled_viewDidLoad]; // 交换后,这里实际调用原 viewDidLoad
}

6.3 安全写法:处理子类未实现的情况

若子类未重写 viewDidLoadclass_getInstanceMethod 会拿到父类的方法。直接交换会导致:父类方法被交换,影响所有子类。更安全的做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = [UIViewController class];
SEL originalSel = @selector(viewDidLoad);
SEL swizzledSel = @selector(swizzled_viewDidLoad);

Method originalMethod = class_getInstanceMethod(cls, originalSel);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSel);

BOOL didAdd = class_addMethod(cls, originalSel,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAdd) {
class_replaceMethod(cls, swizzledSel,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}

6.4 实际应用:全局统计页面访问

1
2
3
4
5
6
7
8
9
10
11
// UIViewController+PageTrack.m
+ (void)load {
[self swizzleInstanceMethod:@selector(viewDidAppear:)
withMethod:@selector(track_viewDidAppear:)];
}

- (void)track_viewDidAppear:(BOOL)animated {
[self track_viewDidAppear:animated];
// 统计逻辑
[Analytics trackPageView:NSStringFromClass([self class])];
}

七、关联对象(Associated Objects)

7.1 为什么需要关联对象?

分类(Category)不能添加实例变量,但我们有时需要给已有类「挂」一些额外数据。关联对象可以在不修改原类的情况下,为实例绑定键值对

7.2 API

1
2
3
4
5
6
7
8
// 设置
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

// 获取
id objc_getAssociatedObject(id object, const void *key);

// 移除
void objc_removeAssociatedObjects(id object); // 移除该对象所有关联,慎用

7.3 内存策略(policy)

Policy 对应属性修饰符 说明
OBJC_ASSOCIATION_ASSIGN assign 弱引用
OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic, strong 强引用,非原子
OBJC_ASSOCIATION_RETAIN atomic, strong 强引用,原子
OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic, copy 拷贝,非原子
OBJC_ASSOCIATION_COPY atomic, copy 拷贝,原子

7.4 示例:为 UIButton 绑定 Block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// UIButton+Block.h
- (void)addAction:(void (^)(UIButton *sender))block forControlEvents:(UIControlEvents)events;

// UIButton+Block.m
#import <objc/runtime.h>

static const void *kButtonBlockKey = &kButtonBlockKey;

- (void)addAction:(void (^)(UIButton *))block forControlEvents:(UIControlEvents)events {
objc_setAssociatedObject(self, kButtonBlockKey, block, OBJC_ASSOCIATION_COPY_NONATOMIC);
[self addTarget:self action:@selector(block_buttonTapped:) forControlEvents:events];
}

- (void)block_buttonTapped:(UIButton *)sender {
void (^block)(UIButton *) = objc_getAssociatedObject(self, kButtonBlockKey);
if (block) block(sender);
}

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

8.1 场景一:无侵入埋点

通过 Method Swizzling Hook viewDidAppear:viewDidDisappear:,自动统计页面停留时长,无需在每个 VC 里手动写埋点代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// PageAnalytics.m
- (void)tracked_viewDidAppear:(BOOL)animated {
[self tracked_viewDidAppear:animated];
self.pageEnterTime = CACurrentMediaTime();
[Tracking logEvent:@"page_enter" properties:@{@"page": NSStringFromClass([self class])}];
}

- (void)tracked_viewDidDisappear:(BOOL)animated {
NSTimeInterval duration = CACurrentMediaTime() - self.pageEnterTime;
[Tracking logEvent:@"page_leave" properties:@{
@"page": NSStringFromClass([self class]),
@"duration": @(duration)
}];
[self tracked_viewDidDisappear:animated];
}

8.2 场景二:防崩溃( unrecognized selector)

利用消息转发,在 forwardInvocation: 中统一处理未实现的方法调用,记录日志并优雅降级,避免 Crash。

1
2
3
4
5
6
7
8
9
10
11
12
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *sig = [super methodSignatureForSelector:aSelector];
if (!sig) {
sig = [NSMethodSignature signatureWithObjCTypes:"v@:"]; // 兜底签名
}
return sig;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
[CrashGuard logUnrecognizedSelector:anInvocation.selector onObject:self];
// 可选择上报、降级处理等
}

8.3 场景三:字典转模型(JSON → Model)

遍历类的属性列表,根据属性名从字典中取值并赋值,实现自动 JSON 转 Model。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+ (instancetype)modelWithDictionary:(NSDictionary *)dict {
id model = [[self alloc] init];
unsigned int count;
objc_property_t *properties = class_copyPropertyList([self class], &count);
for (unsigned int i = 0; i < count; i++) {
const char *name = property_getName(properties[i]);
NSString *key = [NSString stringWithUTF8String:name];
id value = dict[key];
if (value && ![value isKindOfClass:[NSNull class]]) {
[model setValue:value forKey:key];
}
}
free(properties);
return model;
}

8.4 场景四:KVO 防崩溃

KVO 常见崩溃:未配对 removeObserver、重复 remove、对象已释放。可通过 Hook addObserver:forKeyPath:options:context:removeObserver:forKeyPath:,用关联对象维护观察者链表,在 dealloc 时自动移除,实现「自释放」KVO。

8.5 场景五:调试时打印对象属性

利用 class_copyIvarListclass_copyPropertyList 遍历所有属性,valueForKey: 取值并拼接成字符串,方便调试时查看对象完整状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (NSString *)debugDescription {
NSMutableString *desc = [NSMutableString stringWithFormat:@"<%@: %p>\n", [self class], self];
unsigned int count;
Ivar *ivars = class_copyIvarList([self class], &count);
for (unsigned int i = 0; i < count; i++) {
Ivar ivar = ivars[i];
const char *name = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:name];
id value = [self valueForKey:key];
[desc appendFormat:@" %@ = %@\n", key, value];
}
free(ivars);
return desc;
}

九、注意事项与最佳实践

9.1 线程安全

  • objc_msgSend 等查找流程内部有锁,但动态修改类结构(如 class_addMethod)需注意线程安全
  • Method Swizzling 建议在 +load 中、单次执行完成,避免并发问题

9.2 Swizzling 陷阱

  • 只在 +load 中执行,且用 dispatch_once 保证只执行一次
  • 注意子类未实现父类方法时的交换范围
  • 避免多个库对同一方法重复 Swizzling,易产生难以排查的 Bug

9.3 性能考虑

  • Runtime 动态特性有开销,高频路径慎用
  • 方法缓存使大部分调用命中缓存,性能可接受
  • 关联对象查找为哈希查找,量不大时影响较小

9.4 Swift 与 Runtime

  • Swift 类继承自 NSObject 时,仍可使用 Runtime
  • 纯 Swift 类(不继承 NSObject)使用更静态的派发方式,Runtime 能力受限
  • 若需在 Swift 中使用 Runtime,需将类/方法标记为 @objcdynamic

十、总结

主题 要点
本质 OC 是消息型语言,方法调用 = 发消息,由 Runtime 在运行期决定实际执行的 IMP
查找 缓存 → 当前类 → 父类 → 动态解析 → 快速转发 → 完整转发 → 崩溃
Swizzling 交换 IMP,实现无侵入 Hook,注意子类与并发
关联对象 为实例动态绑定数据,弥补 Category 不能加实例变量的限制
应用 埋点、防崩溃、JSON 转模型、调试工具等

理解 Runtime,不仅有助于排查「消息转发」「KVO 崩溃」等问题,还能在需要时写出更灵活、可扩展的架构。建议结合 objc4 源码 和实际项目实践,逐步加深理解。

SDWebImage 源码

由浅入深,从基本概念到源码解析,带你全面掌握图片加载框架的设计与实现


一、什么是 SDWebImage?

1.1 为什么需要图片加载库?

在 iOS 开发中,展示网络图片是极其常见的需求。如果自己实现,需要处理:

  • 异步下载:不能阻塞主线程
  • 缓存策略:内存缓存 + 磁盘缓存,避免重复下载
  • 图片解码:在主线程解码大图会导致卡顿
  • 复用与取消:列表滑动时,旧请求应及时取消,避免错乱
  • 格式支持:JPEG、PNG、GIF、WebP 等

SDWebImage 将这些能力封装成一套成熟方案,被广泛应用于 App 中。

1.2 SDWebImage 简介

SDWebImage 是一个异步图片下载与缓存库,支持 iOS、macOS、watchOS、visionOS。核心特性包括:

特性 说明
异步下载 基于 NSURLSession,不阻塞主线程
内存 + 磁盘缓存 支持自定义缓存策略、过期时间
后台解码 避免主线程解码导致的卡顿
渐进式加载 支持 JPEG 等格式的渐进显示
动图支持 GIF、APNG、WebP 动画
缩略图解码 大图可只解码指定尺寸,节省内存
协议化设计 v5.x 起核心组件可插拔、可替换

1.3 版本演进要点

版本 主要变化
4.x Block 回调、FLAnimatedImageView
5.x 协议化:Loader、Cache、Coder 均可自定义;新增 View Indicator、Image Transform
5.6+ 完善协议体系,架构更清晰

二、核心架构与原理

2.1 整体数据流

一次完整的图片加载流程大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
用户调用 sd_setImageWithURL:


┌───────────────────────────────────────┐
│ UIView+WebCache (便捷入口) │
│ sd_internalSetImageWithURL:... │
└───────────────┬───────────────────────┘


┌───────────────────────────────────────┐
│ SDWebImageManager (调度中心) │
│ loadImageWithURL:options:... │
└───────────────┬───────────────────────┘

┌───────────┼───────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Cache │ │ Loader │ │ Coder │
│ 查找缓存 │ │ 下载图片 │ │ 解码图片 │
└─────────┘ └─────────┘ └─────────┘


显示到 UIImageView

2.2 协议化设计(v5.x 核心)

v5.x 将核心能力抽象为协议,实现可替换、可扩展:

协议 默认实现 职责
SDImageCache SDImageCache 内存 + 磁盘缓存
SDImageLoader SDWebImageDownloader 网络/本地图片加载
SDImageCoder SDImageCodersManager 图片编解码
SDWebImageCacheSerializer - 自定义缓存序列化
SDWebImageIndicator SDWebImageActivityIndicator 加载状态指示器

这种设计让开发者可以:

  • 替换默认下载器(如接入自研 CDN SDK)
  • 使用自定义缓存(如接入 YYCache、PINCache)
  • 支持新图片格式(实现 Coder 协议即可)

2.3 主线程检测的演进

SDWebImage 中使用 dispatch_main_async_safe 确保 UI 更新在主线程/主队列执行。v5.x 对主线程检测做了改进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 旧版:用 [NSThread isMainThread] 判断
#define dispatch_main_async_safe(block)\
if ([NSThread isMainThread]) { block(); } else {\
dispatch_async(dispatch_get_main_queue(), block);\
}

// 新版:用 dispatch_queue 标签判断
#define dispatch_main_async_safe(block)\
if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == \
dispatch_queue_get_label(dispatch_get_main_queue())) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}

原因:主队列 ≠ 主线程。某些场景下,非主队列的任务可能在主线程执行,若依赖 isMainThread,在依赖「主队列」的框架(如 VektorKit)中会出现问题。主队列上的任务一定在主线程执行,反之则需用队列标签判断。


三、源码解析

3.1 入口:UIView + WebCache

所有 View 的图片设置最终汇聚到 sd_internalSetImageWithURL:...

1
2
3
4
5
6
7
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock;

核心步骤(精简):

  1. 取消旧任务sd_cancelImageLoadOperationWithKey:,避免同一 View 的多次请求冲突
  2. 设置占位图:立即显示 placeholder
  3. 调用 ManagerloadImageWithURL:options:context:progress:completed:
  4. 保存 Operation:将返回的 id<SDWebImageOperation> 存入 sd_operationDictionary,便于取消

sd_operationDictionary 使用 NSMapTable,key 强引用、value 弱引用,因为 Operation 由 Manager 的 runningOperations 持有,这里仅作取消用。

3.2 SDWebImageManager:调度中心

Manager 负责串联 Cache、Loader、Coder,核心逻辑在 loadImageWithURL:options:context:progress:completed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1. 生成缓存 Key
NSString *key = [self cacheKeyForURL:url context:context];

// 2. 先查缓存(可选跳过)
if (!(options & SDWebImageFromLoaderOnly)) {
[self callQueryCacheOperationForKey:key ... completed:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
if (cachedImage) {
// 命中缓存,直接回调
completedBlock(cachedImage, cachedData, nil, cacheType, YES, url);
return;
}
// 3. 未命中,执行下载
[self callLoadOperationWithURL:url ... completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
// 4. 下载完成,写入缓存并回调
if (image && (options & SDWebImageCacheMemoryOnly)) {
[self.imageCache storeImage:image forKey:key ...];
}
completedBlock(image, data, error, cacheType, finished, imageURL);
}];
}];
}

流程:查缓存 → 未命中则下载 → 下载完成写缓存 → 回调

3.3 SDWebImageDownloader:下载器

下载器负责发起网络请求,支持并发数、超时、Request/Response 修改等配置:

1
2
3
4
5
6
// 核心下载接口
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

URL 复用:同一 URL 的多次请求会复用同一个 SDWebImageDownloaderOperation,通过 addHandlersForProgress:completed: 累积多个回调,下载完成后依次执行,避免重复下载。

1
2
3
4
5
6
7
8
9
10
NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
if (operation) {
@synchronized (operation) {
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
}
} else {
operation = [self createDownloaderOperationWithUrl:url options:options context:context];
[self.URLOperations setObject:operation forKey:url];
// ...
}

可插拔扩展

  • SDWebImageDownloaderRequestModifier:修改 Request(如加 Header)
  • SDWebImageDownloaderResponseModifier:修改 Response(如校验 MIME-Type)
  • SDWebImageDownloaderDecryptor:解密(如 Base64)

3.4 SDImageCache:缓存

缓存采用内存 + 磁盘二层结构:

1
2
3
4
5
6
7
8
9
10
11
12
// 查询缓存
- (void)queryImageForKey:(NSString *)key
options:(SDImageCacheOptions)options
context:(nullable SDWebImageContext *)context
completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock;

// 存储
- (void)storeImage:(UIImage *)image
imageData:(NSData *)imageData
forKey:(NSString *)key
cacheType:(SDImageCacheType)cacheType
completion:(nullable SDWebImageNoParamsBlock)completionBlock;
  • 内存缓存:基于 NSCache,受内存压力和系统策略自动回收
  • 磁盘缓存:默认使用 NSFileManager 存储到 Library/Caches/default,可配置自定义路径

缓存 Key 默认为 URL 的绝对字符串,可通过 SDWebImageContextcacheKeyFilter 自定义。

3.5 解码与变换

为避免主线程解码导致卡顿,SDWebImage 在后台队列解码:

1
2
3
// 解码在 SDImageIOCoder / SDImageGIFCoder 等中完成
// 通过 SDImageCoder 协议统一
- (UIImage *)decodedImageWithData:(NSData *)data options:(SDImageCoderOptions *)options;

v5.x 支持 Image Transform:下载后可对图片做缩放、旋转、圆角等处理,结果再缓存,避免重复计算。


四、示例

4.1 基础用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#import <SDWebImage/SDWebImage.h>

// 最简单的用法
[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"https://example.com/photo.jpg"]];

// 带占位图和完成回调
[self.imageView sd_setImageWithURL:url
placeholderImage:[UIImage imageNamed:@"placeholder"]
completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
if (error) {
NSLog(@"加载失败: %@", error);
} else {
NSLog(@"加载成功,来源: %@", cacheType == SDImageCacheTypeMemory ? @"内存" :
cacheType == SDImageCacheTypeDisk ? @"磁盘" : @"网络");
}
}];

4.2 进度与选项

1
2
3
4
5
6
7
[self.imageView sd_setImageWithURL:url
placeholderImage:placeholder
options:SDWebImageRetryFailed | SDWebImageProgressiveLoad
progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
CGFloat progress = expectedSize > 0 ? (CGFloat)receivedSize / expectedSize : 0;
self.progressView.progress = progress;
} completed:nil];

常用 options

  • SDWebImageRetryFailed:失败后重试
  • SDWebImageProgressiveLoad:渐进式加载
  • SDWebImageRefreshCached:忽略缓存强制刷新
  • SDWebImageFromLoaderOnly:只从网络加载,不查缓存

4.3 预加载

1
2
3
4
5
6
7
SDWebImagePrefetcher *prefetcher = [SDWebImagePrefetcher sharedImagePrefetcher];
prefetcher.maxConcurrentPrefetches = 4;
[prefetcher prefetchURLs:imageURLs progress:^(NSUInteger no, NSUInteger total) {
NSLog(@"预加载进度: %lu/%lu", (unsigned long)no, (unsigned long)total);
} completed:^(NSUInteger finishedCount, NSUInteger skippedCount) {
NSLog(@"完成: %lu, 跳过: %lu", (unsigned long)finishedCount, (unsigned long)skippedCount);
}];

4.4 自定义缓存 Key

1
2
3
4
5
6
SDWebImageContext *context = @{
SDWebImageContextCacheKeyFilter: [SDWebImageCacheKeyFilter cacheKeyFilterWithBlock:^NSString * _Nullable(NSURL * _Nullable url) {
return [url.absoluteString stringByAppendingString:@"_suffix"]; // 自定义 key
}]
};
[self.imageView sd_setImageWithURL:url placeholderImage:nil context:context];

4.5 图片变换(圆角、缩放)

1
2
3
4
[self.imageView sd_setImageWithURL:url
placeholderImage:placeholder
options:0
context:@{SDWebImageContextImageTransformer: [SDImageRoundCornerTransformer transformerWithRadius:10 corners:UIRectCornerAllCorners borderWidth:1 borderColor:[UIColor whiteColor]]}];

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

5.1 列表图片加载与复用

场景:TableView/CollectionView 中加载头像或商品图。

做法

  • 使用 sd_setImageWithURL: 即可,SDWebImage 会自动取消不可见 cell 的请求
  • 可选 SDWebImageAvoidAutoSetImage,在 completed 中手动设置,便于加入过渡动画
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MyCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyCell"];
NSURL *avatarURL = self.avatars[indexPath.row];
[cell.avatarView sd_setImageWithURL:avatarURL
placeholderImage:[UIImage imageNamed:@"default_avatar"]
options:SDWebImageAvoidAutoSetImage
completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
if (image) {
[UIView transitionWithView:cell.avatarView duration:0.2 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{
cell.avatarView.image = image;
} completion:nil];
}
}];
return cell;
}

5.2 详情页大图预加载

场景:从列表进入详情时,希望大图尽快展示。

做法:在列表滑动到某条数据时,对详情大图 URL 做预加载:

1
2
3
4
5
6
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *detailImageURL = self.detailImageURLs[indexPath.row];
[[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:@[[NSURL URLWithString:detailImageURL]]];
// 再 push 到详情页
[self.navigationController pushViewController:detailVC animated:YES];
}

5.3 统一添加请求头(如 Token)

场景:图片 URL 需要鉴权 Header。

做法:实现 SDWebImageDownloaderRequestModifier

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface MyRequestModifier : NSObject <SDWebImageDownloaderRequestModifier>
@end

@implementation MyRequestModifier
- (NSURLRequest *)modifiedRequestWithRequest:(NSURLRequest *)request {
NSMutableURLRequest *mutable = [request mutableCopy];
[mutable setValue:[MyAuthManager shared].token forHTTPHeaderField:@"Authorization"];
return [mutable copy];
}
@end

// 配置
SDWebImageDownloader *downloader = [SDWebImageManager sharedManager].imageLoader;
downloader.requestModifier = [[MyRequestModifier alloc] init];

5.4 加载状态指示器

场景:图片加载时显示 UIActivityIndicator。

1
2
self.imageView.sd_imageIndicator = SDWebImageActivityIndicator.grayIndicator;
[self.imageView sd_setImageWithURL:url];

或自定义实现 SDWebImageIndicator 协议,适配项目 UI 规范。

5.5 自定义缓存路径与策略

场景:头像与普通图片使用不同缓存目录和过期策略。

1
2
3
4
5
6
7
// 头像缓存:30 天
SDImageCache *avatarCache = [[SDImageCache alloc] initWithNamespace:@"avatar" diskCacheDirectory:[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"AvatarCache"]]];
avatarCache.config.maxDiskAge = 30 * 24 * 60 * 60;

// 通过 Context 指定使用 avatarCache
SDWebImageContext *context = @{SDWebImageContextCustomCache: avatarCache};
[self.avatarView sd_setImageWithURL:avatarURL placeholderImage:nil context:context];

六、小结

SDWebImage 通过协议化设计清晰的职责划分,将图片加载、缓存、解码、展示等环节解耦,既易用又易扩展。掌握其架构与源码,有助于:

  • 合理选用 optionscontext,优化体验与性能
  • 在需要时自定义 Loader、Cache、Coder,适配业务
  • 理解异步加载、缓存、取消等通用模式,迁移到其他场景

建议结合官方 GitHubWiki 阅读源码,逐层从 Category → Manager → Loader/Cache 跟踪调用链,会有更深理解。

iOS 开发中的 ReactiveCocoa (RAC)

由浅入深,从基本概念到源码解析,带你全面掌握 Swift 响应式编程


一、什么是响应式编程?

1.1 从命令式到声明式

传统的命令式编程中,我们通过直接修改状态和调用方法来驱动程序执行:

1
2
3
4
5
6
7
8
9
// 命令式:监听文本框变化
textField.addTarget(self, action: #selector(textDidChange), for: .editingChanged)

func textDidChange() {
let text = textField.text ?? ""
if text.count >= 3 {
searchUsers(text)
}
}

响应式编程则将数据流视为核心概念,用声明式的方式描述「当数据变化时该做什么」:

1
2
3
4
5
6
7
// 响应式:声明数据流关系
textField.reactive.continuousTextValues
.filter { ($0?.count ?? 0) >= 3 }
.debounce(0.3, on: QueueScheduler.main)
.observeValues { [weak self] text in
self?.searchUsers(text ?? "")
}

1.2 ReactiveCocoa 与 ReactiveSwift

  • ReactiveSwift:纯 Swift 实现的响应式核心库,提供 Signal、SignalProducer 等基础类型
  • ReactiveCocoa:基于 ReactiveSwift,为 Cocoa/Cocoa Touch 提供 UI 绑定、扩展和便捷 API

两者关系可以理解为:ReactiveSwift 是引擎,ReactiveCocoa 是上层封装。

1.3 为什么选择 RAC?

  • 统一抽象:将 delegate、回调、通知、KVO、Target-Action 等统一为「事件流」
  • 可组合:通过 map、filter、combineLatest 等操作符组合数据流
  • 减少状态:用数据流替代分散的中间变量
  • 声明式:代码更贴近「业务意图」,易于阅读和维护

二、核心概念

2.1 事件 (Event)

Event 是事件流中的最小传输单元,类似一次性的直播流中的一帧:

1
2
3
4
5
6
public enum Event<Value, Error: Swift.Error> {
case value(Value) // 携带一个值
case failed(Error) // 失败(携带错误)
case completed // 正常完成
case interrupted // 被中断
}

一个典型的流:若干个 .value,最后以 .completed.failed 结束;.interrupted 表示订阅被取消。

2.2 观察者 (Observer)

Observer 负责向事件流发送事件:

1
2
3
4
5
6
let (signal, observer) = Signal<String, Never>.pipe()

// observer 用于发送事件
observer.send(value: "Hello")
observer.send(value: "World")
observer.sendCompleted()

2.3 可销毁对象 (Disposable)

订阅信号会返回 Disposable,用于取消订阅、释放资源:

1
2
3
4
5
6
let disposable = signalProducer.start { value in
print(value)
}

// 不再需要时取消订阅
disposable.dispose()

2.4 核心类型总览

类型 描述 类比
Signal 热信号,单向事件流,所有者控制发送 直播画面
SignalProducer 冷信号,延迟执行,每次 start 创建新流 点播视频
Property 可观察的「有值」盒子,永不失败 播放进度条
Action 串行执行、可启用/禁用的操作 自动售货机
Lifetime 观察的生命周期,用于自动取消 观看时间段

三、Signal 与 SignalProducer

3.1 Signal:热信号

  • 热信号:无论有没有观察者,事件都会持续发送
  • 所有者完全控制何时发送、发送什么
  • 观察者只能订阅,不能影响流的产生
1
2
3
4
5
6
7
8
9
10
// 创建 Signal 的方式:pipe
let (signal, observer) = Signal<String, Never>.pipe()

signal.observeValues { value in
print("收到: \(value)")
}

observer.send(value: "A") // 输出: 收到: A
observer.send(value: "B") // 输出: 收到: B
observer.sendCompleted()

3.2 SignalProducer:冷信号

  • 冷信号:只有在 start 时才真正执行
  • 每次 start 都会创建新的 Signal,重新跑一遍逻辑
  • 适合网络请求、文件读取等「按需执行」的场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let producer = SignalProducer<String, Never> { observer, _ in
print("开始执行") // 每次 start 都会打印
observer.send(value: "Hello")
observer.send(value: "World")
observer.sendCompleted()
}

let d1 = producer.startWithValues { print("观察者1: \($0)") }
// 输出: 开始执行
// 输出: 观察者1: Hello
// 输出: 观察者1: World

let d2 = producer.startWithValues { print("观察者2: \($0)") }
// 再次输出: 开始执行
// 输出: 观察者2: Hello
// 输出: 观察者2: World

3.3 热信号 vs 冷信号

特性 Signal (热) SignalProducer (冷)
执行时机 由发送者决定 start 时执行
多订阅 共享同一流 每个订阅独立执行
典型场景 按钮点击、通知 网络请求、文件读取

四、Property 与 Action

4.1 Property:可观察的盒子

Property 表示「始终有一个当前值」且「永不失败」的流,适合表示 UI 状态、配置等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// MutableProperty:可读可写
let username = MutableProperty<String>("")

// 读取当前值
print(username.value)

// 监听变化
username.signal.observeValues { newValue in
print("用户名变为: \(newValue)")
}

// 更新值
username.value = "张三"
username.modify { $0 = "李四" }

4.2 Action:串行操作

Action 将「输入 → 输出」封装为可复用、可启用/禁用的操作,且保证串行执行(同一时间只处理一个请求):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let searchAction = Action<String, [User], NSError> { keyword in
return searchAPI(keyword) // 返回 SignalProducer
}

// 绑定启用条件(例如:输入非空)
let enabledSearch = Action(state: viewModel.keyword, enabledIf: { $0.count > 0 }) { _, keyword in
return searchAPI(keyword)
}

// 执行
searchAction.apply("Swift").startWithResult { result in
switch result {
case .success(let users):
self.users = users
case .failure(let error):
self.showError(error)
}
}

五、常用操作符

5.1 转换类

map:转换每个值

1
2
3
signal
.map { $0.uppercased() }
.observeValues { print($0) }

filter:过滤值

1
2
3
signal
.filter { $0.count >= 3 }
.observeValues { print($0) }

reduce:聚合为单个值(在 completed 时发出)

1
2
3
signal
.reduce(0) { $0 + $1 }
.observeValues { print("总和: \($0)") }

5.2 组合类

combineLatest:合并多个流的最新值

1
2
3
4
let combined = Signal.combineLatest(usernameSignal, passwordSignal)
combined.observeValues { (user, pwd) in
loginButton.isEnabled = user.count > 0 && pwd.count >= 6
}

zip:按顺序一一配对

1
2
let zipped = Signal.zip(numbersSignal, lettersSignal)
// (1,A), (2,B), (3,C)...

5.3 扁平化 (flatten)

merge:内层多个流的值按到达顺序全部输出

concat:按顺序消费内层流,前一个完成后才订阅下一个

latest:只保留「最新」的内层流,常用于搜索联想(新关键词来时取消旧请求)

1
2
3
4
5
6
7
searchKeywordSignal
.flatMap(.latest) { keyword in
searchAPI(keyword) // 新关键词会取消上一次请求
}
.observeValues { users in
self.updateUI(users)
}

5.4 错误处理

1
2
3
4
producer
.flatMapError { _ in SignalProducer(value: []) } // 失败时返回空数组
.retry(upTo: 3) // 失败重试 3 次
.mapError { CustomError.wrapped($0) } // 转换错误类型

六、源码与实现原理

6.1 Signal 的核心结构

Signal 通过闭包保存「如何向观察者推送事件」的逻辑,内部维护观察者列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Signal 的核心:Generator 闭包
// 当有观察者订阅时,闭包被调用,传入 Observer
public init(_ generator: (Observer) -> Disposable?)

// pipe 创建方式
public static func pipe() -> (Signal, Signal.Observer) {
var observer: Signal.Observer!
let signal = Signal { innerObserver in
observer = innerObserver
return nil
}
return (signal, observer)
}

observer.send(value:) 被调用时,所有已注册的观察者都会收到该值。

6.2 SignalProducer 的延迟执行

SignalProducer 保存的是「如何创建 Signal」的闭包,而不是 Signal 本身:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 每次 start 时:
// 1. 创建一个新的 Signal(通过 pipe 或类似机制)
// 2. 执行 generator 闭包,将 observer 传入
// 3. 闭包内的逻辑开始执行,向 observer 发送事件
public func start(_ observer: Observer) -> Disposable {
let (signal, pipeObserver) = Signal.pipe()
let disposable = CompositeDisposable()
disposable += signal.observe(observer)
disposable += self.startWithSignal { signal, innerDisposable in
pipeObserver.observe(signal)
disposable += innerDisposable
}
return disposable
}

因此每次 start 都会触发一次完整的「创建 + 执行」过程。

6.3 操作符的链式调用

mapfilter 等操作符本质是创建新的 Signal/SignalProducer,内部订阅上游并转换后传给下游:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// map 的简化逻辑
extension Signal {
public func map<U>(_ transform: @escaping (Value) -> U) -> Signal<U, Error> {
return Signal { observer in
return self.observe { event in
switch event {
case let .value(value):
observer.send(value: transform(value))
case .completed:
observer.sendCompleted()
case let .failed(error):
observer.send(error: error)
case .interrupted:
observer.sendInterrupted()
}
}
}
}
}

七、实战示例

7.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
class LoginViewModel {
let username = MutableProperty("")
let password = MutableProperty("")

var canLogin: Property<Bool> {
return Property(
initial: false,
then: Signal.combineLatest(username.signal, password.signal)
.map { user, pwd in
user.count >= 3 && pwd.count >= 6
}
)
}

let loginAction: Action<(String, String), User, Error>

init() {
loginAction = Action(enabledIf: canLogin) { [weak self] _, input in
return loginAPI(username: input.0, password: input.1)
}
}
}

// ViewController 中绑定
viewModel.canLogin.signal
.observeValues { [weak loginButton] canLogin in
loginButton?.isEnabled = canLogin
}

loginButton.reactive.pressed = CocoaAction(viewModel.loginAction) { _ in
(viewModel.username.value, viewModel.password.value)
}

7.2 搜索联想(防抖 + 取消旧请求)

1
2
3
4
5
6
7
8
9
10
11
12
13
searchTextField.reactive.continuousTextValues
.filter { ($0 ?? "").count >= 2 }
.debounce(0.3, on: QueueScheduler.main)
.flatMap(.latest) { [weak self] keyword -> SignalProducer<[User], Error> in
guard let keyword = keyword, !keyword.isEmpty else {
return .init(value: [])
}
return self?.searchAPI(keyword) ?? .empty
}
.observe(on: UIScheduler())
.startWithValues { [weak self] users in
self?.updateSearchResults(users)
}

7.3 多数据源合并展示

1
2
3
4
5
6
7
8
9
10
11
12
13
let localUsers = loadLocalUsers()   // SignalProducer<[User], Never>
let remoteUsers = fetchRemoteUsers() // SignalProducer<[User], Error>

SignalProducer.combineLatest(
localUsers.flatMapError { _ in .init(value: []) },
remoteUsers.flatMapError { _ in .init(value: []) }
)
.map { local, remote in
mergeAndDeduplicate(local: local, remote: remote)
}
.startWithValues { [weak self] users in
self?.tableView.reload(with: users)
}

八、ReactiveCocoa 的 Cocoa 扩展

ReactiveCocoa 为 UIKit 提供了 reactive 命名空间下的扩展:

1
2
3
4
5
6
7
8
9
10
11
12
13
// UIControl 事件
button.reactive.controlEvents(.touchUpInside)
.observeValues { _ in print("点击") }

// UITextField 文本
textField.reactive.continuousTextValues
.observeValues { print($0 ?? "") }

// 双向绑定(需要导入 ReactiveCocoa)
label.reactive.text <~ viewModel.title // 单向:ViewModel -> Label

// CocoaAction 绑定按钮
button.reactive.pressed = CocoaAction(viewModel.submitAction)

九、最佳实践与注意事项

  1. 避免循环引用:在闭包中使用 [weak self],在适当时机 dispose 订阅
  2. 主线程更新 UI:使用 .observe(on: UIScheduler()) 确保 UI 更新在主线程
  3. 合理选择热/冷信号:异步任务、网络请求用 SignalProducer;UI 事件、通知用 Signal
  4. 错误类型:尽量使用 Never 表示不会失败的流,便于组合
  5. 利用 Lifetime:在 ViewController/View 销毁时通过 Lifetime 自动取消订阅

十、总结

概念 要点
Event 事件流的传输单元:value / failed / completed / interrupted
Signal 热信号,由所有者控制,多订阅共享
SignalProducer 冷信号,延迟执行,每次 start 独立运行
Property 有当前值、不失败的流,适合状态
Action 串行、可启用/禁用的操作封装
操作符 map、filter、combineLatest、flatMap 等组合与转换流
实践 表单校验、搜索联想、多源合并、UI 绑定

掌握这些概念后,你就能用响应式思维重构业务逻辑,写出更清晰、更易维护的 Swift/iOS 代码。


参考资源