你不知道的 Electron (一):神奇的 remote 模塊

轉自IMWeb社區,做者:laynechen,原文連接javascript

在上一篇 Electron 進程通訊 中,介紹了 Electron 中的兩種進程通訊方式,分別爲:css

  1. 使用 ipcMainipcRenderer 兩個模塊
  2. 使用 remote 模塊

相比於使用兩個 IPC 模塊,使用 remote 模塊相對來講會比較天然一點。remote 模塊幫咱們屏蔽了內部的進程通訊,使得咱們在調用主進程的方法時徹底沒有感知到主進程的存在。html

上一篇 Electron 進程通訊 中,對 remote 的實現只是簡單的說了下它底層依舊是經過 ipc 模塊來實現通訊:前端

經過 remote 對象,咱們能夠沒必要發送進程間消息來進行通訊。但實際上,咱們在調用遠程對象的方法、函數或者經過遠程構造函數建立一個新的對象,實際上都是在發送一個同步的進程間消息(官方文檔 上說這相似於 JAVA 中的 RMI)。java

也就是說,remote 方法只是不用讓咱們顯式的寫發送進程間的消息的方法而已。在上面經過 remote 模塊建立 BrowserWindow 的例子裏。咱們在渲染進程中建立的 BrowserWindow 對象其實並不在咱們的渲染進程中,它只是讓主進程建立了一個 BrowserWindow 對象,並返回了這個相對應的遠程對象給了渲染進程。web

可是隻是這樣嗎?api

這篇文章會從 remote 模塊的源碼層面進行分析該模塊的實現。promise

"假" 的多進程?

咱們看一個例子,來了解直接使用 IPC 通訊和使用 remote 模塊的區別:緩存

分別經過 IPC 模塊和 remote 模塊實如今渲染進程中獲取主進程的一個對象,再在主進程中修改該對象的屬性值,看下渲染進程中的對象對應的屬性值是否會跟着改變。bash

邏輯比較簡單,直接看代碼。

使用 IPC 模塊

主進程代碼:

const remoteObj = {
  name: 'remote',
};

const getRemoteObject = (event) => {
  // 一秒後修改 remoteObj.name 的值
  // 並通知渲染進程從新打印一遍 remoteObj 對象
  setTimeout(() => {
    remoteObj.name = 'modified name';
    win.webContents.send('modified');
  }, 1000);

  event.returnValue = remoteObj;
}
複製代碼

渲染進程代碼:

index.html :

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Electron</title>
  <style> body { margin: 30px; } #container { font-weight: bold; font-size: 32px; } </style>
</head>
<body>
    <pre id="container"></pre> 
    <script src="./index.js"></script>
</body>
</html>
複製代碼

index.js :

const { remote, ipcRenderer } = window.require('electron');
const container = document.querySelector('#container');

const remoteObj = ipcRenderer.sendSync('getRemoteObject');

container.innerText = `Before modified\n${JSON.stringify(remoteObj, null, ' ')}`;

ipcRenderer.on('modified', () => {
  container.innerText = `${container.innerText}\n After modified\n${JSON.stringify(remoteObj, null, ' ')}`;
});
複製代碼

界面輸出結果以下:

嗯..沒什麼問題,和預期同樣。因爲進程通訊中數據傳遞通過了序列化和反序列化,渲染進程拿到的進程中的對象已經不是同一個對象,指向的內存地址不一樣。

使用 remote 模塊

主進程代碼:

const remoteObj = {
  name: 'remote',
};

const getRemoteObject = (event) => {
  // 一秒後修改 remoteObj.name 的值
  // 並通知渲染進程從新打印一遍 remoteObj 對象
  setTimeout(() => {
    remoteObj.name = 'modified name';
    win.webContents.send('modified');
  }, 1000);

  return remoteObj;
}

// 掛載方法到 app 模塊上,供 remote 模塊使用
app.getRemoteObject = getRemoteObject;
複製代碼

渲染進程代碼:

index.html 文件同上。

index.js 修改成經過 remote 模塊獲取 remoteObj :

...
const remoteObj = remote.app.getRemoteObject();
...
複製代碼

界面輸出結果以下:

咱們發現,經過 remote 模塊拿到的 remoteObj 竟然和咱們拿渲染進程中的對象同樣,是一份引用。難道實際上並無主進程和渲染進程?又或者說 remote 模塊使用了什麼黑魔法,使得咱們在渲染進程能夠引用到主進程的對象?

Java's RMI

官方文檔在 remote 模塊的介紹中提到了它的實現相似於 Java 中的 RMI。

那麼 RMI 是什麼? remote 的黑魔法是否藏在這裏面?

RMI (Remote Method Invoke)

遠程方法調用是一種計算機之間利用遠程對象互相調用實現雙方通信的一種通信機制。使用這種機制,某一臺計算機上的對象能夠調用另一臺計算機上的對象來獲取遠程數據。

若是使用 http 協議來實現遠程方法調用,咱們可能會這麼實現:

雖然 RMI 底層並非使用 http 協議,但大體的思路是差很少的。和 remote 同樣,進程通訊離不開 IPC 模塊。

可是 IPC 通訊是能夠作到對用戶來講是隱藏的。RMI 的目的也同樣,要實現客戶端像調用本地方法同樣調用遠程對象上的方法,底層的通訊不該該暴露給用戶。

RMI 實現原理

RMI 並非經過 http 協議來實現通訊的,而是使用了 JRMP (Java Remote Method Protocol)。下面是經過 JRMP 實現服務端和客戶端通訊的流程:

與 http 相似,可是這裏多了個註冊表。

這裏的註冊表能夠類比於咱們的 DNS 服務器。

服務端須要告訴 DNS 服務器,xxx 域名應該指向這臺服務器的 ip,客戶端就能夠經過域名向 DNS 服務器查詢服務器的 ip 地址來實現訪問服務器。在 RMI 中,服務端向註冊表註冊,rmi://localhost:8000/hello 指向服務端中的某個對象 A,當客戶端經過 rmi://localhost:8000/hello 查找服務端的對象時,就返回這個對象 A。

數據傳遞

註冊表返回對象 A 是怎麼傳遞給客戶端的呢?首先想到的天然是序列化 & 反序列化。 RMI 也是這麼實現的,不過度了幾種狀況:

  1. 簡單數據類型 (int, boolean, double 等):無需序列化直接傳遞便可
  2. 對象:對象序列化來傳遞整個對象的副本
  3. 實現了 java.rmi.Remote 接口的對象(!!重點):遠程引用

RMI 裏面另外一個比較重要的點就是這個遠程對象。RMI 對這些實現了 Remote 接口的對象,進行了一些封裝,爲咱們屏蔽了底層的通訊,達到客戶端調用這些遠程對象上的方法時像調用本地方法同樣的目的。

RMI 的大體流程

比較懵逼?不要緊,看代碼實現:

RMI 簡單實現

(建議你們一塊兒運行下這個例子~不動手實現怎麼會有成就感!!)

客戶端和服務端都有的遠程對象接口文件 HelloRMI.java

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface HelloRMI extends Remote {
    String sayHi(String name) throws RemoteException;
}

複製代碼

服務端實現 HelloRMI 接口的 HelloImpl.java:

import java.rmi.RemoteException;
import java.rmi.server.ServerNotActiveException;
import java.rmi.server.UnicastRemoteObject;

public class HelloRMIImpl extends UnicastRemoteObject implements HelloRMI {
    protected HelloRMIImpl() throws RemoteException {
        super();
    }

    @Override
    public String sayHi(String name) throws RemoteException {
        try {
            System.out.println("Server: Hi " + name + " " + getClientHost());
        } catch (ServerNotActiveException e) {
            e.printStackTrace();
        }
        return "Server";
    }
}

複製代碼

服務端測試程序 Server.java

import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;

public class Server {
    public static void main(String[] args) {
        try {
            // 建立遠程服務對象實例
            HelloRMI hr = new HelloRMIImpl();
            // 在註冊表中註冊
            LocateRegistry.createRegistry(9999);
            // 綁定對象到註冊表中
            Naming.bind("rmi://localhost:9999/hello", hr);
            System.out.println("RMI Server bind success");
        } catch (RemoteException e) {
            e.printStackTrace();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (AlreadyBoundException e) {
            e.printStackTrace();
        }
    }
}

複製代碼

客戶端測試程序 Client.java:

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

public class Client {
    public static void main(String[] args) {
        try {
            HelloRMI hr = (HelloRMI) Naming.lookup("rmi://localhost:9999/hello");
            System.out.println("Client: Hi " + hr.sayHi("Client"));
        } catch (NotBoundException e) {
            e.printStackTrace();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
}

複製代碼

先運行 Server.java,開啓註冊表並向註冊表綁定遠程對象。而後運行客戶端就能夠查找和運行服務端上的遠程對象了。

remote 中的 RMI

咱們看下前面的例子,使用 remote 模塊獲取主進程上的對象背後發生了什麼:

若是說 remote 只是幫咱們屏蔽了 IPC 操做,那麼渲染進程拿到的主進程中的對象,應該與主進程中的對象是沒有任何關係的,不該該受到主進程的修改而影響。那麼 remote 還幫咱們作了什麼呢?

其實重點不在於 remote 背後幫咱們作了 IPC,而是在於數據的傳遞。前面的 RMI 中說到,數據傳遞分爲簡單數據類型、沒有繼承 Remote 的對象和繼承了 Remote 的遠程對象。繼承了 Remote 的遠程對象在數據傳遞的時候是經過遠程引用傳遞而非簡單的序列化和反序列化。在 remote 模塊中,它至關於幫咱們將全部的 Object 都給轉換爲了遠程對象。

經過源碼學習下 remote 是如何進行這種轉換的:

lib/renderer/api/remote.js:

...

const addBuiltinProperty = (name) => {
  Object.defineProperty(exports, name, {
    get: () => exports.getBuiltin(name)
  })
}

const browserModules =
  require('../../common/api/module-list').concat(
  require('../../browser/api/module-list'))

// And add a helper receiver for each one.
browserModules
  .filter((m) => !m.private)
  .map((m) => m.name)
  .forEach(addBuiltinProperty)

複製代碼

這段代碼作的事情是把主進程纔可使用的模塊添加到了 remote 模塊的屬性在中。

...

exports.getBuiltin = (module) => {
  const command = 'ELECTRON_BROWSER_GET_BUILTIN'
  const meta = ipcRenderer.sendSync(command, module)
  return metaToValue(meta)
}
...
複製代碼

getBuiltin 的處理方法就是發送一個同步的進程間消息,向主進程請求某個模塊對象。最後會將返回值 meta 調用 metaToValue 後再返回。一切祕密都在 這個方法中了。

// Convert meta data from browser into real value.
function metaToValue (meta) {
  const types = {
    value: () => meta.value,
    array: () => meta.members.map((member) => metaToValue(member)),
    buffer: () => bufferUtils.metaToBuffer(meta.value),
    promise: () => resolvePromise({then: metaToValue(meta.then)}),
    error: () => metaToPlainObject(meta),
    date: () => new Date(meta.value),
    exception: () => { throw metaToException(meta) }
  }

  if (meta.type in types) {
    return types[meta.type]()
  } else {
    let ret
    if (remoteObjectCache.has(meta.id)) {
      return remoteObjectCache.get(meta.id)
    }

    // A shadow class to represent the remote function object.
    if (meta.type === 'function') {
      let remoteFunction = function (...args) {
        let command
        if (this && this.constructor === remoteFunction) {
          command = 'ELECTRON_BROWSER_CONSTRUCTOR'
        } else {
          command = 'ELECTRON_BROWSER_FUNCTION_CALL'
        }
        const obj = ipcRenderer.sendSync(command, meta.id, wrapArgs(args))
        return metaToValue(obj)
      }
      ret = remoteFunction
    } else {
      ret = {}
    }

    setObjectMembers(ret, ret, meta.id, meta.members)
    setObjectPrototype(ret, ret, meta.id, meta.proto)
    Object.defineProperty(ret.constructor, 'name', { value: meta.name })

    // Track delegate obj's lifetime & tell browser to clean up when object is GCed.
    v8Util.setRemoteObjectFreer(ret, meta.id)
    v8Util.setHiddenValue(ret, 'atomId', meta.id)
    remoteObjectCache.set(meta.id, ret)
    return ret
  }
}
複製代碼

對不一樣類型進行了不一樣的處理。在對函數的處理中,將本來的函數外封裝了一個函數用於發送同步的進程間消息,並將返回值一樣調用 metaToValue 進行轉換後返回。

另外,對 Object 類型對象,還須要對他們的屬性進行相似函數同樣的封裝處理:

function metaToValue (meta) {
    ...
    setObjectMembers(ret, ret, meta.id, meta.members)
    setObjectPrototype(ret, ret, meta.id, meta.proto)
    ...
}
複製代碼

對返回對象屬性重寫 get、set 方法。對調用遠程對象上的屬性,一樣是經過發送同步的進程間消息來獲取,這也就是爲何主進程修改了值,渲染進程就也能感知到的緣由了。

還有一個須要注意的地方是,爲了避免重複獲取遠程對象,對返回的對象 remote 是會進行緩存的,看 metaToValue 的倒數第二行:remoteObjectCache.set(meta.id, ret)

讀者思考

到這裏咱們知道了文章開頭遇到的神奇現象的緣由。這裏拋出個問題給讀者:思考下若是是主進程的函數是異步的(函數返回一個 Promise 對象),Promise 對象是如何實現數據傳遞的?是否會阻塞渲染進程?

總結

經過上述分析咱們知道,remote 模塊不只幫咱們實現了 IPC 通訊,同時爲了達到相似引用傳遞的效果,使用了相似 Java 中的 RMI,對主進程的對象進行了一層封裝,使得咱們在訪問遠程對象上的屬性時,也須要向主進程發送同步進程消息來獲取到當前主進程上該對象實際的值。

【參考資料】

相關文章
相關標籤/搜索