深入浅出Electron:原理、工程与实践
上QQ阅读APP看书,第一时间看更新

2.12 进程间通信

Chromium使用一个名为Mojo(https://chromium.googlesource.com/chromium/src.git/+/51.0.2704.48/docs/mojo_in_chromium.md)的框架完成进程间通信的工作,Electron也顺理成章地使用这个框架在主进程和渲染进程间传递消息。

Mojo框架提供了一套底层的IPC实现,包括消息管道、数据管道和共享缓冲区等。Chromium在Mojo框架之上又做了一层封装,以简化不同语言(如C++、Java或JavaScript等)、不同进程(如主进程、渲染进程等)之间的消息传递。

Electron在shell\common\api\api.mojom文件中定义了通信接口描述文件,我们总览一下这个文件定义的内容,如下所示:

056-1

其中ElectronRenderer和ElectronBrowser两个接口与主进程和渲染进程通信有关。

Electron在shell\common\api\BUILD.gn文件中把api.mojom文件添加到了编译配置文件中,在编译Electron的源码过程中,Mojo框架会把这个通信接口描述文件转义为具体的实现代码,并写入shell/common/api/api.mojom.h文件中。shell\renderer\api\electron_api_ipc_renderer.cc和shell\browser\api\electron_api_web_contents.cc都会引用这个头文件,也就是说渲染进程的底层逻辑和主进程的底层逻辑都引入了这个文件。

当我们在渲染进程的JavaScript代码中使用如下代码向主进程通信时:

this.settingPath = await ipcRenderer.invoke('getAppPath', 'appData')

实际上执行的就是位于shell\renderer\api\electron_api_ipc_renderer.cc文件内的C++代码,如下所示:

v8::Local<v8::Promise> Invoke(v8::Isolate* isolate,
                              gin_helper::ErrorThrower thrower,
                              bool internal,
                              const std::string& channel,
                              v8::Local<v8::Value> arguments) {
  if (!electron_browser_remote_) {
    thrower.ThrowError(kIPCMethodCalledAfterContextReleasedError);
    return v8::Local<v8::Promise>();
  }
  blink::CloneableMessage message;
  if (!electron::SerializeV8Value(isolate, arguments, &message)) {
    return v8::Local<v8::Promise>();
  }
  gin_helper::Promise<blink::CloneableMessage> p(isolate);
  auto handle = p.GetHandle();
  electron_browser_remote_->Invoke(
      internal, channel, std::move(message),
      base::BindOnce(
          [](gin_helper::Promise<blink::CloneableMessage> p,
             blink::CloneableMessage result) { p.Resolve(result); },
          std::move(p)));

  return handle;
}

这段代码中除了创建了一个Promise对象值得关注之外,最重要的就是执行了electron_browser_remote_对象的Invoke方法,electron_browser_remote_对象就是一个Mojo的通信对象,其实例化代码如下:

mojo::Remote<electron::mojom::ElectronBrowser> electron_browser_remote_;

当这个对象调用Invoke方法时,Mojo组织消息,并把这个消息传递给主进程,主进程接到这个消息后,最终执行了electron_api_web_contents.cc文件内的WebContents::Invoke的方法,也就是api.mojom接口描述的方法,代码如下所示:

void WebContents::Invoke(
    bool internal,
    const std::string& channel,
    blink::CloneableMessage arguments,
    electron::mojom::ElectronBrowser::InvokeCallback callback,
    content::RenderFrameHost* render_frame_host) {
  TRACE_EVENT1("electron", "WebContents::Invoke", "channel", channel);
  // webContents.emit('-ipc-invoke', new Event(), internal, channel, arguments);
  EmitWithSender("-ipc-invoke", render_frame_host, std::move(callback),
                 internal, channel, std::move(arguments));
}

这个方法会发射一个名为-ipc-invoke的事件,并把渲染进程传递过来的数据也一并发射出去,这个事件会触发位于lib\browser\api\web-contents.ts的处理逻辑。

this.on('-ipc-invoke' as any, function (event: Electron.IpcMainInvokeEvent, internal: boolean, channel: string, args: any[]) {
  addSenderFrameToEvent(event);
  event._reply = (result: any) => event.sendReply({ result });
  event._throw = (error: Error) => {
    console.error('Error occurred in handler for '${channel}':', error);
    event.sendReply({ error: error.toString() });
  };
  const target = internal ? ipcMainInternal : ipcMain;
  if ((target as any)._invokeHandlers.has(channel)) {
    (target as any)._invokeHandlers.get(channel)(event, ...args);
  } else {
    event._throw('No handler registered for '${channel}'');
  }
});

在这个处理逻辑中,Electron会查找一个Map对象,看用户是否在这个Map对象中注册了自己的处理逻辑,如果有,则执行用户的业务代码,如果没有则抛出异常。

因为是Invoke方法,所以还要把处理结果返回给渲染进程,这个过程是由event.sendReply实现的,它也是基于Mojo框架完成的进程间通信(Mojo框架提供的进程间通信的能力是双向的),其实现代码位于shell\browser\api\event.cc,这里不再赘述。

用户通过ipcMain.handle方法为主进程注册某事件的处理逻辑时,实际上最终执行的是如下代码(lib\browser\ipc-main-impl.ts):

private _invokeHandlers: Map<string, (e: IpcMainInvokeEvent, ...args: any[]) => void> = new Map();
handle: Electron.IpcMain['handle'] = (method, fn) => {
  if (this._invokeHandlers.has(method)) {
    throw new Error('Attempted to register a second handler for '${method}'');
  }
  if (typeof fn !== 'function') {
    throw new Error('Expected handler to be a function, but found type '${typeof fn}'');
  }
  this._invokeHandlers.set(method, async (e, ...args) => {
    try {
      e._reply(await Promise.resolve(fn(e, ...args)));
    } catch (err) {
      e._throw(err);
    }
  });
}

这段代码仅仅是把用户的处理逻辑包装起来存放到Map对象中,以备事件发生时再进行调用。

这就是Electron实现进程间通信的基本逻辑,实际上Chromium也是使用类似的手段完成进程间通信的。跨进程通信可以说是Chromium最关键的技术之一,是Chromium以一种松耦合的方式管理众多进程、众多子项目的技术方案。