ReactNative在游戏营销场景中的实践和探索-新架构介绍

avatar
@字节跳动

作者:字节游戏中台客户端团队 - 熊文源

客户端跨端框架已经发展了很多年了,最近比较流行的小程序、Flutter、ReactNative,都算是比较成功、成熟的框架,面向的开发者也不一样,很多大型App都广泛的使用了,笔者有幸很早就参与学习使用了这些优秀的跨端方案,在这几年的开发和架构设计中,除了在App中支撑了千万级DAU,也慢慢将ReactNative跨端方案运用到了游戏,来提升开发、迭代效率。本次文章我们会分5个章节介绍我们在游戏中的一些探索和实践,相信大家也能从中有所收获:

(本篇为系列最后一篇)

前面章节介绍了我们使用ReactNative在游戏中的一些实践,通过不断的迭代,我们完成了游戏平台的搭建,整体性能和稳定性已经达到了最优,算得上是一个比较成熟的平台了,当然该平台同样适用于现在的客户端开发,集成成本很低。但是框架本身的设计缺陷还是没有办法解决,在复杂的交互性很强的UI场景中,渲染瓶颈很明显,在游戏中也能深刻的体验到。

相信大家也看过我的另外一篇关于ReactNative架构重构的文章《庖丁解牛!深入剖析React Native下一代架构重构》,Facebook 在 2018 年 6 月官方宣布了大规模重构 React Native 的计划及重构路线图。目的是为了让 ReactNative 更加轻量化、更适应混合开发,接近甚至达到原生的体验。文章写的时间比较久了,笔者一直忙于其他事情,对于新进展更新较少,而且最初也只是初步分析了下Facebook的设计想法,经过这么久的迭代新架构有了很多进展,或者说无限接近正式release了,很值得和大家分享分享,这篇文章会向大家更深层次介绍新架构的现状和开发流程。

下面我们会从原理上简单介绍新架构带来的一些变化,下图是新老架构的变化对比:

相信大家也能从中发现一些区别,原有架构JS层与Native的通讯都过多的依赖bridge,而且是异步通讯,导致一些通讯频率较高的交互和设计就很难实现,同时也影响了渲染性能,而新架构正是从这点,对bridge这层做了大量的改造,使得UI和API调用,从原有异步方式,调整到可以同步或者异步与Native通讯,解决了需要频繁通讯的瓶颈问题。

  1. 旧架构设计

在了解新架构前,我们还是先聊下目前的ReactNative框架的主要工作原理,这样也方便大家了解整体架构设计,以及为什么facebook要重构整个框架:

  • ReactNative是采用前端的方式及UI渲染了原生的组件,他同时提供了API和UI组件,也方便开发者自己设计、扩展自己的API,提供了ReactContextBaseJavaModule、ViewGroupManager,其中ReactNative的UI是通过UIManger来管理的,其实在Android端就是UIManagerModule,原理上也是一个BaseJavaModule,和API共享一个native module。
  • ReactNative页面所有的API和UI组件都是通过ReactPackageManger来管理的,引擎初始化instanceManager过程中会读取注入的package,并根据名称生成对应的NativeModule和Views,这里还仅仅是Java层的,实际在C++层会对应生成JNativeModule
  • 切换到以上架构图的部分来看,Native Module的作用就是打通了前端到原生端的API调用,前端代码运行在JSC的环境中,采用C++实现,为了打通到native调用,需要在运行前注入到global环境中,前端通过global对象来操作proxy Native Module,继而执行了JNativeModule

  • 前端代码render生成UI diff树后,通过ReactNativeRenderer来完成对原生端的UIManager的调用,以下是具体的API,主要作用是通知原生端创建、更新View、批量管理组件、measure高度、宽度等:

  • 通过上述一系列的API操作后,会在原生端生成shadow tree,用来管理各个node的关系,这点和前端是一一对应的,然后待整体UI刷新后,更新这些UI组件到ReactRootView

通过上面的分析,不难发现现在的架构是强依赖nativemodule,也就是大家通常说的bridge,对于简单的Native API调用来说性能还能接受,而对于UI来说,每次的操作都是需要通过bridge的,包括高度计算、更新等,且bridge限制了调用频率、只允许异步操作,导致一些前端的更新很难及时反应到UI上,特别是类似于滑动、动画,更新频率较高的操作,所以经常能看到白屏或者卡顿。

  1. 新架构设计

旧的架构JS层与Native的通讯都太依赖bridge,导致一些通讯频率较高的交互和设计就很难实现,同时也影响了渲染性能,这就是Facebook这次重构的主要目标,在新的设计上,ReactNative提出了几个新的概念和设计:

  1. JSI(javascript interface):这是本次架构重构的核心重点,也正是因为这层的调整,将原有重度依赖的native bridge架构解耦,实现了自由通讯。
  2. Fabric:依赖JSI的设计,并将旧架构下的shadow tree层移到C++层,这样可以透过JSI,实现前端组件对UI组件的一对一控制,摆脱了旧架构下对于UI的异步、批量操作。
  3. TuborModule:新的原生API架构,替换了原有的java module架构,数据结构上除了支持基础类型外,开始支持JSI对象,让前端和客户端的API形成一对一的调用
  4. 社区化:在不断迭代中,facebook团队发现,开源社区提供的组件和API越来越多,而且很多组件设计和架构上比ReactNative要好,而且官方组件因为资源问题,投入度并不够,对于一些社区问题的反馈,响应和解决问题也不太及时。社区化后,大量的系统组件会开放到社区中,交个开发者维护,例如现在的webview组件

上面这些概念其实在架构图上已经体现了,主要用于替换原有的bridge设计,下面我们将重点剖析这些模块的原理和作用:

JSI :

JSI在0.60后的版本就已经开始支持,它是Facebook在js引擎上设计的一个适配架构,允许我们向 Javascript 运行时注册方法的 Javascript 接口,这些方法可通过 Javascript 世界中的全局对象获得,可以完全用 C++ 编写,也可以作为一种与 iOS 上的 Objective C 代码和 Android 中的 Java 代码进行通信的方式。任何当前使用Bridge在 Javascript 和原生端之间进行通信的原生模块都可以通过用 C++ 编写一个简单的层来转换为 JSI 模块

  • 标准化的JS引擎接口,ReactNative可以替换v8、Hermes等引擎。
  • 它是架起 JS 和原生 java 或者 Objc 的桥梁,类似于老的 JSBridge架构的作用,但是不同的是采用的是内存共享、代理类的方式,JS所有的运行环境都是在 JSRuntime 环境下的,为了实现和 native 端直接通讯,我们需要有一层 C++ 层实现的 JSI::HostObject,该数据结构只有 get、set 两个接口,通过 prop 来区分不同接口的调用。
  • 原有JS与Native的数据沟通,更多的是采用json和基础类型数据,但有了JSI后,数据类型更丰富,支持JSI object。

所以API调用流程: JS->JSI->C++->JNI->JAVA,每个API更加独立化,不再全部依赖native module,但这也带来了另外一个问题,相比以前的设计更复杂了,设计一个API,开发者需要封装JS、C++、JNI、Java等一套接口。当然Facebook早已经想到了这个问题,所以在设计JSI的时候,就提供了一个codegen模块,帮忙大家完成基础代码和环境的搭建,以下我们会简单为大家介绍怎么使用这些工具:

  1. Facebook提供了一个脚手架工程,方便大家创建Native Module 模块,需提前增加npx命令
npx create-react-native-library react-native-simple-jsi

前面的步骤更多的是在配置一些模块的信息,值得注意的是在选择模块的开发语言时要注意,这边是支持很多种类型的,针对原生端开发我们用Java&OC比较多,也可以选择纯JS 或者C++的类型,大家根据自己的实际情况来选择,完成后需要选择是UI模块还是API模块,这里我们选择API(Native Module)来做测试:

以上是完成后的目录结构,大家可以看到这是个完整的ReactNative App工程,相应的API需要开发者在对应的Android、iOS目录中开发。

下面我们看下C++ Moulde的模式,相比Java模式,多了cpp 模块,并在Moudle中以Native lib的方式加载so:

  1. 其实到这里我们还是没有创建JSI的模块,删掉删掉example目录后,运行下面命令,完成后在Android studio中导入 example/android,编译后app 工程,就能打包我们cpp目录下的C++文件到so
npx react-native init example
cd example
yarn add ../

  1. 到这里我们完成了C++库的打包,但是不是我们想要的JSI Module,需要修改Module模块,代码如下,从代码中我们可以看到,不再有reactmethod标记,而是直接的一些install方法,在这个JSI Module 创建的时候调用注入环境
public class NewswiperJsiModule extends ReactContextBaseJavaModule {
    public static final String NAME = "NewswiperJsi";
    public NewswiperJsiModule(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @Override
    @NonNull
    public String getName() {
        return NAME;
    }

    static {
        try {
            // Used to load the 'native-lib' library on application startup.
 System.loadLibrary("cpp");
        } catch (Exception ignored) {
        }
    }

  private native void nativeInstall(long jsi);

  public void installLib(JavaScriptContextHolder reactContext) {
    if (reactContext.get() != 0) {
      this.nativeInstall(
        reactContext.get()
      );
    } else {
      Log.e("SimpleJsiModule", "JSI Runtime is not available in debug mode");
    }
  }
}
public class SimpleJsiModulePackage implements JSIModulePackage {
 @Override
     public List<JSIModuleSpec> getJSIModules(ReactApplicationContext reactApplicationContext, JavaScriptContextHolder jsContext) {
    reactApplicationContext.getNativeModule(SimpleJsiModule.class).installLib(jsContext);
    return Collections.emptyList();
    }
}
  1. 后面就是我们要创建JSI Object了,用来直接和JS通讯,主要是通过createFromHostFunction 来创建JSI的代理对象,并通过global().setProperty注入到JS运行环境
void install(Runtime &jsiRuntime) {
    auto multiply = Function::createFromHostFunction(jsiRuntime,
                                                     PropNameID::forAscii(jsiRuntime,
                                                                          "multiply"),
                                                     2,
                                                     [](Runtime &runtime,
                                                        const Value &thisValue,
                                                        const Value *arguments,
                                                        size_t count) -> Value {
        int x = arguments[0].getNumber();
        int y = arguments[1].getNumber();

        return Value(x * y);

    });

    jsiRuntime.global().setProperty(jsiRuntime, "multiply", move(multiply));
global.multiply(2,4) // 8

到这里相信大家知道了怎么通过JSI完成JSIMoudle的搭建了,这也是我们TurboModule和Fabric设计的核心底层设计。

Fabric :

Fabric是新架构的UI框架,和原有UImanager框架是类似,前面章节也说明UIManager框架的一些问题,特别在渲染性能上的瓶颈,似乎基于原有架构已经很难再有优化,体验上与原生端组件和动画的渲染性能还是差距比较大的,举个比较常见的问题,Flatlist快速滑动的状态下,会存在很长的白屏时间,交互比较强的动画、手势很难支持,这也是此次架构升级的重点,下面我们也从原理上简单说明下新架构的特点:

  1. JS层新设计了FabricUIManager,目的是支持Fabric render完成组件的更新,它采用了JSI的设计,可以和cpp层沟通,对应C++层UIManagerBinding,其实每个操作和API调用都有对应创建了不同的JSI,从这里就彻底解除了原有的全部依赖UIManager单个Native bridge的问题,同时组件大小的measure也摆脱了对Java、bridge的依赖,直接在C++层shadow完成,提升渲染效率
export type Spec = {|
  +createNode: (
    reactTag: number,
    viewName: string,
    rootTag: RootTag,
    props: NodeProps,
    instanceHandle: InstanceHandle,
  ) => Node,
  +cloneNode: (node: Node) => Node,
  +cloneNodeWithNewChildren: (node: Node) => Node,
  +cloneNodeWithNewProps: (node: Node, newProps: NodeProps) => Node,
  +cloneNodeWithNewChildrenAndProps: (node: Node, newProps: NodeProps) => Node,
  +createChildSet: (rootTag: RootTag) => NodeSet,
  +appendChild: (parentNode: Node, child: Node) => Node,
  +appendChildToSet: (childSet: NodeSet, child: Node) => void,
  +completeRoot: (rootTag: RootTag, childSet: NodeSet) => void,
  +measure: (node: Node, callback: MeasureOnSuccessCallback) => void,
  +measureInWindow: (
    node: Node,
    callback: MeasureInWindowOnSuccessCallback,
  ) => void,
  +measureLayout: (
    node: Node,
    relativeNode: Node,
    onFail: () => void,
    onSuccess: MeasureLayoutOnSuccessCallback,
  ) => void,
  +configureNextLayoutAnimation: (
    config: LayoutAnimationConfig,
    callback: () => void, // check what is returned here
    // This error isn't currently called anywhere, so the `error` object is really not defined
    // $FlowFixMe[unclear-type]
    errorCallback: (error: Object) => void,
  ) => void,
  +sendAccessibilityEvent: (node: Node, eventType: string) => void,
|};

const FabricUIManager: ?Spec = global.nativeFabricUIManager;

module.exports = FabricUIManager;
if (methodName == "createNode") {
  return jsi::Function::createFromHostFunction(
      runtime,
      name,
      5,
      [uiManager](
          jsi::Runtime &runtime,
          jsi::Value const &thisValue,
          jsi::Value const *arguments,
          size_t count) noexcept -> jsi::Value {
        auto eventTarget =
            eventTargetFromValue(runtime, arguments[4], arguments[0]);
        if (!eventTarget) {
          react_native_assert(false);
          return jsi::Value::undefined();
        }
        return valueFromShadowNode(
            runtime,
            uiManager->createNode(
                tagFromValue(arguments[0]),
                stringFromValue(runtime, arguments[1]),
                surfaceIdFromValue(runtime, arguments[2]),
                RawProps(runtime, arguments[3]),
                eventTarget));
      });
}
  1. 有了JSI后,以前批量依赖bridge的UI操作,都可以同步的执行到c++层,而在c++层,新架构完成了一个shadow层的搭建,而旧架构是在java层实现,以下也重点说明下几个重要的设计:
  • FabricUIManager (JS,Java) ,JS 端和原生端 UI 管理模块。
  • UIManager/UIManagerBinding(C++),C++中用来管理UI的模块,并通过binding JNI的方式通过FabricUIManager(Java)管理原生端组件
  • ComponentDescriptor (C++) ,原生端组件的唯一描述及组件属性定义,并注册在CoreComponentsRegistry模块中
  • Platform-specific
  • Component Impl (Java,ObjC++),原生端组件Surface,通过FabricUIManager来管理

  1. 新架构下,开发一个原生组件,需要完成Java层的原生组件及ComponentDescriptor (C++) 开发,难度相较于原有的viewManager有所提升,但ComponentDescriptor本身很多是shadow层代码,比较固定,Facebook后续也会提供codegen工具,帮助大家完成这部分代码的自动生成,简化代码难度

TurboModule:

实际上0.64版本已经支持TurboModule,在分析它的设计原理前,我们先说明下设计这个模块的目的,从上面架构图来看,主要用来替换NativeModule的重要一环:

  1. NativeModule 会包含很多我们初始化过程中就需要注册的的API,随着开发迭代,依赖NativeMoude的API和package会越来越多,解析及校验这些pakcages的时间会越来越长,最终会影响TTI时长
  2. 另外Native module其实大部分都是提供API服务,其实是可以采用单例子模式运行的,而不用跟随bridge的关闭打开,创建很多次

TurboModule的设计就是为了解决这些问题,原理上还是采用JSI提供的能力,方便JS可以直接调用到c++ 的host object,下面我们从代码层简单分析原理:

上面代码就是目前项目里面给出的一个例子,通过实现TurboModule来完NativeModule的开发,其实代码流程和原有的BaseJavaModule大致是一样的,不同的是底层的实现:

  1. 现有版本可以通过 ReactFeatureFlags.useTurboModules来打开这个模块功能
  2. TurboModule 组件是通过TurboModuleManager.java来管理的,被注入的modules可以分为初始化加载的和非初始化加载的组件
  3. 同样JNI/C++层也有一层TurboModuleManager用来管理注册java/C++的module,并通过TurboModuleBinding C++层的proxy moudle注入到JS层,到这里基本就和上面说的基础架构JSI接上轨了,js中可以通过代理的__turboModuleProxy来完成c++层的module调用,c++层透过jni最终完成对java代码的执行,这里facebook设计了两种类型的moudles,longLivedObject 和 非常驻的,设计思路上就和我们上面要解决的问题吻合了
void TurboModuleBinding::install(
    jsi::Runtime &runtime,
    const TurboModuleProviderFunctionType &&moduleProvider) {
  runtime.global().setProperty(
      runtime,
      "__turboModuleProxy",
      jsi::Function::createFromHostFunction(
          runtime,
          jsi::PropNameID::forAscii(runtime, "__turboModuleProxy"),
          1,

          // Create a TurboModuleBinding that uses the global
          // LongLivedObjectCollection
          [binding =
               std::make_shared<TurboModuleBinding>(std::move(moduleProvider))](
              jsi::Runtime &rt,
              const jsi::Value &thisVal,
              const jsi::Value *args,
              size_t count) {
            return binding->jsProxy(rt, thisVal, args, count);
          }));
}
const NativeModules = require('../BatchedBridge/NativeModules');
import type {TurboModule} from './RCTExport';
import invariant from 'invariant';

const turboModuleProxy = global.__turboModuleProxy;

function requireModule<T: TurboModule>(name: string): ?T {
  // Bridgeless mode requires TurboModules
  if (!global.RN$Bridgeless) {
    // Backward compatibility layer during migration.
    const legacyModule = NativeModules[name];
    if (legacyModule != null) {
      return ((legacyModule: $FlowFixMe): T);
    }
  }

  if (turboModuleProxy != null) {
    const module: ?T = turboModuleProxy(name);
    return module;
  }

  return null;
}

CodeGen:

  1. 新架构UI增加了C++层的shadow、component层,而且大部分组件都是基于JSI,因而开发UI组件和API的流程更复杂了,要求开发者具有c++、JNI的编程能力,为了方便开发者快速开发Facebook也提供了codegen工具,帮助生成一些自动化的代码,具体工具参看:github.com/facebook/re…
  2. 以下是代码生成的大概流程,因codegen目前还没有正式release,关于如何使用的文档几乎没有,但也有开发者尝试使用生成了一些代码,可以参考github.com/karol-biszt…

  1. 总结:

上面我们从API、UI角度重新学习了新架构,JSI、Turbormodule已经在最新的版本上已经可以体验,而且开发者社区也用JSI开发了大量的API组件,例如以下的一些比较依赖C++实现的模块:

从最新的代码结构来看,新架构离发布似乎已经进入倒计时了,作为一直潜心学习、研究ReactNative的开发者相信一定和我一样很期待,从Facebook官方了解到Facebook App已经采用了新的架构,预计今年应该就能正式release了,这一次我们可以相信ReactNative应该要正式进入1.0版本了吧,reactnative.dev/blog/2021/0…

字节跳动团队很早就在游戏和app中尝试了采用ReactNative开发方案,整体开发、迭代效率、收益都有很大的提升,同样我们也在持续关注ReactNative的新架构动态,相信整体方案、性能会越来越好,也期待快速迁移到新架构。