React Native调试源码分析——远程代码加载

1、前言

    最近一直思考着如何让react native开发时分步加载js代码,而RN本身就有支持类似的功能,比如hot reload,而hot reload是根据文件的变化而进行差量加载代码的,如果需要实现分步加载的功能,就需要魔改react native相关的加载源码,因此经过几天的研究,总体了解了React Naitve调试过程中的流程,现分享给需要魔改或者了解相关源码的同学们。

   这里大致把内容分成三部分,这里先说“远程代码加载”。

2、RN远程加载的入口

   相信大家开发RN的时候经常有这样的操作——修改代码后摇一摇手机唤出rn dev弹框,点击reload重新加载。现以此为切入点,展开调试源码之旅。

React Native调试源码分析——远程代码加载_第1张图片

UI入口源码如下:

//DevSupportManagerImpl.java

 @Override
  public void showDevOptionsDialog() {
    if (mDevOptionsDialog != null || !mIsDevSupportEnabled || ActivityManager.isUserAMonkey()) {
      return;
    }
    LinkedHashMap options = new LinkedHashMap<>();
    /* register standard options */
    options.put(
        mApplicationContext.getString(R.string.catalyst_reloadjs),
        new DevOptionHandler() {
          @Override
          public void onOptionSelected() {
            handleReloadJS();//这里重载js
          }
        });

handleReloadJS代码如下:

@Override
  public void handleReloadJS() {

    UiThreadUtil.assertOnUiThread();
    // dismiss redbox if exists
    hideRedboxDialog();

    if (mDevSettings.isRemoteJSDebugEnabled()) {
       ///略略略
    } else {
      String bundleURL =
        mDevServerHelper.getDevServerBundleURL(Assertions.assertNotNull(mJSAppBundleName));
      reloadJSFromServer(bundleURL);//从服务端reload
    }
  }

这一步生成了一个bundleURL对象,其生成过程就是根据你当前加载的js入口名进行拼接,给个例子:

http://10.0.2.2:8081/index.delta?platform=android&dev=true&minify=false

然后看看加载bundleURl的函数reloadJSFromServer:

//DevSupportManagerImpl.java

  public void reloadJSFromServer(final String bundleURL) {
    ReactMarker.logMarker(ReactMarkerConstants.DOWNLOAD_START);

    mDevLoadingViewController.showForUrl(bundleURL);
    mDevLoadingViewVisible = true;

    final BundleDownloader.BundleInfo bundleInfo = new BundleDownloader.BundleInfo();

    mDevServerHelper.downloadBundleFromURL(
        new DevBundleDownloadListener() {
          @Override
          public void onSuccess(final @Nullable NativeDeltaClient nativeDeltaClient) {
            ................
            UiThreadUtil.runOnUiThread(
                new Runnable() {
                  @Override
                  public void run() {
                    ReactMarker.logMarker(ReactMarkerConstants.DOWNLOAD_END, bundleInfo.toJSONString());
                    mReactInstanceManagerHelper.onJSBundleLoadedFromServer(nativeDeltaClient);
                  }
                });
          }
        ............
        ...........  
        },
        mJSBundleTempFile,
        bundleURL,
        bundleInfo);
  }

reloadJSFromServer函数把文件下载成功后抛给mReactInstanceManagerHelper加载,中间并无其他逻辑,最后执行如下代码:

//ReactInstanceManager.java 

@ThreadConfined(UI)
  private void onJSBundleLoadedFromServer(@Nullable NativeDeltaClient nativeDeltaClient) {
    Log.d(ReactConstants.TAG, "ReactInstanceManager.onJSBundleLoadedFromServer()");

    JSBundleLoader bundleLoader = nativeDeltaClient == null
        ? JSBundleLoader.createCachedBundleFromNetworkLoader(
            mDevSupportManager.getSourceUrl(),
            mDevSupportManager.getDownloadedJSBundleFile())
        : JSBundleLoader.createDeltaFromNetworkLoader(
            mDevSupportManager.getSourceUrl(), nativeDeltaClient);

    recreateReactContextInBackground(mJavaScriptExecutorFactory, bundleLoader);
  }

这里有根据nativeDeltaClient是否为空来选择加载器,因为reload操作是全量重载,因此这个情况下总是空,因此这里选择的是CachedBundleFromNetworkLoader加载已经下载下来的JSBundleFile。最后就执行我们的recreateReactContextInBackground真正地重新加载代码了,这个函数我就不解释了,网上有许多资料可以自己了解了解。

到这里App端就完成了Reload操作了,而我们的JSBundleFile是从哪里下载呢?

3、Metro打包服务

    metro是谁?其实百分之百的RN开发者都见过它,只是有些人不知道它的名字。

React Native调试源码分析——远程代码加载_第2张图片

看到Running Metro Bundler on port 8081了么?没错!这个进程就是Metro打包服务,调试中100%的打包工作都由它来完成。

我们来看看打包服务大致是怎么接活的吧。

直接开门见山,找到metro代码位置./node_modules/metro,总入口runServer:

//index.js.flow
exports.runServer = async (
  config: ConfigT,
  {
    host,
    onReady,
    onError,
    secure = false,
    secureKey,
    secureCert,
    hmrEnabled = false,
  }: RunServerOptions,
) => {
  // Lazy require
  const connect = require('connect');

  const serverApp = connect();//1 <--

  const {
    attachHmrServer,
    middleware,
    metroServer,//2 <--
    end,
  } = await exports.createConnectMiddleware(config);

  serverApp.use(middleware);

........

  let httpServer;

  if (secure) {
    httpServer = https.createServer(
      {
        key: await readFile(secureKey),
        cert: await readFile(secureCert),
      },
      serverApp,
    );
  } else {
    httpServer = http.createServer(serverApp);
  }

  httpServer.on('error', error => {
    onError && onError(error);
    end();
  });

  if (hmrEnabled) {
    attachHmrServer(httpServer);
  }

  return new Promise((resolve, reject) => {
    httpServer.listen(config.server.port, host, () => {//3 <--
      onReady && onReady(httpServer);
      resolve(httpServer);
    });

    // Disable any kind of automatic timeout behavior for incoming
    // requests in case it takes the packager more than the default
    // timeout of 120 seconds to respond to a request.
    httpServer.timeout = 0;

    httpServer.on('error', error => {
      end();
      reject(error);
    });

    httpServer.on('close', () => {
      end();
    });
  });
};

代码中有三个箭头:

1、metro的服务使用了connect开源框架实现

2、metroServer就是metro服务,它以中间件形式存在

3、listen后代表这个服务已经启动

MetroServer是打包的关键,我们来看看Server的源码:

Server.js.flow

 async _processRequest(
    req: IncomingMessage,
    res: ServerResponse,
    next: (?Error) => mixed,
  ) {
    const urlObj = url.parse(req.url, true);
    const {host} = req.headers;
    debug(`Handling request: ${host ? 'http://' + host : ''}${req.url}`);
    /* $FlowFixMe: Could be empty if the URL is invalid. */
    const pathname: string = urlObj.pathname;

    if (pathname.match(/\.bundle$/)) {
      await this._processBundleRequest(req, res);
    } else if (pathname.match(/\.map$/)) {
      await this._processSourceMapRequest(req, res);
    } else if (pathname.match(/\.assets$/)) {
      await this._processAssetsRequest(req, res);
    } else if (pathname.match(/\.delta$/)) {//1 <--
      await this._processDeltaRequest(req, res);
    }
    .... ....
    else {
      next();
    }
  }


 _processDeltaRequest = this._createRequestProcessor({
    createStartEntry(context) {
      return {
        action_name: 'Requesting delta',
        bundle_url: context.req.url,
        entry_point: context.entryFile,
        bundler: 'delta',
        build_id: context.buildID,
        bundle_options: context.bundleOptions,
        bundle_hash: context.graphId,
      };
    },
    createEndEntry(context) {
      return {
        outdated_modules: context.result.numModifiedFiles,
      };
    },
    build: async ({
      revisionId,
      graphId,
      entryFile,
      transformOptions,
      serializerOptions,
      onProgress,
    }) => {

      let revPromise;
      if (revisionId != null) {
        revPromise = this._bundler.getRevision(revisionId);//2 <--
      }
      // Even if we receive a revisionId, it might have expired.
      if (revPromise == null) {
        revPromise = this._bundler.getRevisionByGraphId(graphId);
      }

      let delta;
      let revision;
      if (revPromise != null) {
        const prevRevision = await revPromise;

        ({delta, revision} = await this._bundler.updateGraph(
          prevRevision,
          prevRevision.id !== revisionId,
        ));
      } else {
        ({delta, revision} = await this._bundler.initializeGraph(//3 <--
          entryFile,
          transformOptions,
          {onProgress},
        ));
      }

      const bundle = deltaJSBundle(
        entryFile,
        revision.prepend,
        delta,
        revision.id,
        revision.graph,
        ... ...
      );

      return {
        numModifiedFiles:
          delta.added.size + delta.modified.size + delta.deleted.size,
        nextRevId: revision.id,
        bundle,
      };
    },
    finish({mres, result}) {//4 <--
      const bundle = serializeDeltaJSBundle.toJSON(result.bundle);
      mres.setHeader(
        FILES_CHANGED_COUNT_HEADER,
        String(result.numModifiedFiles),
      );
      mres.setHeader(DELTA_ID_HEADER, String(result.nextRevId));
      mres.setHeader('Content-Type', 'application/json');
      mres.setHeader('Content-Length', String(Buffer.byteLength(bundle)));
      mres.end(bundle);
    },
  });

1、所有的请求都会走到_processRequest,会根据请求的类型来做不同操作,如reload对应的是_processDeltaRequest

2、_processDeltaRequest中会先根据revisionId来判断这个版本是不是打包过,而reload都是会生成新的revisionId,所以这里获取到的revPromise必为空

3、初始化依赖的graph结构,如果这个revisionId打包过的情况下是生成差量的结构,最后就打包成bundle对象

4、finish的时候会把打包后的bundle回传给app

4、总结

  本节的内容比较简单,主要的流程大概是

1、App通过reload发起重新打包请求

2、Metro服务受理请求,重新计算依赖并重新打包

3、App收到打包后的bundle保存到临时文件中并recreateReactContextInBackground

App  <--->  MetroServer  <--> Bundler

 

你可能感兴趣的:(react,naitve)