Electron渲染进程间通信,可以通过ipc或者global remote进行中转传递。由于ipc模式需要对数据进行序列化处理,所以存在一个问题,对呀ArryaBuffer数据,无法进行序列化处理,所以需要一些特殊的手段进行处理才可以传递。
渲染进程间通信
我们先简单了解下Electron渲染进程之间如何进行通信,这里先简单介绍ipc通信方式;
IPC(remote)
为了实现IPC对渲染进程间通信,我们需要将主进程创建的BrowserWindow窗口信息传递到各个渲染进程之间:
此处使用到了remote对象,在渲染进程下获取窗口实例,在electron高版本下,remote不在由electron单独提供,需要额外的安装包初始化才行;
主进程:
// 主进程
import { BrowswerWindow } from "electron";
// 创建窗口1
const window1 = new BrowserWindow({...})
// 创建窗口2
const window2 = new BrowserWindow({...})
const winInfo = {
win1: window1.id,
win2: window2.id
};
// 在窗口加载完成后,发送窗口信息
window1.webContents.on("did-finish-load", () => {
window1.webContents.send("winInfo", winInfo);
});
// 在窗口加载完成后,发送窗口信息
window2.webContents.on("did-finish-load", () => {
window2.webContents.send("winInfo", winInfo);
});
渲染进程(window1),接收窗口信息,并记录示例引用:
import { ipcRenderer, remote } from "electron";
// 监听主进程传递过来的winInfo信息
ipcRenderer.on("winInfo", (event, data) => {
const win2 = data.win2;
// 获取窗口2的WebContents引用,通过此引用可以发送消息;
const win2Instance = remote.BrowserWindow.fromId(win2).webContents;
cacheWin2Info = { win2, win2Instance };
})
// 向win2窗口发送消息
cacheWin2Info.win2Instance.send("xxx", {});
渲染进程(window2),操作同理window1的内容,此处就不在赘述;
IPC(ipcRenderer)- 推荐
主进程:
// 主进程
import { BrowswerWindow } from "electron";
// 创建窗口1
const window1 = new BrowserWindow({...})
// 创建窗口2
const window2 = new BrowserWindow({...})
const winInfo = {
win1: window1.webContents.id,
win2: window2.webContents.id
};
// 在窗口加载完成后,发送窗口信息
window1.webContents.on("did-finish-load", () => {
window1.webContents.send("winInfo", winInfo);
});
// 在窗口加载完成后,发送窗口信息
window2.webContents.on("did-finish-load", () => {
window2.webContents.send("winInfo", winInfo);
});
渲染进程:
import { ipcRenderer } from "electron";
// 监听主进程传递过来的winInfo信息
ipcRenderer.on("winInfo", (event, data) => {
const win2 = data.win2;
cacheWin2Info = { win2 };
})
// 向win2窗口发送消息
ipcRenderer.sendTo(cacheWin2Info.win2, "xxx", {})
建议通过ipcRenderer方法进行渲染进程间的通信,避免使用废弃的remote方式;
如何传递ArrayBuffer数据
在做音视频模块,存在一个场景,需要将底层的YUV(ArrayBuffer)数据流传递到另一个渲染进程下,此处,由于通过IPC方式需要对数据序列化处理,但YUV数据序列化后,存在异常,无法传递,所以需要进行一定的处理。
如下将介绍三种方式,将YUV数据在渲染进程之间传递。
YUV转化为字符串通过IPC传递
当渲染进程窗口1接收到YUV数据,需要将此数据传到渲染进程窗口2进行跨窗口渲染时,我们可以通过将YUV数据转为字符串形式,然后通过IPC传递,这样可以避开ArrayBuffer序列化异常问题。
// ArrayBuffer转字符串
const ab2str = (buffer) => {
return String.fromCharCode.apply(null, new Uint8Array(buffer));
}
// (通过IPC-ipcRenderer方法传递)接收到buffer数据,转化后向win2窗口发送消息
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的内部处理逻辑。
- 启动断点,此处已经提前在主进程设置好
global.shareObject = { videoFrames: new Uint8Buffer(100) }
:
- 进入
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);
};
- 进入
sendSync
方法:
发现ipcRendererInternal
内部也是适用的ipc通道,并且标记了intrnal
为true
,同时也提供了一个返回值,返回最新设置的值。
- 进入
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;
}
}
当递归遇到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; } }
返回最新的经过转化后的数值。