EIP-1559:为什么需要它,以及 Gas 怎么算

EIP-1559 是以太坊手续费模型的一次关键改造。它没有把 Gas 费“变便宜”,但把原来难以预估的纯竞价模式,换成了更容易理解的“基础费 + 小费”。

这篇就讲三件事:

  1. 为什么会有 EIP-1559
  2. 它具体改了什么
  3. Gas 费用到底怎么计算(含具体数字例子)

一、为什么会有 EIP-1559

在 EIP-1559 之前,以太坊主要用 First-Price Auction(第一价格拍卖)

  • 你给出一个 gasPrice
  • 矿工优先打包 gasPrice 更高的交易
  • 你实际支付的单价就是你自己出的那个价

这个模型在高峰期会很难受:

  • 费用很难预估:用户往往只能“多给一点”,否则交易容易卡住。
  • 过度竞价:很多人同时抬价,结果整体支付高于真实所需。
  • 体验不稳定:同样一笔转账,不同时间价格差异非常大。
  • 钱包策略复杂:钱包要反复估算“现在到底该出多少”。

所以 EIP-1559 不是来承诺“长期低价”的,它主要解决的是定价不透明和体验抖动


二、EIP-1559 具体内容

EIP-1559 引入了几个关键概念。

1) Base Fee(基础费)

  • 每个区块都有一个基础费 baseFeePerGas
  • 这个值由协议自动调整,不是矿工随意定价
  • 基础费会被销毁(burn),不会给打包者

这点很关键:基础费不是“给矿工/验证者”的收入,而是直接销毁。

2) Priority Fee(小费)

  • 也叫 tip,参数通常是 maxPriorityFeePerGas
  • 这部分给打包者(PoW 时期矿工 / PoS 下验证者)
  • 本质上是你给打包者的激励

3) Fee Cap(最高可接受总单价)

  • 参数名是 maxFeePerGas
  • 表示你愿意支付的“每单位 gas 总上限”

交易最终每单位 gas 实际支付单价遵循:

effectiveGasPrice = min(maxFeePerGas, baseFeePerGas + maxPriorityFeePerGas)

并且要满足交易可被执行的基本条件:

maxFeePerGas >= baseFeePerGas

4) 弹性区块(Elastic Block Size)

  • 每个区块有一个“目标 gas 使用量”(target gas)
  • 实际区块可在目标附近弹性波动(上限通常是目标的 2 倍)
  • 如果区块持续高于目标,下一块的 baseFee 上升
  • 如果持续低于目标,下一块的 baseFee 下降

这样做的效果是:短期拥堵不会把手续费一下子拉爆,而是分摊到后续几个区块里。


三、Gas 计算:先看总公式

先看一条最常用的公式:

交易总花费 = gasUsed * effectiveGasPrice

其中(单位常用 gwei):

  • effectiveGasPrice = min(maxFeePerGas, baseFeePerGas + maxPriorityFeePerGas)
  • 用户支付会拆成两部分:
    • 销毁gasUsed * baseFeePerGas
    • 小费给验证者gasUsed * priorityFeePaid

这里:

priorityFeePaid = effectiveGasPrice - baseFeePerGas

你可以把它理解成“愿意付到这里为止”,不是“按这个值强制扣费”。


四、具体算例(带数字)

下面都用 gwei 做单价单位,1 ETH = 1,000,000,000 gwei

例子 1:普通转账

假设:

  • gasUsed = 21,000
  • baseFee = 30 gwei
  • maxPriorityFee = 2 gwei
  • maxFee = 100 gwei

步骤:

  1. 先算 baseFee + priority = 32 gwei
  2. effectiveGasPrice = min(100, 32) = 32 gwei
  3. 总费用:
    21,000 * 32 = 672,000 gwei = 0.000672 ETH

拆分:

  • 销毁:21,000 * 30 = 630,000 gwei = 0.00063 ETH
  • 小费:21,000 * 2 = 42,000 gwei = 0.000042 ETH

例子 2:你把 maxFee 设得很高,但不会按高价全扣

假设:

  • gasUsed = 100,000
  • baseFee = 40 gwei
  • maxPriorityFee = 3 gwei
  • maxFee = 200 gwei

计算:

  • baseFee + priority = 43 gwei
  • effectiveGasPrice = min(200, 43) = 43 gwei
  • 总费用:100,000 * 43 = 4,300,000 gwei = 0.0043 ETH

这个例子想说明一件事:maxFee 设高是为了防止交易卡住,不等于你会按高价成交。

例子 3:maxFee 过低导致交易不可打包

假设:

  • baseFee = 55 gwei
  • 你设置 maxFee = 50 gwei

因为 maxFee < baseFee,交易直接不满足基本条件,节点通常会拒绝或一直 pending。

例子 4:合约调用(更大 gasUsed)

假设一次 DEX 交互:

  • gasUsed = 180,000
  • baseFee = 25 gwei
  • maxPriorityFee = 1.5 gwei
  • maxFee = 35 gwei

先算:

  • baseFee + priority = 26.5 gwei
  • effectiveGasPrice = min(35, 26.5) = 26.5 gwei

总费用:

  • 180,000 * 26.5 = 4,770,000 gwei = 0.00477 ETH

拆分:

  • 销毁:180,000 * 25 = 4,500,000 gwei = 0.0045 ETH
  • 小费:180,000 * 1.5 = 270,000 gwei = 0.00027 ETH

五、设置建议(钱包/脚本)

如果你自己写交易脚本(ethers.js/web3.js),可以按这个思路:

  • maxPriorityFeePerGas:给一个适中小费(例如 1-3 gwei,拥堵时上调)
  • maxFeePerGas:至少覆盖“当前 base fee + 小费”,并留一定缓冲

常见经验做法是:

maxFeePerGas ≈ 2 * 当前baseFee + priority

这样做的好处是,后面 1-2 个区块就算变贵,交易也不至于马上失效。


六、常见误区

误区 1:EIP-1559 之后手续费就会越来越低

不一定。它主要改善的是定价机制和可预测性,不是保证低价。

误区 2:maxFeePerGas 就是实际支付单价

不是。它只是上限,实际支付看当时的 baseFee + priority

误区 3:全部手续费都给验证者

不是。基础费被销毁,验证者拿的是小费部分。


七、小结

EIP-1559 可以记成三句话:

  • 把“纯拍卖”变成“协议定基础价 + 用户给小费”
  • 让费用估算更稳定,减少盲目竞价
  • 通过 burn 机制把基础费从流通里移除

只要把 baseFee / maxPriorityFee / maxFee 这三个参数分清楚,Gas 计算基本就通了。更重要的是根据拥堵情况动态调整,而不是记一套固定数字。

主流公链与测试链对比

主流公链对比(生产环境)

类型 EVM 兼容 Gas 成本 速度/吞吐(体感) 生态成熟度 典型场景
Ethereum Mainnet L1 非常高 高价值 DeFi、核心资产结算
BNB Smart Chain (BSC) L1 交易频繁、成本敏感应用
Polygon PoS 侧链/L2风格网络 游戏、社交、低费应用
Arbitrum One Ethereum L2 (Optimistic) 低-中 很高 DeFi、交易类 dApp
Optimism Ethereum L2 (Optimistic) 低-中 公共品生态、通用 dApp
Base Ethereum L2 (OP Stack) 高(增长快) 消费级应用、社交应用
Avalanche C-Chain L1 子网架构 低-中 中-高 DeFi、企业/子网场景
zkSync Era Ethereum L2 (ZK) 是(有差异) 中-高 低费支付、ZK 叙事应用
Linea Ethereum L2 (ZK) EVM 迁移、低费交互
Solana L1 否(非 EVM) 很低 很快 高频交易、消费级应用

主流测试链对比(开发/联调)

主网 推荐测试链 是否常用水龙头 与主网一致性(开发体感) 备注
Ethereum Sepolia 当前最常用以太坊测试网
BSC BSC Testnet 中-高 需注意节点稳定性差异
Polygon PoS Polygon Amoy 中-高 Mumbai 已逐步被替代
Arbitrum One Arbitrum Sepolia L2 手续费模型更接近主网
Optimism OP Sepolia OP Stack 生态通用性好
Base Base Sepolia Base 开发首选测试网
Avalanche C-Chain Fuji 中-高 AVAX 测试币相对容易获取
zkSync Era zkSync Sepolia 注意部分工具链兼容细节
Linea Linea Sepolia 新生态,文档更新较快
Solana Devnet / Testnet Solana 习惯用 Devnet 做开发

选链建议(简版)

  • 想要最稳和最大生态:Ethereum + Arbitrum/Optimism/Base
  • 想要低费和高频交互:BSC / Polygon / Solana
  • 想做 EVM 且方便迁移:优先 Arbitrum / Base / OP
  • 想提前布局 ZK:zkSync / Linea

React / Flutter 状态管理

由浅入深,从基本概念到原理与源码,再到示例与实际项目应用案例,系统梳理两大主流框架中的状态管理方案

一、状态管理基础概念

1.1 什么是状态(State)?

状态是驱动 UI 变化的数据。当状态改变时,界面随之更新,形成「数据驱动视图」的声明式模式。

1
2
3
4
状态 ──► 视图
▲ │
│ ▼
└── 用户交互 / 网络请求 / 定时器等

1.2 状态的分类

类型 作用域 典型场景 生命周期
本地状态 单组件 输入框内容、展开/折叠、选中项 跟随组件
共享状态 多组件 用户信息、主题、购物车 需要提升或全局管理
服务端状态 与后端同步 API 数据、缓存 异步、需缓存策略

1.3 为什么需要状态管理?

随着应用复杂度上升,会出现:

  • 状态提升导致 props 层层传递(prop drilling)
  • 状态分散导致难以追踪和调试
  • 重复请求缓存失效等数据一致性问题

状态管理方案的目标:集中、可预测、易维护


二、React 状态管理

2.1 内置方案概览

方案 适用场景 特点
useState 本地状态 简单、轻量
useReducer 复杂本地状态 可预测、易测试
Context API 跨层级共享 官方内置、易造成不必要的重渲染

2.2 useState:最简单的本地状态

1
2
3
4
5
6
7
8
9
10
11
12
function Counter() {
const [count, setCount] = useState(0);

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
{/* 函数式更新,避免闭包陷阱 */}
<button onClick={() => setCount(prev => prev + 1)}>+1 (安全)</button>
</div>
);
}

惰性初始化:初始值可以是函数,仅在首次渲染执行。

1
const [state, setState] = useState(() => expensiveComputation());

2.3 useReducer:复杂状态的 reducer 模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
}

function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });

return (
<div>
<span>{state.count}</span>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
}

2.4 Context API:跨层级共享状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const ThemeContext = createContext('light');

function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Page />
</ThemeContext.Provider>
);
}

function Page() {
const { theme } = useContext(ThemeContext);
return <div className={theme}>...</div>;
}

注意:Provider 的 value 变化会导致所有 useContext 的消费者重渲染,需配合 useMemo 或拆分 Context 优化。

2.5 Redux / Redux Toolkit:全局状态管理

Redux 采用单向数据流View → Action → Reducer → Store → View

1
2
3
4
5
┌─────────┐   dispatch    ┌─────────┐   reduce    ┌────────┐
│ View │ ───────────► │ Action │ ─────────► │ Store │
└─────────┘ └─────────┘ └────────┘
▲ │
└──────────────── subscribe ─────────────────────┘

Redux Toolkit 示例(官方推荐写法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// store/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: state => { state.value += 1; },
decrement: state => { state.value -= 1; },
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
1
2
3
4
5
6
7
8
9
10
// 组件中使用
import { useDispatch, useSelector } from 'react-redux';
import { increment } from './store/counterSlice';

function Counter() {
const count = useSelector(state => state.counter.value);
const dispatch = useDispatch();

return <button onClick={() => dispatch(increment())}>{count}</button>;
}

2.6 Zustand:轻量级全局状态

Zustand 基于 Hooks,API 简洁,无 Provider 包裹。

1
2
3
4
5
6
7
8
9
10
11
12
import { create } from 'zustand';

const useStore = create((set) => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
decrement: () => set(state => ({ count: state.count - 1 })),
}));

function Counter() {
const { count, increment } = useStore();
return <button onClick={increment}>{count}</button>;
}

选择器优化:只订阅需要的字段,避免无关更新。

1
const count = useStore(state => state.count); // 仅 count 变化时重渲染

2.7 React 状态管理原理浅析

useState 的链表结构

React 内部用链表存储 Hooks。每个 Hook 对应链表中的一个节点,通过 FibermemoizedState 串联。

1
2
3
Fiber.memoizedState → Hook1 → Hook2 → Hook3 → ...

[state, setState]

这就是为什么 Hooks 必须在顶层调用、不能放在条件/循环中:链表顺序必须稳定。

setState 的批处理(Batching)

React 18 默认对所有更新进行自动批处理,多次 setState 会合并为一次渲染。

1
2
3
4
5
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// 仅触发一次重渲染
}

三、Flutter 状态管理

3.1 方案概览

方案 官方/社区 适用场景 特点
setState 内置 本地状态 简单,整组件重建
InheritedWidget 内置 跨层级共享 底层基础,一般不直接使用
Provider 官方推荐 中小型应用 基于 InheritedWidget,易上手
Riverpod 社区主流 中大型应用 编译期安全、可测试、无 context
Bloc 社区 复杂业务逻辑 事件驱动、可预测、适合团队
GetX 社区 快速开发 全能型,状态+路由+依赖注入

3.2 setState:最简单的本地状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Counter extends StatefulWidget {
@override
_CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
int _count = 0;

void _increment() {
setState(() {
_count++;
});
}

@override
Widget build(BuildContext context) {
return Column(
children: [
Text('$_count'),
ElevatedButton(onPressed: _increment, child: Text('+1')),
],
);
}
}

原理setState 会标记当前 Element 为脏,在下一帧触发 build 重建子树。

3.3 Provider:官方推荐方案

Provider 基于 InheritedWidget,通过 context.watch<T>() 监听变化并重建。

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
// 1. 定义 Model(继承 ChangeNotifier)
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;

void increment() {
_count++;
notifyListeners(); // 通知监听者
}
}

// 2. 在根节点提供
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: MyApp(),
),
);
}

// 3. 在子组件使用
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counter = context.watch<CounterModel>();
return Text('${counter.count}');
}
}

多种 Provider 类型

类型 用途
Provider 不可变值
ChangeNotifierProvider 可变、需 notifyListeners
FutureProvider 异步数据
StreamProvider 流数据
MultiProvider 组合多个 Provider

3.4 Riverpod:下一代状态管理

Riverpod 无 BuildContext 依赖,支持编译期类型安全、易于测试和复用。

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
// 1. 定义 Provider
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});

class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
}

// 2. 在 runApp 外包一层 ProviderScope
void main() {
runApp(ProviderScope(child: MyApp()));
}

// 3. 在组件中使用(无需 context)
class CounterPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: Text('$count'),
);
}
}

ref 的三大方法

方法 作用
ref.watch() 监听变化,值变化时重建
ref.read() 一次性读取,不监听
ref.listen() 监听变化并执行副作用,不重建

3.5 Bloc:事件驱动架构

Bloc 将 UI 与业务逻辑解耦,通过 Event → Bloc → State 的流程管理状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 1. 定义 Event 和 State
abstract class CounterEvent {}
class Increment extends CounterEvent {}
class Decrement extends CounterEvent {}

class CounterState {
final int count;
CounterState(this.count);
}

// 2. 实现 Bloc
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterState(0)) {
on<Increment>((event, emit) => emit(CounterState(state.count + 1)));
on<Decrement>((event, emit) => emit(CounterState(state.count - 1)));
}
}

// 3. 在 UI 中使用
BlocProvider(
create: (_) => CounterBloc(),
child: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Text('${state.count}');
},
),
)

3.6 Flutter 状态管理原理浅析

setState 与 Element 树

1
2
3
4
5
6
7
8
9
10
setState() 被调用


标记 Element 为 dirty


下一帧 SchedulerBinding 触发 build


Element.rebuild() → State.build()

InheritedWidget 与依赖收集

InheritedWidget 通过 context.dependOnInheritedWidgetOfExactType<T>() 建立「依赖关系」。当 InheritedWidget 更新时,依赖它的 Element 会被标记为脏并重建。

Provider 的 notifyListeners() 会触发 InheritedWidget 的更新,从而通知所有 context.watch 的消费者。


四、React vs Flutter 状态管理对比

4.1 概念映射

概念 React Flutter
本地状态 useState setState
复杂本地状态 useReducer 自建 StatefulWidget + 内部逻辑
跨层级共享 Context InheritedWidget / Provider
全局 Store Redux / Zustand Provider / Riverpod / Bloc
选择器/按需订阅 useSelector / useStore(selector) context.select / ref.watch(provider.select())

4.2 设计哲学差异

维度 React Flutter
更新粒度 组件级,虚拟 DOM diff Widget 树重建,Element 复用
数据流 单向(Redux)或自由(Zustand) 多为单向,Bloc 强调事件流
依赖注入 通过 props / Context 通过 context / ref(Riverpod)
服务端状态 React Query / SWR 等 Riverpod 的 FutureProvider、flutter_bloc 等

五、源码层面的理解

5.1 React useState 的调度

React 的 setState 会调用 dispatchSetState,将更新放入 updateQueue,由调度器(Scheduler)在合适的时机批量处理,触发 rendercommit

1
2
3
4
5
6
// 简化流程
setState(newState)
enqueueUpdate(fiber, update)
scheduleUpdateOnFiber(fiber)
→ performConcurrentWorkOnRoot / performSyncWorkOnRoot
→ commitRoot

5.2 Flutter ChangeNotifier 与 Listenable

ChangeNotifier 继承 Listenable,内部维护 _listeners 列表。notifyListeners() 遍历并调用所有监听者。

1
2
3
4
5
6
// 简化逻辑
void notifyListeners() {
for (final listener in _listeners) {
listener(); // 触发 Consumer 等重建
}
}

ProviderInheritedProvideraddListenerChangeNotifier,当 notifyListeners 被调用时,触发自身 updateShouldNotify 并重建子树。


六、实际项目应用案例

6.1 案例一:电商购物车(React + Zustand)

需求:跨页面购物车数量、增删改、持久化。

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
// store/cartStore.js
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

export const useCartStore = create(
persist(
(set) => ({
items: [],
addItem: (product, qty = 1) =>
set((state) => ({
items: state.items.some((i) => i.id === product.id)
? state.items.map((i) =>
i.id === product.id ? { ...i, qty: i.qty + qty } : i
)
: [...state.items, { ...product, qty }],
})),
removeItem: (id) =>
set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
totalCount: (state) => state.items.reduce((sum, i) => sum + i.qty, 0),
}),
{ name: 'cart-storage' }
)
);

// Header 中只订阅 totalCount,避免整 store 变化导致重渲染
const totalCount = useCartStore((s) =>
s.items.reduce((sum, i) => sum + i.qty, 0)
);

6.2 案例二:用户认证流(Flutter + Riverpod)

需求:登录态、token 刷新、路由守卫。

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
// providers/auth_provider.dart
final authStateProvider = StateNotifierProvider<AuthNotifier, AsyncValue<User?>>((ref) {
return AuthNotifier(ref);
});

class AuthNotifier extends StateNotifier<AsyncValue<User?>> {
AuthNotifier(this.ref) : super(const AsyncValue.loading()) {
_init();
}
final Ref ref;

Future<void> _init() async {
final token = await storage.getToken();
if (token == null) {
state = const AsyncValue.data(null);
return;
}
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => api.getCurrentUser());
}

Future<void> login(String email, String pwd) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => api.login(email, pwd));
}

void logout() {
storage.clearToken();
state = const AsyncValue.data(null);
}
}

// 路由守卫:根据 authState 跳转登录页或首页
ref.listen(authStateProvider, (prev, next) {
next.whenData((user) {
if (user == null) navigator.pushReplacement(LoginRoute());
});
});

6.3 案例三:列表筛选与分页(React + Redux Toolkit + RTK Query)

需求:筛选条件、分页、缓存、乐观更新。

1
2
3
4
5
6
7
8
// 使用 RTK Query 管理服务端状态
const { data, isLoading, refetch } = useGetProductsQuery({
page: currentPage,
category: selectedCategory,
});

// 本地筛选状态用 Redux 或 useState 均可
const [filters, setFilters] = useState({ category: '', sort: 'default' });

6.4 案例四:主题与多语言(Flutter + Provider)

需求:亮/暗主题、中英文切换,全局生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用 MultiProvider 组合
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => ThemeModel()),
ChangeNotifierProvider(create: (_) => LocaleModel()),
],
child: MyApp(),
),
);

// 任意子组件
final theme = context.watch<ThemeModel>();
final locale = context.watch<LocaleModel>();

七、选型建议

场景 React 推荐 Flutter 推荐
小项目/原型 useState + Context setState + Provider
中大型项目 Redux Toolkit / Zustand Riverpod / Bloc
强类型、可测试 Redux + TypeScript / Zustand Riverpod
复杂业务流、事件驱动 Redux / XState Bloc
服务端状态 React Query / SWR Riverpod FutureProvider / dio + 自封装

八、总结

  • React:从 useState 起步,全局状态优先考虑 Redux ToolkitZustand,服务端状态用 React Query 等。
  • Flutter:从 setState 起步,共享状态用 Provider 入门,进阶用 RiverpodBloc
  • 选型时关注:团队熟悉度项目规模可测试性与框架生态的契合度

由浅入深掌握上述方案后,可以根据具体业务灵活组合,构建可维护、可扩展的状态管理体系。

实用小软件

  • Snipaste:截图

    • f1截图,f1+fn
    • f3定在屏幕上,f3+fn
    • 取色:shift切换
  • ZoomIt:画笔

    • ctrl+1放大
    • Ctrl+2画图
    • 画图时按住Ctrl是矩形
    • 画图时按住tab是椭圆
    • 画图时按字母切换画笔颜色。r红色、b蓝色、g绿色、p粉色、o橙色
  • XMind:思维导图

    • enter同级
    • tab下级
  • Typora:Markdown

    • 主题可下载

React 18:渲染入口主链路

React 18:渲染入口主链路(简化)

sequenceDiagram
  participant User as 用户(调用 API)
  participant DOM as ReactDOM
  participant Root as FiberRoot / HostRoot Fiber
  participant Queue as updateQueue / lanes
  participant Sched as Scheduler 调度入口

  User->>DOM: createRoot(container)
  DOM->>Root: createContainer(ConcurrentRoot)
  Root->>Root: createFiberRoot + initializeUpdateQueue
  DOM->>DOM: markContainerAsRoot + listenToAllSupportedEvents
  DOM-->>User: 返回 root(ReactDOMRoot)

  User->>DOM: root.render(element)
  DOM->>Root: updateContainer(element, root)
  DOM->>Queue: requestEventTime() + requestUpdateLane()
  DOM->>Queue: createUpdate(eventTime, lane) + update.payload={element}
  DOM->>Queue: enqueueUpdate(HostRootFiber, update, lane)
  DOM->>Sched: scheduleUpdateOnFiber(root, hostFiber, lane, eventTime)
  DOM->>Sched: ensureRootIsScheduled(root, eventTime)
  Sched-->>DOM: 进入并发/同步 render(Day1 Block2)

Life of frame

浏览器中一帧内的事件顺序示意(约 16.6ms,约 60fps)。

总览

一帧时间预算约 16.6ms。从左到右大致为:输入事件 → JavaScript → Begin Frame → requestAnimationFrame → Layout → Paint → 空闲

主流程图(一帧内,左 → 右)

下列阶段在同一段帧预算内顺序推进(与教学用示意图一致;不同浏览器实现细节会略有差异)。

flowchart LR
    A["① 输入事件\n阻塞 touch·wheel\n非阻塞 click·keypress"] --> B["② JavaScript\n定时器 + 任务"]
    B --> C["③ Begin Frame\nresize · scroll · media query"]
    C --> D["④ requestAnimationFrame"]
    D --> E["⑤ Layout"]
    E --> F["⑥ Paint"]
    F --> G["⑦ Idle\nidle 回调"]

Layout / Paint 子步骤(接续 ④ rAF 之后)

flowchart LR
    L1["Recalculate style"] --> L2["Update layout"]
    L2 --> P1["Compositing update"]
    P1 --> P2["Paint invalidation"]
    P2 --> P3["Record"]
    P3 --> IDLE["⑦ Idle · idle 回调"]

说明:若在支持 Mermaid 的编辑器中预览(如 VS Code + 插件、GitHub),上图会渲染为流程图;纯文本阅读时也可对照下文分节标题。


1. 输入事件

  • 阻塞型输入:如 touchwheel
  • 非阻塞型输入:如 clickkeypress

2. JavaScript 执行

  • 定时器setTimeoutsetInterval 等回调。
  • 通用 JavaScript:任务队列中的同步/异步脚本执行。

3. Begin Frame(开始帧)

每帧可能触发的典型事件:

  1. windowresize
  2. scroll
  3. media query 变化

4. requestAnimationFrame

  • 执行通过 requestAnimationFrame 注册的 Frame callbacks
  • 适合与下一屏绘制对齐的更新与测量。

5. Layout(布局)

  1. Recalculate style:计算样式,确定规则如何应用到元素。
  2. Update layout:更新布局,计算几何信息(位置与尺寸)。

6. Paint(绘制)

  1. Compositing update:合成层更新。
  2. Paint invalidation:标记需要重绘的区域。
  3. Record:记录绘制指令。

7. Idle(空闲)

若前述步骤在 16.6ms 内完成,剩余时间为空闲期,可执行 idle 回调(例如 requestIdleCallback),如 idle callback1idle callback2


小结

输入 →(定时器 + JS) Begin Frame 事件(resize / scroll / media)→ rAF → 样式与布局 → 合成 / 失效 / 记录绘制 → 空闲期执行 idle 回调。

React Hooks 与 Fiber:原理与应用

一、React Hooks 的底层实现

1.1 核心问题

函数组件每次渲染都会重新执行,本身不保留状态。Hooks 如何「记住」上一次的值?

1.2 本质:链表 + 调用顺序

React 为每个组件实例维护一个 Fiber 节点,Fiber 上有 memoizedState,是一条链表

1
2
3
4
useState(0)   → Node1 { value: 0 }
useState('') → Node2 { value: '' }
useEffect(fn) → Node3 { effect: fn }
useMemo(calc) → Node4 { value: result }

Hooks 按调用顺序依次读取/更新链表上的节点,不做「按名字查找」。

1.2.1 示例:状态如何挂在 Fiber 链表上

组件与 Fiber 一一对应:每个组件实例有一个 Fiber 节点,Fiber 的 memoizedState 指向 Hooks 链表。

Counter 组件为例:

1
2
3
4
5
6
function Counter() {
const [count, setCount] = useState(0); // Hook 1
const [name, setName] = useState(''); // Hook 2
const [flag, setFlag] = useState(false); // Hook 3
return <div>{count}</div>;
}

首次渲染时,React 在 Fiber 上建立如下链表结构:

1
2
3
4
5
6
7
8
9
Fiber (Counter 组件)
└── memoizedState (链表头)

▼ Node1: { memoizedState: 0 } ← useState(0)
│ next ──────────────────────────────────┐
▼ Node2: { memoizedState: '' } ← useState('')
│ next ──────────────────────────────────┐
▼ Node3: { memoizedState: false } ← useState(false)
next: null

简化版实现(核心逻辑):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const fiber = { type: Counter, memoizedState: null };
let currentHook = null;

function useState(initialValue) {
if (!currentHook) {
const hook = { memoizedState: initialValue, next: null };
// 挂到 fiber.memoizedState 链表...
currentHook = hook;
}
const hook = currentHook;
currentHook = hook.next;

const setState = (newValue) => {
hook.memoizedState = newValue; // 直接改链表节点
scheduleReRender();
};
return [hook.memoizedState, setState];
}

// 每次渲染前重置为链表头
function renderComponent(Component) {
currentHook = fiber.memoizedState;
return Component();
}

两次渲染的流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
首次渲染:
useState(0) → 创建 Node1 { memoizedState: 0 } 返回 [0, setCount]
useState('') → 创建 Node2 { memoizedState: '' } 返回 ['', setName]
useState(false)→ 创建 Node3 { memoizedState: false } 返回 [false, setFlag]

用户点击,setCount(1):
Node1.memoizedState = 1
scheduleReRender()

第二次渲染:
currentHook = fiber.memoizedState (Node1)
useState(0) → 读 Node1,返回 [1, setCount] ✓
useState('') → 读 Node2,返回 ['', setName] ✓
useState(false)→ 读 Node3,返回 [false, setFlag] ✓
调用顺序 Hook 对应链表节点 存的值
1 useState(0) Node1 0 → 1
2 useState(‘’) Node2 ‘’
3 useState(false) Node3 false

结论:状态不在组件函数里,而在该组件对应 Fiber 的 memoizedState 链表上,每个 Hook 按顺序对应链表中的一个节点。

1.3 为什么顺序不能变?

  • 第 1 次调用 Hook → 用链表第 1 个节点
  • 第 2 次调用 Hook → 用链表第 2 个节点
  • 顺序变了,就错位了

因此 Rules of Hooks 要求:顶层调用、不在循环/条件中。

1.4 各 Hook 的简化实现

useState:

1
2
3
4
5
6
7
8
9
10
11
12
13
let hooks = [];
let currentHookIndex = 0;

function useState(initialValue) {
const index = currentHookIndex;
const state = hooks[index] !== undefined ? hooks[index] : initialValue;
const setState = (newValue) => {
hooks[index] = newValue;
scheduleReRender();
};
currentHookIndex++;
return [state, setState];
}

useEffect: 将 callback 和 deps 存入链表,在 commit 阶段执行;依赖变化时重新执行。

useCallback / useMemo: 缓存上一次的值/函数,依赖数组变化才重新计算/创建。

1.4.1 useEffect 的三种情况

写法 何时执行 常见用途
useEffect(fn) 每次渲染后 很少用
useEffect(fn, []) 仅首次挂载后 订阅、初始化、只跑一次
useEffect(fn, [a, b]) a 或 b 变化时 依赖变化时执行副作用

实现思路:React 在链表节点上存「上次的 callback」和「上次的 deps」,每次渲染时比较 deps(用 Object.is):

  • 无 deps → 每次都执行
  • 空数组 [] → 首次执行(prevDeps 为空)
  • 有 deps → 逐项比较,任一变则执行
1
2
3
4
5
// 简化实现
let shouldRun = false;
if (!deps) shouldRun = true; // 无 deps:每次都执行
else if (!prevDeps) shouldRun = true; // 首次挂载
else shouldRun = deps.some((d, i) => !Object.is(d, prevDeps[i]));

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
// 1. 无 deps:每次渲染后执行
useEffect(() => { console.log('每次渲染都执行'); });

// 2. 空 deps:仅首次
useEffect(() => {
console.log('只执行一次');
return () => console.log('卸载时执行');
}, []);

// 3. 有 deps:依赖变化时执行
useEffect(() => {
console.log('count 或 name 变化时执行');
}, [count, name]);
情况 依赖比较 是否执行
无 deps 不比较 每次
[] 首次 prevDeps 为空 只执行一次
[a,b] 用 Object.is 逐项比较 a 或 b 变化时才执行

1.5 小结

概念 本质
状态存储 存在 Fiber 上的链表,不在函数内部
Hook 识别 调用顺序对应到链表节点
Rules of Hooks 保证顺序和数量恒定,才能一一对应

1.6 Hooks 的实现本质

Hooks 的实现本质可归纳为三点:

  1. 外部存储:状态不存于组件函数内部,而是存在 Fiber 的 memoizedState 链表上。组件每次渲染都会重新执行,但链表在组件实例的生命周期内持续存在。

  2. 顺序索引:每次调用 Hook 时,React 通过一个「当前索引」决定读写哪个链表节点。索引随调用递增,因此必须保证每次渲染的调用顺序、数量完全一致。

  3. 调度更新setState 等更新函数会修改链表上的值,并触发 React 的调度器安排一次重渲染。下次渲染时,组件函数重新执行,Hooks 按相同顺序从链表中读出最新值。

三者结合:外部存储解决「函数无状态」问题,顺序索引解决「多个 Hook 如何区分」问题,调度更新解决「变化如何驱动重渲染」问题。

1.7 闭包机制与 Hooks 的关系

Hooks 与闭包紧密相关:

1. setState 依赖闭包捕获索引

1
2
3
4
const setState = (newValue) => {
hooks[index] = newValue; // index 来自闭包
scheduleReRender();
};

setState 在首次渲染时创建,通过闭包捕获了当时的 index。即使用户在 3 秒后点击按钮调用 setState,它仍然能正确写入对应节点,因为闭包保留了 index

2. stale closure(陈旧闭包)问题

事件回调、useEffect 中的函数会闭包捕获当次渲染时的 state。若在回调中直接使用 state,可能拿到旧值:

1
2
3
4
5
6
7
8
9
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // count 被闭包捕获,一直是 0
}, 1000);
return () => clearInterval(id);
}, []);
}

3. 正确做法:函数式更新

1
setCount(prev => prev + 1);  // 传入函数,React 注入最新 state

React 会传入最新的 state,避免闭包捕获 stale 值。

4. useCallback / useMemo 与闭包

useCallback(fn, deps) 缓存的是函数本身;若 deps 不变,返回的是同一个函数引用,其闭包捕获的也是旧依赖。useMemo 同理。因此 deps 必须完整列出闭包中用到的所有变量。

1.8 详细分析:一次更新的完整流程

useState 为例,从点击按钮到界面更新:

1
2
3
4
5
6
7
1. 用户点击 → 触发 onClick → 调用 setState(newValue)
2. setState 通过闭包中的 index 找到链表节点,写入新值
3. 调用 scheduleReRender(),React 将本次更新加入调度队列
4. 调度器在合适时机执行 render 阶段
5. 组件函数重新执行,useState(initialValue) 再次被调用
6. currentHookIndex 从 0 开始,按顺序遍历:hooks[index] 已有新值,返回 [newValue, setState]
7. 组件用新 state 生成新 JSX,进入 commit 阶段,DOM 更新

闭包保证了:异步回调中的 setState,在任意时刻被调用时,仍能通过 index 找到正确的链表节点。 链表保证了:多次渲染之间,状态得以保留。 两者缺一不可。


二、Fiber 的核心原理

2.1 一句话

Fiber 把「一次性递归完整个树」的同步更新,拆成「按节点逐步执行、可中断」的增量更新,让渲染不卡顿主线程。

2.2 为什么需要 Fiber?

旧版 (Stack Reconciler) Fiber 之后
从根递归整棵树,一气做完 拆成一个个小单元
无法暂停 可随时暂停,让出主线程
大树会长时间占用主线程 支持时间分片、优先级调度

2.3 Fiber 是什么?

Fiber = 对应一个 React 元素的「工作单元」,携带该节点的类型、props、子节点引用等。

1
2
3
4
5
6
7
8
9
Fiber {
type, key, props
child: 第一个子 Fiber
sibling: 下一个兄弟 Fiber
return: 父 Fiber(用于回溯)
alternate: 另一棵树的对应 Fiber(双缓冲)
flags: 增/删/改等标记
lane: 优先级
}

2.4 链表式遍历(而非递归)

树被转成链表结构,遍历时可随时停下、下次从断点继续:

1
2
3
4
5
6
7
        App
/ \
Header Main
/ \ |
Nav Logo Content

遍历顺序:App → Header → Nav → Logo → Main → Content ...

2.5 双缓冲 (alternate)

  • current:当前屏幕上的树
  • workInProgress:正在构建的新树

更新时在 workInProgress 上增量构建,commit 阶段一次性切换,避免闪烁。

2.6 总结

问题 回答
Fiber 是什么? 一个工作单元,带 child/sibling/return 的链表节点
为什么用? 实现可中断、可恢复、可调度
如何可中断? 链表遍历 + 时间分片
双缓冲? 两棵树交替,保证更新可复用且一次性提交

三、Fiber 作为一种设计思想

3.1 Fiber 可抽象为通用思想

思想 含义
可中断的增量工作 大任务拆成小单元,做一点可停
时间分片 固定时间片内工作,到点让出主线程
优先级调度 高优任务(如用户输入)插队
链表式遍历 用 child/sibling/return 实现可暂停、可恢复

这些思想可迁移到 UI 渲染、动画、大数据处理等场景。

3.2 Flutter 能否引入?

可以。Flutter 已具备类似机制:

Flutter 机制 对应思想
SchedulerBinding 分阶段调度(microtask、frame、idle)
优先级队列 高优任务优先执行
Isolate 重计算放到后台,减轻 UI 线程压力

若要在 layout 等环节做「增量可中断」,可借鉴 Fiber 的拆解与调度方式。

3.3 通用模式

1
2
3
4
5
1. 把大任务拆成小工作单元
2. 每个单元可单独执行、可暂停
3. 用链表/树记录进度,便于恢复
4. 调度器按优先级和时间片决定执行哪些
5. 高优任务可插队

四、整体逻辑关系

1
2
3
4
5
6
7
8
9
Hooks 底层
→ 状态存在 Fiber 的链表上,靠调用顺序对应
→ 闭包捕获索引,保证 setState 等更新函数能正确写入;需注意 stale closure

Fiber 核心
→ 把递归更新改为可中断的链表遍历 + 时间分片

Fiber 思想
→ 可中断、可调度、增量工作,具有普适性,可迁移到其他框架(如 Flutter)与场景

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 引擎

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


参考资源

vscode实用小记

VSCode插件:

  • Error Lens:报错
  • One Dark Pro:颜色主题
  • Live Server :实时预览,左右分屏
  • Auto Rename Tag:修改名字
  • Open In Browser:浏览器中打开
  • VSCode Icons:设置文件图标
  • Easy Less:less编译
  • CSSREM

vscode快捷键:

shift+alt+下箭头(上箭头):复制一行

ctrl+d:选中多个相同的单词

ctrl+alt+下箭头(上箭头):添加多个光标

ctrl+h:全局替换某个单词

ctrl+g:跳转到指定的某一行

shift+alt+鼠标选中:选中某一区块