客户端跨端框架已经发展了很多年了,最近比较流行的小程序、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通讯,解决了需要频繁通讯的瓶颈问题。
旧架构设计
在了解新架构前,我们还是先聊下目前的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上,特别是类似于滑动、动画,更新频率较高的操作,所以经常能看到白屏或者卡顿。
新架构设计
旧的架构JS层与Native的通讯都太依赖bridge,导致一些通讯频率较高的交互和设计就很难实现,同时也影响了渲染性能,这就是Facebook这次重构的主要目标,在新的设计上,ReactNative提出了几个新的概念和设计:
- JSI(javascript interface):这是本次架构重构的核心重点,也正是因为这层的调整,将原有重度依赖的native bridge架构解耦,实现了自由通讯。
- Fabric:依赖JSI的设计,并将旧架构下的shadow tree层移到C++层,这样可以透过JSI,实现前端组件对UI组件的一对一控制,摆脱了旧架构下对于UI的异步、批量操作。
- TuborModule:新的原生API架构,替换了原有的java module架构,数据结构上除了支持基础类型外,开始支持JSI对象,让前端和客户端的API形成一对一的调用
- 社区化:在不断迭代中,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模块,帮忙大家完成基础代码和环境的搭建,以下我们会简单为大家介绍怎么使用这些工具:
- 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:
- 其实到这里我们还是没有创建JSI的模块,删掉删掉example目录后,运行下面命令,完成后在Android studio中导入 example/android,编译后app 工程,就能打包我们cpp目录下的C++文件到so
npx react-native init example
cd example
yarn add ../
- 到这里我们完成了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();
}
}
- 后面就是我们要创建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快速滑动的状态下,会存在很长的白屏时间,交互比较强的动画、手势很难支持,这也是此次架构升级的重点,下面我们也从原理上简单说明下新架构的特点:
- 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));
});
}
- 有了JSI后,以前批量依赖bridge的UI操作,都可以同步的执行到c++层,而在c++层,新架构完成了一个shadow层的搭建,而旧架构是在java层实现,以下也重点说明下几个重要的设计:
- FabricUIManager (JS,Java) ,JS 端和原生端 UI 管理模块。
- UIManager/UIManagerBinding(C++),C++中用来管理UI的模块,并通过binding JNI的方式通过FabricUIManager(Java)管理原生端组件
- ComponentDescriptor (C++) ,原生端组件的唯一描述及组件属性定义,并注册在CoreComponentsRegistry模块中
- Component Impl (Java,ObjC++),原生端组件Surface,通过FabricUIManager来管理
- 新架构下,开发一个原生组件,需要完成Java层的原生组件及ComponentDescriptor (C++) 开发,难度相较于原有的viewManager有所提升,但ComponentDescriptor本身很多是shadow层代码,比较固定,Facebook后续也会提供codegen工具,帮助大家完成这部分代码的自动生成,简化代码难度
TurboModule:
实际上0.64版本已经支持TurboModule,在分析它的设计原理前,我们先说明下设计这个模块的目的,从上面架构图来看,主要用来替换NativeModule的重要一环:
- NativeModule 会包含很多我们初始化过程中就需要注册的的API,随着开发迭代,依赖NativeMoude的API和package会越来越多,解析及校验这些pakcages的时间会越来越长,最终会影响TTI时长
- 另外Native module其实大部分都是提供API服务,其实是可以采用单例子模式运行的,而不用跟随bridge的关闭打开,创建很多次
TurboModule的设计就是为了解决这些问题,原理上还是采用JSI提供的能力,方便JS可以直接调用到c++ 的host object,下面我们从代码层简单分析原理:
上面代码就是目前项目里面给出的一个例子,通过实现TurboModule来完NativeModule的开发,其实代码流程和原有的BaseJavaModule大致是一样的,不同的是底层的实现:
- 现有版本可以通过 ReactFeatureFlags.useTurboModules来打开这个模块功能
- TurboModule 组件是通过TurboModuleManager.java来管理的,被注入的modules可以分为初始化加载的和非初始化加载的组件
- 同样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:
- 新架构UI增加了C++层的shadow、component层,而且大部分组件都是基于JSI,因而开发UI组件和API的流程更复杂了,要求开发者具有c++、JNI的编程能力,为了方便开发者快速开发Facebook也提供了codegen工具,帮助生成一些自动化的代码,具体工具参看:github.com/facebook/re…
- 以下是代码生成的大概流程,因codegen目前还没有正式release,关于如何使用的文档几乎没有,但也有开发者尝试使用生成了一些代码,可以参考github.com/karol-biszt…
总结:
上面我们从API、UI角度重新学习了新架构,JSI、Turbormodule已经在最新的版本上已经可以体验,而且开发者社区也用JSI开发了大量的API组件,例如以下的一些比较依赖C++实现的模块:
从最新的代码结构来看,新架构离发布似乎已经进入倒计时了,作为一直潜心学习、研究ReactNative的开发者相信一定和我一样很期待,从Facebook官方了解到Facebook App已经采用了新的架构,预计今年应该就能正式release了,这一次我们可以相信ReactNative应该要正式进入1.0版本了吧,reactnative.dev/blog/2021/0…