You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
...
constaddBuiltinProperty=(name)=>{Object.defineProperty(exports,name,{get: ()=>exports.getBuiltin(name)})}constbrowserModules=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)
getBuiltin 的处理方法就是发送一个同步的进程间消息,向主进程请求某个模块对象。最后会将返回值 meta 调用 metaToValue 后再返回。一切秘密都在 这个方法中了。
// Convert meta data from browser into real value.functionmetaToValue(meta){consttypes={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: ()=>newDate(meta.value),exception: ()=>{throwmetaToException(meta)}}if(meta.typeintypes){returntypes[meta.type]()}else{letretif(remoteObjectCache.has(meta.id)){returnremoteObjectCache.get(meta.id)}// A shadow class to represent the remote function object.if(meta.type==='function'){letremoteFunction=function(...args){letcommandif(this&&this.constructor===remoteFunction){command='ELECTRON_BROWSER_CONSTRUCTOR'}else{command='ELECTRON_BROWSER_FUNCTION_CALL'}constobj=ipcRenderer.sendSync(command,meta.id,wrapArgs(args))returnmetaToValue(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)returnret}}
在上一篇 Electron 进程通信 中,介绍了 Electron 中的两种进程通信方式,分别为:
ipcMain
和ipcRenderer
两个模块相比于使用两个 IPC 模块,使用
remote
模块相对来说会比较自然一点。remote
模块帮我们屏蔽了内部的进程通信,使得我们在调用主进程的方法时完全没有感知到主进程的存在。上一篇 Electron 进程通信 中,对
remote
的实现只是简单的说了下它底层依旧是通过 ipc 模块来实现通信:但是只是这样吗?
这篇文章会从
remote
模块的源码层面进行分析该模块的实现。"假" 的多进程?
我们看一个例子,来了解直接使用 IPC 通信和使用
remote
模块的区别:分别通过 IPC 模块和
remote
模块实现在渲染进程中获取主进程的一个对象,再在主进程中修改该对象的属性值,看下渲染进程中的对象对应的属性值是否会跟着改变。逻辑比较简单,直接看代码。
使用 IPC 模块
主进程代码:
渲染进程代码:
index.html
:index.js
:界面输出结果如下:
嗯..没什么问题,和预期一样。由于进程通信中数据传递经过了序列化和反序列化,渲染进程拿到的进程中的对象已经不是同一个对象,指向的内存地址不同。
使用
remote
模块主进程代码:
渲染进程代码:
index.html
文件同上。index.js
修改为通过remote
模块获取 remoteObj :界面输出结果如下:
我们发现,通过
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 也是这么实现的,不过分了几种情况:
java.rmi.Remote
接口的对象(!!重点):远程引用RMI 里面另一个比较重要的点就是这个远程对象。RMI 对这些实现了
Remote
接口的对象,进行了一些封装,为我们屏蔽了底层的通信,达到客户端调用这些远程对象上的方法时像调用本地方法一样的目的。RMI 的大致流程
比较懵逼?没关系,看代码实现:
RMI 简单实现
(建议大家一起运行下这个例子~不动手实现怎么会有成就感!!)
客户端和服务端都有的远程对象接口文件
HelloRMI.java
:服务端实现
HelloRMI
接口的HelloImpl.java
:服务端测试程序
Server.java
:客户端测试程序
Client.java
:先运行
Server.java
,开启注册表并向注册表绑定远程对象。然后运行客户端就可以查找和运行服务端上的远程对象了。remote
中的 RMI我们看下前面的例子,使用
remote
模块获取主进程上的对象背后发生了什么:如果说
remote
只是帮我们屏蔽了 IPC 操作,那么渲染进程拿到的主进程中的对象,应该与主进程中的对象是没有任何关系的,不应该受到主进程的修改而影响。那么remote
还帮我们做了什么呢?其实重点不在于
remote
背后帮我们做了 IPC,而是在于数据的传递。前面的 RMI 中说到,数据传递分为简单数据类型、没有继承Remote
的对象和继承了Remote
的远程对象。继承了Remote
的远程对象在数据传递的时候是通过远程引用传递而非简单的序列化和反序列化。在remote
模块中,它相当于帮我们将所有的Object
都给转换为了远程对象。通过源码学习下
remote
是如何进行这种转换的:lib/renderer/api/remote.js
:这段代码做的事情是把主进程才可以使用的模块添加到了
remote
模块的属性在中。getBuiltin
的处理方法就是发送一个同步的进程间消息,向主进程请求某个模块对象。最后会将返回值meta
调用metaToValue
后再返回。一切秘密都在 这个方法中了。对不同类型进行了不同的处理。在对函数的处理中,将原本的函数外封装了一个函数用于发送同步的进程间消息,并将返回值同样调用
metaToValue
进行转换后返回。另外,对
Object
类型对象,还需要对他们的属性进行类似函数一样的封装处理:对返回对象属性重写 get、set 方法。对调用远程对象上的属性,同样是通过发送同步的进程间消息来获取,这也就是为什么主进程修改了值,渲染进程就也能感知到的原因了。
还有一个需要注意的地方是,为了不重复获取远程对象,对返回的对象
remote
是会进行缓存的,看metaToValue
的倒数第二行:remoteObjectCache.set(meta.id, ret)
读者思考
到这里我们知道了文章开头遇到的神奇现象的原因。这里抛出个问题给读者:思考下如果是主进程的函数是异步的(函数返回一个 Promise 对象),Promise 对象是如何实现数据传递的?是否会阻塞渲染进程?
总结
通过上述分析我们知道,
remote
模块不仅帮我们实现了 IPC 通信,同时为了达到类似引用传递的效果,使用了类似 Java 中的 RMI,对主进程的对象进行了一层封装,使得我们在访问远程对象上的属性时,也需要向主进程发送同步进程消息来获取到当前主进程上该对象实际的值。【参考资料】
The text was updated successfully, but these errors were encountered: