react native 手势动画性能分析

背景

希望了解react native手势动画处理时是否因为跨语言调用产生卡顿。

结论

reactnative作为js/java混合交互的代表应用,确实在手势动画场景下表现出了明显的性能损失。考虑到reactnative已经采用了 异步批处理+offload 两种主流方式对跨语言调用的性能损失进行控制,可以认为当前的常规手段无法解决跨语言的性能瓶颈问题。

基本逻辑介绍

react native 中基本的业务逻辑在js端运算,其响应基本外部事件(如点击、拖动等)并产生界面响应的基本流程如下:
1 native端获取输入event,将其转发给js端
2 js端执行event对应的响应逻辑
3 js端分析响应后UI 元素是否发生变化,如有变化,需要生成绘制新UI所需的命令
4 js端将绘制新UI的命令(可能很多条)打包转换为其内部的folly:dynamic结构(Libraries/BatchedBridge/MessageQueue.js中的enqueueNativeCall),然后将其通过c++实现的jsbridge传递给native端
5 native端解包后,按照其描述逐一调用UI绘制函数
native 端是独立的线程(名称可能为mqt_native_module), js端也是独立的线程(名称可能为mqt_js)。js端也可以在本线程内利用该打包机制直接调用native函数。

除开少数的同步调用外,js与native端的通信基本都是利用这种打包后批处理的方式。
传参的实现可参考ReactCommon/cxxreact/NativeToJsBridge.cpp(class JsToNativeBridge/class NativeToJsBridge)和 MethodCall.cpp等代码。MessageQueue.js中会通过nativeFlushQueueImmediate关联到JsToNativeBridge。

手势动画实例分析

使用reno 10倍变焦版本手机,开发环境为 win10 WSL ubuntu20.04环境(node-v12.14.1-linux-x64)。

建立 reactnative 实例工程

仅体验reactnative的话,安装expo这个应用来引入reactnative很方便。expo是一个android上的应用程序,安装后提供扫描二维码直接下载并部署reactnative程序的能力,结合webide来体验代码效果非常方便。
但是我们需要进一步分析,尤其需要对js部分代码进行profiling,这在expo框架下不可行(需要修改android封装部分的配置,从jsc引擎切换到Hermes引擎后才有profiling能力)。
参考https://reactnative.dev/docs/environment-setup, 安装android相关开发工具后,使用单条命令即可初始化一个reactnative工程

1
npx react-native init AwesomeProject

进入AwesomeProject目录后,执行如下命令启动react-native的后台服务(不要关闭,后续可在该shell按r强制更新程序,按d打开调试窗口,并查看console.log输出的结果)。

1
npx react-native start

然后打开另外一个shell,进入AwesomeProject目录后,执行如下命令编译工程并启动应用。

1
npx react-native run-android

如果adb 已经能正常连接手机,此时手机上应该可以启动这个应用了(adb版本如果不对,可以用PATH进行控制)。

植入手势应用

https://reactnative.dev/docs/panresponder 这里有最简单的手势响应示例,但是由于其太简单,整个手势响应相对还是很流畅的(极慢速度下拖动仍可能偶现卡顿,但是经过分析,这个与语言屏障关系不大)。

为了使得分析和展示更容易进行,从 https://docs.swmansion.com/react-native-gesture-handler/docs/api/gesture-handlers/pan-gh 的手势示例中,选择https://snack.expo.io/@adamgrzybowski/react-native-gesture-handler-demo 的chatHeads 这个稍微复杂一些的手势拖动组件进行进一步的评估。

参考https://docs.swmansion.com/react-native-gesture-handler/docs/ 中的介绍。
在AwesomeProject目录使用如下命令安装该手势处理基础组件。

1
npm install --save react-native-gesture-handler

然后按照网页上的要求修改AwesomeProject/android/app/src/main/java/com/awesomeproject/MainActivity.java。
这步修改一定要做,否则事件传不到js端,屏幕上的组件不会响应手势。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.swmansion.gesturehandler.react.example;

import com.facebook.react.ReactActivity;
+ import com.facebook.react.ReactActivityDelegate;
+ import com.facebook.react.ReactRootView;
+ import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
public class MainActivity extends ReactActivity {

@Override
protected String getMainComponentName() {
return "Example";
}
+ @Override
+ protected ReactActivityDelegate createReactActivityDelegate() {
+ return new ReactActivityDelegate(this, getMainComponentName()) {
+ @Override
+ protected ReactRootView createRootView() {
+ return new RNGestureHandlerEnabledRootView(MainActivity.this);
+ }
+ };
+ }
}

然后将前面chatHeads的js代码拷贝到AwesomeProject中,就可以用标准的react方式引入并使用了。由于我们不需要其他内容,可以直接修改App的返回值,只留下拖动手势的组件,如下代码所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//chatHeads的代码放入gest_test.js
import Gest_test from './gest_test.js';
...
const App: () => Node = () => {
const isDarkMode = useColorScheme() === 'dark';

const backgroundStyle = {
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
};
return (
<Animated.View>
<Gest_test></Gest_test>
</Animated.View>

);
};

为了进一步凸显效果,可以修改前面添加的gest_test.js,增加拖动手势组件中的物体数量。由于修改代码较多,且和本文内容相关性不强,不再赘述。完整的工程可参考附件中的代码包。

对手势应用进行性能分析

使用如下方式将程序编译为发布版启动(不加–variant=release为调试模式,js端性能会下降),然后拖动手势组件头部的圆形图片,进行初步的人工性能评估。

1
npx react-native run-android --variant=release

在运动时,可以观察到较为明显的跳帧,尾部圆形图片的运行流畅性最差。

使用reactnative自带工具查看fps

为了进行详细分析,去掉variant后使用“npx react-native run-android”将程序再编译为调试模式,以便我们可以启动调试功能。
然后在”npx react-native start”启动的shell中按下d键,手机上会弹出调试菜单。选择Show Perf Monitor后,应用右上角会出现fps数值。
拖动图片时观察fps,确实有明显的掉帧,JS的fps在40左右(拖动结束的回弹动画,fps会掉到30以下。),UI的fps基本维持在60。这一现象非常明确的指出性能问题是由于js处理不及时导致的。

使用android性能分析工具明确瓶颈

为了更加明确的定位,按照reactnative教程https://reactnative.dev/docs/profiling,使用android性能分析工具进一步进行定位。
按照教程的建议,使用如下命令构建出release版本(Android分析不需要js端配合),以便准确分析性能瓶颈。

1
npx react-native run-android --variant=release

参考https://perfetto.dev/docs/quickstart/android-tracing 这里的方法,使用如下命令即可完成性能测量。

1
2
3
4
adb shell setprop persist.traced.enable 1
curl -O https://raw.githubusercontent.com/google/perfetto/master/tools/record_android_trace
chmod u+x record_android_trace
./record_android_trace -o ./perf_trace -t 10s sched freq idle am wm gfx view

完成后,可以在弹出的网页中看到mqt_js线程和mqt_native_module线程出现了大块执行时间,占用时间可能高达10ms以上。
选择左侧菜单栏的Switch to legacy UI,可以更容易看到这mqt_js对帧绘制的影响,如下图所示。

图中非常清晰的展示了丢帧的过程,mqt_js是js计算和转发绘制命令的主线程,mqt_native_module 是接受绘制命令的native端线程,这两个部分处理时间太长,经常穿过16ms的渲染时限边界。这样的超时导致native端的渲染流程认为本次16ms内界面没有变化,无需重新渲染。但是实际动画过程没有更新,用户会感知到UI出现不流畅。这与前面reactnative fps工具中给出的js fps下降而UI fps维持60的结果完全吻合。

使用reactnative自带工具对js部分进行profile

为了明确mqt_js/mqt_native_module的性能问题是否与语言屏障有关,可以进一步对js代码的执行时间进行profile。
参考https://reactnative.dev/docs/profile-hermes,可以将reactnative从jsc切换到hermes引擎,以便获得更加详细的js profile信息。
参考https://reactnative.dev/docs/hermes 中的信息,修改android/app/build.gradle,将enableHermes改为true(bundleInDebug: true 本次没有添加。按照说明,这个选项可以添加js的行号信息,但是实测时遇到了偶发的数据紊乱问题。另外,可能由于是WSL的问题,即使添加这个选项出来的js行号也是bundle以后的,不能对应到原始js,可用性较差。)。
然后在AwesomeProject下执行如下命令,即可完成切换(示例中没有用ProGuard,可以不用修改proguard-rules.pro)

1
2
cd android && ./gradlew clean
npx react-native run-android

待程序在手机启动后,在”npx react-native start”所在的shell中按d键,手机端应用会弹出debug菜单,选择Enable Sampling Profiler。
拖动图片完成测试,然后再次按d键调出debug菜单,选择Disable Sampling Profiler(偶发程序崩溃,可以再试一次)后,日志就写入了手机存储中(通常在/data/user/0/com.appName/cache/*.cpuprofile)。
日志生成后,在AwesomeProject下执行如下命令,我们就得到了可以用chrome dev工具打开的性能数据文件。

1
npx react-native profile-hermes

命令执行完后,命令行会打印出当前得到的性能日志json文件名称。打开chrome浏览器,crtl+shift+i打开开发者工具,选择performance一栏,再点击左上角的Load profile按钮,选择前面得到的json即可。
从性能图上,可以获得如下一些有意义的信息:

  • 在本次的演示中,js端只有两个主要的入口函数receiveEvent和callTimers。前者用于对native发过来的事件进行响应,后者用于响应定时器上的handler。
  • 在本次的演示中,receiveEvent 和 callTimers 都可能涉及对动画对象进行更新,最终调用到setNativeProps去更新naitve状态。
  • 与native的交互消耗了非常多的时间,重复测量了三次,enqueueNativeCall这个单纯用于打包native调用的函数耗时(总耗时)都超过了35%。
  • 调用栈中出现了c++函数和js函数多次混杂的情况,通常的js调用栈都含有接近10个c++函数。

js的profile信息进一步支撑了前面的分析结论,在复杂交互的场景下reactnative确实因为语言屏障产生了卡顿。

原生动画对比

为了获得更为完整全面的信息,可以尝试对比同样的手势动画场景下原生android程序的表现。
参考https://blog.csdn.net/cunchi4221/article/details/107477259 找到了android的手势拖动示例 https://github.com/journaldev/journaldev/tree/master/Android/AndroidSpringAnimations。
修改代码使得该用例尽可能接近js示例的逻辑,主要包括删除不需要的组件,被拖动的图案从button切换为圆环图片,调整图片大小,增加被拖动的图片数量,调整spring的参数等等。由于相关修改与本文内容关系不大,不再赘述,具体代码可参考附件。
由于两边spring算法不完全一致,android的原生实现与前面reactnative的示例运动轨迹无法完全对齐。但是,动画效果和其代码实现的逻辑是基本对齐的。从实际测试的结果看,android原生实现流畅度明显优于reactnative版本,没有掉帧。

讨论

两种带VM的语言交互更容易受到 VM<->native 调用消耗的影响

原理分析

两种带VM的语言交互(如js与java)时,由于没有直接的通信桥梁,所有交互都需要经过native中转,单次交互实际上需要跨越两次语言屏障(js->native->java)。即使可以通过 缓存+批处理 减少两次跨越语言屏障的影响,每一次调用仍然会新增一次VM与native的交互。以js函数调用100次java函数为例,带缓存的实现方式如下。

1
2
3
4
//第一步,js可以将100次调用压入native实现的缓存buf中,这里的100次操作没有跨语言性能损失
js ---> native_buf
//第二步,当缓存满或者timer超时,native_buf要通过JNI机制调用java函数。这里100次函数调用就有100次JNI损失。
native_buf ---> java

可以看到,即使通过缓存,100次js->java的调用仍然会产生100次JNI损失,批处理极限也就能减少一半的性能损失(js->native的损失被消除了,这里忽略了js把调用命令压入native_buf的性能损失)。
除了方法调用外,两种语言交互时数据读写成本也大幅度增加。例如js端希望修改某java类的某字段,必然会至少新增一次JNI调用。

根据上面的分析,可以明确两种带VM语言的交互,不可避免将新增大量的跨语言性能损失。整个程序的性能将更容易受到VM<->native屏障的影响。

reactnative实际情况分析

使用simpleperf对reactnative应用进行打点分析时,发现其art jni相关流程的占比确实明显高于原生android程序。如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
4.05%     mqt_native_modu  6670  7016  /apex/com.android.runtime/lib64/libart.so                                                    art::(anonymous namespace)::CheckJNI::DeleteRef(char const*, _JNIEnv*, _jobject*, art::IndirectRefKind)
3.57% mqt_native_modu 6670 7016 /apex/com.android.runtime/lib64/libart.so art::(anonymous namespace)::ScopedCheck::CheckPossibleHeapValue(art::ScopedObjectAccess&, char, art::(anonymous namespace)::JniValueType)
3.19% mqt_native_modu 6670 7016 /apex/com.android.runtime/lib64/libart.so art::IndirectReferenceTable::GetChecked(void*) const
2.95% mqt_native_modu 6670 7016 /apex/com.android.runtime/lib64/libart.so art::(anonymous namespace)::CheckJNI::ExceptionCheck(_JNIEnv*)
2.82% mqt_native_modu 6670 7016 /apex/com.android.runtime/lib64/libart.so art::(anonymous namespace)::ScopedCheck::Check(art::ScopedObjectAccess&, bool, char const*, art::(anonymous namespace)::JniValueType*)
2.48% mqt_native_modu 6670 7016 /apex/com.android.runtime/lib64/libart.so art::gc::Heap::IsValidObjectAddress(void const*) const
2.46% mqt_native_modu 6670 7016 /apex/com.android.runtime/lib64/libart.so art::(anonymous namespace)::CheckJNI::NewRef(char const*, _JNIEnv*, _jobject*, art::IndirectRefKind)
2.22% mqt_native_modu 6670 7016 [kernel.kallsyms] _raw_spin_unlock_irqrestore
2.13% mqt_native_modu 6670 7016 /apex/com.android.runtime/lib64/libart.so art::Thread::DecodeJObject(_jobject*) const
1.71% mqt_native_modu 6670 7016 /data/app/com.awesomeproject-Gn6y81yptgU8XGaqz9B5rQ==/lib/arm64/libreactnativejni.so @plt
1.53% mqt_native_modu 6670 7016 /apex/com.android.runtime/lib64/libart.so art::(anonymous namespace)::ScopedCheck::CheckFieldAccess(art::ScopedObjectAccess&, _jobject*, _jfieldID*, bool, art::Primitive::Type)
1.44% mqt_native_modu 6670 7016 /apex/com.android.runtime/lib64/libart.so art::(anonymous namespace)::ScopedCheck::CheckInstance(art::ScopedObjectAccess&, art::(anonymous namespace)::ScopedCheck::InstanceKind, _jobject*, bool)
1.41% mqt_native_modu 6670 7016 /apex/com.android.runtime/lib64/libart.so art::(anonymous namespace)::CheckAttachedThread(char const*)
1.30% mqt_native_modu 6670 7016 /apex/com.android.runtime/lib64/libart.so art::CodeInfo::Decode(unsigned char const*, art::CodeInfo::DecodeFlags)
1.28% mqt_native_modu 6670 7016 /apex/com.android.runtime/lib64/libart.so art::(anonymous namespace)::CheckJNI::GetField(char const*, _JNIEnv*, _jobject*, _jfieldID*, bool, art::Primitive::Type)
1.15% mqt_native_modu 6670 7016 /apex/com.android.runtime/lib64/libart.so art::IndirectReferenceTable::Remove(art::IRTSegmentState, void*)
1.04% mqt_native_modu 6670 7016 /apex/com.android.runtime/lib64/libart.so art::IndirectReferenceTable::Add(art::IRTSegmentState, art::ObjPtr<art::mirror::Object>, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >*)
0.88% mqt_native_modu 6670 7016 /apex/com.android.runtime/lib64/libart.so art::mirror::FindFieldByNameAndType(art::LengthPrefixedArray<art::ArtField>*, std::__1::basic_string_view<char, std::__1::char_traits<char> >, std::__1::basic_string_view<char, std::__1::char_traits<char> >) (.llvm.25492233093031736)
0.86% mqt_native_modu 6670 7016 /apex/com.android.runtime/lib64/bionic/libc.so pthread_getspecific
0.82% mqt_native_modu 6670 7016 /apex/com.android.runtime/lib64/libart.so art_quick_imt_conflict_trampoline

这与reactnative的调用模型吻合。即使依靠批处理基本消除了js到java语言传递消耗,reactnative的绘图线程需要先从native层解包js打包好的数据,准备好android java api所需的参数后,再利用JNI去启动java 函数完成绘图过程。
从profile获得的函数调用关系可以看得比较清楚,com.facebook.react.uimanager.UIManagerModule.updateView 一直调用到 addDynamicToJArray完成了对folly:dynamic的解包。

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
63
83.69%    0.02%  mqt_native_modu  6670  7016  /data/app/com.awesomeproject-Gn6y81yptgU8XGaqz9B5rQ==/lib/arm64/libreactnativejni.so         facebook::jni::detail::FunctionWrapper...
|
-- facebook::jni::detail::FunctionWrapper...
|
|--99.45%-- facebook::jni::detail::MethodWrapper...
| |--0.28%-- [hit in function]
| |
| |--90.82%-- libreactnativejni.so[+711b0]
| | |
| | |--73.59%-- libreactnativejni.so[+7b620]
| | | |
| | | |--99.98%-- _JNIEnv::CallVoidMethod(_jobject*, _jmethodID*, ...)
| | | | |--0.16%-- [hit in function]
| | | | |
| | | | --99.84%-- art::(anonymous namespace)::CheckJNI::CallVoidMethodV(_JNIEnv*, _jobject*, _jmethodID*, std::__va_list)
| | | | |--0.05%-- [hit in function]
| | | | |
| | | | |--99.94%-- art::(anonymous namespace)::CheckJNI::CallMethodV(char const*, _JNIEnv*, _jobject*, _jclass*, _jmethodID*, std::__va_list, art::Primitive::Type, art::InvokeType)
| | | | | |--0.29%-- [hit in function]
| | | | | |
| | | | | |--97.94%-- art::JNI::CallVoidMethodV(_JNIEnv*, _jobject*, _jmethodID*, std::__va_list)
| | | | | | |--0.13%-- [hit in function]
| | | | | | |
| | | | | | --99.87%-- art::InvokeVirtualOrInterfaceWithVarArgs(art::ScopedObjectAccessAlreadyRunnable const&, _jobject*, _jmethodID*, std::__va_list)
| | | | | | |--0.16%-- [hit in function]
| | | | | | |
| | | | | | |--99.57%-- art::(anonymous namespace)::InvokeWithArgArray(


....

59.07% 0.09% mqt_native_modu 6670 7016 /data/app/com.awesomeproject-Gn6y81yptgU8XGaqz9B5rQ==/lib/arm64/libhermes-inspector.so _JNIEnv::CallVoidMethod(_jobject*, _jmethodID*, ...)
|
-- _JNIEnv::CallVoidMethod(_jobject*, _jmethodID*, ...)
|
--99.85%-- art::(anonymous namespace)::CheckJNI::CallVoidMethodV(_JNIEnv*, _jobject*, _jmethodID*, std::__va_list)
|--99.93%-- art::(anonymous namespace)::CheckJNI::CallMethodV(char const*, _JNIEnv*, _jobject*, _jclass*, _jmethodID*, std::__va_list, art::Primitive::Type, art::InvokeType)
| |--97.98%-- art::JNI::CallVoidMethodV(_JNIEnv*, _jobject*, _jmethodID*, std::__va_list)
| | --99.88%-- art::InvokeVirtualOrInterfaceWithVarArgs(art::ScopedObjectAccessAlreadyRunnable const&, _jobject*, _jmethodID*, std::__va_list)
| | |--99.55%-- art::(anonymous namespace)::InvokeWithArgArray(art::ScopedObjectAccessAlreadyRunnable const&, art::ArtMethod*, art::(anonymous namespace)::ArgArray*, art::JValue*, char const*)
| | | --99.73%-- art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)
| | | |
| | | --99.99%-- art_quick_invoke_stub
| | | |--94.03%-- com.facebook.react.bridge.JavaModuleWrapper.invoke
| | | | |--99.21%-- com.facebook.react.bridge.JavaMethodWrapper.invoke
| | | | | |--45.96%-- art_jni_trampoline
| | | | | | |--99.52%-- art::Method_invoke(_JNIEnv*, _jobject*, _jobject*, _jobjectArray*)
| | | | | | | |--99.89%-- art::InvokeMethod(art::ScopedObjectAccessAlreadyRunnable const&, _jobject*, _jobject*, _jobject*, unsigned long)
| | | | | | | | |--95.40%-- art::(anonymous namespace)::InvokeWithArgArray(art::ScopedObjectAccessAlreadyRunnable const&, art::ArtMethod*, art::(anonymous namespace)::ArgArray*, art::JValue*, char const*)
| | | | | | | | | |--99.43%-- art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)
| | | | | | | | | | --99.85%-- art_quick_invoke_stub
| | | | | | | | | | |--83.48%-- com.facebook.react.uimanager.UIManagerModule.updateView
| | | | | | | | | | | |--98.75%-- com.facebook.react.uimanager.UIImplementation.updateView
| | | | | | | | | | | | |--84.19%-- com.facebook.react.uimanager.ReactShadowNodeImpl.updateProperties
| | | | | | | | | | | | | |--99.49%-- com.facebook.react.uimanager.ViewManagerPropertyUpdater.updateProps
| | | | | | | | | | | | | | |--93.53%-- com.facebook.react.bridge.ReadableNativeMap.getEntryIterator
| | | | | | | | | | | | | | | |--96.92%-- com.facebook.react.bridge.ReadableNativeMap.getLocalMap
| | | | | | | | | | | | | | | | |--93.93%-- art_jni_trampoline
| | | | | | | | | | | | | | | | | |--60.47%-- facebook::jni::detail::FunctionWrapper....
| | | | | | | | | | | | | | | | | | |--99.73%-- facebook::jni::detail::WrapForVoidReturn...
| | | | | | | | | | | | | | | | | | | |--97.01%-- facebook::jni::detail::MethodWrapper...
| | | | | | | | | | | | | | | | | | | | |--78.06%-- facebook::react::ReadableNativeMap::importValues()
| | | | | | | | | | | | | | | | | | | | | |--84.86%-- facebook::react::addDynamicToJArray...

通过在JNI调用中植入延迟进行实际验证

通过在art_jni_trampoline植入nop,可以模拟JNI接口性能变差时的场景。实际测试reactnative的应用确实更容易受到JNI接口性能恶化的影响,支持了前面的分析结论。具体效果可参考下面的视频。
第一个视频为reactnative无干扰时运行的效果

第二个视频为reactnative在单次JNI引入16.8us延迟后的效果。这个延迟相当于把JNI的损耗放大了200倍,对系统的冲击是比较大的。此时luncher在滑动切换桌面时也会出现明显的卡顿。注意到加入JNI延迟后,UI的fps明显下降,同时js的fps也有明显的下降,说明js线程除开绘图动作外还有大量的native调用。

第三个是android原生手势动画的效果,同样在单次JNI引入16.8us后。可以看到其运行仍然是非常流畅的。这也侧面说明响应手势动画本来应该是一个轻载场景(因为此时luncher都已经开始卡顿了,但手势动画仍然很流畅),但是reactinative中由于语言屏障的影响变成了重载场景。

reactnative 多语言交互改进空间讨论

参考https://reactnative.dev/blog/2017/02/14/using-native-driver-for-animated ,reactnative 也可以将动画offload到native端,其本质也是将确定的动画响应过程传递到native端进行计算,避免进行频繁的js/native交互。这个思路和阿里的bindingx一致 (https://github.com/alibaba/bindingx)。这种做法是有限制的,如果卸载设计比较简单,则手势响应这种复杂交互的场景就无法支持。如果卸载设计比较复杂,就丧失了js开发效率的优势。目前看来,移动负载这样的解决方案并不具有太大的吸引力。bindingx已经被废弃了,reactnative 直到现在仍未支持手势交互动画的offload。

从进一步提高异步批处理性能的方向看,一个自然的想法是在跳过native到VM的转换,直接在VM语言中进行完成命令打包和解包(例如js打包,java解包执行)。已经有人这么尝试过了,参考 https://drive.google.com/file/d/1Pqn7aOZ7GyEnegNUzwyl-_KnNxw70UhC/view 。结果显示,即使使用protobuf这样的重度优化后的底层协议,并经过专门的调整优化,也很难追上JNI的性能。目前看起来,继续通过这条路径获得性能提升的可能性很小。

一些推断

应用程序中两种VM语言频繁交互时,将会带来明显的性能损失。
reactnative作为js/java混合交互的代表应用,确实在一些场景下表现出了明显的性能损失(如本文展示的手势动画)。考虑到reactnative已经采用了 异步批处理+offload 两种主流方式对跨语言调用的性能损失进行控制,可以认为当前的常规手段无法解决跨语言的性能瓶颈问题。

附件

https://github.com/majiang31312/gesture_demo
AndroidSpringAnimations.tar.xz 是android 版本示例,可以用android studio打开并构建。
AwesomeProject 是reactnative的示例代码,可以按照前面描述的cli工具构建部署。