
目錄
?? JavaScript語言設(shè)計(jì)缺陷和性能短板
?? 什么是WebAssembly?
?? WebAssembly是如何工作的?
?? 為什么WebAssembly更快?
?? WebAssembly實(shí)戰(zhàn)演練
1. JavaScript語言存在的缺陷和性能短板
1.1 JS 語言設(shè)計(jì)缺陷
-
JavaScript 最初是一種簡單的腳本語言,旨在為充滿輕量級超文本文檔的 Web 應(yīng)用帶來一些交互性。它的設(shè)計(jì)易于學(xué)習(xí)和編寫,并不追求運(yùn)行速度。多年來,瀏覽器在 JavaScript 解析上的重大性能改進(jìn)的眾多瀏覽器引入了即時(shí)(JIT)編譯使得JavaScript運(yùn)行速度快了一個(gè)量級。使得JavaScript運(yùn)行速度快了一個(gè)量級。(大拐點(diǎn))
image.png
但是對于 JavaScript 這種弱數(shù)據(jù)類型的語言來說,要實(shí)現(xiàn)一個(gè)完美的 JIT 非常難。因?yàn)镴avascript 是一個(gè)沒有類型的語言,而且像+這樣的符號又能夠重載,譬如這樣的代碼:
const sum = (a, b, c) => a + b + c;
這是 一個(gè)求和函數(shù),可以直接放在瀏覽器的控制臺下運(yùn)行,如果傳參都是整數(shù)時(shí),結(jié)果是整數(shù)相加的結(jié)果:如,答案是 6。但是,如果至少有一個(gè)是字符串,則結(jié)果是按照字符串拼接出的結(jié)果,如 console.log(sum('1',2,3)),答案是 "123"。也就是說,JIT在遇到sum第一個(gè)參數(shù)時(shí)會編譯成字符串的機(jī)器碼;但是在碰到第二個(gè)sum調(diào)用時(shí),不得不重新編譯一遍。這樣一來,JIT帶來的效率提升便被抵消了。
1.2 JavaScript性能問題
-
我們對性能的期望是無盡的,JIT帶來的性能提升也早已因?yàn)镴avaScript的動態(tài)特性達(dá)到了性能天花板。特別在當(dāng)前Web端視頻音頻、圖形圖像處理、VR、AR游戲的日益增多的情況下,問題顯得日益突出。無法滿足一些大型web項(xiàng)目開發(fā),于是三大瀏覽器巨頭分別提出了自己的解決方案:
image.png
image.png
我們熟知的四大主流瀏覽器廠商 Google Chrome、Apple Safari、Microsoft Edge 和 Mozilla FireFox ,覺得Mozilla FireFox所推出的 asm.js 很有前景,為了讓大家都能使用,于是他們就共同參與開發(fā),基于asm.js制定一個(gè)標(biāo)準(zhǔn),也就是WebAssembly。
2015年, WebAssembly首次發(fā)布,并可直接在瀏覽器中運(yùn)行
2017 年 3 月份, 四大廠商均宣布已經(jīng)于最新版本的瀏覽器中支持了 WebAssembly 的初始版本,這意味著 - WebAssembly 技術(shù)已經(jīng)實(shí)際落地
2019年,被正式加入Web的標(biāo)準(zhǔn)大家庭中
瀏覽器支持

由圖可見,無論是PC、移動端還是服務(wù)器,都已經(jīng)開始支持WebAssembly了,這也說明WebAssembly已經(jīng)開始普及~
2.什么是WebAssembly?
2.1 概念理解
在了解 WebAssembly 之前,讓我們先了解一下機(jī)器語言、匯編語言、高級語言
計(jì)算機(jī)語言分為三類
- 第一代 機(jī)器語言 (相當(dāng)于人類的原始階段)
- 第二代 匯編語言 (相當(dāng)于人類的手工業(yè)階段)
- 第三代 高級語言(相當(dāng)于人類的工業(yè)階段)
2.1.1 機(jī)器語言
機(jī)器語言(machine code)本質(zhì)上是由“0”和“1”組成的二進(jìn)制數(shù)。計(jì)算機(jī)發(fā)明之初,人們只能計(jì)算機(jī)的語言去命令計(jì)算機(jī)干這干那。向計(jì)算機(jī)每發(fā)出一條指令,就要寫出一串串由“0”和“1”組成的指令序列。

因此,使用機(jī)器語言是十分痛苦的,特別是在程序有錯(cuò)需要修改時(shí),更是如此。而且,由于每臺計(jì)算機(jī)的指令系統(tǒng)往往各不相同,所以,在一臺計(jì)算機(jī)上執(zhí)行的程序,要想在另一臺計(jì)算機(jī)上執(zhí)行,必須另編程序,需要進(jìn)行大量重復(fù)繁瑣的工作。
但在當(dāng)時(shí),由于使用的是針對特定型號計(jì)算機(jī)的語言,故而運(yùn)算效率是所有語言中最高的。機(jī)器語言,是第一代計(jì)算機(jī)語言。
- 機(jī)器語言的優(yōu)點(diǎn): 直接執(zhí)行,速度快,資源占用少
- 機(jī)器語言的缺點(diǎn):難讀、難編、難記、可移植性差和易出錯(cuò)
2.1.2 匯編語言
為了減輕使用機(jī)器語言編程的痛苦,人們進(jìn)行了一種有益的改進(jìn):用一些簡潔的英文字母、符號串來替代一個(gè)特定的指令的二進(jìn)制串。比如,用“ADD”代表加法。這樣我們很容易讀懂并理解程序在干什么,糾錯(cuò)及維護(hù)都變得方便了,這種程序設(shè)計(jì)語言就稱為匯編語言,即第二代計(jì)算機(jī)語言(Assembly)。
Assembly是一種低級編程語言,使用匯編器(Assembler)可以在一個(gè)進(jìn)程內(nèi)很方便地轉(zhuǎn)換成機(jī)器碼。

2.1.3 高級語言
不論是機(jī)器語言還是匯編語言都是面向硬件的具體操作的,語言對機(jī)器的過分依賴,要求使用者必須對硬件結(jié)構(gòu)及其工作原理都十分熟悉,這對非計(jì)算機(jī)專業(yè)人員是難以做到的,對于計(jì)算機(jī)的推廣應(yīng)用是不利的。人們意識到,應(yīng)該設(shè)計(jì)一種這樣的語言,這種語言接近于數(shù)學(xué)語言或人的自然語言,同時(shí)又不依賴于計(jì)算機(jī)硬件,編出的程序能在所有機(jī)器上通用。經(jīng)過努力,1954年,第一個(gè)完全脫離機(jī)器硬件的高級語言—FORTRAN問世了,40多年來,共有幾百種高級語言出現(xiàn),有重要意義的有幾十種。其中就包括C++、java等。
高級語言、匯編語言、機(jī)器語言之間的關(guān)系如下:

所有高級編程語言都會被自己的編譯器編譯,比如C++ 被編譯器轉(zhuǎn)換為匯編語言再被匯編器匯編成機(jī)器碼,以便在特定處理器,比如x86\x64\arm上運(yùn)行。不同的處理器架構(gòu)需要不同的機(jī)器代碼和不同的匯編語言(assembly)。

2.2 什么是 webAssembly
WebAssembly(縮寫為 Wasm)是一種基于棧式虛擬機(jī)的二進(jìn)制指令格式。Wasm 是一種底層類匯編語言,能在 Web 平臺上以趨近原生應(yīng)用的速度運(yùn)行。C/C++/Rust 等語言將 Wasm 作為編譯目標(biāo)語言,可以將已有的代碼移植到 Web 平臺中運(yùn)行,以提升代碼復(fù)用度。
咱們能夠從字面上理解,WebAssembly的名字帶個(gè)Assembly(匯編),因此咱們從其名字上就能知道其意思是給Web使用匯編語言,讓W(xué)eb執(zhí)行低級二進(jìn)制語法。而是如下圖,通過編譯器把高級別的語言(C,C++和Rust)編譯為WebAssembly,以便有機(jī)會在瀏覽器中運(yùn)行。能夠看出來它實(shí)際上是一種運(yùn)行機(jī)制,一種新的字節(jié)碼格式(.wasm),而不是新的語言。

WebAssembly 的特點(diǎn):
- 層次低,盡量接近機(jī)器語言,這樣解釋器才更容易進(jìn)行 AOT/JIT 編譯,以趨近原生應(yīng)用的速度運(yùn)行 Wasm 程序;
- 作為目標(biāo)代碼,由其他高級語言編譯器生成;
- 代碼安全可控,不能像真正的匯編語言那樣可以執(zhí)行任意操作;
- 代碼是平臺無關(guān)的(不能是平臺相關(guān)的機(jī)器碼),可以跨平臺執(zhí)行,采用了虛擬機(jī)/字節(jié)碼技術(shù)。
WebAssembly 目前已經(jīng)在瀏覽器端的圖像處理、音視頻處理、游戲、IDE、可視化、科學(xué)計(jì)算等
3. webAssembly是如何工作的?
工作原理:WebAssembly的工作原理簡要來說是:我們把C,C++, Rust等靜態(tài)語言的程序編譯成瀏覽器能夠運(yùn)行的wasm二進(jìn)制文件,當(dāng)瀏覽器下載 WebAssembly 代碼時(shí),可以快速將其轉(zhuǎn)換為任何本地機(jī)器碼后運(yùn)行。
設(shè)計(jì)WebAssembly的主要目標(biāo)之一是可移植性。 要在某個(gè)設(shè)備上運(yùn)行應(yīng)用程序,它必須兼容設(shè)備的處理器架構(gòu)和操作系統(tǒng)。這意味著要為支持的操作系統(tǒng)和CPU架構(gòu)的每個(gè)組合編譯源代碼。 使用 WebAssembly ,只需要一次編譯,您的應(yīng)用程序?qū)⒖梢栽诿總€(gè)現(xiàn)代瀏覽器中運(yùn)行。
3.1 LLVM(Low-Level-Virtural-Machine)編譯模型
我們知道大多數(shù)靜態(tài)高級語言是通過編譯器前端編譯成為中間代碼,然后再由編譯器后端把中間代碼翻譯成目標(biāo)機(jī)器的可執(zhí)行機(jī)器碼的。

3.2 WebAssembly LLVM
而webAssembly對應(yīng)的位置則是生成特定平臺機(jī)器碼之前,類似于一種匯編語言,但是它不對應(yīng)真實(shí)的物理機(jī)器,而是一種瀏覽器抽象成的虛擬處理器,比JavaScript源碼更直接地對應(yīng)機(jī)器碼。瀏覽器下載wasm文件后只需要做簡單的編譯生成特定機(jī)器的機(jī)器碼就能執(zhí)行。

如下圖,是WebAssembly的代碼范例 - 它具有易于閱讀的文本格式(.wat),但實(shí)際提供給瀏覽器的內(nèi)容是二進(jìn)制格式(.wasm)。

如下圖:WebAssembly 允許將 C ,C++ 或 Rust 代碼編譯成 WebAssembly 模塊,可以在Web 應(yīng)用中加載并通過JavaScript調(diào)用。它不是 JavaScript 的替代品,它將與JavaScript一起共存。

4. 為什么WebAssembly更快?
一說到WebAssembly,許多文章都會提及它的快,它的性能優(yōu)勢都是相對于JavaScript來說的,但是他為什么快呢?
4.1 體積小
WebAssembly的二進(jìn)制文件比 JavaScript 文本文件小得多。因而下載速度更快,這在網(wǎng)速低的時(shí)候尤為重要。
4.2 ### JIT原理
為了了解為什么WebAssembly有更好的性能,我們首先需要簡單過一下瀏覽器JIT的工作原理。
首先JavaScript引擎的工作就是把我們看得懂的編程語言轉(zhuǎn)換成機(jī)器能看懂的語言,我們與機(jī)器的溝通介質(zhì)就是JavaScript引擎,沒有這個(gè)翻譯官,我們下達(dá)的命令機(jī)器就沒法理解和執(zhí)行。

編程語言的翻譯有兩種方法:
- 使用解釋器
- 使用編譯器
解釋器是一部分一部分地邊解釋邊執(zhí)行。

編譯器是提前把源代碼整個(gè)編譯成目標(biāo)代碼,直接在支持目標(biāo)代碼的平臺上運(yùn)行,執(zhí)行過程不需要編譯器。

兩種方法各有利弊:
解釋器好處除了易于實(shí)現(xiàn)跨平臺外,最直接的就是對于我們前端開發(fā)人員來說,調(diào)試頁面時(shí),修改一行代碼可以立即看到結(jié)果,不需要等待編譯過程。但是對于同樣的代碼需要執(zhí)行多次的情況弊端就很明顯了,它需要執(zhí)行多次的解釋,就比如說執(zhí)行循環(huán)。
編譯器則可以在編譯的時(shí)候可以對這些重復(fù)的代碼等進(jìn)行優(yōu)化,使得執(zhí)行得更快。由于花了許多時(shí)間在提前優(yōu)化上,所以相對地需要犧牲的就是編譯代碼的時(shí)間。
在JIT出現(xiàn)以前,JavaScript大多都是從由JavaScript引擎解釋執(zhí)行的,因此效率低下,為了解決性能問題,其中一個(gè)辦法就是引入編譯器,將部分代碼優(yōu)化編譯,充分結(jié)合編譯器的優(yōu)勢,結(jié)合解釋器與即時(shí)編譯器(JIT)以提升性能早已在python等語言上得到很好的實(shí)踐證實(shí),所以瀏覽器廠商們就紛紛引入了JIT。
JIT的實(shí)現(xiàn)方法就是在JavaScript引擎中實(shí)現(xiàn)一個(gè)監(jiān)視器,這個(gè)監(jiān)視器監(jiān)視運(yùn)行的代碼,記錄下代碼各自的運(yùn)行次數(shù)并標(biāo)記它們的熱點(diǎn)類型。如果發(fā)現(xiàn)某一段代碼執(zhí)行了較多次,將標(biāo)記為’warm’,如果執(zhí)行了許多次,那么就會被標(biāo)記為’hot’

JIT會把標(biāo)記為warm的代碼送到基線編譯器(Baseline compiler)中編譯,并且存儲編譯結(jié)果,當(dāng)解釋器繼續(xù)解釋時(shí),監(jiān)視器發(fā)現(xiàn)了同樣的代碼,那么就會把剛才編譯好的結(jié)果推給瀏覽器,讓瀏覽器使用較快的編譯版本。

在基線編譯器中,代碼會進(jìn)行一定程度的優(yōu)化,但是由于優(yōu)化需要時(shí)間,代碼只是warm狀態(tài),所以基線編譯器并不會花太長時(shí)間去優(yōu)化。
不過如果標(biāo)記為warm的代碼執(zhí)行了更多次呢? 代碼已經(jīng)非常的hot了,這時(shí)花更多時(shí)間去優(yōu)化它就非常有必要了,所以監(jiān)視器會將這段代碼放到優(yōu)化編譯器(Optimizing compiler)中,生成更加快速高效的代碼,這時(shí)如果監(jiān)視器發(fā)現(xiàn)了同樣的代碼,就會返回這個(gè)更加快的優(yōu)化編譯版本。

然而,由于Javascript是弱類型的語言,它的靈活性可能會導(dǎo)致在幾百個(gè)循環(huán)后某一次循環(huán)中這個(gè)對象少了某個(gè)屬性,那么JIT檢測到后會認(rèn)為這個(gè)優(yōu)化的編譯代碼不合理,會將這個(gè)編譯代碼丟棄掉,轉(zhuǎn)而使用基線編譯版本或者也可能直接回到解釋器。
這個(gè)過程叫做去優(yōu)化(JIT監(jiān)視到如果某段代碼進(jìn)行了幾次優(yōu)化到去優(yōu)化的循環(huán)后,會終止這段代碼的優(yōu)化編譯,防止無限的循環(huán),盡管如此,這里的性能損耗仍不可忽視)

舉個(gè)簡單的例子,對于以下這段循環(huán)代碼,在基線編譯器階段,每一行代碼會被基線編譯器編譯成代碼樁
function arraySum (arr) {
let sum = 0
for (let i = 0; i < arr.length; i++) {
sum += arr[i]
}
}
但是對于累加sum += arr[i]這一句代碼,我們并沒有確定sum和arr是什么類型,如果是數(shù)字,基線編譯器會生成一個(gè)樁(stub),如果是字符串,它也會為它生成一個(gè)樁,也就是說在調(diào)用過程中,可能有一個(gè)以上的樁,這時(shí)瀏覽器再次執(zhí)行這段代碼時(shí),需要進(jìn)行多次分支選擇,進(jìn)行類型檢查。

所以,如果類型不固定,使用的是基線編譯版本,每次循環(huán)都要進(jìn)行一次類型檢查。

如果類型固定,使用的是優(yōu)化編譯器后的版本,在循環(huán)之前就進(jìn)行一次類型檢查就可以了,速度提升是相當(dāng)顯著的。

因此,使用JavaScript在靈活與興能上存在一定的折中關(guān)系,享受靈活的同時(shí)必然有一定的性能損耗。
總的來說通過JIT編譯器,JavaScript的性能有了很大的提升,通過使用基線編譯版本或者優(yōu)化編譯版本,能夠大大減少解釋器的時(shí)間損耗。但是JIT也有一定的瓶頸,主要體現(xiàn)在:
JIT 編譯器花了很多時(shí)間在猜測 Javascript 中的類型。
- 在優(yōu)化和去優(yōu)化過程中造成了很大開銷
- 而使用WebAssembly的出現(xiàn)的原因之一,就是為了消除這些開銷。
3.3 JIT與WebAssembly的時(shí)間耗時(shí)對比

我們來看看運(yùn)行一段JavaScript代碼時(shí),JavaScript引擎所花的時(shí)間的大概分布
- parse : 解釋。對JavaScript源碼進(jìn)行解釋,生成抽象語法樹或者字節(jié)碼,傳遞給解釋器。
- compile + optimize : 編譯+優(yōu)化。解釋器生成字節(jié)碼,并通過編譯器(JIT)編譯優(yōu)化部分字節(jié)碼,生成機(jī)器碼。
- re-optimize : 發(fā)生去優(yōu)化時(shí),重新優(yōu)化所花的時(shí)間。
- execute : 執(zhí)行代碼的過程。
- garbage collection: 清理內(nèi)存的時(shí)間。
需要注意的是,這幾個(gè)部分的工作在線程中是交替進(jìn)行的,一段代碼中某一部分可能在解釋、然后某一部分可能在去優(yōu)化、然后某一部分可能在執(zhí)行。這里的圖的順序只是為了方便描述。
而需要提及的是,過去沒有JIT時(shí),JavaScript的執(zhí)行時(shí)間需要更多的時(shí)間,就如同下圖所示:

那么對于執(zhí)行一段相同功能的WebAssembly代碼的時(shí)間分布大概是怎樣的呢?讓我們逐步比對一下。
WebAssembly本身就以二進(jìn)制形式提供,解析速度更快。它是靜態(tài)類型的,因此與JavaScript不同,引擎在編譯期間不需要類型推斷。大多數(shù)優(yōu)化都是在編譯源代碼期間,在瀏覽器執(zhí)行之前進(jìn)行的。內(nèi)存是手動管理的,就像 C 和 C++ 這樣的語言一樣,所以也沒有垃圾收集。所有這些都是為了提供了更好,更可靠的性能。
正是因?yàn)閃asm的大部分優(yōu)化工作已經(jīng)在LLVM的前端部分完成了,所以編譯優(yōu)化的工作很少,這便是其高性能的主要體現(xiàn)。
WebAssembly代碼在瀏覽器中的執(zhí)行過程:
- 解碼
- 編譯
- 執(zhí)行

WebAssembly就只有解碼、編譯優(yōu)化和執(zhí)行這三部分開銷,對比原生性能開銷減少了許多

5. WebAssembly實(shí)戰(zhàn)演練
WebAssembly是一個(gè)具有WASM擴(kuò)展名的文件,可以把它看作一個(gè)可以導(dǎo)入JavaScript程序的模塊。那么如何生成WASM文件呢?
5.1 emscripten 安裝與使用, 讓C語言出現(xiàn)在前端
官方推薦方式,先下載 emsdk:
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
# 下載并安裝最新的 SDK 工具.
./emsdk install latest
# 為當(dāng)前用戶激活最新的 SDK. (寫入 .emscripten 配置文件)
./emsdk activate latest
# 激活當(dāng)前 PATH 環(huán)境變量
source ./emsdk_env.sh
驗(yàn)證
emcc -v 不報(bào)錯(cuò)就成功了
編譯
接下來就可以編譯代碼啦。
來個(gè)萬年不變的Hello world試試:
- 在emsdk文件夾下創(chuàng)建test.c文件
#include<stdio.h>
void main(){
printf("Hello world!");
}
- 使用剛才已經(jīng)配置過的終端,找到test.c文件,執(zhí)行以下命令
emcc ./test.c -s WASM=1 -o ./test.html
emcc 是Emscripten編譯器行命令
test.c 是咱們的輸入文件
-s WASM=1 指定咱們想要的wasm輸出形式。若是咱們不指定這個(gè)選項(xiàng),Emscripten默認(rèn)將只會生成asm.js。(可參考 emcc --help 參數(shù)說明)
-o ./test.html 指定這個(gè)選項(xiàng)將會生成HTML頁面來運(yùn)行咱們的代碼,而且會生成wasm模塊,以及編譯和實(shí)例化wasm模塊所須要的“膠水”js代碼,這樣咱們就能夠直接在web環(huán)境中使用了。
執(zhí)行后會產(chǎn)生三個(gè)新文件:test.wasm 二進(jìn)制的wasm模塊代碼,雖然本地打不開,可是瀏覽器能夠幫忙翻譯。
test.js 一個(gè)包含了用來在原生C函數(shù)和JavaScript/wasm之間轉(zhuǎn)換的膠水代碼的JavaScript文件
test.html 一個(gè)用來加載,編譯,實(shí)例化你的wasm代碼而且將它輸出在瀏覽器顯示上的一個(gè)HTML文件
啟動http服務(wù)命令,查看運(yùn)行結(jié)果
emrun --no_browser --port 8080 ./test.html
5.2 webAssembly 參與頁面中大計(jì)算量功能demo
- 修改test.c中的C語言代碼為斐波那契數(shù)列:
#include <stdio.h>
int fib(int n)
{
if (n <= 1)
return n;
return fib(n-1) + fib(n-2);
}
- 編譯生成wasm文件
emcc ./test.c -Os -s WASM=1 -s SIDE_MODULE=1 -o ./test.wasm
- emcc就是Emscripten編譯器,
- test.c是咱們的輸入文件
- -Os表示此次編譯須要優(yōu)化(能夠指定優(yōu)化策略。emcc --help)
- -s 后緊跟編譯的配置(setting)WASM=1表示輸出wasm的文件,由于默認(rèn)的是輸出asm.js
- -s 后緊跟編譯的配置(setting)SIDE_MODULE=1表示就只要這一個(gè)模塊,不要給我其余亂七八糟的代碼
- -o 表示輸出的文件,test.wasm是咱們的輸出文件。
所有配置項(xiàng)可以在這里查看。
- 書寫頁面demo
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<title>Page Title</title>
<meta name='viewport' content='width=device-width, initial-scale=1'>
</head>
<body>
<script>
// 斐波那契數(shù)列
let fib;
function fibjs(n) {
if (n <= 1)
return n;
return fibjs(n - 1) + fibjs(n - 2);
}
function loadWebAssembly(path, imports = {}) {
return fetch(path) // 加載文件
.then(response => response.arrayBuffer()) // 轉(zhuǎn)成 ArrayBuffer
.then(buffer => WebAssembly.compile(buffer))
.then(module => {
return new WebAssembly.Instance(module);
})
}
loadWebAssembly('./test.wasm')
.then(instance => {
fib = instance.exports.fib;
});
function perforweb(n){
console.log('webAssembly demo 開始');
let startTime = performance.now();
let c = fib(n);
let endTime = performance.now();
console.log(`webAssembly耗時(shí) ${endTime - startTime} 毫秒,最終結(jié)果為 ${c}`);
return true;
}
function perforjs(n){
console.log('javascript demo 開始');
let startTime = performance.now();
let j= fibjs(n);
let endTime = performance.now();
console.log(`javascript耗時(shí) ${endTime - startTime} 毫秒, 最終結(jié)果為 ${j}`);
}
</script>
</body>
</html>
- 安裝live-server,本地起服務(wù)
npm install -g live-server
-
瀏覽器控制臺中分別執(zhí)行2個(gè)函數(shù)
image.png
可以看到執(zhí)行時(shí)間的對比,webAssembly在計(jì)算大數(shù)據(jù)量,存在大量遞歸,遍歷的操作時(shí),性能提升非常明顯。




