【React-Native】安卓下载离线bundle文件后重新加载不重启APP解决办法

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/knockheart/article/details/90021809

前言

以下内容仅针对安卓处理。做RN开发知道RN项目启动实际上是打包好的js文件,一般打包到android项目assets目录下,启动加载这里的bundle.js文件,当然RN也可以加载手机内存中的bundle.js

本人项目没有用code-push热更新,而是自己实现的,根据是否有新版本的bundle.js文件 下载重新加载本地bundle.js

之前下载新的bundle后都需要重新启动app才能实现重新加载本地bundle.js 体验比较差

目的:不用杀死进程重启应用,可以重新加载JS代码,重启页面。

分析源码

安卓RN项目 applicaiton 实现了ReactApplicaiton接口

package com.facebook.react;

public interface ReactApplication {

  /**
   * Get the default {@link ReactNativeHost} for this app.
   */
  ReactNativeHost getReactNativeHost();
}

自己的Application部分实现


  private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
    @Override
    public boolean getUseDeveloperSupport() {
      return BuildConfig.DEBUG;
    }

    
    @Override
    protected List<ReactPackage> getPackages() {
      return Arrays.<ReactPackage>asList(
          new MainReactPackage(),
            new RNGestureHandlerPackage(),
                    new VectorIconsPackage(),
                    //new RNSentryPackage(),
                    mCommPackage // 自定义模块
            );
        }

        @Nullable
        @Override
        protected String getJSBundleFile() {
            String fileName = PathFactory.getExtraDownloadPath(appContext)+ SPUtil.getStringValue(appContext, PathFactory.REACT)+ File.separator+ FileConstant.JS_BUNDLE_LOCAL_FILE;
//            String fileName = PathFactory.getExtraDownloadPath(appContext)+ "1551941117"+File.separator+FileConstant.JS_BUNDLE_LOCAL_FILE;
//            File file = new File (FileConstant.JS_BUNDLE_LOCAL_PATH);
            File file = new File (fileName);
            if(file != null && file.exists()) {
//                return FileConstant.JS_BUNDLE_LOCAL_PATH;
                return fileName;
            } else {
                return super.getJSBundleFile();
            }
        }

    @Override
    protected String getJSMainModuleName() {
      return "index";
    }
  };

  @Override
  public ReactNativeHost getReactNativeHost() {
    return mReactNativeHost;
  }

getJSBundleFile方法是自己实现的加载本地bundle.js文件完整路径或者super.getJSBundleFile() 【这个等价于"assets://你的bundle文件名" 例如assets://index.android.bundle】

因为Applicaiton是单例启动app后再不关闭的情况下只实例一次,所以对应的getJSBundleFile这个方法也就只执行了一遍

设想

所以如果更新了新的bundle文件,想要重新加载就必须重新让RN调用getJSBundleFile()方法

接着看源码

我们的MainActivity界面是集成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());
  }

由以上代码得知 关键点在 ReactActivityDelegate这个代理,部分代码如下

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();
    mReactRootView.startReactApplication(
      getReactNativeHost().getReactInstanceManager(),
      appKey,
      getLaunchOptions());
    getPlainActivity().setContentView(mReactRootView);
  }

由上可知getReactNativeHost方法获取的nativehost就是我们自己application实现的,loadApp就是加载启动RN界面的入口方法;其核心关键就再getReactNativeHost().getReactInstanceManager()的方法里,下面我们继续看看这个方法到底做了什么

ReactNativeHost是一个抽象类
  /**
   * 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())
      .setUseDeveloperSupport(getUseDeveloperSupport())
      .setRedBoxHandler(getRedBoxHandler())
      .setJavaScriptExecutorFactory(getJavaScriptExecutorFactory())
      .setUIImplementationProvider(getUIImplementationProvider())
      .setJSIModulesPackage(getJSIModulePackage())
      .setInitialLifecycleState(LifecycleState.BEFORE_CREATE);

    for (ReactPackage reactPackage : getPackages()) {
      builder.addPackage(reactPackage);
    }

    String jsBundleFile = getJSBundleFile();
    if (jsBundleFile != null) {
      builder.setJSBundleFile(jsBundleFile);
    } else {
      builder.setBundleAssetName(Assertions.assertNotNull(getBundleAssetName()));
    }
    ReactInstanceManager reactInstanceManager = builder.build();
    ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_END);
    return reactInstanceManager;
  }

核心代码思想就在createReactInstanceManager,创建React实例管理器,一般情况下我们JSMainModuleName是不变更的,所以应该关注的是builder设置JSBundleFile的这个环节。

当然ReactInstanceManager这个类也需要仔细看一遍因为这个类有JSBundleLoader mBundleLoader;JSBundle的加载器

根据以上思路和推断,在自己的MainActivity启动类中做一下逻辑,当新的bundle下载到本地后重新加载bundle

解决方案

 private void loadBundle() {
        final ReactInstanceManager instanceManager;
        try {

            instanceManager = resolveInstanceManager();
            if (instanceManager == null) {
                return;
            }

            //获取本地的js代码 这里就不给出代码了。 如果本地没有就返回assets目录的
//            String latestJSBundleFile = Utils.getJSBundleFileInternal();
            String latestJSBundleFile = getJSBundleFileInternal();

            setJSBundle(instanceManager, latestJSBundleFile);

            new Handler(Looper.getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    try {

                        instanceManager.recreateReactContextInBackground();
                    } catch (Exception e) {
                        // The recreation method threw an unknown exception
                        // so just simply fallback to restarting the Activity (if it exists)
                        loadBundleLegacy();
                    }
                }
            });
        }  catch (Exception e) {
            e.printStackTrace();
            loadBundleLegacy();
        }
    }

    private String getJSBundleFileInternal() {
        String fileName = PathFactory.getExtraDownloadPath(getApplication())+ SPUtil.getStringValue(getApplication(), PathFactory.REACT)+ File.separator+ FileConstant.JS_BUNDLE_LOCAL_FILE;
        File file = new File(fileName);
        if(file.exists()){
            file= null;
            return fileName;
        }
        return FileConstant.JS_BUNDLE_DEFAULT;

    }

    private ReactInstanceManager resolveInstanceManager(){
        ReactInstanceManager instanceManager;
        final Activity currentActivity = MainActivity.this;
        if (currentActivity == null) {
            return null;
        }
        ReactApplication reactApplication = (ReactApplication) currentActivity.getApplication();
        instanceManager = reactApplication.getReactNativeHost().getReactInstanceManager();

        return instanceManager;
    }

    private void setJSBundle(ReactInstanceManager instanceManager, String latestJSBundleFile) throws IllegalAccessException {
        try {
            JSBundleLoader latestJSBundleLoader;
            if (latestJSBundleFile.toLowerCase().startsWith("assets://")) {
                latestJSBundleLoader = JSBundleLoader.createAssetLoader(MainApplication.getContext(), latestJSBundleFile, false);
            } else {
                latestJSBundleLoader = JSBundleLoader.createFileLoader(latestJSBundleFile);
            }
            Field bundleLoaderField = instanceManager.getClass().getDeclaredField("mBundleLoader");
            bundleLoaderField.setAccessible(true);
            bundleLoaderField.set(instanceManager, latestJSBundleLoader);
        } catch (Exception e) {
            throw new IllegalAccessException("Could not setJSBundle");
        }
    }

    private void loadBundleLegacy() {
        Log.d("loadBundleLegacy","loadBundle #3 loadBundleLegacy...");
        final Activity currentActivity =  MainActivity.this;
        if (currentActivity == null) {
            // The currentActivity can be null if it is backgrounded / destroyed, so we simply
            // no-op to prevent any null pointer exceptions.
            return;
        }
        currentActivity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                currentActivity.recreate();
            }
        });
    }

在需要重新加载的地方 调用loadBundle()方法便可。希望对自主实现更新加载bundle的朋友有所帮助。

猜你喜欢

转载自blog.csdn.net/knockheart/article/details/90021809
今日推荐