再识 Electron
Electron
从开发角度上来说,能够认为是一个javascript
的执行环境,这个环境包含了:
- 浏览器环境:Chromium 内核,用于处理各种浏览器渲染、链接等事务。
- Node.Js环境:使用NodeJs作为
javascript
的运行环境,意味着包含了v8
语言解析器。 - 系统API:各种系统原生的API,用于支持
Electron
APP能够有更接近原生app的体验。
以上三个模块的结合对于构建Electron
是密不可分,使Electron
可以运行部分系统api、node api、浏览器api。相反地,macOS 上有各种系统api可以调用,也可以使用node
去运行javascript
,但却无法使用浏览器的各种api;而如果在浏览器中使用node
执行javascript
,也没有办法调用到各种系统api。
NodeJS C++ 扩展
既然 Electron node环境使用的是Node.js
,所以它支持原生Node.js模块的接入,但由于Electron具有与给定Node.js不同的应用二进制接口 (ABI)(由于使用Chromium的 BoringSL 而不是 OpenSSL 等 差异),原生模块需要为Electron重新编译。利用node-gyp
,配合工程配置能够生成Electron能使用的C++扩展。
原生模块编写与整合
举个例子,这是原生实现一个界面的代码,其中有几个关键部分:
- 末尾的
NAPI_MODULE(NODE_GYP_MODULE_NAME, Initialize)
定义了当前原生模块的名称,同时告诉外部注册入口是Initialize
方法 - 创建了
OpenNativeWindo
方法,并提供原生方法的实现 - 在
Initialize
方法中,注册了要使用到的方法,并提供了方法名和对应的方法实现,以及方法的作用域
同时需要注意,在macOS上编写原生模块需要使用Objective-C++
,而且提供给外部使用的方法,需要再用C++
包一层,如OpenNativeWindow
方法,其实是返回napi_value
的C++
方法,在内部才使用了NSRunLoop
等OC方法,因为napi并没有提供直接让Objective-C++
使用的注册方法,所以需要额外转化一层。
在完成了原生模块编写后,需要利用node-gyp
进行打包,其中需要对binding.gyp
文件进行构建的配置:
- target_name: 模块的名称
- cflags: c编译配置,如果有C++配置需要可以使用
cflags_cc
- sources: 需要编译的C++文件
- conditions: 环境配置,这里我们可以配置macOS上Xcode编译选项,也可以配置Windows上Visual Studio的配置选项,具体配置可以查看官方文档
编译完成后就会生成addon.node文件,在Node中使用require
引用就可以使用其中整合的代码。
好简单,但为什么?
为什么编译出来Electron就能加载?又是怎么用到我的原生代码的呢?想搞清楚这个问题,我们一步步了解一下原生模块
原生模块的本质
通过file
指令,可以看出.node
文件实际上是一个Mach-O
文件
再通过otool
命令,可以看到.node
文件包含了两个dylib
动态库,所以.node
实际上是一个dylib
动态对(win上为dll,linux上为efi),这也说明了为什么在mac上安装node的时候需要xcode-select,windows上安装需要vs。
原生模块的加载
既然它是一个动态库,那么它加载的秘密就要在Electron中寻找了,Electron又是基于Node,所以最终我们找到Node的加载模块,模块被required
的时候,它会通过process.DLOpen()
引入:
- 其核心是对
env->TryLoadAddon
的调用,它接受回调以执行实际的加载过程 - 回调函数中马上创建了
dlib_load_mutex
并进行加锁 - 随后使用
dlib->Open()
加载动态库,获取动态库句柄 node_module
开始创建,并通过local_modpending
中获取等待加载的node_module
地址
那么nodule_module
是怎么创建的呢?可以看到我们之前使用了NAPI_MODULE(NODE_GYP_MODULE_NAME, Initialize)
方法定义了模块,而这个宏拆开其实是定义了模块名称,并将方法Initilize
作为_register_addon
,注册方法。
_register_addon
是被constructor
修饰的函数,所以在模块实例化的时候,它就会被调用来注册我们名为addon
的原生模块。它将我们的模块地址传递给napi_module_reigster
,随后将其数据包装在node::node-module
中,再传递给src/node_binding.cc中的node_module_register
。一旦我们通过DLIB::Open
打开原生模块,注册方法就会被调用,并在继续之前,将我们的模块地址保存在上文提到的thread_local_modpending
中,并将刚刚加载的动态库句柄转移到mp上。
mp也就是napi_module
结构体,在拿到地址后,就会继续做剩余的工作,通过mp->nm_register_func
调用我们的注册函数,注册函数返回我们模块的exports,也就是公开的属性和方法,并注册到上下文,最终我们就能够使用我们的插件。
性能对比
排序
图像处理
- JS:使用Jimp图像处理库
- C++:使用OpenCV图像处理
- 统一将图片缩放到 1024*1024 大小
可以看到在执行少量排序时,JS执行效率要比原生模块高,是因为原生模块需要进行加载等操作,加载耗时比排序耗时更长,但在大量排序时,依旧是原生模块表现更好。
在图片处理上,JS效率则远低于原生模块。
使用LLDB进行调试
-
利用
node-gyp
构建一个debug环境的模块:node-gyp build --debug
-
确定当前
node.js
路径:which node
-
·利用LLDB链接到对应Node:
lldb -- your/path/to/node test.js
-
在对应模块文件中调试,添加断点:
breakpoint set -f addon.mm -l 25