文章

原生UI组件封装深度解析

从ViewManager生命周期到事件回调,全面拆解React Native原生UI组件封装的全流程与底层原理

原生UI组件封装深度解析

一句话概括

React Native的原生UI组件封装是通过ViewManager(Android)和RCTViewManager(iOS)将平台原生视图桥接到JavaScript层的完整技术方案,涉及生命周期管理、属性同步、事件派发三大核心机制。

背景与意义

为什么需要封装原生UI组件?

React Native虽然提供了丰富的内置组件,但在实际业务中,我们经常遇到以下几种场景:

  1. 高性能渲染需求:如视频播放器、地图组件、图表库,纯JS渲染无法满足60fps的流畅度
  2. 平台特有功能:如AR/VR组件、指纹识别UI、原生支付SDK
  3. 复用既有Native代码:团队已有成熟的iOS/Android组件库,不想用JS重写
  4. 复杂UI交互:如长列表的Native驱动动画、原生级滑动效果

以美团的RN实践为例,他们的外卖首页大量使用了原生封装的Banner组件和地图组件,保证了与原生App一致的滑动流畅度和交互体验。

技术演进

1
2
3
4
React Native 0.1-0.30 --- 原生组件需手动注册
React Native 0.31-0.59 --- TurboModules概念提出
React Native 0.60-0.70 --- Autolinking + Fabric架构预览
React Native 0.70+     --- New Architecture (Fabric + TurboModules)

当前的”新架构”(New Architecture)将ViewManager升级为使用JSI(JavaScript Interface)直接通信,去除了Bridge的序列化开销。

概念与定义

核心角色

角色Android端iOS端职责
视图管理器ViewManager<T>RCTViewManager创建、更新、销毁原生视图
原生视图继承View的子类继承UIView的子类实际的UI渲染
JS组件requireNativeComponentrequireNativeComponentJS侧的包装层
事件发射器EventDispatcherself.bridge.eventDispatcher将原生事件传递到JS侧

核心流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
JS (React Component)
    │
    │ requireNativeComponent('CustomView')
    ▼
Shadow Node (布局计算)
    │
    │ createViewInstance / updateView
    ▼
Native View (Android View / iOS UIView)
    │
    │ Event回调 (onReceiveNativeEvent)
    ▼
JS Callback / EventEmitter

最小示例

一个完整的原生UI组件封装 —— 展示一个原生绘制的”签名板”组件。

Android端 (Kotlin)

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
// SignatureView.kt - 原生视图
package com.example.rnsignature

import android.content.Context
import android.graphics.*
import android.view.MotionEvent
import android.view.View
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.events.RCTEventEmitter

class SignatureView(context: Context) : View(context) {
    private val path = Path()
    private val paint = Paint().apply {
        color = Color.BLACK
        style = Paint.Style.STROKE
        strokeWidth = 5f
        strokeCap = Paint.Cap.ROUND
        strokeJoin = Paint.Join.ROUND
    }
    private val bitmapPaint = Paint(Paint.DITHER_FLAG)

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.drawPath(path, paint)
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        val x = event.x
        val y = event.y
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                path.moveTo(x, y)
                sendTouchEvent("onDrawStart", x, y)
            }
            MotionEvent.ACTION_MOVE -> {
                path.lineTo(x, y)
                sendTouchEvent("onDrawing", x, y)
            }
            MotionEvent.ACTION_UP -> {
                sendTouchEvent("onDrawEnd", x, y)
            }
        }
        invalidate()
        return true
    }

    private fun sendTouchEvent(eventName: String, x: Float, y: Float) {
        val event = Arguments.createMap()
        event.putDouble("x", x.toDouble())
        event.putDouble("y", y.toDouble())
        val reactContext = context as ReactContext
        reactContext
            .getJSModule(RCTEventEmitter::class.java)
            .receiveEvent(id, eventName, event)
    }

    fun clearCanvas() {
        path.reset()
        invalidate()
    }
}
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
// SignatureViewManager.kt - 视图管理器
package com.example.rnsignature

import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp

class SignatureViewManager : SimpleViewManager<SignatureView>() {
    override fun getName() = "SignatureView"

    override fun createViewInstance(reactContext: ThemedReactContext) = SignatureView(reactContext)

    @ReactProp(name = "strokeColor")
    fun setStrokeColor(view: SignatureView, color: String) {
        // 通过属性更新画笔颜色
    }

    // 导出的命令:JS可调用clearCanvas
    override fun getCommandsMap(): Map<String, Int> {
        return mapOf("clear" to 1)
    }

    override fun receiveCommand(view: SignatureView, commandId: Int, args: ReadableArray?) {
        when (commandId) {
            1 -> view.clearCanvas()
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
// SignaturePackage.kt - 注册包
package com.example.rnsignature

import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager

class SignaturePackage : ReactPackage {
    override fun createNativeModules(reactContext: ReactApplicationContext) = emptyList<NativeModule>()
    override fun createViewManagers(reactContext: ReactApplicationContext) = listOf(SignatureViewManager())
}

iOS端 (Objective-C)

1
2
3
4
5
6
7
// SignatureView.h
#import <UIKit/UIKit.h>

@interface SignatureView : UIView
@property (nonatomic, strong) UIColor *strokeColor;
- (void)clearCanvas;
@end
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
// SignatureView.m
#import "SignatureView.h"
#import <React/RCTEventDispatcher.h>
#import <React/UIView+React.h>

@interface SignatureView ()
@property (nonatomic, strong) UIBezierPath *path;
@property (nonatomic, strong) CAShapeLayer *shapeLayer;
@end

@implementation SignatureView

- (instancetype)init {
    if (self = [super init]) {
        self.path = [UIBezierPath bezierPath];
        self.path.lineWidth = 5.0;
        self.path.lineCapStyle = kCGLineCapRound;
        self.path.lineJoinStyle = kCGLineJoinRound;

        self.shapeLayer = [CAShapeLayer layer];
        self.shapeLayer.strokeColor = [UIColor blackColor].CGColor;
        self.shapeLayer.fillColor = [UIColor clearColor].CGColor;
        self.shapeLayer.lineWidth = 5.0;
        [self.layer addSublayer:self.shapeLayer];
    }
    return self;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    CGPoint pt = [[touches anyObject] locationInView:self];
    [self.path moveToPoint:pt];
    [self sendEventWithName:@"onDrawStart" body:@{@"x": @(pt.x), @"y": @(pt.y)}];
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    CGPoint pt = [[touches anyObject] locationInView:self];
    [self.path addLineToPoint:pt];
    self.shapeLayer.path = self.path.CGPath;
    [self sendEventWithName:@"onDrawing" body:@{@"x": @(pt.x), @"y": @(pt.y)}];
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    CGPoint pt = [[touches anyObject] locationInView:self];
    [self sendEventWithName:@"onDrawEnd" body:@{@"x": @(pt.x), @"y": @(pt.y)}];
}

- (void)sendEventWithName:(NSString *)name body:(id)body {
    if (self.onEvent) {
        self.onEvent(@{@"type": name, @"payload": body});
    }
}

- (void)clearCanvas {
    [self.path removeAllPoints];
    self.shapeLayer.path = nil;
}

@end
1
2
3
4
5
6
7
8
9
// SignatureViewManager.m
#import <React/RCTViewManager.h>
#import "SignatureView.h"

@interface RCT_EXTERN_MODULE(SignatureViewManager, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(strokeColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(onEvent, RCTDirectEventBlock)
RCT_EXTERN_METHOD(clear:(nonnull NSNumber *)reactTag)
@end

JS侧封装

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
// SignatureView.tsx
import { requireNativeComponent, NativeModules, Platform, findNodeHandle } from 'react-native';
import type { ViewProps } from 'react-native';
import React, { useCallback, useRef } from 'react';

// Native组件接口定义
interface SignatureNativeProps extends ViewProps {
  strokeColor?: string;
  onDrawStart?: (e: { nativeEvent: { x: number; y: number } }) => void;
  onDrawing?: (e: { nativeEvent: { x: number; y: number } }) => void;
  onDrawEnd?: (e: { nativeEvent: { x: number; y: number } }) => void;
}

// 加载Native组件
const NativeSignature = requireNativeComponent<SignatureNativeProps>('SignatureView');

// JS包装组件
export const SignatureBoard: React.FC<{ strokeColor?: string; style?: ViewProps['style'] }> = ({
  strokeColor = '#000000',
  style,
}) => {
  const ref = useRef<any>(null);

  const handleClear = useCallback(() => {
    const tag = findNodeHandle(ref.current);
    if (tag) {
      NativeModules.UIManager.dispatchViewManagerCommand(
        tag,
        NativeModules.UIManager.getViewManagerConfig('SignatureView').Commands.clear,
        []
      );
    }
  }, []);

  return (
    <NativeSignature
      ref={ref}
      style={[{ width: '100%', height: 300, backgroundColor: '#fff' }, style]}
      strokeColor={strokeColor}
    />
  );
};

// 导出clear方法
export const clearSignature = (ref: any) => {
  const tag = findNodeHandle(ref);
  if (tag) {
    NativeModules.UIManager.dispatchViewManagerCommand(
      tag,
      NativeModules.UIManager.getViewManagerConfig('SignatureView').Commands.clear,
      []
    );
  }
};

核心知识点拆解

1. ViewManager的生命周期

一个原生视图从创建到销毁经历以下阶段:

graph LR
    A[createViewInstance] --> B[setProps]
    B --> C[onAfterUpdateTransaction]
    C --> D[视图添加到视图树]
    D --> E[onDropViewInstance]
    E --> F[GC回收]

关键方法:

  • createViewInstance:创建原生视图实例,只调用一次
  • onAfterUpdateTransaction:所有属性设置完成后调用,适合做初始化收尾
  • onDropViewInstance:组件卸载时调用,清理资源

2. 属性传递机制

属性从JS到Native的传递链路:

1
JSX props → React Shadow Tree → Native Module JSON序列化 → setter调用

@ReactProp 注解的解析规则:

1
2
3
4
@ReactProp(name = "strokeColor", defaultFloat = 0f)
public void setStrokeColor(SignatureView view, @Nullable String color) {
    // 当JS传递了strokeColor属性或组件首次挂载时触发
}

支持的类型映射:

JS类型Java类型ObjC类型
booleanbooleanBOOL
numberdouble/doubleCGFloat/NSInteger
stringStringNSString
objectReadableMapNSDictionary
arrayReadableArrayNSArray
functionReadableMap/CallbackRCTResponseSenderBlock

3. 事件回调的三种模式

模式一:RCTEventEmitter(传统方式)

1
2
3
val reactContext = context as ReactContext
reactContext.getJSModule(RCTEventEmitter::class.java)
    .receiveEvent(viewId, "onCustomEvent", eventData)

模式二:RCTDirectEventBlock(新架构)

1
2
3
4
5
6
7
// 在ViewManager中导出
RCT_EXPORT_VIEW_PROPERTY(onEvent, RCTDirectEventBlock)

// 在JS中使用
onEvent={(event) => {
    console.log(event.nativeEvent);
}}

模式三:BubblingEvent(事件冒泡)

用于需要事件冒泡的场景,如 onPress 事件在嵌套组件中向上传播。

实战案例:高性能图片预览组件

结合Fresco图片加载库(Android端)和SDWebImage(iOS端),封装一个支持缩放手势的图片预览器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ZoomableImageView.kt
class ZoomableImageView(context: Context) : View(context) {
    private var scaleFactor = 1.0f
    private var lastTouchX = 0f
    private var lastTouchY = 0f

    override fun onTouchEvent(event: MotionEvent): Boolean {
        if (event.pointerCount == 2) {
            // 双指缩放
            val dx = event.getX(0) - event.getX(1)
            val dy = event.getY(0) - event.getY(1)
            val distance = Math.sqrt((dx * dx + dy * dy).toDouble()).toFloat()
            // ... 缩放逻辑
        }
        return true
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ZoomableImage.tsx
import { requireNativeComponent } from 'react-native';
import React, { useState } from 'react';

interface Props {
  source: { uri: string };
  onScaleChanged?: (scale: number) => void;
}

const NativeZoomableImage = requireNativeComponent<Props>('ZoomableImageView');

export const ZoomableImage: React.FC<Props> = ({ source, onScaleChanged }) => {
  const [scale, setScale] = useState(1);

  return (
    <NativeZoomableImage
      source={{ uri: source.uri }}
      onScaleChanged={(e: any) => {
        setScale(e.nativeEvent.scale);
        onScaleChanged?.(e.nativeEvent.scale);
      }}
    />
  );
};

底层原理(源码分析)

Bridge通信机制

在旧架构中,原生UI组件的属性传递经历了完整的JSON序列化/反序列化流程:

1
2
3
4
5
JS层: { strokeColor: '#ff0000' }
   ↓ JSON.stringify
MessageQueue → Bridge
   ↓ JSON.parse (Native侧)
Java: ReadableMap → @ReactProp setter

这段流程对应 ReactNativeBridge.java 的核心代码:

1
2
3
4
5
6
7
8
// ReactNativeBridge.java (简化)
public void execute(JSEvent event) {
    String jsonStr = event.getArgumentsAsJSON();
    // Native侧收到后通过Arguments.fromBundle反序列化
    ReadableMap map = Arguments.fromBundle(jsonStr);
    // 调用ViewManager的updateProperties
    viewManager.updateProperties(view, map);
}

Fabric架构的变化

在新架构中,ViewManager被重构为使用JSI直接调用:

1
2
3
4
5
6
7
// Fabric的Component Descriptor (C++层)
class SignatureViewComponentDescriptor {
    void updateProps(Props::Shared props) {
        // 直接内存访问,无需JSON序列化
        mView.setStrokeColor(props->strokeColor);
    }
};

性能提升数据:

  • 属性传递耗时降低 60-80%
  • 首次渲染速度提升 30%
  • 内存占用减少 25%

EventEmitter源码浅析

1
2
3
4
5
6
7
8
9
10
11
12
13
// UIManagerModule.java
public void receiveEvent(int reactTag, String eventName, @Nullable WritableMap event) {
    // 1. 获取事件调度器
    EventDispatcher dispatcher = mUIImplementation.getEventDispatcher();
    // 2. 分发事件
    dispatcher.dispatchEvent(
        new ReactEvent(reactTag, eventName, event)
    );
    // 3. 通过MessageQueue发送到JS线程
    getReactApplicationContext()
        .getJSModule(RCTEventEmitter.class)
        .receiveEvent(reactTag, eventName, event);
}

高频面试题解析

问题1:@ReactProp 注解的工作原理是什么?

解析@ReactProp 是React Native框架在Android端的注解,用于标记ViewManager的方法为JS属性的setter。其工作原理如下:

  1. 编译时:APT(Annotation Processing Tool)扫描所有带有 @ReactProp 的方法,生成属性到方法的映射表
  2. 运行时:当JS侧更新组件属性时,UIManager根据属性名查找对应的setter方法并调用
  3. 类型转换:注解中声明的参数类型决定了JSON到Java类型的转换规则
1
2
3
4
5
6
7
8
9
10
11
12
// 内部的属性解析逻辑
public void updateProperties(View view, ReactStylesDiffMap props) {
    for (Map.Entry<String, Object> entry : props.entrySet()) {
        String propName = entry.getKey();
        // 查找注册的setter
        Method setter = mPropSetters.get(propName);
        if (setter != null) {
            // 类型转换后调用
            setter.invoke(mViewManager, view, convert(entry.getValue()));
        }
    }
}

关键点@ReactPropdefault* 属性用于处理JS侧未传递该属性的情况,此时将调用带默认值的方法。

问题2:如何选择 requireNativeComponentUIManager.getViewManagerConfig

解析

  • requireNativeComponent:用于声明式组件封装,返回一个可直接在JSX中使用的组件。适用于需要像普通RN组件一样通过JSX属性控制的原生组件
  • UIManager.getViewManagerConfig:用于获取ViewManager的元信息(支持的命令、属性等),与dispatchViewManagerCommand配合使用执行命令式操作
1
2
3
4
5
6
7
8
9
10
11
// 场景一:声明式用法 - 用requireNativeComponent
const NativeMap = requireNativeComponent('MapView');
<NativeMap zoomLevel={15} onMarkerPress={handler} />;

// 场景二:命令式用法 - 用UIManager
const tag = findNodeHandle(mapRef);
UIManager.dispatchViewManagerCommand(
  tag,
  UIManager.getViewManagerConfig('MapView').Commands.centerTo,
  [latitude, longitude]
);

问题3:原生组件卸载时发生的crash如何排查?

解析:原生组件卸载时的crash多由以下原因导致:

  1. 野指针访问:Native视图已被回收但JS侧仍持有引用
    • 解决方案:在 onDropViewInstance 中置空回调引用
  2. 事件发送到已卸载组件
    1
    2
    3
    4
    5
    6
    
    override fun onDropViewInstance(view: SignatureView) {
        super.onDropViewInstance(view)
        // 清理事件监听器引用
        view.setOnTouchListener(null)
        // 取消网络请求等异步任务
    }
    
  3. 主线程阻塞导致ANR:属性更新过于频繁
    • 解决方案:使用 @ReactProp(groups = "batch") 批量更新

排查工具

  • Android: Systrace + Memory Profiler
  • iOS: Instruments(Allocations + Leaks)

总结与扩展

核心要点回顾

  1. 三件套:原生组件封装永远需要 ViewManager + Native View + JS 包装层三个文件
  2. 生命周期原则:在 onDropViewInstance 中释放所有资源,防止内存泄漏
  3. 性能关键:减少属性传递频率,优先使用事件而非轮询
  4. 新架构迁移:Fabric架构下ViewManager升级为使用JSI,通信效率大幅提升

扩展思考

  • TurboModule:新架构中ViewManager是否会被TurboModule取代?
    • 答案:不会完全取代。TurboModule管理的是纯Native Modules(非UI),ViewManager仍然是组件封装的核心,只是底层通信方式从Bridge改为JSI
  • 跨平台视图抽象:未来是否会出现类似Flutter的自渲染引擎?
    • React Native的Fabric正在朝这个方向演进,通过 Yoga 布局引擎 + 平台渲染后端实现更统一的渲染控制
  • 自定义ShadowNode:高级封装中可以自定义ShadowNode来处理复杂的布局逻辑,这在自定义LayoutManager时会用到。
本文由作者按照 CC BY 4.0 进行授权