整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          什么是flash什么是html5?

          今,HTML5 可謂如眾星捧月一般,受到許多業內巨頭的青睞。且不說谷歌、蘋果等業內巨頭把它描繪為互聯網體驗的未來,即便是以不服從標準著稱的微軟,也向它頻頻示好, 決心在 Internet Explorer 9 中加入對 HTML5 的大量支持。然而,HTML5的路途真的將一帆風順么?本文將列舉了一些在HTML5發展和普及過程中需要解決的問題。

             什么是HTML5?

             根據維基百科上的解釋,HTML5 的前身是 Web Applications 1.0,由 WHATWG 于2004年提出。2007年,它被 W3C 接納,并于2008年1月22日發布第一份正式草案。

             以下摘錄自維基百科的文字介紹了 HTML5 的特點和與 HTML4 的差別:

          新應用程序接口(API)
          即時二維繪圖
          定時媒體播放
          儲存
          離線
          編輯
          拖放
          通訊/網絡
          后退按鈕管理
          MIME 和協議處理程序時表頭登記

             與 HTML 4 的不同之處

          新的解析順序
          新的元素:section, video, progress, nav, meter, time, aside, canvas
          input 元素的新屬性:日期和時間,email, url
          新的通用屬性:ping, charset, async
          全域屬性:id, tabindex, repeat
          移除元素:center, font, u, strike, s, frameset, frame, applet

             戰勝 Flash, HTML5 還需要什么?

             決定勝負的因素很多,在此分為兩部分分析。

             一、技術方面

             HTML5 與 Flash 在功能上并不是完全重疊的,比如對于攝像頭等計算機硬件的調用,仍然只能使用 Flash 或其他方法實現。但是 HTML5 卻引入了一些讓 Flash 不得不認真對待的元素。其中最為人所知的和最重要的即為 canvas 和 video 標簽。在我看來,其他的新屬性跟 Flash 的競爭關系很弱,只有此二者是真真正正要搶 Flash 的飯碗,而且一旦普及,將深刻的改變整個互聯網,可謂是 HTML5 的左膀右臂。

             canvas

             在 Flash 流行之前,曾經出現過很多種在網頁中實現繪圖功能的方法,其中包括著名的Java Applet 。這些方法各有千秋,相互競爭不休。但是在 Macromedia 公司推出 Flash 之后,這場戰爭很快就結束了。

             為了對抗 Flash,又有很多新的技術被提出,其中就包括現在 Flash 的東家 Adobe 提出的 SVG。然而這沒有能夠阻止 Flash 迅速地被網民接受。基于 Flash 的動畫、游戲等應用幾乎是在一夜之間蔓延到了互聯網的每個角落。

             而今,新的挑戰者出現了,他就是 HTML5 的新標簽 canvas。

             canvas 相比 Flash 顯然是有其優點的。它不依賴于外部插件、與瀏覽器渲染引擎緊密結合、節約資源,最重要的是極大的簡化了圖形和網頁中其他元素的交互過程。

             對于 Flash 來說,是 Flash中的元素與網頁中其他元素進行交互是要消耗大量時間和資源的,另外在編程上也相當不方便。

          而 canvas 本身就是 HTML5 的一個元素,可以像操作普通 HTML 元素一樣操作它。開發人員可以將所有的代碼整齊地寫在一個文件里,降低了維護與更新的難度。

             然而 canvas 也有其缺點:

             其一,開發者不得不編程描繪每一個點和矢量曲線,在旋轉縮放時更需要和矩陣變換打交道,這會增加描繪復雜圖形的難度。而在 Flash 里,圖形顯示的 API 被封裝在名為“Sprite(顯示列表)”的類里,大部分圖形元素都繼承于此類,開發者可以使用多種工具設計圖形,對圖形進行旋轉放縮只需要簡單的調用類 的函數。

             其二,動畫的實現存在缺憾。canvas 雖然提供了不同于傳統的通過 div 塊實現動畫的方法,但這種方法仍然非常繁瑣。開發者必須在每一幀動畫顯示時清空畫布,然后重畫所有元素,這必然導致包含大量元素的場景動畫緩慢,只移動少 量元素就要重畫整個畫布會浪費大量資源。而且創建動畫也是一件十分繁瑣的事情。相比起來,Flash的實現就方便多了,雖然從最底層來說,動畫時仍然需要 重畫整個畫布,但其被交予 Flash Player 自動處理,無需開發者手動管理。基于字節碼的 Flash 在解析的過程中將會比即時編譯的 HTML5 和 JavaScript 快速。一般來說,復雜動畫將會更流暢。另外,良好封裝的圖形類和強大的設計工具使得動畫的創建非常方便。

             其三,沒有提供一套方便的事件體系。開發者也許需要通過捕獲鼠標在 canvas 中點擊的坐標,判斷用戶到底點擊了什么圖形元素。在這個過程中可能要遍歷所有的顯示元素并判定點是否在圖形內,實現起來比較繁瑣,更不要說實現事件的冒泡 和遞歸模型了。雖然今后出現的圖形庫可以解決這個問題,但這實質上相當于使用 JavaScript 構建了一套事件響應模型,其效率顯然不如內建于瀏覽器的原生事件模型高。在 Flash 中,事件也被良好封裝為類,捕獲點擊等事件自然不在話下,更重要的是提供了判斷兩個圖形是否有交集的事件和函數,這在游戲編程中非常方便。另 外,Flash 的最新版本將會支持多點觸摸事件的響應,而 HTML 想要支持這點恐怕要等到 HTML6 了。

             由以上分析我們可以看出,HTML5 需要的幾個非常重要的東西:一個強大易用的圖形庫、硬件加速的圖形解析和重繪、一個強大的編 機器(IDE)

             目前已經出現了基于 canvas 實現的游戲引擎。但是從效果上看仍然無法與 Flash 媲美。

          WebGL 的提出讓我們看到了硬件加速的希望,這將極大的改進圖形顯示的速度。但是目前它只被少數開發版本的瀏覽器支持。

             IDE 方面,諷刺的是恰恰是 Adobe 為 Adobe Flash CS5 添加了一個將 Flash 轉化為 canvas 的功能。在 JavaScript 方面,鑒于其為非強制類型的編程語言,對其進行代碼提示等非常困難,提高編程效率較難。

             如果以上三個問題不能被良好解決,將會限制 canvas 所能實現的效果的豐富度,增加開發的復雜度,從而最終阻礙其普及。

             圖為一個用 canvas 實現的繪圖應用

             video

             video 標簽可能是 Adobe 最反對的東西了,它極有可能打破 Flash 在在線視頻領域的壟斷地位。

             但目前的情況是作為 video 內容的視頻存在編碼問題,Apple 和微軟所支持的 不是開放標準,瀏覽器廠商必須為其付費。因此,作為三大瀏覽器之一的火狐瀏覽器拒絕支持此編碼格式。谷歌雖然也收購了一套優質的編碼技術,但是目前沒有跡 象表明谷歌會開放這個技術標準。

             根據最近的統計,雖然 Google Chrome 瀏覽器和 Apple Safari 瀏覽器增長很快,但瀏覽器市場還是主要被火狐和 IE 所統治。如果火狐堅持不支持 編碼格式,video 標簽的推廣將會十分困難。

             所以,HTML5 需要一個既開放又優質的視頻編碼標準

             圖為 video 標簽 的演示

             二、商業方面

             團結

             要讓微軟、谷歌、蘋果這三個在很多方面存在競爭關系的業界巨頭團結一心地支持同一套標準是很困難的。

             蘋果方面對 Flash 痛下殺手,微軟方面則極少參與這場論戰。至于谷歌則在支持 HTML5 的同時在 Android 中加入了 Flash 支持,甚至存在將 Flash 納入 Chorme 安全沙箱的計劃。在這種情況下,如果 Adobe 能夠良好利用三大巨頭之間的分歧并加以運作,HTML5 的前景堪憂。

             即便 Adobe 沒有那樣的智慧與能力挑撥三大巨頭之間的關系,三大巨頭自己就可能葬送 HTML5 的未來。前車之鑒就是大名鼎鼎的 OpenGL。這一標準成立之初的聯盟成員幾乎可以用豪華來形容,結果因為各個成員之間為了自己的利益相互爭吵,使得OpenGL的發展速度遠不及 Direct3D,直至到目前這樣游戲市場幾乎被競爭對手占據、應用范圍局限在專業領域的情況。

             用戶的接受

             無論各大廠商如何宣傳,用戶的接受才是最后的檢驗標準。目前來看 HTML5 在普通桌面領域可能的作為不大,與 Flash 的關系必然是長期并存。原因在于用戶并不在意頁面到底使用的是什么技術,而更關心最后的效果怎么樣。HTML5 的 canvas 若要達到 Flash 實現的相同效果所需要的難度更大,這樣限制了中小網站在網頁里使用 canvas 的積極性,如果 canvas 不能普及,就相當于 HTML5 斷了一條腿,而 video 標簽的編碼問題再得不到解決,HTML5 真的就沒辦法和 Flash 競爭了。

             大膽的預測

             在最后,我將對 HTML5 和 Flash 的這場世紀之戰做出我自己的預測。

             首先用一個比喻描述目前的情況:

             谷歌、微軟、蘋果、Adobe 四家圍在一起打牌,其中 Adobe 是莊家,手中的牌最多最好。蘋果、微軟都很想把 Adobe 從莊家的位置上拖下來,但是又不愿意合作。谷歌與 Adobe 關系曖昧,但是也有自己的打算。

             在這場牌局中,Adobe 幾乎是立于不敗之地的,從目前來看 Flash 被 HTML5 完全取代的可能不大,原因在于Flash 已經占領了絕大部分傳統桌面終端的市場,其地位幾乎無法撼動,即便 Flash 做得不夠好,但是只要沒到很不好的地步,還是無法被超越。雖然傳統桌面收到了新興的移動終端的挑戰,但是這一過程將發展得比 Flash 和 HTML5 之間的競爭更緩慢。Adobe 控制著 Photoshop、Dreamweaver 和 Fireworks 等知名軟件,制作網頁即便可以缺少 Flash 卻無法缺少    Photoshop,即便是編制全 HTML5 的網頁,Dreamweaver 依然是首選的網頁制作利器,制作 canvas 也可以使用 Flash CS5 新加入的生成 canvas 的功能。

             圖為 Adobe Flash CS5

             廣受詬病的 Flash Player 并不能給 Adobe 帶來直接的利潤,它的意義在于將富媒體應用的市場和標準掌握在手中。用于制作 Flash 的編輯器才是 Adobe 真正的利潤來源。如果在 HTML5 的時代,開發者仍然不得不選擇 Adobe 的產品來制作基于 canvas 的交互頁面,那么又何必需要 Flash Player 的存在呢?Adobe 更是省下了維護一個復雜系統的費用。

             至于微軟,他內心是非常糾結的,他手里有 Internet Explorer 這張不知道是好是壞的牌,原因在于由 Internet Explorer 6 占領的瀏覽器市場份額仍然沒有被有效釋放,新版本的 Internet Explorer 不得不跟自己的前輩競爭。另外,微軟也急于推廣自己的 SliverLight ,這一產品與 Flash 和 HTML5 都是競爭關系。支持 HTML5 或多或少會對 SliverLight 的推廣有所打擊。所以微軟必然不會全力支持 HTML5,而是只將它作為 SliverLight 的補充。

             蘋果的算盤打得很響,他要從移動終端領域著手,逐步滲透到桌面領域。iPhone 是第一步,也是相當成功的一步,它的存在說明智能手機領域并不需要 Flash 的存在,事實上,Flash 在這一領域表現的確很差。但是真正關鍵的是 iPad。iPad無論是屏幕大小還是操作體驗都更接近與普通桌面電腦,如果平板電腦被證明不需要 Flash 的存在,那么桌面電腦為什么不可以?如果大量用戶通過使用平板電腦而習慣了沒有 Flash 的互聯網體驗,那么 Flash 就真的沒有未來了。iPad發售之后 Adobe 與蘋果之間爭論的升級,從側面證實蘋果和 Adobe 都看到了平板電腦將在這場戰爭中發揮的作用。

             但是蘋果真的能如愿以償么?蘋果的產品即便銷售很好,也不可能做到像微軟的產品這樣普及。原因在于蘋果的產品文化就在于提供高質量和高品位的體驗, 而這種體驗伴隨著高價。必然只有少數人能夠用得起蘋果,必然只有使用蘋果是能夠成為一種身份和品味的象征,蘋果的產品才會有這么大的吸引力。難道除了蘋果 就沒有其他廠商可以提供同樣的技術了么?難道微軟不能像蘋果一樣以用戶體驗為先么?顯然不是,重要的原因在于微軟所要提供的是一個給所有人使用的產品,這 一產品要有廣泛性,要有繼承性,而且不能太昂貴。所以微軟在用戶體驗方面改變的動力不足,微軟試圖在 Visita 里極大的改變用戶體驗的方式,原來使用 XP 非常熟練的用戶到了 Visita 里就變得不知所措了。結果顯而易見。

             事實上,蘋果從一開始就不打算讓所有人都用上蘋果,只要有少部分人愿意掏錢購買,它就能賺足夠的錢。

             到了平板電腦這里也是一樣,iPad 不可能獨自積累到足夠大的用戶群,以至于可以挑戰傳統桌面終端。更具性價比的其他廠商的產品將會讓更多人享受到平板電腦。而且這些平板電腦很可能會支持 Flash。這樣的話,利用新興終端,改變用戶對 Flash 的依賴的計劃就會失敗。

             谷歌方面,這場戰爭的勝負對谷歌的影響都不大,只要能把用戶留在頁面上,它并不在意到底使用的是 HTML5 還是 Flash。然而他卻最終勝負有著非同尋常的影響力。谷歌除了 YouTube 之外,很少在自己的產品中使用 Flash。也許谷歌認為一個開放的標準更容易控制。對于 Flash 這樣封閉的產品,雖然好用,但是谷歌很難對其發展方向有發言權。而今,Chrome 瀏覽器的迅猛發展日益增加了谷歌在 HTML 新標準中的發言權。但是這并不意味著谷歌會完全拋棄 Flash。

             Flash 可以作為谷歌牽制蘋果的重要工具,作為 Android 挑戰 iPhone 的籌碼。谷歌正籌劃將 Flash 納入 Chrome OS 的安全沙箱。如果成功,Flash 飽受詬病的耗電問題、安全問題都能得到較好的解決。

             由此得到結論,Flash 氣數未盡,仍將長期統治互聯網富媒體領域的市場。在移動領域的發展將取決于谷歌的態度,但可以預見的是將會有很多困難。

             至于 HTML5,預計在1-3年內會達到相對普及的程度,但是不會取代 Flash。他們之間甚至是可以和諧相處,取長補短的。然而不要對 HTML5 的發展速度抱有太大希望,畢竟業內巨頭之間矛盾重重,現有標準能夠被各大瀏覽器無差別支持就已經相當困難了,想要加入任何新功能都要很久才會被廣泛支持。 這樣緩慢的發展速度如何能夠體現出強大的競爭力,我們拭目以待。

          瀏覽器的內核是指支持瀏覽器運行的最核心的程序,分為兩個部分的,一是渲染引擎,另一個是 JS 引擎。渲染引擎在不同的瀏覽器中也不是都相同的。目前市面上常見的瀏覽器內核可以分為這四種:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。這里面大家最耳熟能詳的可能就是 Webkit 內核了,Webkit 內核是當下瀏覽器世界真正的霸主。

          本文我們就以 Webkit 為例,對現代瀏覽器的渲染過程進行一個深度的剖析。

          想閱讀更多優質文章請猛戳GitHub 博客。

          頁面加載過程

          在介紹瀏覽器渲染過程之前,我們簡明扼要介紹下頁面的加載過程,有助于更好理解后續渲染過程。

          要點如下:

          • 瀏覽器根據 DNS 服務器得到域名的 IP 地址;
          • 向這個 IP 的機器發送 HTTP 請求;
          • 服務器收到、處理并返回 HTTP 請求;
          • 瀏覽器得到返回內容。

          例如在瀏覽器輸入https://juejin.im/timeline,然后經過 DNS 解析,juejin.im對應的 IP 是36.248.217.149(不同時間、地點對應的 IP 可能會不同)。然后瀏覽器向該 IP 發送 HTTP 請求。

          服務端接收到 HTTP 請求,然后經過計算(向不同的用戶推送不同的內容),返回 HTTP 請求,返回的內容如下:


          其實就是一堆 HMTL 格式的字符串,因為只有 HTML 格式瀏覽器才能正確解析,這是 W3C 標準的要求。接下來就是瀏覽器的渲染過程。

          瀏覽器渲染過程


          瀏覽器渲染過程大體分為如下三部分:

          1)瀏覽器會解析三個東西:

          一是 HTML/SVG/XHTML,HTML 字符串描述了一個頁面的結構,瀏覽器會把 HTML 結構字符串解析轉換 DOM 樹形結構。


          二是 CSS,解析 CSS 會產生 CSS 規則樹,它和 DOM 結構比較像。


          三是 Javascript 腳本,等到 Javascript 腳本文件加載后, 通過 DOM API 和 CSSOM API 來操作 DOM Tree 和 CSS Rule Tree。


          2)解析完成后,瀏覽器引擎會通過 DOM Tree 和 CSS Rule Tree 來構造 Rendering Tree。

          • Rendering Tree 渲染樹并不等同于 DOM 樹,渲染樹只會包括需要顯示的節點和這些節點的樣式信息。
          • CSS 的 Rule Tree 主要是為了完成匹配并把 CSS Rule 附加上 Rendering Tree 上的每個 Element(也就是每個 Frame)。
          • 然后,計算每個 Frame 的位置,這又叫 layout 和 reflow 過程。

          3)最后通過調用操作系統 Native GUI 的 API 繪制。

          接下來我們針對這其中所經歷的重要步驟詳細闡述

          構建 DOM

          瀏覽器會遵守一套步驟將 HTML 文件轉換為 DOM 樹。宏觀上,可以分為幾個步驟:


          瀏覽器從磁盤或網絡讀取 HTML 的原始字節,并根據文件的指定編碼(例如 UTF-8)將它們轉換成字符串。

          在網絡中傳輸的內容其實都是 0 和 1 這些字節數據。當瀏覽器接收到這些字節數據以后,它會將這些字節數據轉換為字符串,也就是我們寫的代碼。

          將字符串轉換成 Token,例如:<html>、<body>等。Token 中會標識出當前 Token 是“開始標簽”或是“結束標簽”亦或是“文本”等信息

          這時候你一定會有疑問,節點與節點之間的關系如何維護?

          事實上,這就是 Token 要標識“起始標簽”和“結束標簽”等標識的作用。例如“title”Token 的起始標簽和結束標簽之間的節點肯定是屬于“head”的子節點。


          上圖給出了節點之間的關系,例如:“Hello”Token 位于“title”開始標簽與“title”結束標簽之間,表明“Hello”Token 是“title”Token 的子節點。同理“title”Token 是“head”Token 的子節點。

          • 生成節點對象并構建 DOM

          事實上,構建 DOM 的過程中,不是等所有 Token 都轉換完成后再去生成節點對象,而是一邊生成 Token 一邊消耗 Token 來生成節點對象。換句話說,每個 Token 被生成后,會立刻消耗這個 Token 創建出節點對象。注意:帶有結束標簽標識的 Token 不會創建節點對象。

          接下來我們舉個例子,假設有段 HTML 文本:

          復制代碼

          <html>
          <head>
           <title>Web page parsing</title>
          </head>
          <body>
           <div>
           <h1>Web page parsing</h1>
           <p>This is an example Web page.</p>
           </div>
          </body>
          </html>
          

          上面這段 HTML 會解析成這樣:


          構建 CSSOM

          DOM 會捕獲頁面的內容,但瀏覽器還需要知道頁面如何展示,所以需要構建 CSSOM。

          構建 CSSOM 的過程與構建 DOM 的過程非常相似,當瀏覽器接收到一段 CSS,瀏覽器首先要做的是識別出 Token,然后構建節點并生成 CSSOM。


          在這一過程中,瀏覽器會確定下每一個節點的樣式到底是什么,并且這一過程其實是很消耗資源的。因為樣式你可以自行設置給某個節點,也可以通過繼承獲得。在這一過程中,瀏覽器得遞歸 CSSOM 樹,然后確定具體的元素到底是什么樣式。

          注意:CSS 匹配 HTML 元素是一個相當復雜和有性能問題的事情。所以,DOM 樹要小,CSS 盡量用 id 和 class,千萬不要過渡層疊下去

          構建渲染樹

          當我們生成 DOM 樹和 CSSOM 樹以后,就需要將這兩棵樹組合為渲染樹。


          在這一過程中,不是簡單的將兩者合并就行了。渲染樹只會包括需要顯示的節點和這些節點的樣式信息,如果某個節點是 display: none 的,那么就不會在渲染樹中顯示。

          我們或許有個疑惑:瀏覽器如果渲染過程中遇到 JS 文件怎么處理

          渲染過程中,如果遇到<script>就停止渲染,執行 JS 代碼。因為瀏覽器有 GUI 渲染線程與 JS 引擎線程,為了防止渲染出現不可預期的結果,這兩個線程是互斥的關系。JavaScript 的加載、解析與執行會阻塞 DOM 的構建,也就是說,在構建 DOM 時,HTML 解析器若遇到了 JavaScript,那么它會暫停構建 DOM,將控制權移交給 JavaScript 引擎,等 JavaScript 引擎運行完畢,瀏覽器再從中斷的地方恢復 DOM 構建。

          也就是說,如果你想首屏渲染的越快,就越不應該在首屏就加載 JS 文件,這也是都建議將 script 標簽放在 body 標簽底部的原因。當然在當下,并不是說 script 標簽必須放在底部,因為你可以給 script 標簽添加 defer 或者 async 屬性(下文會介紹這兩者的區別)。

          JS 文件不只是阻塞 DOM 的構建,它會導致 CSSOM 也阻塞 DOM 的構建

          原本 DOM 和 CSSOM 的構建是互不影響,井水不犯河水,但是一旦引入了 JavaScript,CSSOM 也開始阻塞 DOM 的構建,只有 CSSOM 構建完畢后,DOM 再恢復 DOM 構建。

          這是什么情況?

          這是因為 JavaScript 不只是可以改 DOM,它還可以更改樣式,也就是它可以更改 CSSOM。因為不完整的 CSSOM 是無法使用的,如果 JavaScript 想訪問 CSSOM 并更改它,那么在執行 JavaScript 時,必須要能拿到完整的 CSSOM。所以就導致了一個現象,如果瀏覽器尚未完成 CSSOM 的下載和構建,而我們卻想在此時運行腳本,那么瀏覽器將延遲腳本執行和 DOM 構建,直至其完成 CSSOM 的下載和構建。也就是說,在這種情況下,瀏覽器會先下載和構建 CSSOM,然后再執行 JavaScript,最后在繼續構建 DOM


          布局與繪制

          當瀏覽器生成渲染樹以后,就會根據渲染樹來進行布局(也可以叫做回流)。這一階段瀏覽器要做的事情是要弄清楚各個節點在頁面中的確切位置和大小。通常這一行為也被稱為“自動重排”。

          布局流程的輸出是一個“盒模型”,它會精確地捕獲每個元素在視口內的確切位置和尺寸,所有相對測量值都將轉換為屏幕上的絕對像素。

          布局完成后,瀏覽器會立即發出“Paint Setup”和“Paint”事件,將渲染樹轉換成屏幕上的像素。

          以上我們詳細介紹了瀏覽器工作流程中的重要步驟,接下來我們討論幾個相關的問題:

          幾點補充說明

          1.async 和 defer 的作用是什么?有什么區別?

          接下來我們對比下 defer 和 async 屬性的區別:


          其中藍色線代表 JavaScript 加載;紅色線代表 JavaScript 執行;綠色線代表 HTML 解析。

          1)情況 1<script src="script.js"></script>

          沒有 defer 或 async,瀏覽器會立即加載并執行指定的腳本,也就是說不等待后續載入的文檔元素,讀到就加載并執行。

          2)情況 2<script async src="script.js"></script> (異步下載)

          async 屬性表示異步執行引入的 JavaScript,與 defer 的區別在于,如果已經加載好,就會開始執行——無論此刻是 HTML 解析階段還是 DOMContentLoaded 觸發之后。需要注意的是,這種方式加載的 JavaScript 依然會阻塞 load 事件。換句話說,async-script 可能在 DOMContentLoaded 觸發之前或之后執行,但一定在 load 觸發之前執行。

          3)情況 3 <script defer src="script.js"></script>(延遲執行)

          defer 屬性表示延遲執行引入的 JavaScript,即這段 JavaScript 加載時 HTML 并未停止解析,這兩個過程是并行的。整個 document 解析完畢且 defer-script 也加載完成之后(這兩件事情的順序無關),會執行所有由 defer-script 加載的 JavaScript 代碼,然后觸發 DOMContentLoaded 事件。

          defer 與相比普通 script,有兩點區別:載入 JavaScript 文件時不阻塞 HTML 的解析,執行階段被放到 HTML 標簽解析完成之后。

          在加載多個 JS 腳本的時候,async 是無順序的加載,而 defer 是有順序的加載。

          2. 為什么操作 DOM 慢?

          把 DOM 和 JavaScript 各自想象成一個島嶼,它們之間用收費橋梁連接。——《高性能 JavaScript》

          JS 是很快的,在 JS 中修改 DOM 對象也是很快的。在 JS 的世界里,一切是簡單的、迅速的。但 DOM 操作并非 JS 一個人的獨舞,而是兩個模塊之間的協作。

          因為 DOM 是屬于渲染引擎中的東西,而 JS 又是 JS 引擎中的東西。當我們用 JS 去操作 DOM 時,本質上是 JS 引擎和渲染引擎之間進行了“跨界交流”。這個“跨界交流”的實現并不簡單,它依賴了橋接接口作為“橋梁”(如下圖)。


          過“橋”要收費——這個開銷本身就是不可忽略的。我們每操作一次 DOM(不管是為了修改還是僅僅為了訪問其值),都要過一次“橋”。過“橋”的次數一多,就會產生比較明顯的性能問題。因此“減少 DOM 操作”的建議,并非空穴來風。

          3. 你真的了解回流和重繪嗎?

          渲染的流程基本上是這樣(如下圖黃色的四個步驟):

          1. 計算 CSS 樣式

          2. 構建 Render Tree

          3.Layout – 定位坐標和大小

          4. 正式開畫


          注意:上圖流程中有很多連接線,這表示了 Javascript 動態修改了 DOM 屬性或是 CSS 屬性會導致重新 Layout,但有些改變不會重新 Layout,就是上圖中那些指到天上的箭頭,比如修改后的 CSS rule 沒有被匹配到元素。

          這里重要要說兩個概念,一個是 Reflow,另一個是 Repaint

          重繪:當我們對 DOM 的修改導致了樣式的變化、卻并未影響其幾何屬性(比如修改了顏色或背景色)時,瀏覽器不需重新計算元素的幾何屬性、直接為該元素繪制新的樣式(跳過了上圖所示的回流環節)。

          回流:當我們對 DOM 的修改引發了 DOM 幾何尺寸的變化(比如修改元素的寬、高或隱藏元素等)時,瀏覽器需要重新計算元素的幾何屬性(其他元素的幾何屬性和位置也會因此受到影響),然后再將計算的結果繪制出來,這個過程就是回流(也叫重排)。

          我們知道,當網頁生成的時候,至少會渲染一次。在用戶訪問的過程中,還會不斷重新渲染。重新渲染會重復回流 + 重繪或者只有重繪。

          回流必定會發生重繪,重繪不一定會引發回流。重繪和回流會在我們設置節點樣式時頻繁出現,同時也會很大程度上影響性能。回流所需的成本比重繪高的多,改變父節點里的子節點很可能會導致父節點的一系列回流。

          1)常見引起回流屬性和方法

          任何會改變元素幾何信息 (元素的位置和尺寸大小) 的操作,都會觸發回流,

          • 添加或者刪除可見的 DOM 元素;
          • 元素尺寸改變——邊距、填充、邊框、寬度和高度;
          • 內容變化,比如用戶在 input 框中輸入文字;
          • 瀏覽器窗口尺寸改變——resize 事件發生時;
          • 計算 offsetWidth 和 offsetHeight 屬性;
          • 設置 style 屬性的值。

          2)常見引起重繪屬性和方法


          3)如何減少回流、重繪

          • 使用 transform 替代 top;
          • 使用 visibility 替換 display: none ,因為前者只會引起重繪,后者會引發回流(改變了布局);
          • 不要把節點的屬性值放在一個循環里當成循環里的變量。

          復制代碼

          for(let i = 0; i < 1000; i++) {
           // 獲取 offsetTop 會導致回流,因為需要去獲取正確的值
           console.log(document.querySelector('.test').style.offsetTop)
          }
          
          • 不要使用 table 布局,可能很小的一個小改動會造成整個 table 的重新布局;
          • 動畫實現的速度的選擇,動畫速度越快,回流次數越多,也可以選擇使用 requestAnimationFrame;
          • CSS 選擇符從右往左匹配查找,避免節點層級過多;
          • 將頻繁重繪或者回流的節點設置為圖層,圖層能夠阻止該節點的渲染行為影響別的節點。比如對于 video 標簽來說,瀏覽器會自動將該節點變為圖層。

          性能優化策略

          基于上面介紹的瀏覽器渲染原理,DOM 和 CSSOM 結構構建順序,初始化可以對頁面渲染做些優化,提升頁面性能。

          • JS 優化: <script> 標簽加上 defer 屬性 和 async 屬性 用于在不阻塞頁面文檔解析的前提下,控制腳本的下載和執行。
          • defer 屬性: 用于開啟新的線程下載腳本文件,并使腳本在文檔解析完成后執行。
          • async 屬性: HTML5 新增屬性,用于異步下載腳本文件,下載完畢立即解釋執行代碼。
          • CSS 優化: <link> 標簽的 rel 屬性 中的屬性值設置為 preload 能夠讓你在你的 HTML 頁面中可以指明哪些資源是在頁面加載完成后即刻需要的,最優的配置加載順序,提高渲染性能。

          總結

          綜上所述,我們得出這樣的結論:

          • 瀏覽器工作流程:構建 DOM -> 構建 CSSOM -> 構建渲染樹 -> 布局 -> 繪制。
          • CSSOM 會阻塞渲染,只有當 CSSOM 構建完畢后才會進入下一個階段構建渲染樹。
          • 通常情況下 DOM 和 CSSOM 是并行構建的,但是當瀏覽器遇到一個不帶 defer 或 async 屬性的 script 標簽時,DOM 構建將暫停,如果此時又恰巧瀏覽器尚未完成 CSSOM 的下載和構建,由于 JavaScript 可以修改 CSSOM,所以需要等 CSSOM 構建完畢后再執行 JS,最后才重新 DOM 構建。

          參考文章

          • https://segmentfault.com/q/1010000000640869
          • https://coolshell.cn/articles/9666.html
          • https://juejin.im/book/5bdc715fe51d454e755f75ef/section/5c024ecbf265da616a476638
          • https://mp.weixin.qq.com/s?__biz=MzA5NzkwNDk3MQ==&mid=2650588806&idx=1&sn=408a54e7c8102fd6944c9a40b119015a&chksm=8891d6a2bfe65fb42f493fe9a4dab672dd7e440f31e753196cee0cfbc6696e4f8dd3a669e040&mpshare=1&scene=1&srcid=1228ZrXsmbZKcgCSu7zTVDwy#
          • https://juejin.im/book/5b936540f265da0a9624b04b/section/5bac3a4df265da0aa81c043c
          • https://juejin.im/book/5c47343bf265da612b13e5c0/section/5c4737375188255de8397ae3
          • https://juejin.im/book/5a8f9ddcf265da4e9f6fb959/section/5a8f9f7bf265da4e82635e46

          更多內容,請關注前端之巔。

          、背景

          Web 端實時預覽 H.265 需求一直存在,但由于之前 Chrome 本身不支持 H.265 硬解,軟解性能消耗大,僅能支持一路播放,該需求被擱置。
          去年 9 月份,Chrome 發布 M106 版本,默認開啟 H.265 硬解,使得實時預覽支持 H.265 硬解具備可行性。

          然而 WebRTC 本身支持的視頻編碼格式僅包括 VP8、VP9、H.264、AV1,并不包含 H.265。根據 w3c 發布的 2023 WebRTC Next Version Use Cases 來看,近期也沒有打算支持 H.265 的跡象,因而決定自研實現 WebRTC 對 H.265 的支持。

          2、DataChannel

          背景說到 chrome 支持了 h265 的硬解,但 WebRTC 并不支持直接傳輸 h265 視頻流。但可以通過 datachannel 來繞過這個限制

          WebRTC 的數據通道 DataChannel 是專門用來傳輸除音視頻數據之外的任何數據的(但并不意味著不可以傳輸音視頻數據,本質上它就是一條 socket 通道),如短消息、實時文字聊天、文件傳輸、遠程桌面、游戲控制、P2P加速等。

          1)SCTP協議

          DataChannel 使用的協議是 SCTP(Stream Control Transport Protocol) (是一種與TCP、UDP同級的傳輸協議),可以直接在 IP 協議之上運行。

          但在 WebRTC 的情況下,SCTP 通過安全的 DTLS 隧道進行隧道傳輸,該隧道本身在 UDP 之上運行,同時支持流控、擁塞控制、按消息傳輸、傳輸模式可配置等特性。需注意單次發送消息大小不能超過 maxMessageSize(只讀, 默認65535字節)。

          2)可配置傳輸模式

          DataChannel 可以配置在不同模式中,一種是使用重傳機制的可靠傳輸模式(默認模式),可以確保數據成功傳輸到對等端;另一種是不可靠傳輸模式,該模式下可以通過設置 maxRetransmits 指定最大傳輸次數,或通過 maxPacketLife 設置傳輸間隔時間實現;

          這兩種配置項是互斥的,不可同時設置,當同為null 時使用可靠傳輸模式,有一個值不為 null 時開啟不可靠傳輸模式。

          3)支持數據類型

          數據通道支持 string 類型或 ArrayBuffer 類型,即二進制流或字符串數據。

          后續兩種方案,都是基于 datachannel 來做

          3、方案一 WebCodecs

          官方文檔: github.com/w3c/webcode…

          思路: DataChannel 傳輸 H.265 裸流 + Webcodecs 解碼 + Canvas 渲染。即 WebRTC 的音視頻傳輸通道(PeerConnection) 不支持 H.265 編碼格式,但可采用其數據通道(DataChannel)來傳輸 H.265數據,前端收到后使用 Wecodecs 解碼、Canvas 渲染。

          優點:

          • 直接傳輸 H.265 裸碼流,無需額外封裝,實現簡單方便;無冗余數據,傳輸效率高
          • Wecodecs 解碼延遲低,實時性很高

          缺點:

          • 音頻需額外單獨傳輸、解碼和播放,需處理音視頻同步問題
          • 既有 sdk 基于 video 封裝,webcodes 方案依賴 canvas,既有 video 相關操作,需要全部重寫,比如截圖,錄像等操作
          • 由于線上各項目等歷史原因,既有 sdk 改動大,時間上不允許

          4、方案二 MSE

          官方例子: github.com/bitmovin/ms…

          思路:Fmp4封裝 + DataChannel 傳輸 + MSE 解碼播放。即先將 H.265 視頻數據封裝成 Fmp4 格式,再通過 WebRTC DataChannel 通道進行傳輸,前端收到后采用 MSE 解碼, video 進行播放。

          優點:

          • 復用 video 標簽播放,無需單獨實現渲染
          • 音視頻已封裝到 Fmp4 中,web 端無需考慮音視頻同步問題
          • 整體工作量相比 Wecodecs 小,可快速上線

          缺點:

          • 設備端實現 Fmp4 封裝可能存在性能問題,因此需要云端轉發實時進行解封裝,或者前端解封裝
          • MSE 解碼實時性不好(云端首次切片會有 1~2 秒延遲)

          相關學習資料推薦,點擊下方鏈接免費報名,先碼住不迷路~】

          音視頻免費學習地址:FFmpeg/WebRTC/RTMP/NDK/Android音視頻流媒體高級開發

          【免費分享】音視頻學習資料包、大廠面試題、技術視頻和學習路線圖,資料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以點擊788280672加群免費領取~

          5、方案抉擇

          第一版本先以 MSE 上線。云端,前端開發量相對少,roi 高。

          計劃第二版上 wecodecs,不僅低延遲,而且可以避免云端耗流量的問題,節省成本。假設在第二版期間,WebRTC 官方支持了 H.265,那么直接兼容官方方案即可。


          5.1 細說 Mse 及第一版 sdk 改造

          Media Source Extensions, 媒體源擴展。官方文檔: developer.mozilla.org/zh-CN/docs/…

          引入 MSE 之后,支持 HTML5 的 Web 瀏覽器就變成了能夠解析流協議的播放器了。

          從另一個角度來說,通過引入 MSE,HTML5 標簽不僅可以直接播放其默認支持的 mp4、m3u8、webm、ogg 等格式,還可以支持能夠被 (具備MSE功能的)JS 處理的視頻流格式。如此一來,我們就可以通過 (具備MSE功能的)JS,把一些原本不支持的視頻流格式,轉化為其支持的格式(如 H.264 的 mp4,H.265 的 fmp4)。

          比如 B站開源的 flv.js 就是一個典型應用場景。B站的 HTML5 播放器,通過使用 MSE 技術,將 FLV源用 JS(flv.js) 實時轉碼成 HTML5 支持的視頻流編碼格式,提供給 HTML5 播放器播放。

          // 此 demo 來自下面鏈接的官方示例, 可以直接跑起來,比較直觀
          // https://github.com/bitmovin/mse-demo/blob/main/index.html
          
          <!DOCTYPE html>
          <html lang="en">
          <head>
            <meta charset="UTF-8">
            <title>MSE Demo</title>
          </head>
          <body>
            <h1>MSE Demo</h1>
            <div>
              <video controls width="80%"></video>
            </div>
          
            <script type="text/javascript">
              (function() {
                var baseUrl = 'https://bitdash-a.akamaihd.net/content/MI201109210084_1/video/720_2400000/dash/';
                var initUrl = baseUrl + 'init.mp4';
                var templateUrl = baseUrl + 'segment_$Number$.m4s';
                var sourceBuffer;
                var index = 0;
                var numberOfChunks = 52;
                var video = document.querySelector('video');
          
                if (!window.MediaSource) {
                  console.error('No Media Source API available');
                  return;
                }
                  
                // 初始化 mse
                var ms = new MediaSource();
                video.src = window.URL.createObjectURL(ms);
                ms.addEventListener('sourceopen', onMediaSourceOpen);
          
                function onMediaSourceOpen() {
                  // codecs,初始化 sourceBuffer
                  sourceBuffer = ms.addSourceBuffer('video/mp4; codecs="avc1.4d401f"');
                  sourceBuffer.addEventListener('updateend', nextSegment);
          
                  GET(initUrl, appendToBuffer);
                  
                  // 播放
                  video.play();
                }
          
                function nextSegment() {
                  var url = templateUrl.replace('$Number$', index);
                  GET(url, appendToBuffer);
                  index++;
                  if (index > numberOfChunks) {
                    sourceBuffer.removeEventListener('updateend', nextSegment);
                  }
                }
          
                function appendToBuffer(videoChunk) {
                  if (videoChunk) {
                    // 二進制流轉換為 Uint8Array,sourceBuffer 進行消費
                    sourceBuffer.appendBuffer(new Uint8Array(videoChunk));
                  }
                }
          
                function GET(url, callback) {
                  var xhr = new XMLHttpRequest();
                  xhr.open('GET', url);
                  xhr.responseType = 'arraybuffer';
          
                  xhr.onload = function(e) {
                    if (xhr.status != 200) {
                      console.warn('Unexpected status code ' + xhr.status + ' for ' + url);
                      return false;
                    }
                    // 獲取 mp4 二進制流
                    callback(xhr.response);
                  };
          
                  xhr.send();
                }
              })();
            </script>
          </body>
          </html>
          

          通過上面的 demo,以及測試(將 dmeo 中的 fmp4 片段換成我們自己的 IPC 設備(攝像頭),H.265 類型的)得知,chrome 可以硬解 H.265 類型的 fmp4 片段。So,事情變得明朗了起來。大方向有了,無非就是 H.265 裸流,轉換成 fmp4 片段,chrome 底層硬解。

          5.2 fmp4 前端實時解封裝

          H.265 裸流解封裝 fmp4,調研下來,如果純 js 進行封裝,工作量挺大。嘗試用 wasm 調 c++ 的庫,發現即使解封裝性能也不大好。所以放在前端被 pass 掉了。

          5.3 fmp4 云端實時解封裝

          性能好,對前端 0 侵入。確定了云端解封裝,接下來講講這段時間開發遇到的核心鏈路演變,及最終的流程方案。

          6、階段一

          云端實時解封裝 Fmp4,寫死 codecs(音視頻編碼類型) -> 前端 MSE 解碼播放 -> 播放幾秒后,失敗,MSE 會拋異常,大概意思就是你的數據不對了,前后銜接不上。

          排查下來,是 MSE 處于 updating 的時候,不能進行消費,數據直接被丟掉,導致后續數據銜接不上。那既然不能丟,我們就緩存下來。具體可以看下面的代碼注釋。

          具體可以看代碼注釋:

          const updating = this.sourceBuffer?.updating === true;
          const bufferQueueEmpty = this.bufferQueue.length === 0;
          
            if (!updating) {
              if (bufferQueueEmpty) {
                // 緩存隊列為空: 僅消費本次 buffer
                this.appendBuffer(curBuffer);
              } else {
                // 緩存隊列不為空: 消費隊列 + 本次 buffer
                this.bufferQueue.push(curBuffer);
          
                // 隊列中多個 buffer 的合并
                const totalBufferByteLen = this.bufferQueue.reduce(
                  (pre, cur) => pre + cur.byteLength,
                  0
                );
                const combinedBuffer = new Uint8Array(totalBufferByteLen);
                let offset = 0;
                this.bufferQueue.forEach((array, index) => {
                  offset += index > 0 ? this.bufferQueue[index - 1].length : 0;
                  combinedBuffer.set(array, offset);
                });
          
                this.appendBuffer(combinedBuffer);
                this.bufferQueue = [];
              }
            } else {
              // mse 還在消費上一次 buffer(處于 updating 中), 緩存本次 buffer, 否則會有丟幀問題
              this.bufferQueue.push(curBuffer);
            }
          

          考慮到 Fmp4 數據每一幀都不可丟失,因此 datachannel 走的是可靠傳輸。

          但是測試下來,發現了新的問題。隨著時間的增長,延遲會累積增大。因為丟包后,網絡層會進行重試,重試的時間會累積進延時。我們測試下來,網絡情況不好的時候,延遲會高達 30 秒及以上,理論上會一直增加,如果你拉流時間足夠久的話

          7、階段二

          ok,換個思路,既然不丟幀 + 可靠傳輸帶來的延時問題完全不能接受,那么如果換用不可靠傳輸呢?

          不可靠傳輸,意味著會丟幀。調研下來,Fmp4 可以丟掉一整個切片(一個切片包含多幀),既然如此,我們可以設計一套丟幀算法,只要判斷到一個切片是不完整的,我們就把整個切片丟掉。

          這樣的話,理論上來講,最多只會有一個切片的延遲,大概在2秒左右,業務層可以接受。

          丟幀算法設計思路:在每一幀數據頭部增加 4 個字節的數據,用來標識每一幀的具體信息。

          • segNum: 2個字節,大端模式,Fmp4片段序列號,從1開始,每次加1
          • fragCount: 1個字節,Fmp4片段分片總數,最小為1
          • fragSeq: 1個字節,Fmp4片段分片序列號,從1開始

          前端拿到每幀數據后,對前 4 個字節進行解析,就能獲取到每幀數據的詳細信息。舉個例子,假如我要判斷當前幀是否為最后一幀,只需要判斷 fragCount 是否等于 fragSeq 即可。

          算法大致流程圖:

          具體解釋一下:

          • frameQueue, 用來緩存每一幀的數據,用來跟后面一幀數據進行對比,判斷是否為完整幀
          • bufferQueue, 此隊列中的數據,都是完整的切片數據,保證 MSE 進行消費時,數據沒有缺失
            /**
             * fmp4 切片隊列 frameQueue,處理丟幀,生產 bufferQueue 內容
             *
             * @param frameObj 每一幀的相關數據
             *      每來一幀進行判斷
             *      buffer中加上當前幀是否為連續幀(從第一幀開始的連續幀)
             *        是
             *          當前幀是否為最后一幀
             *            是 拼接buffer幀以及當前幀,組成完整幀,放入另外一個待消費 buffer
             *            否 當前幀入 buffer
             *        否 清空 buffer,當前幀入 buffer
             */
          
          const frameQueueLen = this.frameQueue.length;
          const frameQueueEmpty = frameQueueLen === 0;
          
            // 單一完整分片幀單獨處理,直接進行消費
            if (frameObj.fragCount === 1) {
              if (!frameQueueEmpty) {
                this.frameQueue = [];
              }
              this.bufferQueue.push(frameObj.value);
              return;
            }
          
            if (frameQueueEmpty) {
              this.frameQueue.push(frameObj);
              return;
            }
          
            // 是否為首幀
            let isFirstFragSeq = this.frameQueue[0].fragSeq === 1;
            // 當前幀加上queue幀是否為連續幀
            let isContinuousFragSeq = true;
            for (let i = 0; i < frameQueueLen; i++) {
              const isLast = i === frameQueueLen - 1;
          
              const curFragSeq = this.frameQueue[i].fragSeq;
              const nextFragSeq = isLast
                ? frameObj.fragSeq
                : this.frameQueue[i + 1].fragSeq;
          
              const curSegNum = this.frameQueue[i].segNum;
              const nextSeqNum = isLast
                ? frameObj.segNum
                : this.frameQueue[i + 1].segNum;
          
              if (curFragSeq + 1 !== nextFragSeq || curSegNum !== nextSeqNum) {
                isContinuousFragSeq = false;
                break;
              }
            }
          
            if (isFirstFragSeq && isContinuousFragSeq) {
              // 是否為最后一幀
              const isLastFrame = frameObj.fragCount === frameObj.fragSeq;
              if (isLastFrame) {
                this.frameQueue.forEach((item) => {
                  this.bufferQueue.push(item.value);
                });
                this.frameQueue = [];
                this.bufferQueue.push(frameObj.value);
              } else {
                this.frameQueue.push(frameObj);
              }
            } else {
              // 丟幀則清空 frameQueue,則代表直接丟棄整個 segment 切片
              this.emit(EVENTS_ERROR.frameDropError);
              this.frameQueue = [];
              this.frameQueue.push(frameObj);
            }
          

          原本以為大功告成,結果意想不到的事情發生了。

          當出現丟幀時,通過上面的算法,確實是把整個切片的數據丟棄掉了,但是 MSE 此時居然再次異常了,意思也是說數據序列不對,導致解析失敗。

          可是用 ffplay 在本地測試(丟掉一整個切片后,是可以繼續播放的),陷入僵局,繼續排查。

          8、階段三

          話說最近 chatgpt 不是挺火,嘗試著用了下,確實找到了原因。MSE 在消費 fmp4 數據時,需要根據內部序列號進行索引標識,因此即使是丟掉整個切片數據,還是會播放失敗。怎么辦?難道要回到不可靠傳輸?

          經過一番權衡,最終決定,當出現丟幀時,前端通知云端,重新進行切片,并且此時前端重新初始化 MSE。

          改造下來發現,效果還不錯,我們把不可靠傳輸,datachannel 重傳次數設置為 5。

          出現丟幀的概率大大減小,就算出現丟幀,也只會有不到 2 秒的 loading,然后繼續出畫面,業務層可以接受。

          最終,經過上面 3 個階段的改造,就有了整個鏈路圖。當然其實還有很多細節,沒有講到,比如利用 mp4box 獲取 codec, 前端定時檢查 datachannel 狀態等,就不展開細說了。有興趣的可以留言討論

          完整的鏈路圖,簡單畫了下。

          9、總結

          目前 datachannel + MSE 的方案已經上線,測試下來,線上同時硬解 16 路沒有性能問題。

          后續會嘗試用 webcodes 來進行 H.265 的解析,并處理音視頻同步等問題。徹底解決掉延時的問題。

          下一篇準備寫日常排查 WebRTC 問題的一些思路,也歡迎評論區聊一下日常遇到的一些問題,下篇一起匯總。

          原文 https://juejin.cn/post/7215608036394614844?searchId=20230814145744EF4FC3208E60C3F568D8


          主站蜘蛛池模板: 动漫精品一区二区三区3d | 动漫精品第一区二区三区| 精品一区二区三区四区在线| 无码人妻精品一区二区三区99不卡 | 精品国产一区二区二三区在线观看| 亚洲愉拍一区二区三区| 爆乳熟妇一区二区三区霸乳| 亚洲一区中文字幕在线电影网 | 日韩精品中文字幕无码一区| 成人免费一区二区三区在线观看| 久久99精品免费一区二区| 精品国产福利一区二区| 日韩精品无码一区二区三区四区| 国产99久久精品一区二区| 国产综合无码一区二区辣椒| 日韩一区二区三区电影在线观看| 亚洲AV本道一区二区三区四区| 精品视频在线观看一区二区三区| 国模私拍福利一区二区| 乱子伦一区二区三区| 无码少妇一区二区三区| 冲田杏梨高清无一区二区| 爆乳无码AV一区二区三区| 91在线一区二区| 国产福利91精品一区二区三区| 精品一区二区AV天堂| 亚洲国产一区二区a毛片| 熟女性饥渴一区二区三区| 福利一区福利二区| 影院无码人妻精品一区二区| 久久综合精品国产一区二区三区| 亚洲爽爽一区二区三区| 一区二区不卡久久精品| 伊人激情AV一区二区三区| 久久精品国产一区| 亚洲一区二区三区免费视频| 亚洲性无码一区二区三区| 日本夜爽爽一区二区三区| 成人区精品人妻一区二区不卡| 亚洲爆乳无码一区二区三区| 伊人色综合网一区二区三区|