美文网首页React Native实践
Android源码分析Debug下ReactNative的bun

Android源码分析Debug下ReactNative的bun

作者: 左手木亽 | 来源:发表于2018-03-14 22:13 被阅读12次

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

    相关文章

      网友评论

        本文标题:Android源码分析Debug下ReactNative的bun

        本文链接:https://www.haomeiwen.com/subject/krgqqftx.html