React-Native 热更新尝试(Android)

    xiaoxiao2021-03-25  124

    前言:由于苹果发布的ios的一些rn的app存在安全问题,主要就是由于一些第三方的热更新库导致的,然而消息一出就闹得沸沸扬扬的,导致有些人直接认为“学了大半年的rn白学啦~~!!真是哭笑不得。废话不多说了,马上进入我们今天的主题吧。“

    因为一直在做android开发,所以今天也只是针对于android进行热更新尝试(ios我也无能为力哈,看都看不懂,哈哈~~~)。

    先看一下效果:

    怎么样?效果还是不错的吧?其实呢,实现起来还是不是很难的,下面让我们一点一点的尝试一下吧(小伙伴跟紧一点哦)。

    首先我们来看看当我们执行:

    react-native init xxxx

    命令的时候,rn会自动帮我们创建一个android项目跟ios项目,然后我们看看rn帮我们创建的android项目长啥样:

    我们看到,帮我们创建了一个MainActivity跟一个MainApplication,我们先看一下MainActivity:

    package com.businessstore; import com.facebook.react.ReactActivity; 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 "BusinessStore"; } }

    很简单,就一行代码getMainComponentName,然后返回我们的组件名字,这个名字即为我们在index.android.js中注册的组件名字:

    然后我们看看MainApplication长啥样:

    public class MainApplication extends Application implements ReactApplication { private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { @Override protected boolean getUseDeveloperSupport() { return BuildConfig.DEBUG; } @Override protected List<ReactPackage> getPackages() { return Arrays.<ReactPackage>asList( new MainReactPackage() ); } }; @Override public ReactNativeHost getReactNativeHost() { return mReactNativeHost; } @Override public void onCreate() { super.onCreate(); SoLoader.init(this, /* native exopackage */ false); } }

    也是没有几行代码…..

    好啦~那我们的rn页面是怎么出来的呢? 不急,我们来一步一步往下看,首先点开MainActivity:

    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 "BusinessStore"; } }

    一个activity要显示一个页面的话肯定得setContentView,既然我们的activity没有,然后就找到它的父类ReactActivity:

    public abstract class ReactActivity extends Activity implements DefaultHardwareBackBtnHandler, PermissionAwareActivity { private final ReactActivityDelegate mDelegate; protected ReactActivity() { mDelegate = createReactActivityDelegate(); } /** * Returns the name of the main component registered from JavaScript. * This is used to schedule rendering of the component. * e.g. "MoviesApp" */ protected @Nullable String getMainComponentName() { return null; } /** * 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 onKeyUp(int keyCode, KeyEvent event) { return mDelegate.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event); } @Override public void onBackPressed() { if (!mDelegate.onBackPressed()) { super.onBackPressed(); } } @Override public void invokeDefaultOnBackPressed() { super.onBackPressed(); } @Override public void onNewIntent(Intent intent) { if (!mDelegate.onNewIntent(intent)) { super.onNewIntent(intent); } } @Override public void requestPermissions( String[] permissions, int requestCode, PermissionListener listener) { mDelegate.requestPermissions(permissions, requestCode, listener); } @Override public void onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults) { mDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults); } protected final ReactNativeHost getReactNativeHost() { return mDelegate.getReactNativeHost(); } protected final ReactInstanceManager getReactInstanceManager() { return mDelegate.getReactInstanceManager(); } protected final void loadApp(String appKey) { mDelegate.loadApp(appKey); } }

    代码也不是很多,可见,我们看到了activity的很多生命周期方法,然后都是由一个叫mDelegate的类给处理掉了,所以我们继续往下走看看mDelegate:

    // Copyright 2004-present Facebook. All Rights Reserved. package com.facebook.react; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.support.v4.app.FragmentActivity; import android.view.KeyEvent; import android.widget.Toast; import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.Callback; import com.facebook.react.common.ReactConstants; import com.facebook.react.devsupport.DoubleTapReloadRecognizer; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import com.facebook.react.modules.core.PermissionListener; import javax.annotation.Nullable; /** * 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 int REQUEST_OVERLAY_PERMISSION_CODE = 1111; private static final String REDBOX_PERMISSION_GRANTED_MESSAGE = "Overlay permissions have been granted."; private static final String REDBOX_PERMISSION_MESSAGE = "Overlay permissions needs to be granted in order for react native apps to run in dev mode"; private final @Nullable Activity mActivity; private final @Nullable FragmentActivity mFragmentActivity; private final @Nullable String mMainComponentName; private @Nullable ReactRootView mReactRootView; private @Nullable DoubleTapReloadRecognizer mDoubleTapReloadRecognizer; private @Nullable PermissionListener mPermissionListener; private @Nullable Callback mPermissionsCallback; public ReactActivityDelegate(Activity activity, @Nullable String mainComponentName) { mActivity = activity; mMainComponentName = mainComponentName; mFragmentActivity = null; } public ReactActivityDelegate( FragmentActivity fragmentActivity, @Nullable String mainComponentName) { mFragmentActivity = fragmentActivity; mMainComponentName = mainComponentName; mActivity = null; } protected @Nullable Bundle getLaunchOptions() { return null; } protected ReactRootView createRootView() { return new ReactRootView(getContext()); } /** * Get the {@link ReactNativeHost} used by this app. By default, assumes * {@link Activity#getApplication()} is an instance of {@link ReactApplication} and calls * {@link ReactApplication#getReactNativeHost()}. Override this method if your application class * does not implement {@code ReactApplication} or you simply have a different mechanism for * storing a {@code ReactNativeHost}, e.g. as a static field somewhere. */ protected ReactNativeHost getReactNativeHost() { return ((ReactApplication) getPlainActivity().getApplication()).getReactNativeHost(); } public ReactInstanceManager getReactInstanceManager() { return getReactNativeHost().getReactInstanceManager(); } protected void onCreate(Bundle savedInstanceState) { boolean needsOverlayPermission = false; if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // Get permission to show redbox in dev builds. if (!Settings.canDrawOverlays(getContext())) { needsOverlayPermission = true; Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getContext().getPackageName())); FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE); Toast.makeText(getContext(), REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show(); ((Activity) getContext()).startActivityForResult(serviceIntent, REQUEST_OVERLAY_PERMISSION_CODE); } } if (mMainComponentName != null && !needsOverlayPermission) { 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(); mReactRootView.startReactApplication( getReactNativeHost().getReactInstanceManager(), appKey, getLaunchOptions()); getPlainActivity().setContentView(mReactRootView); } protected void onPause() { if (getReactNativeHost().hasInstance()) { getReactNativeHost().getReactInstanceManager().onHostPause(getPlainActivity()); } } protected void onResume() { if (getReactNativeHost().hasInstance()) { getReactNativeHost().getReactInstanceManager().onHostResume( getPlainActivity(), (DefaultHardwareBackBtnHandler) getPlainActivity()); } if (mPermissionsCallback != null) { mPermissionsCallback.invoke(); mPermissionsCallback = null; } } protected void onDestroy() { if (mReactRootView != null) { mReactRootView.unmountReactApplication(); mReactRootView = null; } if (getReactNativeHost().hasInstance()) { getReactNativeHost().getReactInstanceManager().onHostDestroy(getPlainActivity()); } } public void onActivityResult(int requestCode, int resultCode, Intent data) { if (getReactNativeHost().hasInstance()) { getReactNativeHost().getReactInstanceManager() .onActivityResult(getPlainActivity(), requestCode, resultCode, data); } else { // Did we request overlay permissions? if (requestCode == REQUEST_OVERLAY_PERMISSION_CODE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Settings.canDrawOverlays(getContext())) { if (mMainComponentName != null) { loadApp(mMainComponentName); } Toast.makeText(getContext(), REDBOX_PERMISSION_GRANTED_MESSAGE, Toast.LENGTH_LONG).show(); } } } } public boolean onKeyUp(int keyCode, KeyEvent event) { if (getReactNativeHost().hasInstance() && getReactNativeHost().getUseDeveloperSupport()) { if (keyCode == KeyEvent.KEYCODE_MENU) { getReactNativeHost().getReactInstanceManager().showDevOptionsDialog(); return true; } boolean didDoubleTapR = Assertions.assertNotNull(mDoubleTapReloadRecognizer) .didDoubleTapR(keyCode, getPlainActivity().getCurrentFocus()); if (didDoubleTapR) { getReactNativeHost().getReactInstanceManager().getDevSupportManager().handleReloadJS(); return true; } } return false; } public boolean onBackPressed() { if (getReactNativeHost().hasInstance()) { getReactNativeHost().getReactInstanceManager().onBackPressed(); return true; } return false; } public boolean onNewIntent(Intent intent) { if (getReactNativeHost().hasInstance()) { getReactNativeHost().getReactInstanceManager().onNewIntent(intent); return true; } return false; } @TargetApi(Build.VERSION_CODES.M) public void requestPermissions( String[] permissions, int requestCode, PermissionListener listener) { mPermissionListener = listener; getPlainActivity().requestPermissions(permissions, requestCode); } public void onRequestPermissionsResult( final int requestCode, final String[] permissions, final int[] grantResults) { mPermissionsCallback = new Callback() { @Override public void invoke(Object... args) { if (mPermissionListener != null && mPermissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults)) { mPermissionListener = null; } } }; } private Context getContext() { if (mActivity != null) { return mActivity; } return Assertions.assertNotNull(mFragmentActivity); } private Activity getPlainActivity() { return ((Activity) getContext()); } }

    我们终于看到了一些有用的代码了,这个类就是处理跟activity生命周期相关的一些方法,包括(给activity添加contentview、监听用户回退、按键、6.0的一些运行时权限等等…)我们看到onCreate方法:

    protected void onCreate(Bundle savedInstanceState) { boolean needsOverlayPermission = false; if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // Get permission to show redbox in dev builds. if (!Settings.canDrawOverlays(getContext())) { needsOverlayPermission = true; Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getContext().getPackageName())); FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE); Toast.makeText(getContext(), REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show(); ((Activity) getContext()).startActivityForResult(serviceIntent, REQUEST_OVERLAY_PERMISSION_CODE); } } if (mMainComponentName != null && !needsOverlayPermission) { loadApp(mMainComponentName); } mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer(); }

    我们看到这么一个判断,这个是做什么的呢?是为了检测是不是具有弹出悬浮窗的权限:

    if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // Get permission to show redbox in dev builds. if (!Settings.canDrawOverlays(getContext())) {

    getReactNativeHost().getUseDeveloperSupport()返回的即为我们在MainApplication中写的:

    @Override protected boolean getUseDeveloperSupport() { return BuildConfig.DEBUG; }

    也就是说,当我们运行debug包的时候,会去检测app是不是具有弹出悬浮窗的权限,没有权限的话就会去请求权限,悬浮窗即为rn的调试menu:

    好啦~!有点偏离我们今天的主题了,我们继续往下走…往下我们看到会去执行一个叫loadApp的方法:

    if (mMainComponentName != null && !needsOverlayPermission) { loadApp(mMainComponentName); }

    我们点开loadApp:

    protected void loadApp(String appKey) { if (mReactRootView != null) { throw new IllegalStateException("Cannot loadApp while app is already running."); } mReactRootView = createRootView(); mReactRootView.startReactApplication( getReactNativeHost().getReactInstanceManager(), appKey, getLaunchOptions()); getPlainActivity().setContentView(mReactRootView); }

    好啦~~! 看到这里我们看到直接给activity设置了一个叫mReactRootView的组件,而这个组件正是rn封装的组件,我们在js中写的组件都会被转换成native组件,然后添加进mReactRootView这个组件中,那么问题来了,这些rn的组件又是在何时添加进我们的mReactRootView这个组件的呢???我们继续往下走,看到rootview有一个startReactApplication方法:

    public void startReactApplication( ReactInstanceManager reactInstanceManager, String moduleName, @Nullable Bundle launchOptions) { UiThreadUtil.assertOnUiThread(); // TODO(6788889): Use POJO instead of bundle here, apparently we can't just use WritableMap // here as it may be deallocated in native after passing via JNI bridge, but we want to reuse // it in the case of re-creating the catalyst instance Assertions.assertCondition( mReactInstanceManager == null, "This root view has already been attached to a catalyst instance manager"); mReactInstanceManager = reactInstanceManager; mJSModuleName = moduleName; mLaunchOptions = launchOptions; if (!mReactInstanceManager.hasStartedCreatingInitialContext()) { mReactInstanceManager.createReactContextInBackground(); } // We need to wait for the initial onMeasure, if this view has not yet been measured, we set which // will make this view startReactApplication itself to instance manager once onMeasure is called. if (mWasMeasured) { attachToReactInstanceManager(); } }

    我们看到这么一行代码:

    if (!mReactInstanceManager.hasStartedCreatingInitialContext()) { mReactInstanceManager.createReactContextInBackground(); }

    看名字就知道肯定是加载了某些东西,可是点进去我们发现居然是一个抽象的方法,尴尬了~~!!:

    public abstract void createReactContextInBackground();

    那么肯定有它的实现类,我们看到这个mReactInstanceManager是我们在调用loadApp这个方法的时候传进入的:

    protected void loadApp(String appKey) { if (mReactRootView != null) { throw new IllegalStateException("Cannot loadApp while app is already running."); } mReactRootView = createRootView(); mReactRootView.startReactApplication( getReactNativeHost().getReactInstanceManager(), appKey, getLaunchOptions()); getPlainActivity().setContentView(mReactRootView); }

    而mReactInstanceManager又是调用getReactNativeHost().getReactInstanceManager()方法获取的,getReactNativeHost()这个返回的对象即为我们在MainApplication中创建的host对象:

    public class MainApplication extends Application implements ReactApplication { private static final String FILE_NAME = "index.android"; private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {

    然后我们顺着getReactNativeHost().getReactInstanceManager()一直往下找最后发现mReactInstanceManager的实现类在这里被创建了:

    我们赶紧找到XReactInstanceManagerImpl类,然后看一下createReactContextInBackground这个方法:

    @Override public void createReactContextInBackground() { Assertions.assertCondition( !mHasStartedCreatingInitialContext, "createReactContextInBackground should only be called when creating the react " + "application for the first time. When reloading JS, e.g. from a new file, explicitly" + "use recreateReactContextInBackground"); mHasStartedCreatingInitialContext = true; recreateReactContextInBackgroundInner(); }

    然后我们继续往下:

    private void recreateReactContextInBackgroundInner() { ... @Override public void onPackagerStatusFetched(final boolean packagerIsRunning) { UiThreadUtil.runOnUiThread( new Runnable() { @Override public void run() { ... recreateReactContextInBackgroundFromBundleLoader(); } } }); } }); } return; }

    我们找到recreateReactContextInBackgroundFromBundleLoader继续往下:

    private void recreateReactContextInBackgroundFromBundleLoader() { recreateReactContextInBackground( new JSCJavaScriptExecutor.Factory(mJSCConfig.getConfigMap()), mBundleLoader); }

    然后看到recreateReactContextInBackground方法:

    private void recreateReactContextInBackground( JavaScriptExecutor.Factory jsExecutorFactory, JSBundleLoader jsBundleLoader) { UiThreadUtil.assertOnUiThread(); ReactContextInitParams initParams = new ReactContextInitParams(jsExecutorFactory, jsBundleLoader); if (mReactContextInitAsyncTask == null) { // No background task to create react context is currently running, create and execute one. mReactContextInitAsyncTask = new ReactContextInitAsyncTask(); mReactContextInitAsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, initParams); } else { // Background task is currently running, queue up most recent init params to recreate context // once task completes. mPendingReactContextInitParams = initParams; } }

    recreateReactContextInBackground这就是我们今天需要找的方法,传递了两个参数:一个是执行js代码的线程池、另外一个是我们bundle文件的加载器(bundle文件可以是我们的npm服务器中的文件(debug模式),也可以是我们assert目录中的bundle文件(发布版))。

    既然如此,那我们热更新方案是不是可以这样呢? 1、请求服务器接口,当接口中返回的版本号跟我们rn中存储的版本号不一致的时候,那么这个时候就需要更新版本了。 2、服务器接口返回一个jsbundle文件的下载地址,然后我们app中拿到地址下载到本地,替换掉当前版本的jsbundle文件。 3、重新执行一下recreateReactContextInBackground方法,让app重新加载新的jsbundle文件。

    好啦~! 有了思路以后,我们就可以写我们的代码了:

    首先,我们模拟一个后台接口:

    { "url": "/business/version", "method": "post", "response": { "code": "0", "message": "请求成功", "body": { "versionName": "2.0.0", "description":"添加了热更新功能", "url":"http://www.baidu.com" } } },

    然后在我们的rn中我们对应定义了一个常量叫version:

    可以看到我们rn中定义的为1.0.0,所以待会我去请求接口,当接口返回的2.0.0不等于1.0.0的时候,我就去下载更新bundle文件了,于是在我们rn主页面的时候,我们就发送一个请求,然后做判断:

    componentDidMount() { this._versionCheck(); } _versionCheck() { this.versionRequest = new HomeMenuRequest(null, 'POST'); this.versionRequest.start((version)=> { version = version.body; if (version && version.versionName != AppConstant.version) { if (Platform.OS == 'android') { Alert.alert( '发现新版本,是否升级?', `版本号: ${version.versionName}\n版本描述: ${version.description}`, [ { text: '是', onPress: () => { this.setState({ currProgress: Math.random() * 80, modalVisible: true }); NativeModules.UpdateAndroid.doUpdate('index.android.bundle_2.0', (progress)=> { let pro = Number.parseFloat('' + progress); if (pro >= 100) { this.setState({ modalVisible: false, currProgress: 100 }); } else { this.setState({ currProgress: pro }); } }); } }, { text: '否' } ] ) } } }, (erroStr)=> { }); } }

    会弹出一个对话框:

    当我们点击是的时候:

    NativeModules.UpdateAndroid.doUpdate('index.android.bundle_2.0', (progress)=> { let pro = Number.parseFloat('' + progress); if (pro >= 100) { this.setState({ modalVisible: false, currProgress: 100 }); } else { this.setState({ currProgress: pro }); } });

    我们执行了native中的doUpdate并传递了两个参数,一个是下载地址,一个是当native完成热更新后的回调:

    NativeModules.UpdateAndroid.doUpdate()

    这里声明一下,因为我这边用的服务器是mock的服务器,所以没法放一个文件到服务器上,我就直接把需要下载的bundle_2.0放在了跟1.0同级的一个目录中了,然后我们去copy 2.0到内存卡(模拟从网络上获取),替换掉1.0的版本。

    再次声明,我们发布apk的时候需要把本地的js文件打成bundle,然后丢到assets目录中,所以最初的版本应该是index.android.bundle_1.0,这里出现了一个index.android.bundle_2.0是为了模拟从服务器下载,我就直接丢在了assert目录了(正常这个文件是在我们的远程服务器中的)。

    如果还不知道怎么发布apk的童鞋可以去看我前面的一篇博客:

    React-Native打包发布(Android)

    接下来就看看我们native的代码如何实现了….

    我就直接拿发布版的例子来说了,我们首先看看我们的MainApplication中该怎么写:

    public class MainApplication extends Application implements ReactApplication { private static final String FILE_NAME = "index.android"; private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { @Override protected boolean getUseDeveloperSupport() { //这里返回false的话即为发布版,否则为测试版 //发布版的话,app默认就会去assert目录中找bundle文件, // 如果为测试版的话,就回去npm服务器上获取bundle文件 return false; } @Override protected List<ReactPackage> getPackages() { return Arrays.<ReactPackage>asList( new MainReactPackage(), new VersionAndroidPackage(), new UpdateAndroidPackage() ); } @Nullable @Override protected String getJSBundleFile() { File file = new File(getExternalCacheDir(), FILE_NAME); if (file != null && file.length() > 0) { return file.getAbsolutePath(); } return super.getJSBundleFile(); } }; @Override public ReactNativeHost getReactNativeHost() { return mReactNativeHost; } @Override public void onCreate() { super.onCreate(); copyBundle(); SoLoader.init(this, /* native exopackage */ false); } private void copyBundle(){ if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { return; } File file = new File(getExternalCacheDir(), FILE_NAME); if (file != null && file.length() > 0) { return; } BufferedInputStream bis = null; BufferedOutputStream bos = null; try { bis = new BufferedInputStream(getAssets().open("index.android.bundle_1.0")); bos = new BufferedOutputStream(new FileOutputStream(file)); int len = -1; byte[] buffer = new byte[512]; while ((len = bis.read(buffer)) != -1) { bos.write(buffer, 0, len); bos.flush(); } } catch (Exception e) { e.printStackTrace(); } finally { try { if (bis != null) { bis.close(); } if (bos != null) { bos.close(); } } catch (IOException e) { e.printStackTrace(); } } } }

    可以看到我们多重写了一个方法,然后还写了一段copy bundle文件到内存卡的代码:

    @Nullable @Override protected String getJSBundleFile() { File file = new File(getExternalCacheDir(), FILE_NAME); if (file != null && file.length() > 0) { return file.getAbsolutePath(); } return super.getJSBundleFile(); } };

    因为rn默认是去assert目录中加载bundle文件的,当指定了bundle文件的地址后,rn会去加载我们指定的目录。所以当我们第一次运行app的时候,我们首先把assert中的bundle文件拷贝到了内存卡,然后让rn去内存卡中加在bundle文件。

    好啦~~!!此时的rn已经知道去内存卡中加载bundle文件了,我们要做的就是: 1、根据rn 中传递的地址去下载最新的bundle文件。 2、替换掉内存卡中的bundle文件。 3、调用createReactContextInBackground方法重新加载bundle文件。

    至于rn怎么去跟native交互,我这里简单的说一下哈: 首先我们需要建一个叫UpdateAndroid去继承ReactContextBaseJavaModule,然后注释声明为react的module:

    @ReactModule(name = "UpdateAndroid") public class UpdateAndroid extends ReactContextBaseJavaModule {

    然后重写里面的一个叫getName的方法给这个module取一个名字:

    @Override public String getName() { return "UpdateAndroid"; }

    最后声明一个类方法,让rn调取:

    @ReactMethod public void doUpdate(String url, Callback callback) { if (task == null) { task = new UpdateTask(callback); task.execute("index.android.bundle_2.0"); } }

    全部代码:

    @ReactModule(name = "UpdateAndroid") public class UpdateAndroid extends ReactContextBaseJavaModule { private UpdateTask task; public UpdateAndroid(ReactApplicationContext reactContext) { super(reactContext); } @ReactMethod public void doUpdate(String url, Callback callback) { if (task == null) { task = new UpdateTask(callback); task.execute("index.android.bundle_2.0"); } } @Override public String getName() { return "UpdateAndroid"; } private class UpdateTask extends AsyncTask<String, Float, File> { private Callback callback; private static final String FILE_NAME = "index.android"; private UpdateTask(Callback callback) { this.callback = callback; } @Override protected File doInBackground(String... params) { return downloadBundle(params[0]); } @Override protected void onProgressUpdate(Float... values) { // if (callback != null && values != null && values.length > 0){ // callback.invoke(values[0]); // Log.e("TAG", "progress-->" + values[0]); // } } @Override protected void onPostExecute(File file) { if (callback != null) callback.invoke(100f); //重写初始化rn组件 onJSBundleLoadedFromServer(file); } private void onJSBundleLoadedFromServer(File file) { if (file == null || !file.exists()) { Log.i(TAG, "download error, check URL or network state"); return; } Log.i(TAG, "download success, reload js bundle"); Toast.makeText(getCurrentActivity(), "Downloading complete", Toast.LENGTH_SHORT).show(); try { ReactApplication application = (ReactApplication) getCurrentActivity().getApplication(); Class<?> RIManagerClazz = application.getReactNativeHost().getReactInstanceManager().getClass(); Method method = RIManagerClazz.getDeclaredMethod("recreateReactContextInBackground", JavaScriptExecutor.Factory.class, JSBundleLoader.class); method.setAccessible(true); method.invoke(application.getReactNativeHost().getReactInstanceManager(), new JSCJavaScriptExecutor.Factory(JSCConfig.EMPTY.getConfigMap()), JSBundleLoader.createFileLoader(file.getAbsolutePath())); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } } /** * 模拟bundle下载链接url * * @param url */ private File downloadBundle(String url) { if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { return null; } //删除以前的文件 File file = new File(getReactApplicationContext().getExternalCacheDir(), FILE_NAME); if (file != null && file.length() > 0) { file.delete(); } BufferedInputStream bis = null; BufferedOutputStream bos = null; try { //模拟网络下载过程,我这直接放在了assert目录了 long size = getReactApplicationContext().getAssets().open(url).available(); bis = new BufferedInputStream(getReactApplicationContext().getAssets().open(url)); bos = new BufferedOutputStream(new FileOutputStream(file)); int len = -1; long total = 0; byte[] buffer = new byte[100]; while ((len = bis.read(buffer)) != -1) { total += len; bos.write(buffer, 0, len); bos.flush(); float progress = total * 1.0f / size; publishProgress(progress); } return file; } catch (Exception e) { e.printStackTrace(); } finally { try { if (bis != null) { bis.close(); } if (bos != null) { bos.close(); } } catch (IOException e) { e.printStackTrace(); } } return null; } } }

    然后创建UpdateAndroidPackage类把module加入进来:

    public class UpdateAndroidPackage implements ReactPackage{ @Override public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) { List<NativeModule>modules=new ArrayList<NativeModule>(); modules.add(new UpdateAndroid(reactContext)); return modules; } @Override public List<Class<? extends JavaScriptModule>> createJSModules() { return Collections.emptyList(); } @Override public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) { return Collections.emptyList(); } }

    最后在MainApplication中把这个package注册进rn中:

    @Override protected List<ReactPackage> getPackages() { return Arrays.<ReactPackage>asList( new MainReactPackage(), new VersionAndroidPackage(), new UpdateAndroidPackage() ); }

    然后我们就可以在我们的rn中调用了:

    NativeModules.UpdateAndroid.doUpdate('index.android.bundle_2.0', (progress)=> { let pro = Number.parseFloat('' + progress); if (pro >= 100) { this.setState({ modalVisible: false, currProgress: 100 }); } else { this.setState({ currProgress: pro }); } });

    好啦~~~篇幅有点长,如果还是不懂的童鞋可以私下问我哈,一起学习。

    热更新android工程github链接: https://github.com/913453448/BusinessStore1

    vv_小虫 认证博客专家 JavaScript CSS 前端框架 6 年开发经验,前端架构师,目前主要负责企业级应用前端技术平台建设工作,在前端工程化实现、Node 应用开发、Android 技术、Vue 技术、React 技术、移动开发等方向有丰富实践。
    转载请注明原文地址: https://ju.6miu.com/read-6927.html

    最新回复(0)