尝试了用Markdown写PPT,阅读效果更佳,欢迎品尝:http://www.vmfor.com/md/2019-01-10ed7ba094-ae05-42c7-a998-8b3d5d31f46c.md
[slide style="background-image:url('/img/4.jpg')"]
Android-RN 应用程序
混合开发
热更新
麦文昌 2019-01
[slide style="background-color:#31456A"]
前言
- 原生APP在性能方面具有优势,而ReactNative更加灵活 {:&.fadeIn}
- 我们将逐渐把APP往原生方面过渡,包括第三方库以及重要的逻辑和界面。
- Native层提供React原生模块以供React层使用,包括API,UI和后台任务。
[slide style="background-color:#31456A"]
目录
- ReactNative启动流程 {:&.fadeIn}
- ReactNative混合开发
- Native模块
- 热更新
- 启动优化
- bundle文件安全
- 总结
[slide style="background-color:#31456A"]
ReactNative启动流程
{:&.zoomIn}
AppRegistry.registerComponent('cassecapp', () => cassecapp);
[slide style="background-color:#31456A"]
ReactNative启动流程
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
protected String getJSBundleFile() {//“index.android.bundle”文件的存储路径
return CodePush.getJSBundleFile();
}
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new CodePush(BuildConfig.CODEPUSH_KEY, getApplicationContext(), BuildConfig.DEBUG, "https://codepush.cassmall.com/"),
...
);
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
GrowingIO.startWithConfiguration(this, new Configuration()
.useID()
.trackAllFragments()
.setChannel("测试")
);
CrashReport.initCrashReport(getApplicationContext(), "b8b801654b", true);
}
}
[slide style="background-color:#31456A"]
ReactNative启动流程
public class MainActivity extends ReactActivity {
...
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "cassecapp";
}
...
}
[slide style="background-color:#31456A"]
ReactNative启动流程
package com.facebook.react;
public abstract class ReactActivity extends Activity
implements DefaultHardwareBackBtnHandler, PermissionAwareActivity {
private final ReactActivityDelegate mDelegate;
protected ReactActivity() {
mDelegate = createReactActivityDelegate();
}
...
/**
* Called at construction time, override if you have a custom delegate implementation.
*/
protected ReactActivityDelegate createReactActivityDelegate() {
return new ReactActivityDelegate(this, getMainComponentName());
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mDelegate.onCreate(savedInstanceState);
}
@Override
protected void onPause() {
super.onPause();
mDelegate.onPause();
}
@Override
protected void onResume() {
super.onResume();
mDelegate.onResume();
}
@Override
protected void onDestroy() {
super.onDestroy();
mDelegate.onDestroy();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
mDelegate.onActivityResult(requestCode, resultCode, data);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return mDelegate.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event);
}
...
}
[slide style="background-color:#31456A"]
ReactNative启动流程
package com.facebook.react
/**
* Delegate class for {@link ReactActivity} and {@link ReactFragmentActivity}. You can subclass this
* to provide custom implementations for e.g. {@link #getReactNativeHost()}, if your Application
* class doesn't implement {@link ReactApplication}.
*/
public class ReactActivityDelegate {
...
private final @Nullable Activity mActivity;
private final @Nullable FragmentActivity mFragmentActivity;
private final @Nullable String mMainComponentName;
private @Nullable ReactRootView mReactRootView;
public ReactActivityDelegate(Activity activity, @Nullable String mainComponentName) {
mActivity = activity;
mMainComponentName = mainComponentName;
mFragmentActivity = null;
}
protected ReactRootView createRootView() {
return new ReactRootView(getContext());
}
protected ReactNativeHost getReactNativeHost() {
return ((ReactApplication) getPlainActivity().getApplication()).getReactNativeHost();
}
public ReactInstanceManager getReactInstanceManager() {
return getReactNativeHost().getReactInstanceManager();
}
protected void onCreate(Bundle savedInstanceState) {
if (mMainComponentName != null) {
loadApp(mMainComponentName);
}
mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
}
protected void loadApp(String appKey) {
if (mReactRootView != null) {
throw new IllegalStateException("Cannot loadApp while app is already running.");
}
mReactRootView = createRootView();//ReactRootView: 加载React视图的容器
mReactRootView.startReactApplication(
//ReactInstanceManager: 负责加载JS,并且管理原生与JS通信
getReactNativeHost().getReactInstanceManager(),
appKey,//“cassecapp”
getLaunchOptions());//加载界面时的一些设置
getPlainActivity().setContentView(mReactRootView);
}
...
}
[slide style="background-color:#31456A"]
ReactNative启动流程
package com.facebook.react;
/**
* Simple class that holds an instance of {@link ReactInstanceManager}. This can be used in your
* {@link Application class} (see {@link ReactApplication}), or as a static field.
*/
public abstract class ReactNativeHost {
private final Application mApplication;
private @Nullable ReactInstanceManager mReactInstanceManager;
protected ReactNativeHost(Application application) {
mApplication = application;
}
/**
* Get the current {@link ReactInstanceManager} instance, or create one.
*/
public ReactInstanceManager getReactInstanceManager() {
if (mReactInstanceManager == null) {
ReactMarker.logMarker(ReactMarkerConstants.GET_REACT_INSTANCE_MANAGER_START);
mReactInstanceManager = createReactInstanceManager();
ReactMarker.logMarker(ReactMarkerConstants.GET_REACT_INSTANCE_MANAGER_END);
}
return mReactInstanceManager;
}
protected ReactInstanceManager createReactInstanceManager() {
ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_START);
ReactInstanceManagerBuilder builder = ReactInstanceManager.builder()
.setApplication(mApplication)//必要
.setJSMainModulePath(getJSMainModuleName())//必要,对应index.js
.setUseDeveloperSupport(getUseDeveloperSupport()//必要
.setRedBoxHandler(getRedBoxHandler())
.setJavaScriptExecutorFactory(getJavaScriptExecutorFactory())
.setUIImplementationProvider(getUIImplementationProvider())
.setJSIModulesPackage(getJSIModulePackage())
.setInitialLifecycleState(LifecycleState.BEFORE_CREATE);//必要,声明加载时机
for (ReactPackage reactPackage : getPackages()) {
builder.addPackage(reactPackage);//提供给RN交互的原生模块
}
String jsBundleFile = getJSBundleFile();
if (jsBundleFile != null) {
// 外部存储目录下的index.android.bundle文件
builder.setJSBundleFile(jsBundleFile);
} else {
// assets/index.android.bundle
builder.setBundleAssetName(Assertions.assertNotNull(getBundleAssetName()));
}
ReactInstanceManager reactInstanceManager = builder.build();
ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_END);
return reactInstanceManager;
}
}
[slide style="background-color:#31456A"]
ReactNative启动流程
{:&.zoomIn}
ReactRootView {:.highlight}
ReactInstanceManager {:.highlight}
[slide style="background-color:#31456A"]
ReactNative混合开发
如何在一个在原生界面上显示React组件?
..
[slide style="background-color:#31456A"]
ReactNative混合开发
{:&.zoomIn}
import React from 'react';
import {AppRegistry, StyleSheet, Text, View} from 'react-native';
export default class HelloWorld extends React.Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.hello}>Hello, World</Text>
</View>
);
}
}
// .../index.js
AppRegistry.registerComponent('HelloWorld', () => HelloWorld);
AppRegistry.registerComponent('RNOnLayout', () => RNOnLayout);
AppRegistry.registerComponent('ExternalBundle', () => ExternalBundle);
[slide style="background-color:#31456A"]
ReactNative混合开发
public class HelloWorldActivity extends Activity {
private ReactInstanceManager mReactInstanceManager;
private ReactRootView mReactRootView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mReactRootView = new ReactRootView(this);
mReactInstanceManager = ReactInstanceManager.builder()
.setApplication(getApplication())
.setBundleAssetName("index.android.bundle")//assets 文件目录下的bundle文件
.setJSMainModulePath("index")//对应index.js
.addPackage(new MainReactPackage())
.setUseDeveloperSupport(BuildConfig.DEBUG)
.setInitialLifecycleState(LifecycleState.RESUMED)
.build();
// "HelloWorld" 对应index.js中的:
// AppRegistry.registerComponent('HelloWorld', () => HelloWorld);
mReactRootView.startReactApplication(mReactInstanceManager, "HelloWorld", null);
setContentView(mReactRootView);
}
}
[slide style="width:400"]
ReactNative混合开发
..
[slide style="background-color:#31456A"]
ReactNative混合开发
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="#f2f2f2"
android:gravity="center"
android:text="这里是原生部分"
android:textColor="#000000" />
<com.facebook.react.ReactRootView
android:id="@+id/react_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
[slide style="background-color:#31456A"]
ReactNative混合开发
public class NativeReactActivity extends BaseReactActivity {
private ReactInstanceManager mReactInstanceManager;
private ReactRootView mReactRootView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_react_test);
//找到布局中的ReactRootView
mReactRootView = findViewById(R.id.react_view);
mReactInstanceManager = ReactInstanceManager.builder()
.setApplication(getApplication())
.setBundleAssetName("index.android.bundle") //assets 文件目录下的bundle文件
.setJSMainModulePath("index") //对应index.js
.addPackage(new MainReactPackage())
.setUseDeveloperSupport(BuildConfig.DEBUG)
.setInitialLifecycleState(LifecycleState.RESUMED)
.build();
mReactRootView.startReactApplication(mReactInstanceManager, "RNOnLayout", null);
}
}
[slide style="background-color:#31456A"]
ReactNative混合开发
..
[slide style="background-color:#31456A"]
ReactNative混合开发
public class ExternalReactActivity extends BaseReactActivity {
private ReactInstanceManager mReactInstanceManager;
private ReactRootView mReactRootView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_react_test);
//找到布局中的ReactRootView
mReactRootView = findViewById(R.id.react_view);
//获取bundle文件路径:在外部存储空间"test"文件夹下
String path = Environment.getExternalStorageDirectory() + "/test/index.android.bundle";
//初始化ReactInstanceManager
mReactInstanceManager = ReactInstanceManager.builder()
.setApplication(getApplication())
// .setBundleAssetName("index.android.bundle")
.setJSMainModulePath("index") //对应index.js
.addPackage(new MainReactPackage())//原生模块
.setUseDeveloperSupport(BuildConfig.DEBUG)
.setJSBundleFile(path) //设置文件路径
.setInitialLifecycleState(LifecycleState.RESUMED)
.build();
//"ExternalBundle" 对应js中 :AppRegistry.registerComponent('ExternalBundle', () => ExternalBundle);
mReactRootView.startReactApplication(mReactInstanceManager, "ExternalBundle", null);
}
}
[slide style="background-color:#31456A"]
ReactNative混合开发
..
[slide style="background-color:#31456A"]
Native原生模块
..
[slide style="background-color:#31456A"]
Native原生模块(UI)
/**
* 1.创建一个ViewManager类
* 例子: 实现一个图片显示器
*/
public class RCTImageManager extends SimpleViewManager<RCImageView> {
private static final String RC_IMAGE_VIEW = "RCImageView";
/**
* 2. 实现getName方法,返回该组件的名字
*/
@Override
public String getName() {
return RC_IMAGE_VIEW;
}
/**
* 3. 实现createViewInstance方法,创建视图,并初始化为默认的状态
*/
@Override
protected RCImageView createViewInstance(ThemedReactContext reactContext) {
return new RCImageView(reactContext, Fresco.newDraweeControllerBuilder(), null, null);
}
/*
* 4. 通过@ReactProp(或@ReactPropGroup)注解来导出属性的设置方法。
*/
//url属性 src = {'https://www.baidu.com/img/bd_logo1.png'}
@ReactProp(name = "src")
public void setSrc(ReactImageView view, @Nullable String sources) {
WritableMap map = Arguments.createMap();
map.putString("uri", sources);
WritableArray array = Arguments.createArray();
array.pushMap(map);
//转换成ReadableArray传给View
view.setSource(array);
}
@ReactProp(name = "borderRadius", defaultFloat = 0f)
public void setBorderRadius(ReactImageView view, float borderRadius) {
view.setBorderRadius(borderRadius);
}
@ReactProp(name = ViewProps.RESIZE_MODE)
public void setResizeMode(ReactImageView view, @Nullable String resizeMode) {
view.setScaleType(ImageResizeMode.toScaleType(resizeMode));
}
/**
* 注册回调事件
*/
@javax.annotation.Nullable
@Override
public Map<String, Object> getExportedCustomBubblingEventTypeConstants() {
return MapBuilder.<String, Object>builder()
//把topChange注册到系统中
.put("topChange",
MapBuilder.of(
"phasedRegistrationNames",
MapBuilder.of("bubbled", "onChange")))//onChange是暴露给 R 层的接口
.build();
}
}
[slide style="background-color:#31456A"]
Native原生模块(UI)
参数对照表
|Java | JavaScript|
:-------|:-------|:------
布尔型|Boolean | Bool
整形|Integer | Number
双精度浮点型|Double | Number
浮点型|Float | Number
字符串|String | String
回调|Callback | function
映射|ReadableMap | Object
数组|ReadableArray | Array
[slide style="background-color:#31456A"]
Native原生模块(UI)
@Override
public class NativePackage implements ReactPackage {
/**
* 5.在ReactPackage中注册ViewManager
*/
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new RCTImageManager() //原生图片显示UI
);
}
}
[slide style="background-color:#31456A"]
Native原生模块(UI)
//6.实现对应的JavaScript模块
class NativeUIExample extends Component {
static navigationOptions = {
title: 'NativeUIExample',
};
_onButtonPress(){
alert("onButtonPress");
}
render() {
return (
<View style={styles.container}>
<RCImageView
src = {'http://www.casstime.com/images/Product2.jpg'}
borderRadius= {25}
style={{width: 300, height: 225}}
onChangeMessage = {this._onButtonPress}
/>
</View>
);
}
...
}
[slide style="background-color:#31456A"]
Native原生模块(UI)
..
[slide style="background-color:#31456A"]
Native原生模块(API)
原生模块给React层提的供API:
- 原生的第三方库API {:&.fadeIn}
- 可复用的原生代码
- 高性能的、多线程的原生代码、譬如图片处理、数据库、或者各种高级扩展等等
[slide style="background-color:#31456A"]
Native原生模块(API)
/*
* 1. 创建一个新的Java类继承ReactContextBaseJavaModule,并命名为ToastModule.java
* 例子: 实现一个吐司模块
*/
public class ToastModule extends ReactContextBaseJavaModule {
private static final String TOAST_MODULE = "ToastExample";
private static final String DURATION_SHORT_KEY = "SHORT";
private static final String DURATION_LONG_KEY = "LONG";
public ToastModule(ReactApplicationContext reactContext) {
super(reactContext);
}
//2. 实现getName方法,返回该模块的名字
@Override
public String getName() {
return TOAST_MODULE;
}
//3. 实现getConstants方法,返回需要导出给JavaScript使用的常量
@Nullable
@Override
public Map<String, Object> getConstants() {
Map<String, Object> constants = new HashMap<>();
constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT);
constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG);
return constants;
}
//4. 使用注解@ReactMethod导出一个方法给JavaScript使用
@ReactMethod(isBlockingSynchronousMethod = false)
public void show(String message, int duration) {
Toast.makeText(getReactApplicationContext(), message, duration).show();
}
}
[slide style="background-color:#31456A"]
Native原生模块(API)
public class NativePackage implements ReactPackage {
/**
* 5.在ReactPackage中注册ToastModule模块
*/
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(
new ToastModule(reactContext),//吐司模块
}
}
[slide style="background-color:#31456A"]
Native原生模块(API)
{:&.fadeIn}
// ToastExample.js
// 通常我们都会把原生模块封装成一个 JavaScript 模块
import { NativeModules } from "react-native";
module.exports = NativeModules.ToastExample;
// 在JavaScript代码中可以这样调用:
import ToastExample from "./ToastExample";
ToastExample.show("Awesome", ToastExample.SHORT);
[slide style="background-color:#31456A"]
Native原生模块(API)
其他特性
- 回调函数:
com.facebook.react.bridge.Callback
- Promises: 原生模块还可以使用
Promise
来简化代码 - 事件发射器:
RCTDeviceEventEmitter
- 生命周期监听:
ActivityEventListener
&LifecycleEventListener
[slide style="background-color:#31456A"]
Native原生模块(Task)
/*
* 1.创建一个类继承HeadlessJsTaskService
* 例子: 模拟网络状态监听
*/
public class OnNetworkChangeTaskService extends HeadlessJsTaskService {
/**
* 2.配置任务
*/
@Nullable
@Override
protected HeadlessJsTaskConfig getTaskConfig(Intent intent) {
if (intent==null)return null;
Bundle extras = intent.getExtras();
if (extras == null) return null;
HeadlessJsTaskConfig taskConfig = new HeadlessJsTaskConfig(
"NetworkChangeTask", //任务名称
Arguments.fromBundle(extras), // 任务参数
5000, //任务超时时间
true // 是否允许在前台运行,默认false
);
return taskConfig;
}
}
[slide style="background-color:#31456A"]
Native原生模块(Task)
/*
* 3.在APP切换到后台的时候开启后台任务监听网络状态,
* 并且在网络发生改变时执行JavaScript任务;
*/
Intent serviceIntent = new Intent(context, OnNetworkChangeTaskService.class);
serviceIntent.putExtra("hasInternet", hasInternet);
context.startService(serviceIntent);
[slide style="background-color:#31456A"]
Native原生模块(Task)
// 5. 创建执行任务的NetworkChangeTask.js文件
module.exports = async (taskData) => {
if (taskData["hasInternet"]) {
console.log('网络可用')
}else{
console.log('网络不可用')
}
};
/*
*5.在index.js中引用创建执行任务的NetworkChangeTask.js文件;
* 并注册`NetworkChangeTask`任务
*/
import NetworkChangeTask from './NetworkChangeTask';
AppRegistry.registerHeadlessTask("NetworkChangeTask", () => require("NetworkChangeTask"));
[slide style="background-color:#31456A"]
Native原生模块(Task)
module.exports = async (taskData) => {
if (taskData["hasInternet"]) {
console.log('网络可用')
}else{
console.log('网络不可用')
}
};
[slide style="background-color:#31456A"]
热更新
ReactNative热更新原理:
- 版本更新时,从服务器重新拉取bundle文件和图片资源到本地; {:&.fadeIn}
- 在setJSBundleFile()方法中返回新的bundle文件路径;
- ReactInstanceManager加载新的bundle文件。
[slide style="background-color:#31456A"]
热更新 第三方库
react-native-pushy
ReactNative中文网推出的代码热更新服务:
- 每个应用每个月不超过10000次下载; {:&.fadeIn}
- 基于bsdiff算法创建的超小更新包,通常版本迭代后在1-10KB之间,避免数百KB的流量消耗;
- 支持崩溃回滚,安全可靠;
- 开放API,提供更高扩展性;
- 跨越多个版本进行更新时,只需要下载一个更新包,不需要逐版本依次更新。
[slide style="background-color:#31456A"]
热更新 第三方库
react-native-code-push
微软推出的代码热更新服务:
- 支持版本回滚; {:&.fadeIn}
- 支持图片增量更新;
- 官方服务器在国外,国内访问受限,建议自建服务;
- 没有开放后台源码。
[slide style="background-color:#31456A"]
热更新
搭建自己的热更新服务
- 打包bundle文件和图片文件,并上传服务器; {:&.fadeIn}
- 使用bsdiff对比两个版本的bundle文件得到补丁文件;
- 检查更新,下载补丁文件和图片;
- 使用bspatch对文件打补丁;
- 增量更新bundle文件和图片文件(Bsdiff工具);
- ReactInstanceManager空闲时重新加载bundle文件(或重启app时生效)。
[slide style="background-color:#31456A"]
启动优化
jsBundle文件预加载
- ReactRootView.startReactApplication() {:&.fadeIn}
- ReactInstanceManager.recreateReactContextInBackground()
- ReactInstanceManager.runCreateReactContextOnNewThread()
public void startReactApplication(
ReactInstanceManager reactInstanceManager,
String moduleName,
@Nullable Bundle initialProperties) {
Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "startReactApplication");
try {
...
if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
//后台创建ReactContext(加载jsBundle)
mReactInstanceManager.createReactContextInBackground();
}
attachToReactInstanceManager();
} finally {
Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
}
}
[slide style="background-color:#31456A"]
启动优化
jsBundle文件预加载
private void runCreateReactContextOnNewThread(final ReactContextInitParams initParams) {
...
//开启子线程
mCreateReactContextThread =
new Thread((Runnable)()-> {
...
try {
Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY);
final ReactApplicationContext reactApplicationContext =
createReactContext(
//加载jsBundle
initParams.getJsExecutorFactory().create(),
initParams.getJsBundleLoader());
...
Runnable setupReactContextRunnable = new Runnable() {()->{
...
//在主线程设置ReactRootView
setupReactContext(reactApplicationContext);
...
}};
reactApplicationContext.runOnNativeModulesQueueThread(setupReactContextRunnable);
UiThreadUtil.runOnUiThread(maybeRecreateReactContextRunnable);
} catch (Exception e) {
mDevSupportManager.handleException(e);
}
}
});
ReactMarker.logMarker(REACT_CONTEXT_THREAD_START);
mCreateReactContextThread.start();
}
[slide style="background-color:#31456A"]
启动优化
- 全局单例的ReactInstanceManager {:&.fadeIn}
- ReactInstanceManager的创建时机
[slide style="background-color:#31456A"]
启动优化
- 预加载ReactRootView {:&.fadeIn}
- 缓存并复用ReactRootView
[slide style="background-color:#31456A"]
bundle文件安全
裸奔的jsBundle
-
网络接口安全。 {:&.fadeIn}
-
jsBundle防篡改。为防止篡改js入侵app业务,需对jsBundle做签名校验,
一是下载文件后校验其完整性,二是每次加载jsBundle时校验。 -
jsBundle业务安全。因为jsBundle是明文,所以业务中需要进行加解密等
敏感措施就不能在js层实现,应该在native层实现加解密暴露给js接口调用。
[slide style="background-color:#31456A"]
总结
- ReactNative启动流程 {:&.fadeIn}
- ReactNative混合开发
- Native模块
- 热更新
- 启动优化
- bundle文件安全
[slide style="background-color:#31456A"]
网友评论