特别说明

这是一个由 simviso 团队对2019 Google I/O 大会中关于面向Web开发人员的WebAssembly相关话题进行翻译的文档,内容并非直译,其中有一些是译者自身的思考。Surma是Google公司WEB基础的贡献者,也是open web平台的开发倡导者。
下一篇博文地址:面向Web开发人员的WebAssembly 2019 Google I/O 下

视频地址:面向Web开发人员的WebAssembly 2019 Google I/O 上
视频地址:面向Web开发人员的WebAssembly 2019 Google I/O 下
视频同时也获得了谷歌大佬Surma和Deepti的官方推特分享和点赞

clipboard.png
1.png
视频翻译文字版权归 simviso 所有

clipboard.png

本次参与翻译人员

clipboard.png

前言

我是Surma,我是open web平台的开发倡导者,在伦敦与谷歌的Chrome团队合作。今天很高兴可以跟大家谈一谈我最近发现的一个让我充满激情的东西,那就是WebAssembly。

clipboard.png
你可能对它有所耳闻,如果你以后有什么问题的话,可以在 Twitter 上联系我。稍后我的同事,来自web工程团队的 Deepti 会分享一些关于WebAssembly未来的话题。

定义

在我们开始讲之前,我想让大家一起看下这张图。

clipboard.png
因为 WebAssembly 通常与 C++ 紧密相连,以至于很多人都认为它都是关于 C++ 的,事实上,WebAssembly 远不止于此。你可以在网上找到很多关于 C++ 和 Emscripten 的 demo。 这很有意义,因为 Emscripten 是一个令人惊叹的工具。但对Web开发人员而言,意识到WebAssembly不仅限于C++,这点很关键!

WebAssembly 本身其实是一个非常有用的工具,你值得拥有!这就是我想在这次演讲中谈论的内容。 我想展示一些支持 WebAssembly 的其他语言,以此来让你在不学习新语言的情况下使用WebAssembly 。然后,就像我说的,Deepti 接下来将讨论 WebAssembly 的未来。

clipboard.png
所以要确保每个人都知道或者希望在场的人都知道这个网站(WebAssembly官网),它上面解释了什么是 WebAssembly,它是一个基于堆栈的虚拟机

如果你不知道什么是基于Web 堆栈的虚拟机,那也没有关系。
重要的是,你能意识到它是一个虚拟机, 意味着它不是一个实际存在的处理器,它的设计理念在于 我们可以很容易的将通用代码编译到真实的运行环境下可执行的代码,这就是所谓的可移植性。因此,对虚拟机而言,设计之初需要优先考虑可移植性。

因此,当你使用任意语言编写某些代码并将其编译到 WebAssembly 时,也就是说这些代码会被编译成虚拟机可执行的指令集,然后将这些指令存储到以二进制格式存储到.wasm文件中。

clipboard.png
因为该虚拟机可以很轻易的根据对应平台下的处理器进行代码编译,所以.wasm文件可以在运行时被读取,此时我们这里运行的上下文极有可能是浏览器。
浏览器能将.wasm文件转换成当前机器可执行的机器码,并在浏览器上执行该代码。

在一开始,WebAssembly就是为了面向过程安全而设计的。你可以在裸机上运行该代码,但这并不意味着它是不安全的。

实际上我们已经在上次的Google I/O 大会上讨论过WebAssembly了。
作为一项技术,它以惊人的速度快速成长,并占据了一席之地。之前我们也讨论了一些大公司是如何使用WebAssembly来运行他们之前已经存在的一些产品,这些产品很可能是用C++来编写的。

举个例子,就拿我们使用多年的AutoCAD来讲,这是一个非常知名的产品,但现在他们正努力将它编译到WebAssembly上,当你想用它时,你就可以立即在浏览器上运行它,想想都令人感到不可思议。

另一个例子则是Unity游戏引擎以及虚幻引擎,它们现在都已经开始支持WebAssembly。
通常这些游戏引擎已经内建了一套抽象,因为你需要将你的游戏构建并编译到PlayStation,XBox或者其他游戏平台之上。

但现在WebAssembly已经成为它们的另一个编译目标,那给我们印象很深的就是通过浏览器和WebAssembly就可以提供这些游戏运行所需的性能。

例子讲解

这让我觉得很神奇,这些神奇的事情还在继续发生着。

二维码扫描

clipboard.png
你通过WebKeynote就可以看到,我的同事Paul Lewis建立了一个感知工具包,它可以帮你建立一种沉浸式的体验。他们希望通过二维码和图像检测的形式来和我们的现实世界建立联系。

所以浏览器能够通过使用图形检测API(Shape Detection API)检测二维码,
clipboard.png
这一功能。

所以他们要做的就是该通过什么途径使用这个图形检测API。如果不可用,他们可以通过将二维码库编译到WebAssembly上,然后就可以按需加载。这样我们就可以找到它。

并且图像检测根本不在Web平台上。所以他们需要自己去构建,并使用 WebAssembly 为浏览器提供新的功能。

QT Lib

clipboard.png

UI工具包QT(QT是跨平台的软件开发工具包)宣布他们现在也支持 WebAssembly。因此这也意味着其实你现在可以使用一个旧版本的Lib QT应用程序, 并将其编译为 WebAssembly。然后在浏览器标签体验中有一个奇怪的窗口,这看起来不太理想,这里只是为了表明它起作用了。

但QT是一个强大且通用的UI库。所以,他们网站底部有很多Demo,实际上他们使用 Lib QT 和WebAssembly构建了一种优良且原生的UI。

Emscripten

clipboard.png
所以如果你对 WebAssembly 不甚了解,那么你可能会问它们是怎么做到的?
针对这些例子,给出的答案是Emscripten

Emscripten的目标是替代C或C++编译器,取代将代码编译成你所需的本地机器码,而是将代码编译到WebAssembly上。
他们真的尝试着进行这方面的取代工作。无论你编写什么语言的代码,都可以跑在一个系统上,即可以很神奇地在web上面运行。这也是两种编译器之间最明显的区别。

为了实现这一目标, Emscripten 在幕后做了很多繁重的工作。

我之所以这么思考的原因,在于它在这么多种情况下都可以很紧密和WebAssembly一起很好的工作。

clipboard.png
最初,Emscripten 是一个asm.js编译器。这是Mozilla的一个概念,他们编写了这个编译器,用于将C语言代码并将其转换为 JavaScript。

因此可以看到,右手边的这个就是asm.js。
它只是普通的 JavaScript,也就是说,只要可以执行 JavaScript 的浏览器都可以执行asm.js。

但这个计划旨在让其它浏览器支持asm.js,从而让它们运行这类程序的时候更加快速。
所以你需要分配一块内存,同时接收一些变量。瞬间,你的C++ 代码就能够运行在你的 JavaScript 引擎上!

但 C 和 C++ 经常使用其他API,比如 fileopen 和 OpenGL。
因此,Emscripten 可以使用 WebGL 伪装成 OpenGL ,也可以通过模拟一个文件系统,使你看起来好像在处理真实的文件一样。
基本上,他们是真模仿一整套POSIX操作系统来使代码运行在 Web 上,而这些代码从来都不是为 Web 编写的。
所以他们做到了!

所以当 WebAssembly 出现时,对Emscripten来讲, 只是添加了一种新的输出格式,依然保留着他们在进行模拟时可以做到的所有功能。
因此,Emscripten 可以按照之前的经验进行使用。

我们知道,将 POSIX 代码通过WebAssembly在 Web 上运行。
这样,他们就能够以极快的速度围绕 WebAssembly 来提供非常令人印象深刻并且成熟的Demo和工具。

他们值得得到大家的称赞,因为他们,所有其他语言有了这样一个平台。
我想这就是为什么 WebAssembly 与 C++ 如此紧密的缘故,在于 Emscripten 的快速成熟的发展。

Web开发人员

但是,对于Web 开发人员呢?

clipboard.png
可能你是在 Web 机构工作或者你也可能是一个自由开发者,那么WebAssembly 对你会有什么样的帮助?

你必须要学C++吗? NO!

当你是一个Web开发人员,你会想,Oh,我应该需要学习 C++的,只有这样你才可以使用 WebAssembly,可能很多人都会这样想。

clipboard.png
因为当你知道JavaScript的时候,你在想C++到底是什么?有趣的是,反过来也是一样。当我看到C++开发人员第一次看到或写JavaScript代码时,他们的表情一模一样。

我不是说因为一种语言比另一种语言好,仅仅是因为他们需要这样一种截然不同的思维方式来编写代码。
这两种语言我写的都很专业。但每当我稍微切换一下,总是需要一些时间才能想到(思维方式的不同)。我想说的是,对于Web开发人员来说,至今还没有去学习 C++ 的动力。

因此,这两种编程语言都掌握非常好的人非常少。这就导致,WebAssembly似乎是一个非常小众的技术。实际上,它是一个非常有用且值得你拥有的工具!

WebAssembly与Javascript

这里,我想说的是,每当我想到 WebAssembly 时,我通常会通过两个主要例子来讨论。
一方面,我想谈谈在 JS 应用程序中微小模块的替换、热传递和 WebAssembly 的瓶颈,同时也想谈谈 WebAssembly 比 JavaScript 更快的神奇之处。

clipboard.png

但首先,我想谈谈关于生态系统的其他方面。

这看起来有点奇怪,因为当我说 JavaScript 生态系统非常庞大时,没人会反对。
我的意思是,仅仅只是在NPM上看,它就非常巨大。
但事实上并不是每个主题的首选都是 JavaScript,也有可能是其他语言。

所以有时候你可能会去面临一个问题,你需要寻找库去解决这些问题。你可以在 C 中或者 Rust 中找到这些答案,而不是在 JavaScript 中找到。

因此,你要么坐下来编写自己的 JavaScript 对应实现,要么通过WebAssembly来使用其他语言的相关实现。
这正是我们对 Squoosh 所做的。

clipboard.png
Squoosh 是一个完全可以在浏览器中运行的图像压缩应用程序,可以在脱机状态下工作。你可以放入图片,然后可以使用不同的编解码器压缩它们。
以观察这些不同的编解码器如何对你的图片的视觉质量产生不同的影响。
如果你了解的话,可以发现浏览器现在已经提供了这一性能。

clipboard.png
通过使用 Canvas,你可以决定你要对图片进行编码的图片格式。
你甚至可以控制图片质量!

但事实证明,浏览器通过对这些编解码器的优化而不是优化压缩质量或视觉质量来提高压缩速度。
老实说,有点不尽人意。
而且,你有点受浏览器支持的编解码器的约束。

所以直到最近,也只有 Chrome 可以编码为 WebP 格式,其他浏览器却没有。
所以这对我们来说还不够。

我们利用谷歌查阅了一些相关资料,并找到了一些使用 JS 写的有关 JPEG 的编解码器
但有点奇怪,我们并没有找到一个使用JavaScript写的专门针对WebP 的编码器。
因此,我们认为我们得看看其他东西。。

所以我们从其他地方查阅了一番,并在C和C++中找到了大量的相关编码器。
因此,我们选择WebAssembly 。
所以呢,我们要做的就是编译。

clipboard.png

举个例子,将MozJPEG 这个库编译到WebAssembly上,并加载到浏览器中,然后将浏览器的JPEG 编码器替换成我们自己加载的这个
8.png
这样我们在相同视觉质量下得到了更小体积的图片,酷吧!
不仅如此, WebAssembly还允许我们加载这个库所暴露的专业选项,而这些在浏览器中显然是不可能暴露的。

因此,配置诸如色度子采样或者不同的量化算法等选项,不仅有助于将最后几个字节从图片中舍弃,而且还可以作为一种学习工具,方便我们去查看这些选项究竟是怎么影响我们的图片视觉和文件大小的。

这里的重点是我们采用了一段旧代码,仿佛 MozJPEG 又回到了1991年(mozjpeg里面有个头文件是在1991开始的)

它绝对不是以 Web 为目的写的,但我们还是将它使用在了 Web 上,并用它来改善 Web 平台。

Emscripten操作举例

还有我们用过的 Emscripten。
所以将它与 Emscripten 一起向你展示它是如何工作的。

我通常会分两步进行。
9.png
第一步是编译库,以便稍后你可以链接到它。
图片编解码器通常会用到多线程和 SIMD(单指令多数据流),因为图片压缩是一个高并发任务。
但不管是 JavaScript 还是 WebAssembly 都还不支持多线程或 SIMD。

稍后Deepti 会带着大家来讨论这方面的内容。

但是这里我们通过禁用SIMD 以确保在运行过程中不会出现任何问题
10.png
11.png
在第二步中,我们需要写一段桥接代码

这个函数我稍后将使用JavaScript来调用,
所以,它需要接收图片、图片尺寸,然后通过MozJPEG 来对它进行压缩,并返回一个包含JPEG 图片的数组类型Buffer。
12.png
当我们写完这段桥接代码,我们调用EMCC(Emscripten Compiler Frontend )命令,Emscripten C编译器将编译我们的C++文件和库文件,然后链接到一起供后续使用。
13.png
这个过程没有报错,然后得到输出文件:一个已经配置好了的JS文件和对应的WebAssembly文件。

这里要记住,Emscription作为一个替代者,已经将Emulations相关的复杂工作全都帮你做了(Emulations 是C++11的一个库)
你需要一直关注你文件大小,因为这种文件系统emulations 相关api代码需要其他的相关底层输入。
因此如果你使用了大量的C语言的API,那么生成的文件将会很大,尤其是JS文件。

我们一直在与Emscripten团队密切合作,为了让生成的文件尽可能的小。但是如果你想让Emscripten成为一个替代品,你只能尽你所能做的更多。所以还是要注意生成文件的大小。

关于WebAssembly 的另外一个例子就是使用Squoosh进行图片的缩放。
为了将一张图片放大或者缩小,有很多方式可以通过许多不同的视觉效果和视觉输出来实现这一点。
可以通过多种不同的视觉效果和视觉输出进行实现。
14.png
因此对于浏览器,如果你只是使用浏览器缩放图片,你会得到一张缩放的图片。
它可能会很快,可能看起来也不错。
但有时候通过对这张图片缩放操作进行不同的变量控制真的可以产生巨大的视觉冲击力。
15.png
16.png
所以,在这个视频上,你可以看到我在 Lanczos3 和浏览器拥有的算法之间来回切换。
你可以看到,通过Lanczos3算法,我会有一个线性RGB色彩空间转换。

实际上,我对这张图片中的亮度有更真实的感知。
所以在这种情况下,它实际上是一段非常有用的代码。
我们在 Squoosh 中使用的这些图像缩放算法,实际上是从 Rust 生态系统中提取的。
17.png
Mozilla 为 Rust 生态系统进行了大力投资,
他们的团队为 Rust 编写了 WebAssembly 工具,同时社区也将这些东西抽象为通用工具。
其中一个工具是 wasm-pack,它真的很趁手,可以帮你将你的 Rust 代码转化为
WebAssembly 模块,一个现代化形式的 JavaScript(译者注:JS可以调用.wasm文件,变相对功能拓展,所以称之现代化) ,并且它非常小。
我觉得这种东西真的很有趣。

所以对于Rust,同样的道理,我们有一个库,用来编写我们这么一小点桥接代码。
18.png
在这个例子中,我想通过JS调用这个resize函数。它通过接收图片、我输入的图片大小和输出的大小,然后通过这个函数我会得到调整后的图片。
19.png
然后你就可以将所有这些代码通过wasm-pack转换为一个我们可用的WebAssembly模块,
现在比较文件大小有失公平,因为它是一个不同的库,而且这个库相对较小。

所以不能直接从文件大小层面比较。但是就平均而言,讲道理说,Rust往往会产生更小的胶水代码。因为Rust没有做任何关于POSIX的文件系统模拟

你不能在rust使用关于文件的函数,因为它根本就没有做文件系统模拟。
如果你想要的话,也可以放入一些它们提供的封装模块。它更像是一种可插拔的配置。
20.png
因此,最低我们也要用到 Squoosh,也就是我们需要使用至少来自两种不同语言的四个不同的库。
这些库与web 无关,但我们仍然继续要在web上使用它们。
21.png
这正是我想要你从这整件事中得到的东西,如果你在Web平台上发现一个(技术)空白,并且该空白其他语言已经实现了很多次,但却不是用于Web上或者不是通过JavaScript实现的。

两种语言的比较

WebAssembly 可能是一个工具。

但现在让我们来谈谈在你的 JavaScript中关于Hot Path(有大量循环迭代的代码实现)的替换,以及 WebAssembly 比JavaScript 更快的神奇之处。

现在这对我来说非常重要,而这就是为什么我想出了使用这张图来表达我的意思。
22.png
JavaScript 和 WebAssembly 都有相同的峰值性能。
它们运行都很快,但是在运行比较稳定的代码(fast path)这点上,使用 WebAssembly比JavaScript快得多(译者注:fast path 是已经编译后的代码,slow path是需要临时进行编译的代码)

fast path: Frontend -> simple code generator -> assembler
slow path: Frontend -> IR optimizer (sometimes more than one level of IR) -> code-generator -> assembler 

或者反过来讲,如果你运行一些不稳定的代码(slow path),相比WebAssembly 而言,js就更加容易出现一些意想不到的意外。目前,WebAssembly 正在寻找支持多线程和 SIMD 的解决方案,而这是JavaScript 永远做不到的因此,一旦这些技术实现,WebAssembly 将有机会真正的超越 JavaScript 。但在目前状态下,它们的峰值性能都是相同的。

Chrome引擎

为了理解在Fast Path环节下JS的整体性能下降,让我们来谈谈一点关于Chrome 的 JavaScript V8引擎 和 WebAssembly 引擎吧。

JavaScript 文件和WebAssembly 文件有两个不同的引擎入口点。 JavaScript 文件传递给V8的解释器:Ignition。
所以,它可以将JavaScript 文件作为文本读取,并解析和运行它。
23.png
当这个解释器运行时,它会搜集有关代码行为的分析数据,然后通过Turbofan这个优化编译器来生成机器码。
24.png
另一方面,WebAssembly文件将会传给Liftoff这个流式WebAssembly编译器
25.png
一旦这个编译器完成操作,turbofan 就会启动并生成优化代码。
这里有一些区别。
26.png
第一个明显的区别是他们有不同的名称和不同的Logo,但也有概念上的区别。
ignition是一个解释器,Liftoff是一个生成机器代码的编译器。

虽然我们不能总是以偏概全的去说机器码永远要比解释性程序代码跑得更快。
但总的来讲,确实是这么一回事。

那么,第一个区别就是速度感知方面
27.png
但更重要的是JavaScript的这一区别。
但对于JavaScript来讲,更重要的区别是优化编译器只会在最后启动,在这之前JS代码必须进行运行并进行观察才能被优化。

因为某些假设是根据观察结果得出的。 生成机器码,然后再运行机器码。 但是一旦这些假设没有办法再Hold住状况,你必须回到解释器。因为我们不能再保证机器是否可以正常工作。这被我们称为deopt,即负优化。

有了WebAssembly,Turbofan总是在Liftoff编译器之后起作用,并且你能一直留在Turbofan 上输出。你能一直留在fast path 上,并且你永远不会出现在deopt状况,我想这也是大家对于WebAssembly速度更快这个观点的在认知上的误区。但在Javascript上你会很容易就得到deopt状态,而在WebAssembly上就不会。
29.png

基准测试

来自Rust WebAssembly 团队的 Nick Fitzgerald 其实做了一个非常好的基准测试,他为JavaScript 和 WebAssembly 编写了一个基准测试。

图中JavaScript是红色部分,WebAssembly 是蓝色部分,运行在不同的浏览器上。
看到这里,你会说Yes,OK,WebAssembly 更快。

但是这里主要的时间消耗可以看出JavaScript消耗跨度比较大。
它需要多长时间才是不可预料的,而无论在哪个浏览器上WebAssembly始终如一,消耗的时间基本相同。

我想这才是关键,这也是我想带给你们的。WebAssembly 为你提供了可预测性更强的性能。
它提供了比 JavaScript 可预测性更强的性能。

这就是我想通过Squoosh这个例子来告诉大家的东西,
我们想旋转一个图片。
所以,我们想好了,我们用 Canvas 实现吧。但是我们不能使用它,因为Canvas 运行在主线程上。

在Chrome中的OffscreenCanvas很少运行在主线程上
(译者注:OffscreenCanvas提供了一个可以脱离屏幕渲染的canvas对象,是针对上面提到的主线程的补充)

所以,我们最终手写了一段 JavaScript 来旋转它或者只需重新排列像素即可旋转图片。
它真的很管用,非常快,但事实证明,我们在其他浏览器测试得越多,它就变得有点奇怪。

所以在这个测试案例中,我们将一张4K分辨率的图片进行旋转。
30.png
这不是为了比较浏览器。而是关于比较 JavaScript 的。

最快的浏览器需要花费400毫秒,
最慢的浏览器需要8秒,
甚至在脱离主线程的情况下,对于用户来说,按下按钮旋转图像消耗的时间实在太长了。

从这里你切实可以看到,很明显,我们可以在一个浏览器中停留在fast path 上,但我们在另一个浏览器中却失败了。
不是这个浏览器不够快,只是一些浏览器的优化有所不同。

所以,我们将我们写的旋转代码运行在WebAssembly 上或通过其他语言实现这段代码并编译到WebAssembly ,然后来进行性能比较。
31.png
你能从这幅图中看到什么?几乎所有支持WebAssembly 的语言都能使我们的运行时间降低到大约500毫秒左右。

我认为这是可预测的。 我的意思是这些语言仍然存在一些差异,但抛开JavaScript的数据,其它的就没什么差距,这简直是指数级的降低。

同样,我们也可以从这里看到,我有注意到在WebAssembly 最优性能体现这块,
WebAssembly和JavaScript的最优性能几乎是一样的。

AssemblyScript

32.png
因此,如果你看图表,你可能想知道 AssemblyScript 是什么。
如果你还不了解它,我会很兴奋,因为 AssemblyScript 确实将我带回了我这节课要演讲的主题,那就是Web开发人员的WebAssembly 。

AssemblyScript是一个从TypeScript到WebAssembly的编译器。
33.png
这么说可能会误导你,
因为你不能将你现有的 TypeScript放到这个编译器中,并从中获取WebAssembly 。

因为在WebAssembly 中你没有Dom API,这样你就不能只使用相同的代码。
但是AssemblyScript 可以使用TypeScript语法写的不同类型的库。
34.png
这也就意味着你根本不需要去学一门全新的语言用来写WebAssembly,这也是让我感到最为惊艳的一点
所以这看起来像什么,就是Typescript语言,只不过有一点点区别。
35.png
就好像图中的i32,它实际上并不是JavaScript中的一种类型,但它却是WebAssembly的一种所属类型。

然后,如load和store这些内置函数,可以从内存中读取和写入值。接着,AssemblyScript编译器会将这些转变为WebAssembly模块。

so,你现在完全可以在不学习一门新语言的情况下,你就能够去写WebAssembly,并从中获益并且能够利用所有WebAssembly所能提供给你的好处,这是非常强大的。

有些事情需要你记住,WebAssembly并不像TypeScript那样具有垃圾回收功能,至少现在还没有。 Deepti之后会关于这个进一步讨论。
至少目前而言,你需要自己做内存管理。
36.png
所以AssemblyScript提供给我们一系列的内存管理模块。你只需要将它们从仓库里拉下来,然后用这些C语言风格的代码对它们进行内存分配管理
这是我们目前需要去习惯的,但它确实十分有用。一旦WebAssembly支持垃圾回收,它会变得更好。AssemblyScript已经完全开源,但它还是一个处于起步阶段的小项目。背后有许许多多充满激情的人在为它作支持。尽管它已经有了几个赞助商,但还是比不上像有Mozilla在背后做支持的Rust或Emscripten。

所有人都说这是肯定有用,并且体验感很好。我的同事Arron Turnner用AssemblyScript写了一套完整的模拟器。如果你因为使用它感到心累,那么你可以在GitHub上看他是如何写这个代码的。

建议

在这里,我想向各位明确的是,目前而言,将所有的东西都编译为wasm并不是一个明智的决定。JavaScript和WebAssembly并不是竞争对手,相反它们更应该是合作关系。在一起使用它们的同时,并不会将对方所取代。比如调试WebAssembly会变得更难。还有在对WebAssembly代码做code split(代码拆分)的时候,会比javascript更加困难。

你必须来回调用函数,这并不是一个很好的体验。已经有不少人在推特告诉我说,他们想用C++来编写他们的Web组件。
37.png
我不知道为什么他们想这么做,如果他们真的想这么做,我也不推荐。
我想说一些适合WebAssembly来做的一些事,比如做一些性能审查,测量你的性能瓶颈在哪里,看看WebAssembly是否可以帮助你。如果你有发现平台上的空白,你可以用其他语言来弥补它。
再次强调,WebAssembly只是你的工具。

相关链接

联系我们

欢迎加群和我们交流
45.png
如果感兴趣可以关注我们的微信公众号
公众号.png


知秋o
20 声望14 粉丝

《Java编程方法论:响应式RxJava与代码设计实战》 作者,simviso开源分享团队核心,专注于源码解读与基础库编写