文章

原生UI组件封装深度解析

从ViewManager生命周期到事件回调,拆解RN原生UI组件封装的完整流程和工程化要点。

原生UI组件封装深度解析

一句话概括

RN 原生 UI 组件封装就是「写个原生 View → 配个 ViewManager 管理生命周期 → 在 JS 侧包一层组件」的三件套流程,核心掌握 ViewManager 生命周期、属性同步、事件回传三种机制。

核心知识点

1. 封装三件套——缺一不可

任何原生 UI 组件都由三个文件组成:

1
2
3
4
5
Native View (Android: View子类 / iOS: UIView子类)  ← 实际渲染
  ↕
ViewManager (Android: SimpleViewManager / iOS: RCTViewManager)  ← 生命周期、属性、命令
  ↕
JS 包装层 (requireNativeComponent)  ← JSX 中使用

为什么不能跳过 ViewManager 直接用 View? 因为 RN 框架不认识你的原生 View——ViewManager 是 RN 和原生 View 之间的「翻译官」:告诉 RN 这个 View 叫什么名字、有哪些属性、支持哪些命令。

2. ViewManager 生命周期——这四个方法最要紧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyViewManager extends SimpleViewManager<MyView> {
    @Override
    public String getName() { return "MyView"; }  // ① JS侧通过这个名字引用

    @Override
    protected MyView createViewInstance(ThemedReactContext ctx) {
        return new MyView(ctx);  // ② 只调用一次,创建原生View
    }

    @Override
    public void onAfterUpdateTransaction(MyView view) {
        // ③ 所有属性设置完成后调用,做收尾工作(如触发布局刷新)
    }

    @Override
    public void onDropViewInstance(MyView view) {
        super.onDropViewInstance(view);
        view.release();  // ④ 清理资源(取消网络请求、注销监听器)
    }
}

最容易踩的坑: 在 onDropViewInstance 里忘记清理资源。组件卸载后异步回调还在跑,访问已回收的 View → 野指针 crash。必须把异步任务、监听器、定时器都在这里取消掉。

3. 属性传递——JS 写到原生 View 上

1
2
3
4
5
6
7
8
9
@ReactProp(name = "strokeColor")  // JS侧: <MyView strokeColor="#ff0000" />
public void setStrokeColor(MyView view, @Nullable String color) {
    view.setStrokeColor(Color.parseColor(color));
}

@ReactProp(name = "strokeWidth", defaultFloat = 2f)  // 有默认值,JS 不传就用 2
public void setStrokeWidth(MyView view, float width) {
    view.setStrokeWidth(width);
}

关键点:

  • @ReactProp 的方法在组件首次创建时全部调用一次(应用默认值),之后每次属性变化再增量调用
  • 属性名和 JS 侧的 prop 名保持一致
  • 复杂对象用 ReadableMap,数组用 ReadableArray

4. 事件回传——原生怎么「告诉」JS

核心 API: 原生侧把事件数据打包,通过 EventDispatcher 发送给 JS。

1
2
3
4
5
6
7
8
9
10
11
12
// 原生侧发送事件
private void emitEvent(String eventName, WritableMap data) {
    ReactContext reactContext = (ReactContext) getContext();
    reactContext
        .getJSModule(RCTEventEmitter.class)
        .receiveEvent(getId(), eventName, data);
}

// JS侧接收
<MyView
  onTouchEvent={(e) => console.log(e.nativeEvent.x, e.nativeEvent.y)}
/>

注意: 组件卸载后不能再发事件。务必在 onDropViewInstance 中把事件发送逻辑关掉,否则 receiveEvent 发送到一个不存在的 viewId → crash。

5. 命令式操作——JS 调原生方法

当属性不够用时(比如「清空画布」这种动作),用命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 原生侧注册命令
@Override
public Map<String, Integer> getCommandsMap() {
    return Map.of("clear", 1, "undo", 2);  // 命令名 → 命令ID
}

@Override
public void receiveCommand(MyView view, int commandId, @Nullable ReadableArray args) {
    switch (commandId) {
        case 1: view.clear(); break;
        case 2: view.undo(); break;
    }
}
1
2
3
4
5
6
7
// JS侧调用
const tag = findNodeHandle(ref.current);
UIManager.dispatchViewManagerCommand(
  tag,
  UIManager.getViewManagerConfig('MyView').Commands.clear,
  []
);

新架构替代方案: Fabric 下命令被 NativeCommands 替代,不需要 findNodeHandle 这套黑魔法了——直接 ref.current.clear()(前提是 Codegen 生成了对应的 TS 类型)。

其实你每天都在用

  • 地图组件(react-native-maps): 底层是原生 MapView(Android) / MKMapView(iOS),通过 ViewManager 桥接到 JS——缩放、定位、标注全是原生渲染,所以 60fps
  • 视频播放器(react-native-video): ExoPlayer(Android) / AVPlayer(iOS) 封装——缓冲、画中画、倍速这些全靠原生 API,JS 只负责传配置
  • WebView(react-native-webview): 原生 WebView / WKWebView 的封装——你看到的 onMessage、injectJavaScript 背后就是 receiveEvent + dispatchCommand
  • 扫码相机: react-native-camera 在原生侧开 Camera2 / AVCaptureSession,扫码结果通过事件回传给 JS
  • Lottie 动画: lottie-react-native 封装的 LottieAnimationView——JSON 动画文件在原生线程渲染,JS 侧只控制播放/暂停

常见误解(FAQ)

❌ 误区1:「原生组件封装只要 ViewManager 就够了,不需要写 Native View」

ViewManager 是管理者,Native View 是执行者——就像餐厅经理和厨师的关系。经理可以调度但不做菜。必须有一个实际的 View/UIView 子类来做渲染、触摸处理。

❌ 误区2:「属性更新很快,@ReactProp 的方法每次都走 Bridge 也没事」

如果 onScroll 里每帧更新一个属性,每 16ms 就触发一次 Bridge 序列化往返。当用户快速滑动时,Bridge 消息过多会导致 JS 线程追不上,表现为「滑动跟手性差」。高频属性更新应该考虑用原生驱动的事件替代。

❌ 误区3:「iOS 和 Android 的 ViewManager 可以写成一样的」

Android 用 SimpleViewManager<T extends View> + @ReactProp 注解;iOS 用 RCTViewManager + RCT_EXPORT_VIEW_PROPERTY 宏。语法不同,逻辑一样。如果想复用,可以把核心渲染逻辑写在 C++ 层,Android 通过 JNI 调、iOS 通过 ObjC++ 调。

❌ 误区4:「requireNativeComponent 拿到的组件可以直接用 ref 调用原生方法」

不行。requireNativeComponent 返回的 ref 指向的是 React 组件实例,不是原生 View。要调原生方法必须走 UIManager.dispatchViewManagerCommand 这套命令式 API。

一句话总结

原生 UI 组件封装 = ViewManager 管生命周期 + @ReactProp 管属性 + EventEmitter 管通信 + 命令系统管操作——记住在 onDropViewInstance 清理一切,这是避免线上 crash 的底线。

本文由作者按照 CC BY 4.0 进行授权