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

本文主要分析在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交互的底层代码还是很大的一部分代码,有待分析理解。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351

推荐阅读更多精彩内容