教女朋友学前端之深入理解JS引擎

发表于 3年以前  | 总阅读数:243 次

食堂老板娘:老板,Chrome V8 引擎工作原理面试会问吗?

食堂老板:这块的知识不仅面试可能会问,学会了 JS 引擎的工作原理,可以更好的理解 JavaScript、更好的理解前端生态中 Babel 的词法分析和语法分析,ESLint 的语法检查原理以及 React、Vue 等前端框架的实现原理。总之,学习引擎原理可谓是一举多得。

食堂老板娘:好好好,别罗嗦了,快开始吧~

宏观视角看 V8

V8 是我们前端届的网红,它用 C++ 编写,是谷歌开源的高性能 JavaScript 和 WebAssembly 引擎,主要用在 Chrome、Node.js、Electron...中。

在开始讲我们的主角 V8 引擎之前,先来从宏观视角展开谈谈 V8 所处的位置,建立一个世界观。

在信息科技高速发展的今天,这个疯狂的大世界充斥着各种电子设备,我们每天都用的手机、电脑、电子手表、智能音箱以及现在马路上跑的越来越多的电动汽车。

作为软件工程师,我们可以将它们统一理解为“电脑”,它们都是由中央处理器(CPU)、存储以及输入、输出设备构成。CPU 就像厨师,负责按照菜谱执行命令烧菜。存储如同冰箱,负责保存数据以及要执行的命令(食材)。

当电脑接通电源,CPU 便开始从存储的某个位置读取指令,按照指令一条一条的执行命令,开始工作。电脑还可以接入各种外部设备,比如:鼠标、键盘、屏幕、发动机等等。CPU 不需要全部搞清楚这些设备的能力,它只负责和这些设备的端口进行数据交换就好。设备厂商也会在提供设备时,附带与硬件匹配的软件,来配合 CPU 一起工作。说到这里,我们便得到了最基础的计算机,也是计算机之父冯·诺伊曼在 1945 年提出的体系结构。

不过由于机器指令人类读起来非常不友好,难以阅读和记忆,所以人们发明了编程语言和编译器。编译器可以把人类更容易理解的语言转换为机器指令。除此之外,我们还需要操作系统,来帮我们解决软件治理的问题。我们知道操作系统有很多,如 Windows、Mac、Linux、Android、iOS、鸿蒙等,使用这些操作系统的设备更是数不胜数。为了消除客户端的多样性,实现跨平台并提供统一的编程接口,浏览器便诞生了。

所以,我们可以将浏览器看作操作系统之上的操作系统,而对于我们前端工程师最熟悉的 JavaScript 代码来说,浏览器引擎(如:V8)就是它的整个世界。

星球最强 JavaScript 引擎

毫无疑问,V8 是最流行、最强的 JavaScript 引擎,V8 这个名字的灵感来源于 50 年代经典的“肌肉车”的引擎。

Programming Languages Software Award

V8 也曾获得了学术界的肯定,拿到了 ACM SIGPLAN 的 Programming Languages Software Award[1]。

主流 JS 引擎

JavaScript 的主流引擎如下所示:

  • V8 (Google) [2]
  • SpiderMonkey (Mozilla) [3]
  • JavaScriptCore (Apple) [4]
  • Chakra (Microsoft) [5]
  • duktape(IOT)[6]
  • JerryScript(IOT)[7]
  • QuickJS[8]
  • Hermes(Facebook-React Native)[9]

V8 发布周期

V8 团队使用 4 种 Chrome 发布渠道向用户推送新版本。

  • Canary releases 金丝雀版 (每天)
  • Dev releases 开发版 (每周)
  • Beta releases 测试版 (每 6 周)
  • Stable releases 稳定版 (每 6 周)

想要了解更多,请戳 V8 引擎版本发布流程[10]。

V8 架构演进史

2008 年 9 月 2 日,V8 与 Chrome 在同一天开源,最初的代码提交日期可追溯到 2008 年 6 月 30 日,你可以通过下面的链接查看 V8 代码库的可视化演化进程。

  • 使用 gource 创建的 V8 代码库可视化演化进程[11]

当时的 V8 架构简单粗暴,只有一个 Codegen 编译器。

2010 年,V8 中加入了 Crankshaft 优化编译器,大大提升了运行时性能。Crankshaft 生成的机器代码比之前的 Codegen 编译器快两倍,而体积减少了 30%。

2015 年,为了进一步提升性能,V8 引入了 TurboFan 优化编译器。

接下来到了分水岭,在此之前,V8 都是选择将源码直接编译为机器码的架构。不过随着 Chrome 在移动设备的普及,V8 团队发现了这种架构下存在的致命问题:编译时间过长、机器码的内存占用很大。

所以,V8 团队对引擎架构进行了重构,在 2016 年引入了 Ignition 解释器和字节码

2017 年,V8 默认开启全新的编译 pipeline(Ignition + TurboFan),并移除了 Full-codegen 和 Crankshaft

高性能的 JS 引擎不仅需要 TurboFan 这样高度优化的编译器,在编译器有机会开始工作之前的性能,也存在着大量的优化空间。

于是在 2021 年,V8 引入新的编译管道 Sparkplug

对于 Sparkplug 想要了解更多,请戳Sparkplug[12]

  • 关于 V8 架构演进史,想要了解更多请戳庆祝 V8 诞生 10 周年[13]

食堂老板娘:原来 V8 架构经历了这么多的变化

食堂老板:是的,V8 团队为了不断的优化引擎的性能,做了很多努力。

V8 工作机制

敲黑板,进入本文的重点。

食堂老板娘:拿出小本本记好

V8 执行 JavaScript 代码的核心流程分为以下两个阶段:

  • 编译
  • 执行

编译阶段指 V8 将 JavaScript 转换为字节码或者二进制机器码,执行阶段指解释器解释执行字节码,或者 CPU 直接执行二进制机器码。

为了对 V8 整体的工作机制有更好的理解,我们先来搞懂下面几个概念。

机器语言、汇编语言、高级语言

CPU 的指令集就是机器语言,CPU 只能识别二进制的指令。但是对人类来说,二进制难以阅读和记忆,所以人们将二进制转换为可以识别、记忆的语言,也就是汇编语言,通过汇编编译器可以将汇编指令转换为机器指令。

不同的 CPU 有不同的指令集,使用汇编语言编程需要兼容不同的 CPU 架构,如 ARM、MIPS 等,学习成本比较高。汇编语言这层抽象还远远不够,所以高级语言应运而生,高级语言屏蔽了计算机架构的细节,兼容多种不同的 CPU 架构。

CPU 同样不认识高级语言,一般有两种方式执行高级语言的代码,也就是:

  • 解释执行
  • 编译执行

解释执行、编译执行

解释执行会先将输入的源码通过解析器编译成中间代码,再直接使用解释器解释执行中间代码,输出结果。

编译执行也会将源码转换为中间代码,然后编译器会将中间代码编译成机器码,通常编译成的机器码以二进制文件形式存储,执行二进制文件输出结果。编译后的机器码还可以保存在内存中,可以直接执行内存中的二进制代码。

JIT (Just In Time)

解释执行启动速度快,执行速度慢,而编译执行启动速度慢,执行速度快。

V8 权衡利弊后同时采用了解释执行和编译执行这两种方式,这种混合使用的方式称为 JIT (即时编译)

V8 在执行 JavaScript 源码时,首先解析器会将源码解析为 AST 抽象语法树,解释器 (Ignition) 会将 AST 转换为字节码,一边解释一边执行。

解释器同时会记录某一代码片段的执行次数,如果执行次数超过了某个阈值,这段代码便会被标记为热代码(Hot Code),同时将运行信息反馈给优化编译器 TurboFan,TurboFan 根据反馈信息,会优化并编译字节码,最后生成优化的机器码。

食堂老板娘:也就是说,当这段代码再次执行时,解释器就可以直接运行优化后的机器码,不需要再次解释,这样会提升很多性能吧?

食堂老板:对的!

V8 的解释器和编译器的名字寓意非常有趣,解释器 Ignition 代表点火器,编译器 TurboFan 代表涡轮增压,代码启动时通过点火器发动,TurboFan 一旦介入,执行效率会越来越高。

了解了 V8 的大体工作机制,接下来我们继续深入,看一下 V8 核心模块的工作原理。

V8 核心模块工作原理

V8 的核心模块包括:

  • Parser[14]:解析器负责将 JavaScript 代码转换成 AST 抽象语法树。
  • Ignition[15]:解释器负责将 AST 转换为字节码,并收集 TurboFan 需要的优化编译信息。
  • TurboFan[16]:利用解释器收集到的信息,将字节码转换为优化的机器码。

V8 需要等编译完成后才可以运行代码,所以解析和编译过程中的性能十分重要。

解析器 Parser

解析器的解析过程分为两个阶段:

  • 词法分析 (Scanner 词法分析器)
  • 语法分析 (Pre-Parser、Parser 语法分析器)

词法分析

Scanner 负责接收 Unicode Stream 字符流,将其解析为 tokens,提供给解析器 Parser

比如下面这段代码:

let myName = '童欧巴'

会被解析成 letmyName='童欧巴',它们分别是关键字、标识符、赋值运算符以及字符串。

语法分析

接下来,语法分析会将上一步生成的 tokens,根据语法规则转换为 AST,如果源码存在语法错误,在这一阶段就会终止并抛出语法错误。

可以通过这个网站查看 AST 的结构:https://astexplorer.net/[17]

也可以通过这个链接https://resources.jointjs.com/demos/javascript-ast[18]直接生成图片,如下所示:

得到了 AST,V8 便会生成该段代码的执行上下文

惰性解析

主流的 JavaScript 引擎都采用了惰性解析(Lazy Parsing),因为源码在执行前如果全部完全解析的话,不仅会造成执行时间过长,而且会消耗更多的内存以及磁盘空间。

惰性解析就是指如果遇到并不是立即执行的函数,只会对其进行预解析(Pre-Parser),当函数被调用时,才会对其完全解析。

预解析时,只会验证函数的语法是否有效、解析函数声明以及确定函数作用域,并不会生成 AST,这项工作由 Pre-Parser 预解析器完成。

解释器 Ignition

得到了 AST 和执行上下文,接下来解释器会将 AST 转换为字节码并执行。

食堂老板娘:为什么要引入字节码呢?

引入字节码是一种工程上的权衡,从图中可以看出,仅仅是一个几 KB 的文件,生成的机器码就已经占用了大量的内存空间。

相比机器码,字节码不仅占用内存少,而且生成字节码的时间很快,提升了启动速度。虽然字节码没有机器码执行速度快,但是牺牲了一点执行效率,换来的收益还是很值得的。

况且,字节码与特定类型的机器码无关,通过解释器将字节码转换为机器码后才可以执行,这样也使得 V8 更加方便的移植到不同的 CPU 架构。

你可以通过如下命令,查看 JavaScript 代码生成的字节码。

node --print-bytecode index.js

也可以通过如下链接进行查看:

  • V8 解释器的头文件,包括所有字节码[19]

我们来看一段代码:

// index.js
function add(a, b) {
    return a + b
}

add(2, 4)

上面的代码在执行命令后,会生成如下的字节码:

[generated bytecode for function: add (0x1d3fb97c7da1 <SharedFunctionInfo add>)]
Parameter count 3
Register count 0
Frame size 0
   25 S> 0x1d3fb97c8686 @    0 : 25 02             Ldar a1
   34 E> 0x1d3fb97c8688 @    2 : 34 03 00          Add a0, [0]
   37 S> 0x1d3fb97c868b @    5 : aa                Return
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 8)
0x1d3fb97c8691 <ByteArray[8]>

其中,Parameter count 3 表示三个参数,包括传入的 a,b 以及 this。字节码的详细信息如下:

Ldar a1 // 表示将寄存器中的值加载到累加器中
Add a0, [0] // 从 a0 寄存器加载值并且将其与累加器中的值相加,然后将结果再次放入累加器
Return // 结束当前函数的执行,并把控制权传给调用方,将累加器中的值作为返回值

每行字节码都对应着特定的功能,一行行字节码就如同搭乐高积木一样,组装到一起就构成了完整的程序。

解释器通常有两种类型,基于栈基于寄存器的解释器,早期的 V8 解释器也是基于栈的,现在的 V8 解释器采用了基于寄存器的设计,支持寄存器的指令操作,使用寄存器来保存参数和中间计算结果。

Ignition 解释器在执行字节码时,主要使用了通用寄存器累加寄存器,相关的函数参数和局部变量会保存在通用寄存器中,累加寄存器会保存中间结果。

在执行指令的过程中,CPU 需要对数据进行读写,如果直接在内存中读写的话,会严重影响程序的执行性能。所以 CPU 就引入了寄存器,将一些中间数据存放到寄存器中,提升 CPU 的执行速度。

编译器 TurboFan

在编译方面,V8 团队同样做了很多优化,我们来看下内联和逃逸分析。

内联 inlining

关于内联,我们先来看一段代码:

function add(a, b) {
  return a + b
}
function foo() {
  return add(2, 4)
}

如上代码所示,我们在 foo 函数中调用了函数 add,add 函数接收 a,b 两个参数,返回他们的和。如果不经过编译器优化,则会分别生成这两个函数所对应的机器码。

为了提升性能,TurboFan 优化编译器会将上面两个函数进行内联,然后再进行编译。内联后的函数如下所示:

function fooAddInlined() {
  var a = 2
  var b = 4
  var addReturnValue = a + b
  return addReturnValue
}

// 因为 fooAddInlined 中 a 和 b 的值都是确定的,所以可以进一步优化
function fooAddInlined() {
  return 6
}

内联优化后,编译生成的机器码会精简很多,执行效率也有很大的提升。

逃逸分析 Escape Analysis

逃逸分析也不难理解,它的意思就是分析对象的生命周期是否仅限于当前函数,我们来看一段代码:

function add(a, b){
  const obj = { x: a, y: b }
  return obj.x + obj.y
}

如果对象只在函数内部定义,并且对象只作用于函数内部的话,就会被认为是“未逃逸”的,我们可以将上面代码进行优化:

function add(a, b){
  const obj_x = a
  const obj_y = b
  return obj_x + obj_y
}

优化后,无需再有对象定义,而且我们可以直接将变量加载到寄存器上,不再需要从内存中访问对象属性。不仅减少了内存消耗,而且提升了执行效率。

关于逃逸分析,Chrome 曾经也爆出过安全漏洞,使整个互联网变慢,感兴趣请戳V8 团队的一个错误,使得整个互联网变慢[20]

除了上述提到的各种优化方案和模块,V8 还有很多优化手段和核心模块,如:使用隐藏类快速获取对象属性、使用内联缓存提升函数执行效率、Orinoco[21] 垃圾回收器、Liftoff[22] WebAssembly 编译器等等,本文不再过多介绍,大家感兴趣可以自行学习。

小结

本文从宏观视角看 V8、V8 架构演进史、V8 的工作机制以及 V8 核心模块的工作原理几个方面进行了介绍和总结,我们可以发现,无论是 Chrome 还是 Node.js,它们只是一个桥梁,负责把我们前端工程师编写的 JavaScript 代码运输到最终的目的地,转换成对应机器的机器码并执行。在这段旅程中,V8 团队做了很大的努力,给他们最大的 respect。

虽然 CPU 的指令集是有限的,但是我们软件工程师编写的程序不是固定的,正是这些程序最终被 CPU 执行,才有了改变世界的可能。

你们是最棒的,改变世界的程序们!

参考资料

[1]Programming Languages Software Award: http://www.sigplan.org/Awards/Software/

[2]V8 (Google) : https://v8.dev/

[3]SpiderMonkey (Mozilla) : https://spidermonkey.dev/

[4]JavaScriptCore (Apple) : https://developer.apple.com/documentation/javascriptcore/

[5]Chakra (Microsoft) : https://github.com/microsoft/ChakraCore/

[6]duktape(IOT): https://github.com/svaarala/duktape/

[7]JerryScript(IOT): https://github.com/jerryscript-project/jerryscript/

[8]QuickJS: https://github.com/bellard/quickjs/

[9]Hermes(Facebook-React Native): https://github.com/facebook/hermes/

[10]V8 引擎版本发布流程: https://zhuanlan.zhihu.com/p/35038142

[11]使用 gource 创建的 V8 代码库可视化演化进程: https://www.youtube.com/watch?v=G0vnrPTuxZA

[12]Sparkplug: https://v8.dev/blog/sparkplug

[13]庆祝 V8 诞生 10 周年: https://v8.dev/blog/10-years

[14]Parser: https://v8.dev/blog/scanner/

[15]Ignition: https://v8.dev/docs/ignition/

[16]TurboFan: https://v8.dev/docs/turbofan

[17]https://astexplorer.net/: https://astexplorer.net/

[18]https://resources.jointjs.com/demos/javascript-ast: https://resources.jointjs.com/demos/javascript-ast

[19]V8 解释器的头文件,包括所有字节码: https://github.com/v8/v8/blob/master/src/interpreter/bytecodes.h/

[20]V8 团队的一个错误,使得整个互联网变慢: https://segmentfault.com/a/1190000011413430

[21]Orinoco: https://v8.dev/blog/trash-talk/

[22]Liftoff: https://v8.dev/blog/liftoff/

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/kineBLpdjrYWGS5zKVcykg

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录