# How Browser Works

1. Navigation

1.1 DNS Lookup

導航的第一步就是 尋找資源的定位,例如當你訪問 https://example.com , 該 HTML 頁面被 serve 在IP地址爲 93.184.216.34 的服務器上。 如果你首次訪問這個站點,那麼必定會發生一次 DNS 查找。

你的瀏覽器,請求一次 DNS 查找,最終由 域名服務器 返回一個 IP 地址。 初次訪問之後,該IP 會被緩存一段時間,這樣避免了重複 DNS 查找, 加速了後續的子請求時間。

通常 DNS 查找針對一個頁面只會執行一次, 但是如果你的頁面中有 其他的三方域名所引用的資源, 那麼會多次查找。

latency

所有當頁面中三方引用資源過多的時候, 移動端在網絡不好的時候,可能會由於過多的 DNS 查找導致網頁加載很慢。

1.2 TCP Handshake

一旦知道了 IP 地址,瀏覽器將會通過 TCP 三次握手以在 目標服務器客戶端瀏覽器 之間建立一個連接。

TCP 三次握手的過程以 SYN , SYN-ACK , ACK 標識,這三次消息用於協商並在兩臺計算機之間啓動TCP會話。

1.3 TLS Negotiation#協商

對於建立在 HTTPS 上的安全連接, 還需要 另一次 “握手”。 這個握手更確切的說是 TLS 協商 #TLS negotiation,決定了後續的通信採取什麼加密算法進行加密,以及服務器驗證,並在真正傳輸數據前建立安全連接。 這意味着,在獲取網頁內容的請求真正被髮送前,還需要三輪往返協商。

ssl

TSL 協商以使連接安全,自然也會延遲頁面加載的時間,但爲了安全的,這些時間上的開銷是值得的,因爲在瀏覽器和web服務器之間傳輸的數據無法被第三方解密。

2. Response

一旦和 Web 服務器建立了連接,瀏覽器會發起一個 初始化 HTTP GET 請求#Initial HTTP GET Request。

對於網站來說,通常是請求一個 HTML 文件

一旦服務器接收到了請求,它會返回相關的響應頭以及 HTML 的內容。

<!doctype HTML>
<html>
 <head>
  <meta charset="UTF-8"/>
  <title>My simple page</title>
  <link rel="stylesheet" src="styles.css"/>
  <script src="myscript.js"></script>
</head>
<body>
  <h1 class="heading">My Page</h1>
  <p>A paragraph with a <a href="https://example.com/about">link</a></p>
  <div>
    <img src="myimage.jpg" alt="image description"/>
  </div>
  <script src="anotherscript.js"></script>
</body>
</html>

初始化請求 的響應包含了接收到數據的第一個字節,Time to First Byte (TTFB) 是用戶請求發起,到接收到 HTML 數據第一個數據包的時間。 通常這個內容的 chunk 爲 14KB。

在上方的示例中,請求肯定遠小於14KB, 但是被鏈接的資源直到瀏覽器在解析過程中遇到鏈接時才被請求,如下所述。

2.1 TCP Slow Start / 14KB rule

首個響應包的大小將會是 14KB, 這是 TCP slow start 的一部分。 它是一個衡量網速的一個算法, Slow start 會使得後續數據包逐漸增加傳送的數據量,直到能達到服務器能夠承受的最大網絡帶寬。

congestioncontrol

這種 TCP 慢起始#slow start 的機制逐漸增加數據傳送量的算法,能夠避免網絡擁阻。

2.2 Congestion control#擁阻控制

當服務器以TCP數據包的形式發送數據時,用戶的客戶端通過返回確認信息(ack)來確認數據的傳遞。根據硬件和網絡條件,連接的容量有限。如果服務器以太快的速度發送太多的包,它們將被丟棄。這也就意味着,沒有ack。服務器將其註冊爲缺少ack。擁塞控制算法使用發送數據包流和ack來確定發送速率。

關於 TCP Slow Start 可以參看下方 附屬內容1

3. Parsing

一旦瀏覽器接收到了數據的首個 chunk, 它就可以開始解析接收到的信息。 解析就是將接收的數據轉化爲 DOM 和 CSSOM 的過程, 被 renderer 用於在屏幕上繪製頁面。

即使請求頁面的 HTML 遠大於首個 14KB 的數據包,瀏覽器還是會基於已有的數據,嘗試解析數據,和嘗試渲染。 這也是爲什麼對於web性能優化來說,在開始渲染一個頁面時或者至少是一個頁面模板,包含一切瀏覽器需要的東西是非常重要的。 也就是說在首個 14KB 大小的數據包中需要包含首屏渲染的 CSS 和 HTML.

但是在屏幕渲染任何東西之前,HTML, CSS 和 JavaScript 必須先被解析 。

3.1 Building the DOM tree

我們在 critical rendering path 中描述了五個步驟。

critical rendering path第一步,處理 HTML 標記,並構建 DOM 樹。 HTML 解析包含了 tokenization and tree construction.
HTML tokens 包括了開始和結束標籤,同時還有 屬性名,屬性值。如果 document 編寫的比較好,那麼解析也會更快一些。

DOM 樹描述了文檔的內容,<html> 元素是文檔樹的第一個根節點,樹描述了 不同標籤的關係以及嵌套層級。 DOM 的節點越多,DOM 樹的構建就越耗時。

dom

當解析器發現了一個非塊級元素資源,例如 圖片,瀏覽器就會請求這些資源,並繼續解析。 當遇到CSS 文件的時候,可以繼續解析但是 <script> 標籤——尤其是哪些沒有 async 或者 defer 屬性的標籤,將會阻塞頁面的渲染,並暫停HTML 的解析。 儘管 瀏覽器的 preload scanner#預加載掃描 加速了這一過程,但是過多的腳本仍然會導致瓶頸。

3.2 Preload scanner

當瀏覽器構建DOM 樹的時候,這個過程將會佔用主線程, 這時候, preload scanner 將會解析可用的內容,提高 CSS, JavaScript, 字體等資源的請求優先級。多虧了 preload scanner, 我們纔不用必須等到解析器找到外部資源的引用才請求它,它將會在後臺檢索資源,這樣,當主HTML解析器到達所請求的資源時,它們可能已經在運行中了,或者已經被下載。 預加載掃描器提供的優化,減少了頁面阻塞。

<link rel="stylesheet" src="styles.css"/>
<script src="myscript.js" async></script>
<img src="myimage.jpg" alt="image description"/>
<script src="anotherscript.js" async></script>

在這個示例中,當主線程 正在 解析 HTML 和 CSS 時, preload scanner 將會尋找 scripts 和 圖片,並開始下載它們,爲了確保腳本不會阻塞該過程,可以給<script> 添加 async 屬性, 或者如果 JavaScript 的解析和執行的順序很重要時,則添加 defer 屬性。

等待接收 CSS 並不會阻塞 HTML 的解析或者下載,但是他會阻塞 JavaScript, 因爲 JavaScript 經常用於查詢應用於元素的CSS 屬性。

3.3 Building the CSSOM

critical rendering path第二步,就是處理 CSS 並構建 CSSOM 樹(CSS 對象模型樹), 它和 DOM 很相似, DOM 和 CSSOM 都是樹。 它們是單獨的數據結構, 瀏覽器將 CSS 規則轉變爲它可以理解並處理的樣式 map。 瀏覽器遍歷 CSS 中的每個規則集, 基於CSS 選擇器,創建一個有父親,孩子,以及兄弟關係的節點樹。

與 HTML 一樣,瀏覽器需要將接收到的 CSS 規則轉換爲它可以處理的內容, 因此,它重複 html 到對象的過程, 但是針對的是 CSS。

CSSOM 樹包含了用戶端樣式表的樣式。 瀏覽器從適用於節點的最通用規則開始,並通過應用更加具體的規則,遞歸的完成計算樣式。 換句話說,它層疊了屬性值。

構建 CSSOM 是非常非常快的,並且在當前的開發工具中不會以一種獨特的顏色顯示。 相反, 開發工具中的 "重新計算樣式#Recalculate Style" 顯示瞭解析CSS、 構造CSSOM樹和遞歸計算樣式所需的總時間。 在web性能優化方面,有一些更容易實現的目標,因爲創建CSSOM 的總時間通常小於一次 DNS 查找的時間。

4. Other Processes

4.1 JavaScript Compilation

當 CSS 正被解析且 CSSOM 正被創建, 其他的靜態資源, 包括 JavaScript 文件,也在下載(感謝 preload scanner)。 JavaScript 被解釋#interpreted,編譯#compiled,解析#parsed,並執行#executed。這些腳本被解析稱爲 AST(abstract syntax trees#抽象語法樹)。 有一些瀏覽器引擎,將AST傳遞給 interpreter, 輸出主線程執行的字節碼。 這也通常稱作 JavaScript 編譯。

4.2 Building the Accessibility Tree

瀏覽器也會構建 accessibility tree,用於輔助設備解析和解釋內容。 AOM(accessibility object model#輔助對象模型) 很像 DOM 的語義化版本。當DOM 更新時, 瀏覽器也會更新 accessibility 樹。 而輔助技術設備不能夠修改 accessibility 樹。

屏幕閱讀器不能夠被訪問,知道 AOM 被構建。

5. Render

Rendering 步驟 包括了 樣式,佈局,繪製,在某些情況下還包括合成#compositing。 在解析#parsing 步驟所創建的CSSOM 和 DOM 樹被組合#combined 成一個 渲染樹#render tree, 然後計算所有可見元素的佈局,接着,將其繪製到屏幕。 在某些情況下,內容可以被提升#promoted 到它們自己的層並被合併#composited, 通過在GPU上而不是CPU上繪製屏幕的部分內容來提高性能,釋放主線程。

5.1 Style

critical rendering path第三步 是結合 DOM 和 CSSOM 到一個渲染樹。 計算樣式樹(或者渲染樹) 以DOM 樹的 根#root 作爲結構的起點,遍歷所有的可見節點。

標籤不會被展示,像<head>,以及它的子節點,還有所有css 屬性爲display:none 的任意節點。 但是屬性爲visibility:hidden 的節點會被包含在 渲染樹中 。因爲它們會佔據空間。

每個可見的節點都有自己的 CSSOM 應用到自身。 渲染樹 hold 了所有的可見節點的內容以及計算樣式 ——將所有相關樣式匹配到DOM樹中的每個可見節點,並根據CSS級聯 #cascade 確定每個節點的計算樣式。

5.2 Layout

critical rendering path第四步 是在渲染樹上運行佈局,以計算每個節點的幾何形狀。

Layout #佈局, 是確定渲染樹中所有節點的寬度、高度和位置,以及確定頁面上每個對象的大小和位置的過程。

Reflow #迴流,是對頁面或整個文檔的任何部分的後續大小和位置的確定。

一旦渲染樹被建立,佈局開始。渲染樹標識了要顯示哪些節點(即使不可見),以及它們的計算樣式,但沒有標識每個節點的尺寸或位置。爲了確定每個對象的確切大小和位置,瀏覽器從渲染樹的根開始並遍歷它。

在網頁上,幾乎所有東西都是一個box。不同的設備和不同的桌面偏好意味着無限數量的不同視口大小。在這個階段,瀏覽器將可視區域的大小作爲基礎,決定所有不同box在屏幕上顯示的尺寸。佈局通常從主體開始,佈局主體的所有後代的尺寸,每個元素的box model屬性,爲被替換的不知道尺寸的元素提供佔位符空間,例如我們的圖像。

第一次確定節點的大小和位置稱爲layout。隨後重新計算節點大小和位置稱爲reflows。在我們的例子中,假設初始佈局發生在圖像返回之前。因爲我們沒有聲明圖像的大小,所以一旦知道圖像的大小,就會有一個reflow。

5.3 Paint

critical rendering path最後一個步驟,就是將單個節點繪製到屏幕, 首次繪製的時刻,被稱作爲 first meaningful paint 。In the painting or rasterization phase #在揮着或者柵格化階段,瀏覽器將 layout 階段計算的所有 box 轉化爲屏幕顯示的實際像素。 繪製包括了將一個元素的所有可見部分繪製到屏幕的過程, 包括了text,color,border, shadow, 以及替換元素如 buttons, images。 瀏覽器需要非常快的去做這些。

爲了確保流暢的滾動和動畫, 所有佔用了主線程的事情,例如樣式計算,以及 reflow#迴流 和 paint繪製,必須在16.67ms 內去完成。在2048 X 1536的分辨率下,iPad的屏幕可以顯示超過3145,000像素。這是大量的像素,需要快速繪製。爲了確保重新繪製#repainting 比初始繪製更快,屏幕上的繪圖通常被分解成幾個圖層。如果出現這種情況,那麼就需要合成。

繪製#Painting 能夠將layout tree#佈局樹種的元素分解到不同的圖層,在GPU(而不是CPU的主線程)上提升內容的層次,可以提高 paint 和 repaint 的性能。有特定的屬性和元素來實例化一個層,包括 <video><canvas> ,以及任何具有CSS屬性不透明度、3D轉換、將會改變的元素,以及其他一些。這些節點將被繪製到它們自己的層上,以及它們的後代層上,除非後代層由於上述一個(或多個)原因需要它自己的層。

層確實可以提高性能,但在內存管理方面代價高昂,所以不應該作爲web性能優化策略的一部分過度使用。

5.4 Compositing

當文檔的不同部分#sections 在不同的圖層被繪製的時候,相互重疊,合成是必要的, 以確保它們以正確的順序繪製到屏幕上,並正確的呈現內容。

當頁面繼續加載資產時,可能會發生reflows(回想一下我們的例子中遲到的圖片)。迴流觸發 repaint 和 re-composite 。如果我們定義了圖像的大小,就沒有必要 reflow 了,只有需要重新繪製的圖層纔會重新繪製#repainted,如果需要的話就進行合成。但我們沒有包括圖像大小! 從服務器獲得圖像後,呈現過程回到佈局步驟並從那裏重新啓動。

6. Interactivity

一旦主線程完成了頁面的繪製,您可能會認爲我們就“萬事俱備”了。事實並非如此。如果加載包含了JavaScript,並且它被正確地延遲#was correctly deferred了,並且只在onload事件觸發後執行,那麼主線程可能會很忙,無法進行滾動、觸摸和其他交互。

Time to Interactive (TTI) 用於度量從首次請求,到DNS 查找 在到 SSL 連接,直到頁面可交互這段時間的耗時。—— interactive是指在第一次內容繪製# First Contentful Paint 之後,頁面在50ms內響應用戶交互的時間點。如果主線程被解析、編譯和執行JavaScript佔用,它就不可用,因此無法及時(小於50ms)響應用戶交互。

在我們的例子中,可能圖片加載得很快,但可能另一個script.js文件是2MB,我們的用戶的網絡連接很慢。在這種情況下,用戶可以非常快地看到頁面,但在沒有jank的情況下,用戶無法滾動,直到腳本被下載、解析和執行。這不是一個好的用戶體驗。避免佔用主線程,如本WebPageTest示例所示

visa_network

在本例中,DOM內容加載過程耗時超過1.5秒,主線程在這段時間內被完全佔用,對點擊事件或屏幕點擊沒有響應。

translate @from link.

如果你對瀏覽器如何工作有進一步瞭解的需求,可以參看這裏 :How Browsers Work: Behind the scenes of modern web browsers

總結

瀏覽器如何工作的?

換句話說,瀏覽器是怎麼展示網頁的?

其大致的階段可以分爲四個大的步驟

graph LR subgraph Step1 A[用戶訪問URL] end subgraph Step2 B[向目標服務器發起資源請求] end subgraph Step3 C[接受到服務器的響應] end subgraph Step4 D[解析響應數據渲染到屏幕] end A --> B --> C --> D

而這個過程,還可以被細化:

例如 Step1 :

graph TB subgraph Step1.用戶訪問URL a1[Step1.1 用戶通過地址欄或者url 訪問域名地址] a2[Step1.2 瀏覽器攜帶域名地址向域名服務器發起 DNS 查找] a3[Step1.3 瀏覽器接收到域名服務器返回的實際目標服務器ip] a4[Step1.4 瀏覽器嘗試和目標ip服務器三次握手建立連接] a5[Step1.5 如果目標服務器是HTTPS,那麼還需要進行 TSL/SSL 協商纔會建立連接] a1 --> a2 --> a3 --> a4 --> a5 end

而 Step4, 可以被 Critical Render Path (CRP ) 這個概念所總結:

graph LR subgraph Critical Render Path 關鍵步驟 s1[DOM 構建] --> s2[CSSOM 構建] --> s3[Render Tree] --> s4[Layout] --> s5[Paint] end

關於更過 CRP 部分,可以見下方的 附屬內容2

附屬內容1 :

TCP Slow Start

關於TCP Slow Start, 我們可以通過抓包工具 wireshark,結合 curl 命令行工具,看一看大概是怎麼回事。

爲什麼要用curl ?

因爲我們只想要看一次HTTP請求的過程, 網頁中的資源鏈接,在瀏覽器接收到HTML 文件,解析的時候纔會開始發起請求,也叫做subrequest

curl 是命令行工具,並不是瀏覽器,所以不會去解析HTML,因此也就不會產生subrequest 了。

我的本地環境是wsl,wireshark 有專門的監聽通道,點擊 “開始捕獲分組” 後,命令行:

$  curl https://www.cnblogs.com/jaycethanks/

然後暫停 捕獲, 統計 -- TCP流圖形 -- 時間序列(Stevens) , 類型 -- 吞吐量,就可以看到了:

image-20220525225206478

可以看到,隨着時間的增加,吞吐量逐漸增大, 在整個數據從服務器發送到客戶端,並不是一次傳完的,而是分爲了很多個 Segment, 每個 Segment 都是一個"測試", 測試網絡條件, 每次接受到數據, 客戶端都會響應一個 ack,服務器會根據ack決定下一次 Segment 的大小。 這樣做的目的能夠降低網絡擁阻。 他是 TCP 協議的一個特性。

附屬內容2

Critical rendering path

Critical Render Path (關鍵渲染路徑,下文簡稱 CRP) 就是瀏覽器,將HTML,CSS,JavaScript轉換成屏幕上像素的一系列有序的步驟。

關鍵渲染路徑 包括了 DOM (Document Object Model), CSSOM(CSS Object Model), Render 樹,以及Layout。

DOM 在 HTML 被解析的時候被創建。 HTML 可能會請求 JavaScript,而這些請求反過來又有可能會改變 DOM。 HTML 中還有可能含有 樣式內容,或者會請求外部樣式表,這些樣式則會被構建成 CSSOM。

瀏覽器引擎,將 DOM + CSSOM 結合在一起變作 Render Tree. 而 Layout 決定了頁面上一切元素的大小和位置, 一旦佈局過程完成,緊接着就是將這些元素逐像素繪製到屏幕。

優化 CRP 將提升渲染性能,進而縮短首次渲染的時間。 因此理解並優化 CRP 對於

  1. 保證 reflows 和 repaints 能夠以60幀的頻率執行;
  2. 確保用戶交互性能
  3. 避免 jank

來說,是關鍵。

Understanding CRP

Web 性能包括了 :服務器請求與響應、loading、scripting、rendering、layout、painting。

  1. 一個網頁渲染,以一次 HTML 請求開始;
  2. 服務器返回 HTML —— 響應 header + data;
  3. 瀏覽器開始解析 HTML, 將接收的字節數據,轉化爲 DOM tree;
    1. 當在解析過程中,每當瀏覽器發現了有鏈接到外部的資源,如 樣式表,js腳本文件,或者內嵌的圖片,就會發起請求。有些請求會阻塞,這就意味着,餘下的 HTML 將暫停解析,直到請求的資源被處理完。這個 解析HTML + 請求資源 + 構建DOM 的過程。直到最後瀏覽器構建了 CSSOM。
    2. 當 DOM + CSSOM 構建完成,瀏覽器會將二者結合爲 Render Tree, 計算所有可見內容的樣式。
    3. Render Tree 構建完成,就開始執行 layout,即佈局步驟, 這期間,將會定義 Render Tree 上每個節點元素的大小和位置。 這個過程類似在構建 "藍圖"。
    4. 一但 佈局 完成,瀏覽器就會將,所有元素節點繪製到 屏幕。

1. Document Object Model (DOM)

注意: DOM 結構式漸進式的。 HTML 響應 -- tokens -- nodes -- DOM Tree。 單個 DOM 節點以 startTag token 開始, 以 endTag token 結束。節點包含了 HTML 元素所有相關的信息。 這些信息以tokens 描述。 Nodes 被關聯到 一個基於 token 層級的 DOM tree,即如果 一組 startTag 和 endTag tokens 被嵌置於 另一組 startTag 和 endTags,那麼就會得到一個嵌套的 Node, 這就是 DOM tree 的層級結構如何被定義的。

節點的數量越多,那麼 CRP 接下來的處理就會越耗時,也就意味着性能會受影響。

2. CSS Onject Model (CSSOM)

DOM 包含了頁面的所有內容, CSSOM則包含了DOM 所有的樣式。 CSSOM 和 DOM 很近似,但是又不同。 DOM是漸進式的, 但是 CSSOM 不是。 CSS 阻塞渲染(#css is render blocking) : 瀏覽器阻塞頁面渲染直到接收並處理完了所有的 CSS。 而之所以CSS 會阻塞渲染,是因爲 css 規則會被覆寫, 因此 CSSOM還沒有 完成之前,內容無法被正確渲染。

CSS 有它自己的一套規則去識別有效的 tokens。 別忘了 "CSS" 中的 "C" 表示 "Cascade(#級聯)" 。當解析器將 tokens 轉化爲 nodes 的時候,後代節點將會繼承父節點的部分樣式, 所以HTML 的漸進(#incremental) 特性不會應用於 CSS。 CSSOM 在 CSS 被解析的時候得以創建, 但是直到 CSS 被完全解析完成之前,還無法構建 Render Tree。 因爲後續的解析可能會覆蓋之前解析的樣式結果,被覆蓋掉的樣式不應該被渲染到屏幕。

當談及 樣式選擇器的性能, 更少的選擇器規則會比更復雜的快。 例如, .foo{} 會比 .bar .foo{} 要快。 因爲,當瀏覽器找到了 .foo , 它還要去 DOM 中檢查 .foo 是不是有一個名爲 .bar 的祖先。

不過,如果你去測量 CSS 的解析時間,你會發現起始瀏覽器在CSS 的解析上的處理是非常快的。雖然 更多的選擇器規則,意味着瀏覽器需要在 DOM tree 中遍歷更多的 節點,但是這個開銷通常是很小的。 所以針對選擇器的優化通常是毫秒級別的提升。

這裏有一些其他的 CSS 優化,ways to optimize CSS

3. Render Tree

Render Tree 是 DOM + CSSOM 的結合,瀏覽器會檢查每個節點, 從 DOM tree 的 root 開始,並檢測附着了哪些 CSS 樣式規則。

Render Tree 只會捕獲 可見的內容 (visibility:hidden會被捕獲,但是display:none 不會)。

通常<head> 標籤中的信息都是不可見的。 因此display:none 所應用的節點及其後代節點,和沒有可見內容的<head> 元素,都不會被捕獲進 Render Tree。

4. Layout

一旦渲染樹被構建, 就可以進行 佈局(#layout) 了。 佈局是基於 屏幕的尺寸,佈局步驟決定了元素該被如何定位,元素的寬高如何被定義,以及它們是如何相互關聯的。

什麼是元素的 width ? 塊級元素,默認的有一個 100% 的寬度定義

viewport 元數據標籤,定義了 佈局的視圖區域寬度, 他將影響佈局,如果沒有定義,瀏覽器將會使用默認的寬度。

節點的數量越多,也就意味佈局耗時會越多,

爲了減少佈局事件的頻率和持續時間,請批處理更新並避免使用box屬性去實現動畫。

5. Paint

這是 CRP 的最後一步,將像素點繪製到屏幕。 一旦渲染樹被創建,開始佈局,像素就可以被繪製到屏幕上。 在 window.onload 之後,整個屏幕已經被繪製完成。 因爲瀏覽器通常被優化爲最小區域重繪(#repaint),因此在 load 之後,僅屏幕上被影響的區域 會被 repainted 。

Optimizing for CRP

提高頁面的加載速度可以通過以下tips :

  1. 通過推遲(#defer) / 刪除非關鍵資源的加載,以縮減關鍵資源的數量。
  2. 優化請求資源的大小
  3. 通過優化關鍵資源的下載順序,從而縮短 the critical path length

transate @from https://developer.mozilla.org/en-US/docs/Web/Performance/Critical_rendering_path

References

  1. https://developer.mozilla.org/en-US/docs/Web/Performance/How_browsers_work#tls_negotiation
  2. https://developer.mozilla.org/en-US/docs/Web/Performance/Critical_rendering_path
  3. https://www.html5rocks.com/en/tutorials/internals/howbrowserswork/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章