Android源码分析Debug下ReactNative的bundle文件加载流程

1,207 阅读4分钟
原文链接: blog.csdn.net

本文主要分析在debug环境下Android是怎么加载到bundle文件的主要加载流程,不涉及太底层的代码均是Java代码分析。

开始

首先我们也在AndroidStudio中多多少少看过RN的源码,也知道它其实就是一个ReactRootView,而且是通过下面这段代码进行加载相对应的视图呈现我们要的UI效果:

mReactRootView.startReactApplication(
              getReactNativeHost().getReactInstanceManager(),
              mainComponentName,
              getLaunchOptions());

可以知道mainComponentName这个是我们重写了ReactActivity中相对应的

public String getMainComponentName() {
        return "RN_Demo";
    }

但是ReactInstanceManager这个类了解的还是比较,根据代码的追踪还是比较容易的就找到了相对应的初始化代码:

protected ReactInstanceManager createReactInstanceManager() {
  ReactInstanceManagerBuilder builder = ReactInstanceManager.builder()
    .setApplication(mApplication)
    .setJSMainModulePath(getJSMainModuleName())
    .setUseDeveloperSupport(getUseDeveloperSupport())
    .setRedBoxHandler(getRedBoxHandler())
    .setJavaScriptExecutorFactory(getJavaScriptExecutorFactory())
    .setUIImplementationProvider(getUIImplementationProvider())
    .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()));
  }
  return builder.build();
}

为什么要看这些代码,主要是为了在debug环境的时候能明白这些参数是什么从哪里来,所以这里主要关系下面两个函数:

// 返回用于拼接bundle的名字
protected String getJSMainModuleName() {
  return "index.android";
}

// debug的时候是返回true
public boolean getUseDeveloperSupport() {
  return BuildConfig.DEBUG;
}

记住这两个方法之后,我们继续看ReactRootView的加载代码:createReactContextInBackground -> recreateReactContextInBackgroundInner
然后这边涉及到了一个全新的类DevSupportManager这个是一个接口他的具体实现类:
DisabledDevSupportManager: 用于线上的版本
DevSupportManagerImpl: 用于debug的版本

// DevSupportManager的初始化方式:
mDevSupportManager =
        DevSupportManagerFactory.create(
            applicationContext,
            createDevHelperInterface(),
            mJSMainModulePath,
            useDeveloperSupport,
            redBoxHandler,
            devBundleDownloadListener,
            minNumShakes);

根据ReactInstanceManager的初始化我们可以知道 mJSMainModulePath = getJSMainModuleName() = “index.android” .. 然后我们很自然的跟进入看看是怎么初始化的原来是通过反射大法:

String className =
        new StringBuilder(DEVSUPPORT_IMPL_PACKAGE)
          .append(".")
          .append(DEVSUPPORT_IMPL_CLASS)
          .toString();
      Class<?> devSupportManagerClass =
        Class.forName(className);
      Constructor constructor =
        devSupportManagerClass.getConstructor(
          Context.class,
          ReactInstanceManagerDevHelper.class,
          String.class,
          boolean.class,
          RedBoxHandler.class,
          DevBundleDownloadListener.class,
          int.class);
      return (DevSupportManager) constructor.newInstance(
        applicationContext,
        reactInstanceManagerHelper,
        packagerPathForJSBundleName,
        true,
        redBoxHandler,
        devBundleDownloadListener,
        minNumShakes);

当然如果是非debug的时候会返回:

if (!enableOnCreate) {
  return new DisabledDevSupportManager();
}

不容易呀,终于是找到debug相关的类,既然初始化完成了那么我们就进入看看里面做了什么?

加载

我们在DevSupportManagerImpl的构造函数中重点关注一些重要类的初始化:

// DevServerHelper初始化
  public DevServerHelper(DevInternalSettings settings, String packageName) {
    mSettings = settings;
    mClient = new OkHttpClient.Builder()
      .connectTimeout(HTTP_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
      .readTimeout(0, TimeUnit.MILLISECONDS)
      .writeTimeout(0, TimeUnit.MILLISECONDS)
      .build();
    mBundleDownloader = new BundleDownloader(mClient);

    mRestartOnChangePollingHandler = new Handler();
    mPackageName = packageName;
  }

很显然这里主要是初始化了一个OkHttpClient对象,自然这里肯定是用于请求地址用的。

mJSBundleTempFile = new File(applicationContext.getFilesDir(), JS_BUNDLE_FILE_NAME);

初始化了一个ReactNativeDevBundle.js文件 很好一切都就绪之后开始执行mDevSupportManager.handleReloadJS这个方法:

  public void handleReloadJS() {
  // 其他的代码都忽略只看这部分的代码即可
    PrinterHolder.getPrinter()
          .logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: load from Server");
      String bundleURL =
        mDevServerHelper.getDevServerBundleURL(Assertions.assertNotNull(mJSAppBundleName));
      reloadJSFromServer(bundleURL);
  }

重要部分的来了mDevServerHelper.getDevServerBundleURL:

  public String getDevServerBundleURL(final String jsModulePath) {
    return createBundleURL(
        mSettings.getPackagerConnectionSettings().getDebugServerHost(),
        jsModulePath,
        getDevMode(),
        getJSMinifyMode(),
        mSettings.isBundleDeltasEnabled());
  }

跟着流程继续看是怎么拼接host地址的getDebugServerHost进入这个方法最后会发现:

public static final String EMULATOR_LOCALHOST = "10.0.2.2";
public static final String GENYMOTION_LOCALHOST = "10.0.3.2";
public static final String DEVICE_LOCALHOST = "localhost";

private static String getServerIpAddress(int port) {
    // Since genymotion runs in vbox it use different hostname to refer to adb host.
    // We detect whether app runs on genymotion and replace js bundle server hostname accordingly

    String ipAddress;
    if (isRunningOnGenymotion()) {
      ipAddress = GENYMOTION_LOCALHOST;
    } else if (isRunningOnStockEmulator()) {
      ipAddress = EMULATOR_LOCALHOST;
    } else {
      ipAddress = DEVICE_LOCALHOST;
    }

    return String.format(Locale.US, "%s:%d", ipAddress, port);
  }

这边系统还会判断是否是Genymotion或者自带的Emulator模拟器,当然这些我们都没有设置,所以这里直接返回的是DEVICE_LOCALHOST这个本地的地址,所以最后的拼接出来的的地址是:localHost:8081 .. 然后我们回到createBundleURL这个方法中得到最最最最最终的拼接地址是:

http://localHost:8081/index.android.bundle?platform=android&dev=true&jsMinify=false

然后我们前面已经初始化好了OKHttpClient对象,接下来就是执行这个地址文件的下载,并下载到mJSBundleTempFile也就是ReactNativeDevBundle.js这个文件并保存到我们Context.getFilesDir路径下面。

最后我们下载成功之后会回调到ReactInstanceManager类的

  private void onJSBundleLoadedFromServer() {
    Log.d(ReactConstants.TAG, "ReactInstanceManager.onJSBundleLoadedFromServer()");
    recreateReactContextInBackground(
        mJavaScriptExecutorFactory,
        JSBundleLoader.createCachedBundleFromNetworkLoader(
            mDevSupportManager.getSourceUrl(), mDevSupportManager.getDownloadedJSBundleFile()));
  }

然后继续recreateReactContextInBackground这个方法下去,这个方法那就做太多事情了,里面涉及到jni相关的能力有限分析不下去了,所以就此结束了。

总结

在我们写完RN代码的时候 react-native run-android 命令启动后你能看到: 我们会启动一个Http服务并监听8081端口,然后我们把我们写的效果经过Android一系列的构造流程打包成apk并安装。在我们敲入命令的时候我们会发现:

request:/index.android.bundle?platform=android&dev=true

大致原理:RN会在我们本地帮我们把相关的数据打包完成并上传到这个地址去,所以我们在debug的时候可以通过下载得到相对应的bundle文件。

额外发现

我们在生成这个下载地址的时候是否发现这么一行代码:

  public String getDebugServerHost() {

    String hostFromSettings = mPreferences.getString(PREFS_DEBUG_SERVER_HOST_KEY, null);
    if (!TextUtils.isEmpty(hostFromSettings)) {
      return Assertions.assertNotNull(hostFromSettings);
    }
    return host;
  }

也就是说如果PREFS_DEBUG_SERVER_HOST_KEY这个对应的Preferences不为空那么我们就从这个地址上加载相对应的bundle文件,所以我们可以根据这个原理弄个输入框只要写入ip以及端口我们就能读取别人写好的RN并实现调试了,如:

PreferenceManager.getDefaultSharedPreferences(applicationContext)
.put("192.168.1.1:8081");

是吧,这样子我们就能直接读取这个ip下的bundle文件了,当然前提是要存在相对应的bundle文件。

稍微走了一遍流程清楚多了只怎么个加载原理,当然Android跟RN交互的底层代码还是很大的一部分代码,有待分析理解。