0


鸿蒙NEXT(五):鸿蒙版React Native架构浅析

鸿蒙版React Native架构

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如图,React Native for OpenHarmony 在 React Native 的新架构(0.68以及之后的版本)的基础上,进行了鸿蒙化的适配。按照功能可以进行如下的划分:

  • RN 应用代码:开发者实现的业务代码。
  • RN 库代码:在 React Native 供开发者使用的组件和API的封装与声明。
  • JSI(JavaScript Interface):JavaScript 与 CPP 之间进行通信的API。
  • React Common:所有平台通用的 CPP 代码,用于对 RN 侧传过来的数据进行预处理。
  • OpenHarmony 适配代码:接收并处理 React Common 传过来的数据,对接原生的代码,调用 ArkUI 的原生组件与 API。主要包括了两个部分:分别是 TurboModule 与 Fabric。
  • OS代码:对接系统底层功能,根据适配层代码传过来的数据进行渲染,或完成对应的功能。

React Native库代码

在现行的 React Native 中,有很多属性是在React侧完成的封装,也有很多属性是平台独有的。为了达成这个效果,React Native 在JS侧根据Platform增加了很多判断。所以,React Native 的鸿蒙化适配也需要增加HarmonyOS相关的平台判断,与相应的组件属性的封装。为此,鸿蒙化团队提供了react-native-harmony的tgz包,并通过更改metro.config.js配置,将该tgz包应用到 Metro Bundler中。

React Native 还提供了很多库的封装,例如Codegen、打包工具等。为此,鸿蒙化团队提供了react-native-harmony-cli的包,对这些库进行了HarmonyOS平台的适配,用于向开发者提供相关的功能。

Fabric

Fabric 是 React Native 的组件渲染系统。接收 React Native 传过来的组件信息,处理后发送给原生OS,由OS完成页面的渲染。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在适配方案中,组件不通过复杂的流程对接到ArkUI的声明式范式上,而是直接使用XComponent对接到ArkUI的后端接口进行渲染,缩短了流程,提高了组件渲染的效率。C-API的性能收益包括以下的几个部分:

  • C端最小化、无跨语言的组件创建和属性设置;
  • 无跨语言前的数据格式转换,不需要将string,enum等数据类型转换为object,可以在CPP侧直接使用对应的数据进行处理;
  • 可以进行属性Diff,避免重复设置,降低了属性设置的开销。

渲染流水线请参考渲染三阶段。

TurboModule

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
TurboModule 是 React Native 中用于 JavaScript 和原生代码进行交互的模块,为RN JS应用提供调用系统能力的机制。根据是否依赖 HarmonyOS系统相关的能力,可以分为两类:cxxTurboModule和ArkTSTurboModule。

  1. ArkTSTurboModule:
  • ArkTSTurboModule为 React Native 提供了调用ArkTS原生API的方法。可以分为同步与异步两种。
  • ArkTSTurboModule依赖NAPI进行原生代码与CPP侧的通信。包括JS与C之间的类型转换,同步和异步调用的实现等。
  1. cxxTurboModule:
  • cxxTurboModule主要提供的是不需要系统参与的能力,例如NativeAnimatedTurboModule主要提供了数据计算的相关能力。
  • cxxTurboModule不依赖于系统的原生API,为了提高相互通信的效率,一般是在cpp侧实现,这样可以减少native与cpp之间的通信次数,提高性能。

React Native线程模型

RNOH线程模型

RNOH的线程一共有3个:
enum TaskThread {MAIN=0,// main thread running the eTS event loopJS,// React Native's JS runtime threadBACKGROUND,// background tasks queue};
MAIN/UI线程

RN业务主线程,也是应用主线,应用UI线程。该线程在应用中有唯一实例。

RN在MAIN线程中主要承担的业务功能是:

  • ArkUI组件的生命周期管理:CREATE, UPDATE, INSERT, REMOVE, DELETE;
  • ArkUI组件树管理;
  • RN TurboModule业务功能运行;
  • 交互事件、消息处理。
JS线程

JS线程通过虚拟机执行React(JS)代码,通过React代码与RN Common的核心代码交互完成React Native的Render阶段任务。

RN在JS线程中主要承担的业务功能是:

  • 加载Bundle,执行Bundle依赖的React代码和Bundle的业务代码。
  • 由React业务代码驱动,创建RN ShadowTree,设置ShadowTree的属性。
  • 利用Yoga引擎进行组件布局,文本测量和布局。
  • 比较完成布局的新、老ShadowTree,生成差异结果mutations。将mutations提交到MAIN线程触发Native的显示刷新。
  • 交互事件、消息处理。

JS线程与RNInstance的实例绑定,有多个RNInstance,则有多个对应的JS线程。

BACKGROUND线程

BACKGROUND线程是RN的实验特性,开启BACKGROUND线程后,会将JS线程的部分布局、ShadowTree比较的任务迁移到该线程执行,从而降低JS线程的负荷。

由于开启BACKGROUND涉及复杂的线程间通信,在稳定性方面带来风险,因此正式商用版本中不要开启BACKGROUND线程。

RNOH线程的长期演进

MAIN线程和JS线程承担了RN框架的全部业务,在重载情况下可能会造成性能瓶颈。RN的业务也受同线程的其他应用代码的影响,造成执行延迟或阻塞等问题。

在长期演进时,可以考虑进行线程扩展:

  • 增加唯一TM线程,将TurboModule的业务代码放到TM线程来执行,从而降低MAIN线程负荷。
  • 增加单独的TIMER线程,确保时间基准稳定执行。

典型线程Trace图

  • 线程号53130:MAIN线程
  • 线程号53214:JS线程实例1
  • 线程号53216:JS线程实例2

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

命令式组件

XComponent接入

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

CAPI 版本使用XComponent总共分成了两个步骤:

  1. createSurface的时候创建XComponentSurface;
  2. startSurface的时候将CPP的XComponentSurface连接到ArkUI的Xcomponent上。

createSurface的时候主要做了以下的操作:

  1. 创建并将XComponentSurface记录到Map中:
void RNInstanceCAPI::createSurface(
facebook::react::Tag surfaceId,
std::string const& moduleName) {
m_surfaceById.emplace(
    surfaceId,
    XComponentSurface(
        ···
        surfaceId,
        moduleName));
}
  1. 在XComponentSurface中创建rootView,用于挂载C-API的组件,并在Surface上统一处理Touch事件:
XComponentSurface::XComponentSurface(
···
SurfaceId surfaceId,
std::string const& appKey)
: 
  ···
  m_nativeXComponent(nullptr),
  m_rootView(nullptr),
  m_surfaceHandler(SurfaceHandler(appKey, surfaceId)) {
m_scheduler->registerSurface(m_surfaceHandler);
m_rootView = componentInstanceFactory->create(
    surfaceId, facebook::react::RootShadowNode::Handle(), "RootView");
m_componentInstanceRegistry->insert(m_rootView);
m_touchEventHandler = std::make_unique<SurfaceTouchEventHandler>(m_rootView);
}

startSurface的时候主要做了以下的操作:

  1. 在ArkTS侧创建XComponent,并设置id,type与libraryname属性。其中:
  • id:组件的唯一标识,又由InstanceID和SurfaceID共同组成,记录了此XComponent属于哪一个Instance与Surface;
  • type:node,标识该XComponent是一个占位组件,组件的实现都在CAPI侧;
  • libraryname:表示C-API组件在哪个so库中实现,并加载该so库,自动调用该so中定义的Init函数。当前React Native for OpenHarmony默认的so名字为rnoh_app。
XComponent({
    id:this.ctx.rnInstance.getId()+"_"+this.surfaceHandle.getTag(),type:"node",
    libraryname:'rnoh_app'})
  1. 在CPP侧的Init中调用registerNativeXComponent函数,该函数中调用了OH_NativeXComponent_GetXComponentId用于获取ArkTS设置的id,并根据id找到对应的Instance与Surface。同时还要获取nativeXComponent对象,记录ArkTS侧的XComponent。
if (OH_NativeXComponent_GetXComponentId(nativeXComponent, idStr, &idSize) !=
  OH_NATIVEXCOMPONENT_RESULT_SUCCESS) {
    ···
}
std::string xcomponentStr(idStr);
std::stringstream ss(xcomponentStr);
std::string instanceId;
std::getline(ss, instanceId, '_');
std::string surfaceId;
std::getline(ss, surfaceId, '_');
  1. 调用OH_NativeXComponent_AttachNativeRootNode,将XComponentSurface中记录的rootView连接到ArkTS侧的XComponent上:
OH_NativeXComponent_AttachNativeRootNode(
    nativeXComponent,
    rootView.getLocalRootArkUINode().getArkUINodeHandle());
  1. 将rootView连接到XComponent后,rootView就作为CAPI组件的根节点,后续的子孙节点通过Mutation指令逐个插入到组件树上。

CAPI组件向上对接RN指令

  1. 在RN鸿蒙适配层中,SchedulerDelegate.cpp负责处理RN Common传递下来的指令。
void SchedulerDelegate::schedulerDidFinishTransaction(MountingCoordinator::Shared mountingCoordinator) {
    ...
}
  1. 在 MountingManagerCAPI.cpp 的didMount中对各个指令进行处理。
MountingManagerCAPI::didMount(MutationList const& mutations) {
    ...
}

在didMount函数中,先根据预先配置的arkTsComponentNames获取ArkTs组件和CAPI组件的指令,分别进行处理。其中CAPI组件的指令会在handleMutation方法中逐个遍历每个指令,根据指令的类型(Create 、Delete、Insert、Remove、Update)进行不同的处理。

  • Create指令:接收到Create指令后,会根据指令的tag、componentName和componentHandle信息创建出一个对应组件类型的ComponentInstance,比如Image组件的Create指令,会创建对应的ImageComponentInstance。创建完组件之后,调用updateComponentWithShadowView方法设置组件的信息。其中,setLayout设置组件的布局信息,setEventEmitter设置组件的事件发送器,setState设置组件的状态,setProps设置组件的属性信息。
  • Delete指令:根据接收到的Delete指令的tag,删除对应组件的ComponentInstance。
  • Insert指令:根据接收到Insert指令中包含父节点的tag和子节点的tag,将子节点插入到对应的父节点上。
  • Remove指令:接收到Remove指令中包含父节点的tag和子节点的tag,在父节点上移除对应的子节点。
  • Update指令:接收到Update指令后,调用组件的setLayout、setEventEmitter、setState、setProps更新组件相关信息。

适配层事件分发逻辑

1.适配层事件的注册

当手势触碰屏幕后会命中相应的结点,通过回调发送对应事件,但是需要注册事件,如一个Stack节点注册了NODE_ON_CLICK事件。

StackNode::StackNode()
:ArkUINode(NativeNodeAPi::getInstance()->createNode(ArkUI_NodeType::ARKUI_NODE_STACK)),
    m_stackNodeDelegate(nullptr)
    {
        maybeThrow(NativeNodeApi::getInstance()->registerNodeEvent(m_nodeHandle,NODE_ON_CLICK,0,this));
        maybeThrow(NativeNodeApi::getInstance()->registerNodeEvent(m_nodeHandle,NODE_ON_HOVER,0,this));
    }

SurfaceTouchEventHandler注册了NODE_TOUCH_EVENT事件。

SurfaceTouchEventHandler(
    ComponentInstance::Shared rootView,
    ArkTSMessageHub::Shared arkTSMessageHub,int rnInstanceId):
    ArkTSMessageHub::Observer(arkTSMessageHub),
    m_rootView(std::move(rootView)),
    m_rnInstanceId(rnInstanceId)
    {
        ArkUINodeRegistry::getInstance().registerTouchHandler(
            &m_rootView->getLocalRootArkUINode(),this);
            NativeNodeApi::getInstance()->registerNodeEvent(
                m_rootView->getLocalRootArkUINode().getArkUINodeHandle(),
                NODE_TOUCH_EVENT,
                NODE_TOUCH_EVENT,
                this);
    }
2.适配层事件的接收

ArkUINodeRegistry的构造中注册了一个回调,当注册了事件的节点被命中后,该事件通过回调传递处理。

ArkUINodeRegistry::ArkUINodeRegistry(ArkTSBridge::Shared arkTSBridge):m_arkTSBridge(std::move(arkTSBridge))
{
    NativeNodeApi::getInstance()->registerNodeEventReceiver(
        [](ArkUI_NodeEvent* event){
            ArkUINodeRegistry::getInstance().receiveEvent(event);
            });
}
3.适配层事件的处理

回调传递的参数event通过OH_ArkUI_NodeEvent_GetEventType获取事件类型,通过OH_ArkUI_NodeEvent_GetNodeHandle获取触发该事件的结点指针。

auto eventType = OHArkUI_NodeEvent_GetEventType(event);
auto node = OH_ArkUI_NodeEvent_GetNodeHandle(event);

首先判断事件类型是否为Touch事件,如果是,就从一个存储了所有TouchEventHandler的Map中通过结点指针作为key去查找对应的TouchEventHandler,如果没找到,这次Touch事件不处理。

if(eventType == ArkUI_NodeEventType::NODE_TOUCH_EVENT)
{
    auto it = m_touchHandlerByNodeHandle.find(node);
    if(it == m_touchHandlerByNodeHandle.end())
    {
        return;
    }
}

如果找到了对应的TouchEventHandler,通过OH_ArkUI_NodeEvent_GetInputEvent获取输入事件指针,若输入事件指针不为空,通过OH_ArkUI_UIInputEvent_GetType判断输入事件指针的类型是否为Touch事件,如果不是,这次Touch事件不处理。

auto inputEvent = OH_ArkUI_NodeEvent_GetInputEvent(event);
if(inputEvent == nullptr || OH_ArkUI_UIInputEvent_GetType(inputEvent) != ArkUI_UIInputEvent_Type::ARKUI_UIINPUTEVENT_TYPE_TOUCH)
{
    return;
}

如果上述两个条件都满足,就通过TouchEventHandler去处理Touch事件。

it->second->onTouchEvent(inputEvent);

如果事件类型不为Touch事件,就从一个存储了所有ArkUINode的Map中通结点指针作为key去查找对应的ArkUINode,若未找到,这次事件不处理。

auto it = m_nodeByHandle.find(node);
if(it == m_nodeByHandle.end())
{
    return;
}

如果找了对应的ArkUINode,通过OH_ArkUI_NodeEvent_GetNodeComponentEvent获取组件事件指针,该指针的data字段保留了arkUI传递过来的参数,并通过ArkUINode处理该事件。

auto commponentEvent = OH_ArkUI_NodeEvent_GetNodeComponentEvent(event);
if(commponentEvent != nullptr)
{
    it->second->onNodeEvent(eventType,compenentEvent->data);
    return;
}
4.Touch事件的传递给JS侧

上文中写明TouchEventHandler对Touch事件进行处理,以xcomponentSurface举例,xcomponentSurface有一个继承了TouchEventHandler的成员变量,这个成员变量通过dispatchTouchEvent处理这次Touch事件。

void onTouchEvent(ArkUI_UIInputEvent* event)override
{
    m_touchEventDispatcher.dispatchTouchEvent(event,m_rootView);
}

对于Touch事件首先通过Touch的位置等因素,获取对应touchTarget(每个componentInstance就是一个touchTarget,下图的名字是eventTarget)。

class ComponentInstance:public TouchTarget,public std::enable_shared_from_this<ComponentInstance>
for(auto const& targetTouches:touchByTargetId)
{
    auto it = m_touchTargetByTouchId.find(targetTouches.second.begin()->identifier);
    if(it == m_touchTargetByTouchId.end())
    {
        continue;
    }
    auto eventTarget = it->second.lock();
    if(eventTarget == nullptr)
    {
        m_touchTargetByTouchId.erase(it);
        continue;
    }
}

然后通过componentInstance保存的m_eventEmitter发送对应的事件给js侧,从而触发页面的刷新等操作。 Touch事件有以下四种类型:

  • UI_TOUCH_EVENT_ACTION_DOWN
  • UI_TOUCH_EVENT_ACTION_MOVE
  • UI_TOUCH_EVENT_ACTION_UP
  • UI_TOUCH_EVENT_ACTION_CANCEL
switch(action)
{
    case UI_TOUCH_EVENT_ACTION_DOWN:
    eventTarget->getTouchEventEmitter()->onTouchStart(touchEvent);
    break;
    case UI_TOUCH_EVENT_ACTION_MOVE:
    eventTarget->getTouchEventEmitter()->onTouchMove(touchEvent);
    break;
    case UI_TOUCH_EVENT_ACTION_UP:
    eventTarget->getTouchEventEmitter()->onTouchEnd(touchEvent);
    break;
    case UI_TOUCH_EVENT_ACTION_CANCEL:
    default:
    eventTarget->getTouchEventEmitter()->onTouchCancel(touchEvent);
    break;
}
5、非Touch事件的传递给js侧

上文中写明,非Touch事件由ArkUINode处理,对于每个继承了ArkUINode的类,重载了onNodeEvent方法,以StackNode举例,说明RN适配层是如何区分Click事件和Touch事件。前文说明,StackNode注册了Click事件,所以通过回调,会走到StackNode的onNodeEvent部分,这里会先判断这个事件类型,这里是NODE_ON_CLICK类型,符合要求,但是对于第二个条件eventArgs[3].i32(即上文描述的arkUI传递过来的参数),如果是触屏手机,其值为2不满足eventArgs[3].i32 != 2的条件。

void StackNode::onNodeEvent(ArkUI_NodeEventType eventType,EventArgs& eventArgs)
{
    if(eventType == ArkUI_NodeEventType::NODE_ON_CLICK && eventArgs[3].i32 != 2)
    {
        onClick();
    }
    if(eventType == ArkUI_NodeEventType::NODE_ON_HOVER)
    {
        if(m_stackNodeDelegate != nullptr)
        {
            if(eventArgs[0].i32)
            {
                m_stackNodeDelegate->onHoverIn();
            }else
            {
                m_stackNodeDelegate->onHoverOut();
            }
        }
    }
}

所以此时实际上不会触发Click的事件,因此Touch事件和Click事件不会冲突。如果触发了Click事件,StackNode会通过代理StackNodeDelegate发送事件。

void StackNode::onClick()
{
    if(m_stackNodeDelegate != nullptr)
    {
        m_stackNodeDelegate->onClick();
    }
}

其中ViewComponentInstance继承了StackNodeDelegate,所以实际上走的是ViewComponentInstance的onClick函数。

namespace rnoh
{
    class ViewComponentInstance
    :public CppComponentInstance<facebook::react::ViewShardowNode>,public StackNodeDelegate
    {
    }
}

这个函数通过ViewComponentInstance的m_eventEmitter发送事件给JS,从而触发页面的刷新。

void ViewComponentInstance::onClick()
{
    if(m_eventEmitter != nullptr)
    {
        m_eventEmitter->dispatchEvent("click",[=](facebook:jsi::Runtime& runtime)
        {auto payload = facebook::jsi::Object(runtime);
                return payload;
        });
    }
}

鸿蒙版React Native启动流程

鸿蒙RN启动阶段分为RN容器创建、Worker线程启动、NAPI方法初始化、RN实例创建四个阶段,接下来加载bundle和界面渲染,类图如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

React Native容器创建

  • EntryAbility全局Ability,App的启动入口。
  • Index.etsApp页面入口。
  • RNApp.ets- 配置appKey,和JS侧registerComponent注册的appKey关联;- 配置初始化参数initialProps,传递给js页面;- 配置jsBundleProvider,指定bundle加载路径;- 配置ArkTS混合组件wrappedCustomRNComponentBuilder;- 配置rnInstanceConfig,指定开发者自定义package,注入字体文件fontResourceByFontFamily,设置BG线程开关,设置C-API开关;- 持有RNSurface,作为RN页面容器。
  • RNSurface.ets

RN页面容器,持有XComponent用于挂载ArkUI的C-API节点和响应手势事件。

Worker线程启动

TurboModule运行在worker线程,worker线程是在程序启动时创建。

  • WorkerThread.tsEntryAbility创建时会创建RNInstancesCoordinator,RNInstancesCoordinator的构造函数中获取worker线程类地址,然后调用WorkerThread的create方法启动worker线程,如下:
const workerThread = new WorkerThread(logger, new worker.ThreadWorker(scriptUrl, { name: name }), onWorkerError)
  • RNOHWorker.etsWorkerThread中配置的scriptUrl即RNOHWorker.ets路径,RNOHWorker.ets内部调用setRNOHWorker.ets的setRNOHWorker方法配置worker线程收发消息通道。
  • setRNOHWorker.etssetRNOHWorker方法配置worker线程收发消息通道,createTurboModuleProvider方法注册系统自带和开发者自定义的运行在worker线程的TurboModule。

NAPI方法初始化

  • RNOHAppNapiBridge.cpp

Init方法是静态方法,在程序启动时调用,配置了18个ArkTS调用C++的方法,如下:

registerWorkerTurboModuleProvider,
getNextRNInstanceId, 
onCreateRNInstance,                  // 创建RN实例
onDestroyRNInstance,                 // 销毁RN实例
loadScript,                          // 加载bundle
startSurface,
stopSurface,
destroySurface,
createSurface,                       // 创建RN界面
updateSurfaceConstraints,
setSurfaceDisplayMode,
onArkTSMessage,
emitComponentEvent,                  // 给RN JS发消息
callRNFunction, 
onMemoryLevel,
updateState,
getInspectorWrapper,
getNativeNodeIdByTag
  • NapiBridge.ts

ArkTS侧RNInstance.ts、SurfaceHandle.ts调用C++的桥梁。

React Native实例创建

在RNInstance.ts中创建RN实例,分为以下步骤:

  1. 获取RNInstance的id:在RNInstanceRegistry.ets中通过NAPI调用getNextRNInstanceId方法获取。
  2. 注册ArkTS侧TurboModule:在RNInstance.ts中调用processPackage方法注册系统自带和开发者自定义的运行在UI线程上的TurboModule。
  3. 注册字体:在RNInstanceFactory.h中调用FontRegistry.h的registerFont方法注册应用侧扩展字体,接着通过图形接口注入字体信息。
  4. 注册RN官方能力和开发者自定义能力:RNInstanceFactory.h中通过PackageProvider.cpp的getPackage方法获取RN系统自带和开发者自定义TurboModule,接着注册系统View、系统自带TurboModule、开发者自定义View、开发者自定义TurboModule。
  5. 注册ArkTS混合组件:在RNInstanceFactory.cpp中注册ArkTS侧传递到C++的ArkTS组件。
  6. 初始化JS引擎:在RNInstanceInternal.cpp中初始化JS引擎Hermes或者JSVM,通过JS引擎驱动JS消息队列。
  7. 注册TM的JSI通道:在RNInstanceCAPI.cpp中调用createTurboModuleProvider创建TurboModuleProvider,注入__turboModuleProxy对象给JS侧。
  8. 注入Scheduler:在RNInstanceInternal.cpp中初始化Fabric的Scheduler对象,ReactCommon的组件绘制找到鸿蒙适配层注入的SchedulerDelegate才能进行界面绘制。
  9. 注册Fabric的JSI通道:在RNInstanceInternal.cpp中调用UIManagerBinding.cpp的createAndInstallIfNeeded方法注入nativeFabricUIManager对象给JS侧。

加载bundle

RN实例创建完毕则开始加载bundle,如下:

ArkTS侧加载bundle、C++侧加载bundle,切线程到ReactCommon的Instance.cpp中加载bundle:

RNApp.ets > RNInstance.ts > RNOHAppNapiBridge.cpp > RNInstanceInternal.cpp > Instance.cpp

总结

本文详细介绍了鸿蒙版 React Native 架构。包括按功能划分的架构组成,如 RN 应用代码、库代码、JSI、React Common、OpenHarmony 适配代码及 OS 代码等。还阐述了 Fabric、TurboModule、线程模型、命令式组件、启动流程等方面内容。启动流程分为 RN 容器创建、Worker 线程启动、NAPI 方法初始化、RN 实例创建及加载 bundle 等阶段。整体架构复杂且功能明确,为开发者提供了在鸿蒙平台上使用 React Native 的技术支持。


本文转载自: https://blog.csdn.net/qq_44924880/article/details/143364672
版权归原作者 纯爱掌门人 所有, 如有侵权,请联系我们删除。

“鸿蒙NEXT(五):鸿蒙版React Native架构浅析”的评论:

还没有评论