Electron渲染进程间通信,可以通过ipc或者global remote进行中转传递。由于ipc模式需要对数据进行序列化处理,所以存在一个问题,对呀ArryaBuffer数据,无法进行序列化处理,所以需要一些特殊的手段进行处理才可以传递。

渲染进程间通信

我们先简单了解下Electron渲染进程之间如何进行通信,这里先简单介绍ipc通信方式;

IPC(remote)

为了实现IPC对渲染进程间通信,我们需要将主进程创建的BrowserWindow窗口信息传递到各个渲染进程之间:

此处使用到了remote对象,在渲染进程下获取窗口实例,在electron高版本下,remote不在由electron单独提供,需要额外的安装包初始化才行;

主进程:

  1. // 主进程
  2. import { BrowswerWindow } from "electron";
  3. // 创建窗口1
  4. const window1 = new BrowserWindow({...})
  5. // 创建窗口2
  6. const window2 = new BrowserWindow({...})
  7. const winInfo = {
  8. win1: window1.id,
  9. win2: window2.id
  10. };
  11. // 在窗口加载完成后,发送窗口信息
  12. window1.webContents.on("did-finish-load", () => {
  13. window1.webContents.send("winInfo", winInfo);
  14. });
  15. // 在窗口加载完成后,发送窗口信息
  16. window2.webContents.on("did-finish-load", () => {
  17. window2.webContents.send("winInfo", winInfo);
  18. });

渲染进程(window1),接收窗口信息,并记录示例引用:

  1. import { ipcRenderer, remote } from "electron";
  2. // 监听主进程传递过来的winInfo信息
  3. ipcRenderer.on("winInfo", (event, data) => {
  4. const win2 = data.win2;
  5. // 获取窗口2的WebContents引用,通过此引用可以发送消息;
  6. const win2Instance = remote.BrowserWindow.fromId(win2).webContents;
  7. cacheWin2Info = { win2, win2Instance };
  8. })
  9. // 向win2窗口发送消息
  10. cacheWin2Info.win2Instance.send("xxx", {});

渲染进程(window2),操作同理window1的内容,此处就不在赘述;

IPC(ipcRenderer)- 推荐

主进程:

  1. // 主进程
  2. import { BrowswerWindow } from "electron";
  3. // 创建窗口1
  4. const window1 = new BrowserWindow({...})
  5. // 创建窗口2
  6. const window2 = new BrowserWindow({...})
  7. const winInfo = {
  8. win1: window1.webContents.id,
  9. win2: window2.webContents.id
  10. };
  11. // 在窗口加载完成后,发送窗口信息
  12. window1.webContents.on("did-finish-load", () => {
  13. window1.webContents.send("winInfo", winInfo);
  14. });
  15. // 在窗口加载完成后,发送窗口信息
  16. window2.webContents.on("did-finish-load", () => {
  17. window2.webContents.send("winInfo", winInfo);
  18. });

渲染进程:

  1. import { ipcRenderer } from "electron";
  2. // 监听主进程传递过来的winInfo信息
  3. ipcRenderer.on("winInfo", (event, data) => {
  4. const win2 = data.win2;
  5. cacheWin2Info = { win2 };
  6. })
  7. // 向win2窗口发送消息
  8. ipcRenderer.sendTo(cacheWin2Info.win2, "xxx", {})

建议通过ipcRenderer方法进行渲染进程间的通信,避免使用废弃的remote方式;

如何传递ArrayBuffer数据

在做音视频模块,存在一个场景,需要将底层的YUV(ArrayBuffer)数据流传递到另一个渲染进程下,此处,由于通过IPC方式需要对数据序列化处理,但YUV数据序列化后,存在异常,无法传递,所以需要进行一定的处理。
如下将介绍三种方式,将YUV数据在渲染进程之间传递。

YUV转化为字符串通过IPC传递

当渲染进程窗口1接收到YUV数据,需要将此数据传到渲染进程窗口2进行跨窗口渲染时,我们可以通过将YUV数据转为字符串形式,然后通过IPC传递,这样可以避开ArrayBuffer序列化异常问题。

  1. // ArrayBuffer转字符串
  2. const ab2str = (buffer) => {
  3. return String.fromCharCode.apply(null, new Uint8Array(buffer));
  4. }
  5. // (通过IPC-ipcRenderer方法传递)接收到buffer数据,转化后向win2窗口发送消息
  6. ipcRenderer.sendTo(cacheWin2Info.win2, "buffer", ab2str(buffer))

渲染进程窗口2接受buffer数据,并将字符串转为arraybuffer数据:

import { ipcRenderer } from "electron";

const str2ab = (str) => {
    const buf = new ArrayBuffer(str.length);
    const bufView = new Uint8Array(buf);
    const strLen = str.length;

    for(let i = 0; i < strLen; i++) {
        bufView[i] = str.charCodeAt(i);
    }

    return buf;
}

ipcRenderer.on("buffer", (event, data) => {
    const buffer = str2ab(data);

    // 伪代码,通过renderer渲染器渲染YUV Buffer数据;
    renderer.draw(buffer);
});

YUV转为Uint8Array传递

渲染进程1,将buffer数据转为Uint8Arrya数据,并通过ipc传递:

// (通过IPC-ipcRenderer方式传递)接收到buffer数据,转化后向win2窗口发送消息
ipcRenderer.sendTo(cacheWin2Info.win2, "buffer", new Uint8Arrya(buffer))

渲染进程2,接收Uint8Array数据,直接渲染:

ipcRenderer.on("buffer", (event, data) => {
    // 伪代码,通过renderer渲染器渲染YUV Buffer数据;
    renderer.draw(data);
});

YUV通过remote方式绑定到全局global对象上

主进程,设置remote gloabl数据:

global.sharedObject = {
    localVideoStream: null
}

渲染进程1,将接收到的buffer数据绑定到全局global对象上:

import { remote } from "electron";

remote.getGlobal("sharedObject").localVideoStream = buffer;

渲染进程2,定时从global上读取值:

import { remote } from "electron";

// 读取global上的localVideoStream buffer数据
const buffer = remote.getGlobal("sharedObject").localVideoStream;

// 伪代码,通过renderer渲染器渲染YUV Buffer数据;
renderer.draw(buffer);

注意:由于global的值需要提前在主进程下设置,不支持动态设置key值,所以如果有动态的key值数据,建议将对象数据绑定到global上,在对象数据上进行动态的key值设定;

Remote如何共享全局对象数据?

上面我们通过两种方式来传递ArrayBuffer数据,一种是ipcRenderer,一种是通过remote的getGlobal方式获取全局共享对象数据。
此处我们比较好奇remote的全局共享数据内容是如何实现的,有必要分析一下整体的流程,为了快速分析,我们采用断点的形式,来快速review一边remote的内部处理逻辑。

  1. 启动断点,此处已经提前在主进程设置好 global.shareObject = { videoFrames: new Uint8Buffer(100) }

wecom-temp-05706059d9ab36fbe4541c11b4ca2de3.png

  1. 进入remote.getGlobal函数内部:

此处我们看到调用了ipcRendererInternal.sendSync方法,怀疑是内部的ipc通道,并且返回了最新的ipc值,即meta数据;

// Get a global object in browser.
exports.getGlobal = (name) => {
  const command = 'ELECTRON_BROWSER_GLOBAL';
  const meta = ipcRendererInternal.sendSync(command, contextId, name);
  return metaToValue(meta);
};
  1. 进入sendSync方法:

发现ipcRendererInternal内部也是适用的ipc通道,并且标记了intrnaltrue,同时也提供了一个返回值,返回最新设置的值。
wecom-temp-b5fc65dd8310ed5ec63fab62a82c3d7e.png

  1. 进入metaToValue(meta):

下面内容复制源码内容,并简化了代码,整体思路是meta值进行类型区分,然后做深拷贝操作。此处,我们向global对象绑定了一个对象的值:videoFrames,此值是一个Uint8Buffer数据。
electron内容会先处理videoFrames对象数据,并将此对象数据转为proxy代理值,并监听值的变化,重新调用ipcRendererInternal.sendSync和metaToValue方法返回最新的数据。

function metaToValue (meta) {
  const types = {
    value: () => meta.value,
    array: () => meta.members.map((member) => metaToValue(member)),
    nativeimage: () => deserialize(meta.value),
    buffer: () => bufferUtils.metaToBuffer(meta.value),
    promise: () => Promise.resolve({ then: metaToValue(meta.then) }),
    error: () => metaToPlainObject(meta),
    date: () => new Date(meta.value),
    exception: () => { throw errorUtils.deserialize(meta.value); }
  };

  if (Object.prototype.hasOwnProperty.call(types, meta.type)) {
    return types[meta.type]();
  } else {
    let ret = {};
    if (remoteObjectCache.has(meta.id)) {
      v8Util.addRemoteObjectRef(contextId, meta.id);
      return remoteObjectCache.get(meta.id);
    }

    setObjectMembers(ret, ret, meta.id, meta.members);
    setObjectPrototype(ret, ret, meta.id, meta.proto);
    Object.defineProperty(ret.constructor, 'name', { value: meta.name });
    v8Util.setRemoteObjectFreer(ret, contextId, meta.id);
    v8Util.setHiddenValue(ret, 'atomId', meta.id);
    v8Util.addRemoteObjectRef(contextId, meta.id);
    remoteObjectCache.set(meta.id, ret);
    return ret;
  }
}
  1. 当递归遇到Uint8Buffer数据后,执行上面line13行代码,进行buffer数据的copy处理。

    function metaToBuffer (value: BufferMeta) {
    const constructor = typedArrays[value.type];
    const data = getBuffer(value.data);
    
    if (constructor === Buffer) {
     return data;
    } else if (constructor === ArrayBuffer) {
     return data.buffer;
    } else if (constructor) {
     return new (constructor as any)(data.buffer, data.byteOffset, value.length);
    } else {
     return data;
    }
    }
    

    返回最新的经过转化后的数值。