从架构出发探究Electron运行原理
Posted duansamve
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从架构出发探究Electron运行原理相关的知识,希望对你有一定的参考价值。
早期桌面应用的开发主要借助原生 C/C++ API 进行,由于需要反复经历编译过程,且无法分离界面 UI 与业务代码,开发调试极为不便。后期出现的 QT 和 WPF 在一定程度上解决了界面代码分离和跨平台的问题,却依然无法避免较长时间的编译过程。近几年伴随互联网行业的迅猛发展,尤其是 NodeJS、Chromium 这类基于 W3C 标准开源应用的不断涌现,原生代码与 Web 浏览器开发逐步走向融合,Electron 在这种背景下诞生。
基于Electron实现的产品
Electron提供了丰富的本地(操作系统)API,使你能够使用纯javascript来创建桌面应用程序。与其它各种的Node.js运行时不同的是Electron专注于桌面应用程序而不是Web服务器。
Electron通过集成浏览器内核,使用Web技术来实现不同平台下的渲染,并结合了 Chromium 、Node.js 和用于调用系统本地功能的 API 三大板块。
- Electron通过将Chromium和Node.js合并到同一个运行时环境中,并将其打包为Mac,Windows和Linux系统下的应用;
- Chromium 为 Electron 提供强大的 UI 渲染能力,由于 Chromium 本身跨平台,因此无需考虑代码的兼容性。
- Chromium 并不具备原生 GUI 的操作能力,因此 Electron 内部集成 Node.js,编写 UI 的同时也能够调用操作系统的底层 API,例如 path、fs、crypto 等模块。
- Native API 为 Electron 提供原生系统的 GUI 支持,借此 Electron 可以调用原生应用程序接口。
总结起来,Chromium 负责页面 UI 渲染,Node.js 负责业务逻辑,Native API 则提供原生能力和跨平台。
上面粗略讲解了 Electron 的跨端原理,下面我们来深究一下。
Chromium 架构
Chromium 是 Chrome 的开源版,也是一个浏览器,Google Chrome 浏览器正是基于它。
Electron底层基于Chromium,Chromium的设计理念是基于多进程的,每个Tab都是一个独立的进程,称之为Renderer Process,有多少个Tab就有多少个Renderer Process。(图源: Chromium 官网)
另外还有一个,有且只有一个的主进程,称之为Main Process(浏览器整体的Window),它负责其他众多Renderer Process的创建、分配,还有其他众多整体上的控制。
因此如果有一个Tab崩溃的话,不会影响到其他的Tab,浏览器可以继续运行。
Chromium 的多进程模式主要由三部分组成: 浏览器端(Browser)、渲染器端(Render)、浏览器与渲染器的通信方式(IPC)
1.浏览器进程
浏览器进程 Browser 只有一个,当 Chrome 打开时,进程启动。浏览器为每个渲染进程维护对应的 RenderProcessHost,负责浏览器与渲染器的交互。RenderViewHost 则是与 RenderView 对象进行交互,渲染网页的内容。浏览器与渲染器通过 IPC 进行通信。
2.渲染进程管理
每个渲染进程都有一个全局 RenderProcess 对象,可以管理其与父浏览器进程之间的通信,并维护其全局状态。
3.view 管理
每个渲染器可以维护多个 RenderView 对象,当新开标签页或弹出窗口后,渲染进程就会创建一个 RenderView,RenderView 对象与它在浏览器进程中对应的 RenderViewHost 和 Webkit 嵌入层通信,渲染出网页内容(这里是我们日常主要关注的地方)。
Electron 架构解析
Electron 架构参考了 Chromium 的多进程架构模式,即将主进程和渲染进程隔离,并且在 Chromium 多进程架构基础上做一定扩展。
将上面复杂的 Chromium 架构简化:
Chromium 运行时由一个 Browser Process,以及一个或者多个 Renderer Process 构成。Renderer Process 负责渲染页面 Web ,Browser Process 负责管理各个 Renderer Process 以及其他功能(菜单栏、收藏夹等)
下面我们看一下 Electron 架构有那些变化?
Electron 架构中仍然使用了 Chromium 的 Renderer Process 渲染界面,Renderer Process 可以有多个,互相独立不干扰。由于 Electron 为其集成了 Node 运行时,Renderer Process 还可以调用 Node API。
相较于 Chromium 架构,Electron 对 Browser 进程做了很多改动,将其更改名 Main Process,每个应用程序只能有一个主进程,主进程位于 Node.js 下运行,因此其可以调用系统底层功能,其主要负责:渲染进程的创建;系统底层功能及原生资源的调用;应用生命周期的控制(包裹启动、推出以及一些事件监听),可以把它看做页面和计算机沟通的桥梁。
经过上面的分析,Electron 多进程的系统架构可以总结为下图:
可以发现,主进程和渲染进程都集成了 Native API 和 Node.js,渲染进程还集成 Chromium 内核,成功实现跨端开发。
在Electron中,GUI组件仅在主进程可用,在渲染进程中不可用。那如果想要在渲染进程中使用GUI组件,势必需要和主进程进行通信。ipc模块就是用来实现主进程和渲染进程之间的通信。在主进程中使用ipcMain模块进行对渲染进程的通信进行控制和处理。而在渲染进程中,则使用ipcRenderer模块,来向主进程发送消息或者接受主进程的回应。
Node 与 Chromium
没有Chromium就没有V8(Chromium内置的高性能JavaScript执行引擎),没有V8就没有Node.js。Chromium的高性能并不单单是多进程架构的功劳,V8引擎也居功甚伟,V8引擎以超高性能执行JavaScript脚本著称,Node.js的作者也是因为这一点才决定封装V8,把JavaScript程序员的战场引向客户端和服务端。
Node 的事件循环与浏览器的事件循环有明显不同,Chromium 既然是 Chrome 的实验版,自然与浏览器实现相同。
Node 的事件循环基于 libuv 实现,而 Chromium 基于 message bump 实现。主线程只能同时运行一个事件循环,因此需要将两个完全不同的事件循环整合起来。
有两种解决方案:
- 使用 libuv 实现 message bump 将 Chromium 集成到 Node.js
- 将 Node.js 集成到 Chromium
Electron 最初的方案是第一种,使用 libuv 实现 message bump,但不同的 OS 系统 GUI 事件循环差异很大,例如 mac 为 NSRunLoop,Linux 为 glib,实现过程特别复杂,资源消耗和延迟问题也无法得到有效解决,最终放弃了第一种方案。
Electron 第二次尝试使用小间隔的定时器来轮询 GUI 事件循环,但此方案 CPU 占用高,并且 GUI 响应速度慢。
后来 libuv 引入了 backend_fd 概念,backend_fd 轮询事件循环的文件描述符,因此 Electron 通过轮询 backend_fd 来得到 libuv 的新事件实现 Node.js 与 Chromium 事件循环的融合(第二种方案)。
下面这张 PPT 完美的描述了上述过程(图源:Electron: The Event Loop Tightrope - Shelley Vohr | JSHeroes 2019)
如何开发?
开发Electron应用的方式有很多,下面以React为例做个简单的说明:
1. 热调试
在React项目目录下安装Electron
npm install electron
修改package.json文件,增加或将已有的main属性值修改为main.js,在scriptes中添加"electron-start": "electron .",最终配置文件如下:
"name": "electron-react",
"version": "0.1.0",
"main": "main.js",
"private": true,
"dependencies":
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.5.0",
"electron": "^20.0.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
,
"scripts":
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"electron-start": "electron ."
,
"eslintConfig":
"extends": [
"react-app",
"react-app/jest"
]
,
"browserslist":
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
打开main.js,将
const app, BrowserWindow, globalShortcut = require("electron");
const path = require("path");
function createWindow()
const win = new BrowserWindow(
width: 800,
height: 600,
webContents:
openDevTools: true, //不想要控制台直接把这段删除
);
win.loadFile("index.html");
app.whenReady().then(() =>
createWindow();
app.on("activate", () =>
if (BrowserWindow.getAllWindows().length === 0)
createWindow();
);
);
app.on("window-all-closed", () =>
if (process.platform !== "darwin")
app.quit();
)
中的
win.loadFile("index.html");
修改为
win.loadURL("http://localhost:3000/")
打开两个终端,一个运行React
npm start
另一个执行
npm run electron-start
程序运行正常
问底王帅:深入PHP内核——弱类型变量原理探究
来源:CSDN http://www.csdn.net/article/2014-09-15/2821685-exploring-of-the-php
作者:王帅
PHP是一门简单而强大的语言,提供了很多Web适用的语言特性,其中就包括了变量弱类型,在弱类型机制下,你能够给一个变量赋任意类型的值。
PHP的执行是通过Zend Engine(下面简称ZE),ZE是使用C编写,在底层实现了一套弱类型机制。ZE的内存管理使用写时拷贝、引用计数等优化策略,减少再变量赋值时候的内存拷贝。
下面不光带你探索PHP弱类型的原理,也会在写PHP扩展角度,介绍如何操作PHP的变量。
1. PHP的变量类型
PHP的变量类型有8种:
- 标准类型:布尔boolen,整型integer,浮点float,字符string
- 复杂类型:数组array,对象object
- 特殊类型:资源resource
PHP不会严格检验变量类型,变量可以不显示的声明其类型,而在运行期间直接赋值。也可以将变量自由的转换类型。如下例,没有实现声明的情况下,$i可以赋任意类型的值。
- <? php $i = 1; //int $i = ‘show me the money’; //string $i = 0.02; // float $i = array(1, 2, 3); // array $i = new Exception(‘test’, 123); // object $i = fopen(‘/tmp/aaa.txt’, ‘a’) // resource ?>
如果你对弱类型原理理解不深刻,在变量比较时候,会出现“超出预期”的惊喜。
- <? PHP $str1 = null; $str2 = false; echo $str1==$str2 ? ‘相等’ : ‘不相等’; $str3 = ”; $str4 = 0; echo $str3==$str4 ? ‘相等’ : ‘不相等’; $str5 = 0; $str6 = ‘0’; echo $str5==$str6 ? ‘相等’ : ‘不相等’; ?>
以上三个结果全部是相等,因为在变量比较的时候,PHP内部做了变量转换。如果希望值和类型同时判断,请使用三个=(如,$a===0)来判断。也许你会觉得司空见惯,也许你会觉得很神奇,那么请跟我一起深入PHP内核,探索PHP变量原理。
2. 变量的存储及标准类型介绍
PHP的所有变量,都是以结构体zval来实现,在Zend/zend.h中我们能看到zval的定义:
- typedef union _zvalue_value { long lval; /* long value */ double dval; /* double value */ struct { char *val; int len; /* this will always be set for strings */ } str; /* string (always has length) */ HashTable *ht; /* an array */ zend_object_value obj; /* stores an object store handle, and handlers */ } zvalue_value;
属性名 | 含义 | 默认值 |
refcount__gc | 表示引用计数 | 1 |
is_ref__gc | 表示是否为引用 | 0 |
value | 存储变量的值 | |
type | 变量具体的类型 |
zvalue_value能够实现变量弱类型的核心,定义如下:
- typedef union _zvalue_value { long lval; /* long value */ double dval; /* double value */ struct { char *val; int len; /* this will always be set for strings */ } str; /* string (always has length) */ HashTable *ht; /* an array */ zend_object_value obj; /* stores an object store handle, and handlers */ } zvalue_value;
布尔型,zval.type=IS_BOOL,会读取zval.value.lval字段,值为1/0。如果是字符串,zval.type=IS_STRING,会读取zval.value.str,这是一个结构体,存储了字符串指针和长度。
C语言中,用”\0″作为字符串结束符。也就是说一个字符串”Hello\0World”在C语言中,用printf来输出的话,只能输出hello,因为”\0″会认为字符已经结束。PHP中是通过结构体的_zval_value.str.len来控制字符串长度,相关函数不会遇到”\0″结束。所以PHP的字符串是二进制安全的。
如果是NULL,只需要zval.type=IS_NULL,不需要读取值。
通过对zval的封装,PHP实现了弱类型,对于ZE来说,通过zval可以存取任何类型。
3. 高级类型Array和Object数组Array
数组是PHP语言中非常强大的一个数据结构,分为索引数组和关联数组,zval.type=IS_ARRAY。在关联数组中每个key可以存储任意类型的数据。PHP的数组是用Hash Table实现的,数组的值存在zval.value.ht中。
后面会专门讲到PHP哈希表的实现。
对象类型的zval.type=IS_OBJECT,值存在zval.value.obj中。
4. 特殊类型——资源类型(Resource)介绍
资源类型是个很特殊的类型,zval.type=IS_RESOURCE,在PHP中有一些很难用常规类型描述的数据结构,比如文件句柄,对于C语言来说是一个指针,不过PHP中没有指针的概念,也不能用常规类型来约束,因此PHP通过资源类型概念,把C语言中类似文件指针的变量,用zval结构来封装。资源类型值是一个整数,ZE会根据这个值去资源的哈希表中获取。
资源类型的定义:
- typedefstruct_zend_rsrc_list_entry { void *ptr; int type; int refcount; }zend_rsrc_list_entry;
其中,ptr是一个指向资源的最终实现的指针,例如一个文件句柄,或者一个数据库连接结构。type是一个类型标记,用于区分不同的资源类型。refcount用于资源的引用计数。
内核中,资源类型是通过函数ZEND_FETCH_RESOURCE获取的。
- ZEND_FETCH_RESOURCE(con, type, zval *, default, resource_name, resource_type);
5. 变量类型的转换
按照现在我们对PHP语言的了解,变量的类型依赖于zval.type字段指示,变量的内容按照zval.type存储到zval.value。当PHP中需要变量的时候,只需要两个步骤:把zval.value的值或指针改变,再改变zval.type的类型。不过对于PHP的一些高级变量Array/Object/Resource,变量转换要进行更多操作。
变量转换原理分为3种:
5.1 标准类型相互转换
比较简单,按照上述的步骤转化即可。
5.2 标准类型与资源类型转换
资源类型可以理解为是int,比较方便转换标准类型。转换后资源会被close或回收。
- <? php $var = fopen(‘/tmp/aaa.txt’, ‘a’); // 资源 #1 $var = (int) $var; var_dump($var); // 输出1 ?>
5.3 标准类型与复杂类型转换
Array转换整型int/浮点型float会返回元素个数;转换bool返回Array中是否有元素;转换成string返回’Array’,并抛出warning。
详细内容取决于经验,请阅读PHP手册: http://php.net/manual/en/language.types.type-juggling.php
5.4 复杂类型相互转换
array和object可以互转。如果其它任何类型的值被转换成对象,将会创建一个内置类stdClass的实例。
在我们写PHP扩展的时候,PHP内核提供了一组函数用于类型转换:
void convert_to_long(zval* pzval) |
void convert_to_double(zval* pzval) |
void convert_to_long_base(zval* pzval, int base) |
void convert_to_null(zval* pzval) |
void convert_to_boolean(zval* pzval) |
void convert_to_array(zval* pzval) |
void convert_to_object(zval* pzval) |
void convert_object_to_type(zval* pzval, convert_func_t converter) |
内核访问zval容器的API | |
宏 | 访问变量 |
Z_LVAL(zval) | (zval).value.lval |
Z_DVAL(zval) | (zval).value.dval |
Z_STRVAL(zval) | (zval).value.str.val |
Z_STRLEN(zval) | (zval).value.str.len |
Z_ARRVAL(zval) | (zval). value.ht |
Z_TYPE(zval) | (zval).type |
Z_LVAL_P(zval) | (*zval).value.lval |
Z_DVAL_P(zval) | (*zval).value.dval |
Z_STRVAL_P(zval_p) | (*zval).value.str.val |
Z_STRLEN_P(zval_p) | (*zval).value.str.len |
Z_ARRVAL_P(zval_p) | (*zval). value.ht |
Z_OBJ_HT_P(zval_p) | (*zval).value.obj.handlers |
Z_LVAL_PP(zval_pp) | (**zval).value.lval |
Z_DVAL_PP(zval_pp) | (**zval).value.dval |
Z_STRVAL_PP(zval_pp) | (**zval).value.str.val |
Z_STRLEN_PP(zval_pp) | (**zval).value.str.len |
Z_ARRVAL_PP(zval_pp) | (**zval). value.ht |
6. 变量的符号表与作用域
PHP的变量符号表与zval值的映射,是通过HashTable(哈希表,又叫做散列表,下面简称HT),HashTable在ZE中广泛使用,包括常量、变量、函数等语言特性都是HT来组织,在PHP的数组类型也是通过HashTable来实现。
举个例子:
- <? php $var = ‘Hello World’; ?>
$var的变量名会存储在变量符号表中,代表$var的类型和值的zval结构存储在哈希表中。内核通过变量符号表与zval地址的哈希映射,来实现PHP变量的存取。
为什么要提作用域呢?因为函数内部变量保护。按照作用域PHP的变量分为全局变量和局部变量,每种作用域PHP都会维护一个符号表的HashTable。当在PHP中创建一个函数或类的时候,ZE会创建一个新的符号表,表明函数或类中的变量是局部变量,这样就实现了局部变量的保护–外部无法访问函数内部的变量。当创建一个PHP变量的时候,ZE会分配一个zval,并设置相应type和初始值,把这个变量加入当前作用域的符号表,这样用户才能使用这个变量。
内核中使用ZEND_SET_SYMBOL来设置变量:
- ZEND_SET_SYMBOL( EG(active_symbol_table), “foo”, foo);
查看_zend_executor_globals结构
- Zend/zend_globals.h
- struct _zend_executor_globals { //略 HashTable symbol_table;//全局变量的符号表 HashTable *active_symbol_table;//局部变量的符号表 //略 };
在写PHP扩展时候,可以通过EG宏来访问PHP的变量符号表。EG(symbol_table)访问全局作用域的变量符号表,EG(active_symbol_table)访问当前作用域的变量符号表,局部变量存储的是指针,在对HashTable进行操作的时候传递给相应函数。
为了更好的理解变量的哈希表与作用域,举个简单的例子:
- <? php $temp = ‘global’; function test() { $temp = ‘active’; } test(); var_dump($temp); ?>
创建函数外的变量$temp,会把这个它加入全局符号表,同时在全局符号表的HashTable中,分配一个字符类型的zval,值为‘global‘。创建函数test内部变量$temp,会把它加入属于函数test的符号表,分配字符型zval,值为’active’ 。
7. PHP扩展中变量操作
创建PHP变量
我们可以在扩展中调用函数MAKE_STD_ZVAL(pzv)来创建一个PHP可调用的变量,MAKE_STD_ZVAL应用到的宏有:
- #define MAKE_STD_ZVAL(zv) ALLOC_ZVAL(zv);INIT_PZVAL(zv) #define ALLOC_ZVAL(z) ZEND_FAST_ALLOC(z, zval, ZVAL_CACHE_LIST) #define ZEND_FAST_ALLOC(p, type, fc_type) (p) = (type *) emalloc(sizeof(type)) #define INIT_PZVAL(z) (z)->refcount__gc = 1;(z)->is_ref__gc = 0;
MAKE_STD_ZVAL(foo)展开后得到:
- (foo) = (zval *) emalloc(sizeof(zval)); (foo)->refcount__gc = 1; (foo)->is_ref__gc = 0;
可以看出,MAKE_STD_ZVAL做了三件事:分配内存、初始化zval结构中的refcount、is_ref。
内核中提供一些宏来简化我们的操作,可以只用一步便设置好zval的类型和值。
API Macros for Accessing zval | |
宏 | 实现方法 |
ZVAL_NULL(pvz) | Z_TYPE_P(pzv) = IS_NULL |
ZVAL_BOOL(pvz) | Z_TYPE_P(pzv) = IS_BOOL; Z_BVAL_P(pzv) = b ? 1 : 0; |
ZVAL_TRUE(pvz) | ZVAL_BOOL(pzv, 1); |
ZVAL_FALSE(pvz) | ZVAL_BOOL(pzv, 0); |
ZVAL_LONG(pvz, l)(l 是值) | Z_TYPE_P(pzv) = IS_LONG;Z_LVAL_P(pzv) = l; |
ZVAL_DOUBLE(pvz, d) | Z_TYPE_P(pzv) = IS_DOUBLE;Z_LVAL_P(pzv) = d; |
ZVAL_STRINGL(pvz, str, len, dup) | Z_TYPE_P(pzv) = IS_STRING;Z_STRLEN_P(pzv) = len; if (dup) { {Z_STRVAL_P(pzv) =estrndup(str, len + 1);} }else { {Z_STRVAL_P(pzv) = str;} } |
ZVAL_STRING(pvz, str, len) | ZVAL_STRINGL(pzv, str,strlen(str), dup); |
ZVAL_RESOURCE(pvz, res) | Z_TYPE_P(pzv) = IS_RESOURCE;Z_RESVAL_P(pzv) = res; |
ZVAL_STRINGL(pzv,str,len,dup)中的dup参数
先阐述一下ZVAL_STRINGL(pzv,str,len,dup); str和len两个参数很好理解,因为我们知道内核中保存了字符串的地址和它的长度,后面的dup的意思其实很简单,它指明了该字符串是否需要被复制。值为 1 将先申请一块新内存并赋值该字符串,然后把新内存的地址复制给pzv,为 0 时则是直接把str的地址赋值给zval。
ZVAL_STRINGL与ZVAL_STRING的区别
如果你想在某一位置截取该字符串或已经知道了这个字符串的长度,那么可以使用宏 ZVAL_STRINGL(zval, string, length, duplicate) ,它显式的指定字符串长度,而不是使用strlen()。这个宏该字符串长度作为参数。但它是二进制安全的,而且速度也比ZVAL_STRING快,因为少了个strlen。
ZVAL_RESOURCE约等于ZVAL_LONG
在章节4中我们说过,PHP中的资源类型的值是一个整数,所以ZVAL_RESOURCE和ZVAL_LONG的工作差不多,只不过它会把zval的类型设置为 IS_RESOURCE。
8. 总结
PHP的弱类型是通过ZE的zval容器转换完成,通过哈希表来存储变量名和zval数据,在运行效率方面有一定牺牲。另外因为变量类型的隐性转换,在开发过程中对变量类型检测力度不够,可能会导致问题出现。
不过PHP的弱类型、数组、内存托管、扩展等语言特性,非常适合Web开发场景,开发效率很高,能够加快产品迭代周期。在海量服务中,通常瓶颈存在于数据访问层,而不是语言本身。在实际使用PHP不仅担任逻辑层和展现层的任务,我们甚至用PHP开发的UDPServer/TCPServer作为数据和cache的中间层。
以上是关于从架构出发探究Electron运行原理的主要内容,如果未能解决你的问题,请参考以下文章
电子构建失败并出现 ERR_ELECTRON_BUILDER_CANNOT_EXECUTE