React Native Debug原理淺析

第一次在segmentfault寫博客,很緊張~~~公司項目上ReactNative,以前也是沒有接觸過,因此也是一邊學習一邊作項目了,最近騰出手來更新總結了一下RN的Debug的一個小知識點,不是說怎麼去Debug,而是Debug的代碼原理,下面開始正文。html

Debug過程涉及到三個對象,一個是App(Android或iOS),一個是Server,另一個就是瀏覽器(Chrome或FireFox或其餘)。Server是App和瀏覽器之間通訊的橋樑,好比App發Http請求給Server,Server再經過WebSocket發送給瀏覽器,反過來也是。首先確定須要準備一下中介,就是Servernode

1.Server

這裏的Server不用專門準備一臺服務器,只須要配置一個Node.js環境,而後啓動npm start就行。npm start在package.json中進行配置了,也就是會執行cli.js腳本。react

"scripts": {
    "start": "node node_modules/react-native/local-cli/cli.js start"
  },

而後cli.js會執行runServer.js,在這裏啓動一個NodeJS Server:android

const serverInstance = args.https
    ? https.createServer(
        {
          key: fs.readFileSync(args.key),
          cert: fs.readFileSync(args.cert),
        },
        app,
      )
    : http.createServer(app);

  serverInstance.listen(args.port, args.host, 511, function() {
    attachHMRServer({
      httpServer: serverInstance,
      path: '/hot',
      packagerServer,
    });

    wsProxy = webSocketProxy.attachToServer(serverInstance, '/debugger-proxy');
    ms = messageSocket.attachToServer(serverInstance, '/message');
    readyCallback(reporter);
  });

有了中介Server後就能夠創建App與瀏覽器之間的關係了。git

2.創建鏈接

在手機菜單中點擊Debug JS Remotely,App就會發出一個Http請求github

GET /launch-js-devtools HTTP/1.1

Server接收到這個請求會執行opn操做,主要作兩件事:web

  1. 打開Chrome的一個tab
  2. 讓這個tab打開urlhttp://localhost:8081/debugger-ui/

這個界面就是咱們打開Debug時在瀏覽器見到的第一個界面
瀏覽器調試界面.pngnpm

這個界面的文件就是Server的index.html,我截取了body的代碼:json

<body>
  <div class="content">
    <label for="dark">
      <input type="checkbox" id="dark" onclick="Page.toggleDarkTheme()"> Dark Theme
    </label>
    <label for="maintain-priority">
      <input type="checkbox" id="maintain-priority" onclick="Page.togglePriorityMaintenance()"> Maintain Priority
    </label>
    <p>
      React Native JS code runs as a web worker inside this tab.
    </p>
    <p>Press <kbd id="shortcut" class="shortcut">⌘⌥I</kbd> to open Developer Tools. Enable <a href="https://stackoverflow.com/a/17324511/232122" target="_blank">Pause On Caught Exceptions</a> for a better debugging experience.</p>
    <p>You may also install <a href="https://github.com/facebook/react-devtools/tree/master/packages/react-devtools" target="_blank">the standalone version of React Developer Tools</a> to inspect the React component hierarchy, their props, and state.</p>
    <p>Status: <span id="status">Loading...</span></p>
  </div>
</body>

瀏覽器在執行index.html的時候會發出下面的請求:segmentfault

GET /debugger-proxy?role=debugger&name=Chrome HTTP/1.1

咱們來看看發出這個請求有什麼目的,扒一扒源碼:

function connectToDebuggerProxy() {
    const ws = new WebSocket('ws://' + window.location.host + '/debugger-proxy?role=debugger&name=Chrome'); //Chrome經過websocket和Packager保持通信
  
      //WebSocket註冊監聽
    ws.onopen = function() {
      Page.setState({status: {type: 'connecting'}});
    };

    ws.onmessage = async function(message) {
      if (!message.data) {
        return;
      }
      const object = JSON.parse(message.data);

      if (object.$event === 'client-disconnected') {
        shutdownJSRuntime();
        Page.setState({status: {type: 'disconnected'}});
        return;
      }

      if (!object.method) {
        return;
      }

      // Special message that asks for a new JS runtime
      if (object.method === 'prepareJSRuntime') {
        shutdownJSRuntime();
        console.clear();
        createJSRuntime();
        ws.send(JSON.stringify({replyID: object.id}));
        Page.setState({status: {type: 'connected', id: object.id}});
      } else if (object.method === '$disconnected') {
        shutdownJSRuntime();
        Page.setState({status: {type: 'disconnected'}});
      } else if (object.method === 'executeApplicationScript') {
        worker.postMessage({
          ...object,
          url: await getBlobUrl(object.url),
        });
      } else {
        // Otherwise, pass through to the worker.
        worker.postMessage(object);
      }
    };

    ws.onclose = function(error) {
      shutdownJSRuntime();
      Page.setState({status: {type: 'error', error}});
      if (error.reason) {
        console.warn(error.reason);
      }
      setTimeout(connectToDebuggerProxy, 500);
    };

    // Let debuggerWorker.js know when we're not visible so that we can warn about
    // poor performance when using remote debugging.
    document.addEventListener('visibilitychange', updateVisibility, false);
  }

首先就是經過new WebSocket瀏覽器創建與Server的聯繫,WebSocket就是能夠保持長鏈接的全雙工通訊協議,在握手階段經過Http進行,後面就和Http沒有什麼關係了。而後會給這個webSocket註冊一些監聽:

ws.onopen 
ws.onmessage
ws.onclose

在webSocket收到消息時會回調ws.onmessage。

到這裏App和瀏覽器之間就已經創建鏈接了,接下來App會發出幾個消息讓瀏覽器加載須要調試的代碼, 接着往下看。

3.加載調試代碼

首先須要強調的就是瀏覽器加載項目代碼確定不能在UI線程加載吧,要否則確定影響瀏覽器的正常工做。那怎麼去加載?啓一個後臺線程,有的小夥伴就要不信了,別急,咱們接着去扒一扒源碼。
App發出一個消息讓瀏覽器準備JS的運行環境:
準備JS運行環境.png

在收到‘prepareJSRuntime’消息會調用createJSRuntime。
// Special message that asks for a new JS runtime
      if (object.method === 'prepareJSRuntime') {
        shutdownJSRuntime();
        console.clear();
        createJSRuntime();
        ws.send(JSON.stringify({replyID: object.id}));
        Page.setState({status: {type: 'connected', id: object.id}});
      } else if (object.method === '$disconnected') {
        shutdownJSRuntime();
        Page.setState({status: {type: 'disconnected'}});
      } else if (object.method === 'executeApplicationScript') {
        worker.postMessage({
          ...object,
          url: await getBlobUrl(object.url),
        });
      } else {
        // Otherwise, pass through to the worker.
        worker.postMessage(object);
      }

接着看‘createJSRuntime’這個函數, 主要工做就是‘new Worker’,看下Worker的定義:

Web Workers is a simple means for web content to run scripts in
background threads. The worker thread can perform tasks without
interfering with the user interface.
也就是會起一個後臺線程,來運行‘debuggerWorker.js’這個腳本。
function createJSRuntime() {
      // This worker will run the application JavaScript code,
      // making sure that it's run in an environment without a global
      // document, to make it consistent with the JSC executor environment.
      worker = new Worker('debuggerWorker.js');
      worker.onmessage = function(message) {
        ws.send(JSON.stringify(message.data));
      };
      window.onbeforeunload = function() {
        return 'If you reload this page, it is going to break the debugging session. ' +
          'You should press' + refreshShortcut + 'in simulator to reload.';
      };
      updateVisibility();
}

接着看看debuggerWorker.js,主要就是一個消息的監聽,能夠看到在messageHandlers裏主要處理兩類消息:

'executeApplicationScript', 'setDebuggerVisibility'
/* global __fbBatchedBridge, self, importScripts, postMessage, onmessage: true */
/* eslint no-unused-vars: 0 */

'use strict';

onmessage = (function() {
  var visibilityState;
  var showVisibilityWarning = (function() {
    var hasWarned = false;
    return function() {
      // Wait until `YellowBox` gets initialized before displaying the warning.
      if (hasWarned || console.warn.toString().includes('[native code]')) {
        return;
      }
      hasWarned = true;
      console.warn(
        'Remote debugger is in a background tab which may cause apps to ' +
        'perform slowly. Fix this by foregrounding the tab (or opening it in ' +
        'a separate window).'
      );
    };
  })();

  var messageHandlers = {
    'executeApplicationScript': function(message, sendReply) {
      for (var key in message.inject) {
        self[key] = JSON.parse(message.inject[key]);
      }
      var error;
      try {
        importScripts(message.url);
      } catch (err) {
        error = err.message;
      }
      sendReply(null /* result */, error);
    },
    'setDebuggerVisibility': function(message) {
      visibilityState = message.visibilityState;
    },
  };

  return function(message) {
    if (visibilityState === 'hidden') {
      showVisibilityWarning();
    }

    var object = message.data;

    var sendReply = function(result, error) {
      postMessage({replyID: object.id, result: result, error: error});
    };

    var handler = messageHandlers[object.method];
    if (handler) {
      // Special cased handlers
      handler(object, sendReply);
    } else {
      // Other methods get called on the bridge
      var returnValue = [[], [], [], 0];
      var error;
      try {
        if (typeof __fbBatchedBridge === 'object') {
          returnValue = __fbBatchedBridge[object.method].apply(null, object.arguments);
        } else {
          error = 'Failed to call function, __fbBatchedBridge is undefined';
        }
      } catch (err) {
        error = err.message;
      } finally {
        sendReply(JSON.stringify(returnValue), error);
      }
    }
  };
})();

App在點擊調試的時候會給瀏覽器還發送這麼一個‘executeApplicationScript’消息,讓瀏覽器去加載項目代碼:
屏幕快照 2018-03-06 下午7.23.01.png
這個messageEvent的數據比較多,我就截取一部分,裏面包含了方法名,url(這個url就是後面瀏覽器須要去下載bundle的地方),inject包含的數據最多,主要是會賦值給瀏覽器全局對象的方法。

{
  "id": 1,
  "method": "executeApplicationScript",
  "url": "http://localhost:8081/index.android.bundle?platform=android&dev=true&minify=false",
  "inject": {
    "__fbBatchedBridgeConfig": "{\"remoteModuleConfig\":[[\"AccessibilityInfo\",{},[\"isTouchExplorationEnabled\"]],[\"LocationObserver\",{},[\"getCurrentPosition\",\"startObserving\",\"stopObserving\"]],[\"CameraRollManager\",{},[\"getPhotos\",\"saveToCameraRoll\"],[0,1]],[\"NetInfo\",{},[\"getCurrentConnectivity\",\"isConnectionMetered\"],[0,1]],[\"PlatformConstants\",{\"ServerHost\":\"localhost:8081\",\"reactNativeVersion\":{\"patch\":0,\"prerelease\":null,\"minor\":51,\"major\":0},\"Version\":21,\"isTesting\":false}],[\"TimePickerAndroid\",{}
}

webSocket首先接收到這個消息, 而後經過worker.postMessage給上面的worker發送‘executeApplicationScript’消息

ws.onmessage = async function(message) {
     ......
      // Special message that asks for a new JS runtime
      if (object.method === 'prepareJSRuntime') {
        shutdownJSRuntime();
        console.clear();
        createJSRuntime();
        ws.send(JSON.stringify({replyID: object.id}));
        Page.setState({status: {type: 'connected', id: object.id}});
      } else if (object.method === '$disconnected') {
        shutdownJSRuntime();
        Page.setState({status: {type: 'disconnected'}});
      } else if (object.method === 'executeApplicationScript') {
        worker.postMessage({
          ...object,
          url: await getBlobUrl(object.url),
        });
      } else {
        // Otherwise, pass through to the worker.
        worker.postMessage(object);
      }
    };

worker接收到這個消息在messageHandlers找到相應的處理方法,在裏面首選循環取出inject裏面的字段和value而後賦值給self,在這裏我理解就是這個worker線程的全局對象,而後經過 importScripts(message.url)去加載bundle。

var messageHandlers = {
    'executeApplicationScript': function(message, sendReply) {
      for (var key in message.inject) {
        self[key] = JSON.parse(message.inject[key]);
      }
      var error;
      try {
        importScripts(message.url);
      } catch (err) {
        error = err.message;
      }
      sendReply(null /* result */, error);
    },
    ......
  };

爲了證實我上面的分析沒錯,決定捉包看下發起的請求是否是這樣的:
捉包.png

在加載bundle後面還有一個map,體積也很大,有1.74MB的體積,這個是用於映射bundle裏面的代碼成一個個工程項目裏的類文件,這樣就和在代碼編譯器裏面調試效果同樣了。

4.總結

根據上面的捉包請求簡單總結下創建鏈接的過程,首先經過/launch-jsdevtools打開調試Tab,瀏覽器經過/debugger-proxy創建與Server的WebSocket鏈接,而後瀏覽器打開index.html文件,發起/debugger-ui/debuggerWorker.js創建後臺線程,經過這個後臺線程加載bundle。

到這裏創建Debug鏈接的原理分析就差很少了,但願對小夥伴們有幫助,歡迎點贊和關注哈。

謝謝你們!

相關文章
相關標籤/搜索