背景
希望了解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输出的结果)。
然后打开另外一个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
| 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工具构建部署。