整合營銷服務商

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

          免費咨詢熱線:

          釘釘小程序快照技術初窺

          釘釘小程序快照技術初窺

          者:孫然(煮蝦)

          對于小程序技術來說,容器加載和前端異步渲染的過程中固然不可避免的會有白屏或 loading 頁的展示,短則一瞬間,長則需要數秒才能展示首屏。如果白屏時間長,將非常影響用戶的體驗。根據 Google 的統計,如果頁面加載耗時超過了 3s,那么有 53% 的用戶會選擇直接退出該頁面了。

          為加速小程序首頁的展示,支付寶和手淘運用了基于 HTML 的快照技術,主要思路都是緩存首頁 HTML 供下次啟動時與數據一起優先渲染以提前首屏展示的時間,適用于傳統 WebView 渲染的小程序場景。這種基于 HTML 的快照技術能夠極大縮短啟動時的白屏時間,但首屏展示的速度還是不夠快,期間用戶仍然會有可見的白屏感受。并且快照展示的仍然是無法點擊操作的頁面,需要等待 JS 部分 ready 后才可點擊交互。

          為了追求極致的體驗效果,我們提出了一種全新的小程序快照技術,目標是既做到徹底消除白屏現象,同時也要能夠響應用戶交互。

          核心思路

          不同于現有的基于 HTML 的快照技術,我們提出了一種 native 的圖像級別的快照技術,主要由以下三個步驟:

          • 步驟1:在小程序啟動后合適的時機將小程序首頁保存為圖片,我們稱之為快照
          • 步驟2:下次打開小程序時先展示上次保存的快照,再啟動小程序
          • 步驟3:當小程序啟動完后的合適時機,隱藏快照,展示出真實的小程序首頁,并保存當前界面視圖作為下次的快照(同步驟1)

          效果

          現在釘釘中的新建 DING 日程頁面就運用了快照技術,前后效果對比如下:

          before

          after

          可以看出,通過快照技術,該頁面實現了首屏秒開的效果,啟動白屏的現象徹底消失,頁面的首屏渲染耗時從 1700ms 左右降低到了 300ms 以下。

          下面我會對快照技術的幾個關鍵考慮點進行詳細介紹。

          場景和時機

          理想中的快照,應當是能夠和首屏頁面完全重合,并且在快照隱藏時不會產生任何視覺變化的。那么生成快照的時機和運用快照的場景就直接決定了快照技術能夠達到的優化效果。

          什么頁面適合用快照?

          并不是所有的小程序都適合使用快照技術來提升首屏體驗。如果使用不當,快照可能還會成為體驗的減分項。為達到最佳效果,一般首屏頁面滿足下面幾個條件是比較適合使用快照的:

          • 首屏頁面較為固定。如果首屏不固定,很難找到合適的快照時機來保證快照與下次的首屏重合
          • 首屏頁面不含用戶隱私數據。用戶的隱私數據不應當被快照下來

          什么時候生成快照?

          如果快照的時機過早,快照可能展示的也是白屏或者未渲染完整的首頁框架。

          如果快照的時機過晚,可能用戶已經對首屏有了交互(滾動、點擊等),容易生成無法和首屏重合的快照。

          所以需要根據不同的首屏場景來確定最佳的快照時機。一般我們考慮快照的時機有:

          • 小程序首屏 Page 的 onReady 生命周期回調里。但此時頁面很可能仍然沒有渲染完成,可以考慮適當延時后進行快照
          • 小程序首屏的數據如果需要遠程拉取,可以在遠程獲取到首屏數據后進行
          • 用戶發生滾動時、點擊等交互后不再進行快照

          什么時候隱藏快照?

          我們一般考慮在生成快照時去隱藏當前已經展示的快照。二者的順序一般是在隱藏快照后立即生成快照,才能實現快照和真實頁面的無縫銜接。

          當然,也需要考慮小程序啟動可能失敗的場景。這里需要對展示快照的時間設置一個展示的上限,如果展示時間達到上限時小程序首屏仍然沒有啟動成功,那么快照將直接隱藏,以防造成首屏對用戶可見但一直不響應的尷尬局面。

          這里我們在還隱藏快照時做了一個小小的視覺優化。考慮到在隱藏快照時,如果直接隱藏快照,一旦快照和真實頁面稍有差別,在視覺上可能就會有閃爍的體感發生。

          所以在隱藏快照時,我們會做一個 200ms 的淡出動畫,來緩解這種快照和真實頁面差異導致的閃爍感。因為有時快照的時機可能會稍提前于首頁網絡數據加載成功、圖片加載成功等這些異步事件成功的時機,導致快照比真實頁面元素缺少或者數據不準確,而淡出動畫能夠有效淡化這些差錯造成的視覺異常感。下面的 demo 對比了這兩種情況:

          直出

          淡出

          可交互

          由于快照和真實的首屏頁面基本是相同的,從用戶體感上用戶會以為首屏已經成功展現,也應該是個可交互的頁面。所以單純展示死的快照頁面是遠遠不夠的,做到可交互是我們快照的重要能力

          我們的快照支持響應用戶的點擊行為,具體方法就是在用戶點擊快照時先暫存用戶的點擊事件,待快照隱藏時將此次事件分發到真實頁面上去。

          此過程中如果用戶有多次點擊事件,我們只會響應最后一次點擊事件。

          從用戶體感上來說,可能用戶會感覺到此次點擊的響應會比較慢,但不會讓用戶感知到它點擊的是快照還是真實首頁。

          如果是小程序啟動較慢的場景,還可以考慮在用戶點擊后展示 loading:

          為進一步提高快照層的可交互性,我們甚至還可以允許開發者設定一些快照層的點擊區域和簡單操作,使用戶在點擊快照層的時候快速響應點擊事件。例如釘釘的工作臺就非常適合這種場景:工作臺中的各個應用一般不會頻繁變化,并且有很明確的分塊區域:

          可以配置好不同的點擊區域以及對應的 action (例如:跳轉到其它頁面/應用),形如:

          [{
            area: {
              left: 100, 
              top: 100, 
              width: 100, 
              height: 100
            },
            action: {
              type: 'openLink', 
              params: { url: 'http://xxx' }
            }
          }, ...]

          這樣用戶在點擊快照指定區域時就能直接實現跳轉,而無需等待到小程序啟動完成。

          存儲與安全

          快照屬于敏感數據,并且只保存在客戶端本地不能進行上傳,管理其存儲必須格外小心,否則很容易釀成一起公關事件。

          對于快照的存儲,我們考慮了以下幾點:

          1 加密存儲

          快照數據必須進行加密存儲,這里加密方式用的是集團無線保鏢里的加密方法。

          2 隱私保護

          快照里不能含有用戶的隱私數據。也就是說,快照應當只含有一些 UI 元素或者無意義的默認數據,而不應該含有用戶隱私數據。

          不含用戶隱私數據

          含用戶隱私數據

          那么如何做到獲取到不含用戶隱私數據的首頁快照呢?可以考慮在前端從網絡、緩存中獲取數據之前進行快照。但這樣的快照必定是不完整的,會損失一定的體驗,這也是我們不推薦在有用戶數據的首屏場景使用快照的原因。

          3 快照清理

          快照被存儲在客戶端中,需要有存儲上限。當快照數據達到一定量后,需要淘汰一些老的快照數據。
          其次,在小程序版本更新、用戶登出切換用戶時都應該考慮將現存的對應快照數據清理掉。

          準確性

          當快照上線后,我們需要對快照的用戶體驗進行感知。最佳的體驗是用戶根本就沒有感知到快照的存在,也就是快照和真實頁面完全重合;而如果快照和真實頁面相差較大,則會讓用戶體驗大大下降,這是我們需要感知到的。

          這里我們主要關注快照的準確性指標,也就是快照和真實頁面的相似(重合)程度。準確性越高,則說明快照與真實頁面的過渡約自然,體驗越好;反之不但不會提升體驗,可能還會對用戶帶來困惑。

          如何判斷快照準確性

          在每次生成快照時,我們會將本次快照與上一次快照進行對比,得出量化的指標進而反映快照的準確性。那么下一步的問題就變成了如何判斷兩張圖片的相似度了。

          這里可能首先會想到直接使用像素逐個對比的方式來計算出兩張快照中不同像素點的比例,比例越高則快照越準確,但實際上這種方法無法體現出真正的相似度和用戶的體感。例如,當兩張快照位置只要稍有偏移,得出來的相似度值可能很低;或者是兩張色差很小的快照,也可能得到很差的結果。并且,快照的像素數可能達到上百萬量級,測試發現逐個的像素對比工作一次可能就會耗時數秒鐘。

          我們現在使用了 Google 以圖搜圖中用到的“感知哈希算法”來量化快照的準確性。算法本身流程大致是將圖片壓縮后得出一些“指紋”信息,然后通過對比不同圖片的指紋信息計算出“差異指數”。差異指數越高,則說明二者相似度越低。此算法能夠體現出兩次快照的相似程度,并且其效率比像素逐個比對的方法有了極大提升,線上數據統計到整個算法的耗時不超過 3ms。

          我們對一些場景進行了實驗并得出差異指數。可以看出,對于微小字符改變的場景,差異指數非常低;而有明顯視覺差距的場景中,差異指數會變高。這樣得出的量化值能夠體現快照對用戶帶來的真實體感的影響。



          場景差異指數視覺效果

          少量字符變化

          1

          整體偏移

          6

          如何復原錯誤快照場景

          能夠感知快照的準確性后,對于準確性較差的快照,我們還需要知道快照和真實頁面相差在什么地方,進而改進快照的時機。

          這里,我們是通過獲取快照時前端頁面 DOM 樹的方式來追查當前的真實頁面情況。具體操作是在生成快照時獲取當前小程序 HTML 頁面脫敏后的 DOM 樹信息,然后再依賴小程序框架的 CSS 文件,最后直接用瀏覽器就可以恢復出快照時的界面了

          其它能力

          局部快照

          快照的一個比較大的局限性就是無法適應多變的首屏場景,這種場景使用快照很容易導致每次快照都無法跟真實首頁重合,反而降低了用戶體驗。所以我們考慮提供一種能力,只對首頁中每次都基本不變的部分進行快照,而其它多變的部分不進行快照,這樣也能夠每次使首屏部分內容實現秒出。

          例如釘釘里的人脈首頁,上半部分是相對固定的展示,而下半部分 feed 流可能每次打開都會展示不同的信息。那么在這種場景下我們就沒必要每次都對整個首頁第一屏進行快照,可以指定一定高度的部分進行快照,讓首頁的一部分實現秒出。

          超一屏快照

          當首頁可滾動時,我們甚至可以考慮超過一屏長度的快照,并且下次小程序啟動時展示快照時讓快照可滾動。此方案需要注意兩個問題:

          1. 快照大小
            線上統計顯示,一屏的快照文件平均大小在 100K 左右。如果是超一屏的快照,大小可能會達到幾百 K。需要在生成快照時預估一個長度上限或快照大小上限,以防快照使用時在低端機中出現 OOM 等異常情況。
          2. 快照滾動
            如果快照在展示時用戶進行了滾動操作,那么在隱藏快照時需要記錄當前滾動的偏移量,以便將真實首頁也滾動到指定位置,才能讓快照和真實頁面重合。

          性能

          對于快照的性能表現,我們進行了實驗室測試和線上的數據統計。

          實驗室測試中我們構造了一個超大的快照(5.2M)的極限場景,并在低端機上與正常快照進行了對比:



          普通場景極端場景

          快照大小

          262K

          5.2M

          內存占用

          1840K

          3245K

          加載視覺體驗

          直接出現

          有極短暫延時

          快照加載過程并沒有影響正常的頁面切換,只是在過大快照的加載可能有短暫的延時。

          線上數據顯示,帶快照的頁面加載耗時在 280ms 左右,快照的平均大小約 110K。

          快照的生成和準確性檢測等工作都是在異步線程中進行的,此時用戶交互并未開始,并且在用戶滾動、交互后不會進行快照,不會對性能造成太多影響。

          這里還有個比較有趣的數據:用戶對快照的平均點擊次數是 0.6 次,首次點擊時間約為 1500ms。也就是說,當快照展示 1.5s 后,有一半多的人會開始首次的交互。這也足矣說明讓快照具備可交互能力的重要性。

          展望

          快照技術雖然緣起是為解決小程序啟動性能問題,但實際運用場景可以擴展到更多地方。

          理論上來說,任何形式的異步渲染場景,不論是現在 WebView 還是 weex 渲染的小程序,或者就是普通的 H5 網頁,甚至是一些 native 的場景(需要 loading 的場景),只要是一塊能夠在客戶端中展示的視圖,都能夠運用快照技術解決其過程中的白屏或 loading 問題,并且都能做到秒出、可交互。因為快照是一個純 native 的技術,它的實現本來就不依賴于真實頁面的渲染方式,它更需要關心的是更合適的快照時機和應用場景從而獲取更佳的體驗。

          總結

          我們提出了一種全新的小程序快照技術,實現了小程序首頁的秒開和可交互。它能夠徹底消除小程序打開過程中的 loading 或白屏現象,讓小程序打開達到了 native 的體驗,還可以響應用戶點擊交互。

          它是一種純 native 的技術,不依賴于小程序容器和前端的渲染,只要有視圖就能快照,只要有快照數據就能立即展示,甚至可擴展運用于其它非小程序場景。

          而其局限性主要是依賴首屏樣式和快照時機選擇,多變、含用戶隱私數據的首屏不適合快照,而且優質快照的生成的時機要求比較苛刻。在快照準確性保障方面,快照的相似度對比方法上也仍然有很大的優化空間,這些都還需要在今后不斷打磨。

          nap2HTML綠色單文件版是一款簡單小巧的文件夾快照生成工具,軟件可以幫助用戶輕松創建文件夾結構快照,生成html文件列表,內置文件搜索器,所有內容都會包含在同個html文件內,生成Windows目錄列表,便于用戶的查找、收藏和整理,支持導出文件列表作為txt純文本、JSON或CSV格式使用,非常方便!喜歡的伙伴們可以移步簡易下載站獲取!

          Snap2HTML綠色中文版

          Snap2HTML綠色單文件版功能介紹:

          1.自帶搜索功能,文件查找更方便

          2.由風之遐想漢化,懶得勤快綠色單文件便攜制作,全中文界面易于使用

          3.生成界面類似早期網盤,顯示文件的大小、日期以及數量等常用的信息

          4.文件夾快照、目錄快照,支持生成html文件列表,便于文件的整理搜索和使用


          H5頁面給人的感覺通常是開發成本低、迭代速度快但使用體驗不佳。其中最容易被用戶感知的體驗問題就是首屏速度,由于H5頁面的所有資源都需要實時從網絡上下載,提升這些資源的加載速度就可以明顯提升首屏速度。我們通常將提前下發資源到App,并在打開H5頁面時使用預載資源加速訪問的技術稱為“離線包”。

          B站的離線包技術方案與大部分互聯網公司實現的方案在底層邏輯上是一致的,都實現了基礎的資源下發和攔截匹配機制。但在此基礎上我們也有一些創新,例如頁面快照技術、AB實驗能力,同時也做了很多優化,包括用掃碼調試降低調試成本、版本快速收斂、預約定時錯峰發布等。目前我們的離線包技術已經接入183個項目,覆蓋12個業務線,在公司內被廣泛使用。


          頁面加載速度的瓶頸分析



          當我們用工具分析一個典型H5活動頁(2024紀錄片開放周)時,可以發現一個頁面的速度瓶頸主要在這些方面:

          HTML請求

          通常按照HTML是否動態生成,將其分為SSR(服務器端渲染)和CSR(客戶端渲染)頁面。

          SSR受服務器負載、頁面復雜度和網絡拓撲影響,首字節時間相對較晚,但一旦收到HTML響應,瀏覽器引擎就會立刻開始解析和繪制,所以首屏時間相比CSR會更有優勢。

          CSR的優勢在于可以很容易地使用部署在各地的CDN服務節點來加速,且可以配置一定時間的緩存,所以首字節時間更短,但所有頁面結構都需要JS運行后才可以產出,首屏時間會晚很多。

          通常CSR頁面的響應時間大概在10ms-50ms,SSR頁面通常50ms - 數百毫秒。SSR頁面在等待服務器渲染上會耗費更多時間,根據之前一些案例的經驗結果,SSR對中低端機型的首屏提升比離線包大,離線包在對高端機型的首屏提升比SSR大,其原因在于離線包下渲染頁面屬于單純的CPU密集型任務,高端機型處理器較好,相對服務器端需要排隊處理的SSR會有更快的響應。



          主JS下載&編譯

          目前前端項目通常都會使用JS框架,疊加引入3-15個左右的第三方庫,產出的JS Bundle一般會有數百KB-2M左右的大小,下載需要花費數百毫秒的時間。較大的JS文件也會讓JS引擎的分析過程更加漫長,一般看CPU速度,需要十幾毫秒到幾十毫秒的時間。

          在SSR項目中,主JS過大通常導致用戶首次交互時間變晚,一些場景下,用戶會發現頁面一開始無法響應交互,在JS運行完成后才可以正常使用。

          在CSR項目中,主JS過大會導致頁面白屏時間較長,需要等待一段時間后才能出現頁面內容。

          請求頁面主接口

          單個接口通常消耗30ms-100ms的時間。但JS往往會等待接口返回后,才會開始生成實際的框架內虛擬DOM樹,所以這個同步等待的時間越長,白屏時間也越長。SSR項目通常會在渲染服務器上調用這個接口,利用內網網絡高帶寬低延遲的特性,降低這塊的耗時。CSR項目可以考慮提前加載時機(接口預取或請求前置)或緩存上一次的結果來盡量減少請求耗時。

          應用虛擬DOM樹生成

          這一步通常都是Vue或者React這類框架在運行應用代碼,生成虛擬DOM,進而將虛擬DOM掛載到實際的DOM樹中。這個過程伴隨大量的CPU密集計算,可能需要幾十毫秒到百毫秒的時間才能完成。這一步由于視圖結構受業務形態影響很大,通常難以優化,比較可行的方案是先加載首屏的、重要的組件,延后加載非首屏、不重要的組件。

          加載首屏資源

          當頁面實際的DOM構建完成后,瀏覽器才會知道需要加載哪些資源,并開始下載。這個過程大概需要數百毫秒。這一步通常會使用CDN加速、文件名帶哈希+強緩存、優化圖片和其他資源體積等方式,優化加載速度。

          綜上,一般一個頁面從開始加載到用戶看到相對穩定的頁面,通常需要1.2秒到2.5秒左右的時間。其中相當的一部分時間耗費在網絡IO(下載資源和請求接口)上,剩余的主要就是JS編譯運行的開銷,以及瀏覽內核處理Layout和Render的時間。其中比較容易優化的主要是網絡IO時間。而在JS代碼運行和界面布局效率方面,在不同的業務場景下有較大的差異,需要根據業務場景量身定制。


          離線包技術方案簡介


          離線包的技術基礎就在于Webview為調用層提供的請求劫持能力,匹配資源路徑后可直接返回本地文件內容。

          Android系統可以直接使用shouldInterceptRequest API來實現,可以攔截https或者私有協議的請求。而IOS是基于WKURLSchemeHandler hook系統方法來實現https的攔截,但由于系統Bug在post請求中存在body丟失和上傳圖片可能導致進程崩潰的問題。對于這個問題,我們一方面要求H5頁面使用JSAPI來發起POST請求,另一方面限制僅在命中離線包的情況下開啟https攔截。在此限制下,因此這套方案基本可用。

          解決了請求劫持問題,接下來就可以實現透明的資源加速能力了。我們需要先完成資源的準備:


          代碼資源的準備


          資源準備的過程比較簡單,與構建工具無關,只要收集頁面所需的資源文件,并且配置好URL到本地路徑的映射關系就可以了。

          通常會有兩種代碼來源:

          1. 代碼工程:由前端工程構建產出的文件。這類情況下,枚舉每個產出的文件,去掉一些過大或不常用的文件,加上一些需要額外加載的線上資源,就可以打包發布了
          2. 線上抓取:其他無法控制源代碼的線上業務頁面,例如低代碼平臺產出的頁面。這類情況下,可以使用模擬瀏覽器環境,抓取實際請求的資源列表,按照用戶定義的規則去掉部分文件后,就可以打包發布了

          最后打包的目錄,大概包含以下內容

          • config.json用來描述客戶端需要攔截的URL列表,攔截的一些基礎信息和資源映射表
          • html 入口文件,客戶端根據config.json的描述找到這個入口文件,并交給webview加載這個文件
          • 其他資源文件(css、js、圖片、字體、svg等),這些資源會在頁面運行時發起請求,由客戶端查表攔截,并返回給webview使用

          一個典型的代碼產出目錄如圖所示:



          config.json示例:



          離線包的下發機制


          離線包的下發依賴Fawkes(客戶端統一平臺)的ModManager,使用與客戶端統一的下發渠道,有利于統一調度,控制下載期間的CPU、帶寬占用和存儲空間占用。

          ModManager是用于下發客戶端資源的一個統一渠道,絕大部分需要動態下發的資源都會通過ModManager統一管理和分發,并具備了諸如增量包生成、資源錯峰下發、熱推送下發、版本和網絡環境限制等諸多能力。復用這套基礎能力省去了我們額外建立一套下發鏈路的成本,也能更容易地管控CDN流量以及客戶端存儲空間占用。

          離線包平臺有三種發布模式,分為常規發布、錯峰發布和預約發布,這些發布模式的實現方案有一定的差異:



          常規發布


          代碼包將直接發布到線上,所有符合限制條件(版本、時間、灰度等策略)的冷啟動App的用戶都將按照資源包優先級逐步下載到新版本。

          如果用戶本地沒有這個包的舊版本,那么會下載全量包;如果已經存在較近的舊版本,則會下載增量包。但即使有增量更新能力,在用戶使用的高峰時段,常規發布仍會帶來較大的帶寬壓力,所以高峰期發布需要額外審批。



          如圖所示,在常規發布后,資源包的下載會快速制造一個流量高峰,而后隨著覆蓋率的提升,緩慢下降,直到大部分用戶完成更新。因此,我們除了常規發布外,還需要更智能的發布策略來降低成本。


          錯峰發布


          錯峰發布模式下,會設置離線包延后1-3天生效,在此期間,會借用流量低谷下載離線資源包。從而有效利用CDN的閑時流量,節約成本。

          錯峰發布適合第一次發布的包,可以利用閑時流量緩解第一次全量下載對用戶的流量壓力。后續版本再發布時則可根據上線時間要求靈活使用發布模式。


          預約發布


          在離線包的實踐中有這樣一個問題:直播業務的大部分頁面都接入了離線包,但某些頁面常常需要在晚上高峰期上線發布,但在高峰期上線離線包又會帶來很高昂的帶寬費用。如何既保證可以及時發布,又可以兼顧離線包呢?我們設計了預約發布功能。

          預約發布允許設置后續24小時內的某個低峰時間點發布。設置后,當前線上使用的包版本可以設置即刻失效,用戶降級請求線上頁面訪問新版本內容。而新版本會在指定時間點發布,在此后更新完成的用戶,就可以正常使用加速能力。

          預約發布適合需要在高峰時段上線新功能,且對短時間內性能下降不敏感的業務。即可以取得足夠快的發布速度,又可以避免較高的帶寬費用。缺點是會影響這個時間段內用戶訪問頁面的速度,也可能對線上服務造成突發壓力。


          資源匹配機制



          如圖所示,當Webview加載一個URL時,會經歷以下環節:

          1. 確認當前是否可以使用離線包。這包括確認全局開關、url開關和協議匹配情況。
          2. 如果當前頁面URL命中了離線包,那么則確認本地包版本是否在接口下發的版本白名單中,如果存在,則進入到離線包加載流程,否則就正常走線上請求。
          3. 進入離線包加載流程后,會開啟容器的https攔截能力,此后所有實例內的資源請求都會被攔截來確認是否存在對應的離線資源。同時開始加載離線的Html資源。
          4. html加載后,Webview會發起對JS和圖片等資源的請求,此時請求攔截器識別到這些網址都在資源映射表中,就會使用本地文件系統的資源返回。對于不在列表的文件,則正常拉取線上資源后返回
          5. 此外還有一種特殊的資源類型,名叫公共資源。這類資源位于單獨的公共包中,當頁面命中離線包后,如果url符合公共資源的命名要求,就會嘗試去加載本地資源。
          6. 對于所有本地資源加載失敗的場景,都會回退到請求線上資源,至少保證功能可用。


          版本控制和快速回滾策略


          離線包由于是預先給客戶端下發資源,所以不可避免地會遇到更新時效的問題。在更新時效上,我們之前在使用ModManager時會遇到這些問題:

          1. 更新不及時。比如在修復線上bug或者響應業務的臨時改動時,希望用戶能盡快更新到新版本,但要等到大部分用戶更新到新版本需要很長的時間(一般發布完1小時能覆蓋90%,2小時覆蓋97%)
          2. 更新觸發頻率低。常規優先級的包,只會在冷啟動時才會拉取,所以很多用戶如果一直開著App,就會一直停留在老版本
          3. 版本殘留。由于更新時間是在App冷啟動時,而冷啟動后可能有很多資源包需要更新,所以往往會在沒有更新完成前就使用到了對應的H5頁面,訪問到舊版本內容。
          4. 下線后資源包被刪除。當用戶有緊急情況需要臨時下線資源包時,如果一個包的所有版本都被下線,則會刪除這mo d個包的資源。再次啟用這個包時,就需要一次全量下載。

          考慮到前端離線包的主要目的是加速,不同于客戶端包下載到資源才可以使用對應功能的限制,我們可以靈活利用線上兜底策略,來解決這幾個問題。

          于是在ModManager機制的基礎上,我們又額外引入了一個接口(x/offline/version接口),用來告知客戶端當前可以使用的版本白名單。客戶端會在每次冷啟動和切后臺返回時刷新接口數據(有較短時間的限頻和緩存邏輯),從而可以實現以下能力:

          1. 發布新版本時設置舊版本過期時間。靈活控制舊版本的下線時間,如果在指定時間后,用戶仍然沒有更新到新版本,那么就會直接請求線上資源。一般設置2個小時過期,就能保證大部分用戶仍然可以享受到離線包的提速,也能保證不存在版本殘留問題和更新不及時的問題。
          2. 快速下線。在出現線上緊急Bug需要修復時,可以使用版本白名單,強制客戶端走線上請求,但仍然保留本地離線資源包,從而方便后續發布新版本修復問題。
          3. 預約發布。預約在指定時間發布離線資源包,在此期間會強制用戶走線上請求,從而兼顧上線速度和流量成本。

          版本接口的大概形式是這樣的:



          這個接口采取版本白名單機制,在接口參數中確定了App環境信息后,會查詢出這個環境這個App下可用的包版本列表。在發布新版本時,白名單里會同時出現2個版本,從而允許舊版本離線包仍然使用一段時間,在指定時間后,會只下發最新版本號,舊版用戶在更新完成前會請求線上資源。

          版本接口為了提供較高的時效性,每次切后臺返回或者冷啟動都會請求刷新,會有較高的QPS,我們在接口上做了充足的緩存策略,所有數據均是異步定時更新并完成計算,在請求到達時始終從內存緩存中返回數據,服務資源占用率比較可控



          上圖表示了一個設置了1小時舊版本過期的項目在占有率變化上的趨勢。版本控制功能主要解決舊版本過期問題,可以使得舊版本在過期后立刻失效,而新版本不受影響。


          調試流程優化


          離線包由于需要等待客戶端將包下載完成才可以體驗,因此在開發調試階段會帶來很大的痛苦。為此,我們仿照小程序的開發流程,設計了掃碼調試功能,在開發、預覽等場景上,均可以使用掃碼調試功能快速預覽頁面在離線包下的效果。



          掃碼后,在預覽調試頁面,會調用JSAPI要求客戶端下載指定的zip資源包,放置到特定的調試目錄。后續在App存續期間再次打開對應的H5頁面時,會優先使用調試目錄內的資源,而其他機制保持一致,因此在大部分場景下可以完美還原線上離線包的使用體驗。

          另外在打開的調試頁面上,還會注入離線包調試工具,幫助業務發現有哪些未命中離線的資源以及查看Performance Timing API數據,幫助優化離線包的文件組合,提升加速效果。


          HTML快照


          在很多需要優化首屏速度的場景,使用SSR都是一個可行的方式,在服務器端提前渲染好整個頁面的HTML結構,這樣瀏覽器拉取到HTML后就可以立刻開始渲染,用戶就能更快地看到內容。

          SSR能夠加速的核心條件是提前渲染完的HTML,如果我們可以緩存上一次的頁面渲染結果,是否也可以用于加速下一次用戶進入時的首屏速度呢?顯然是可以的,只不過需要先解決一些問題,例如

          1. DOM一致性問題。比如JS運行時會修改DOM實現動態樣式、rem布局等能力,這些修改的副作用需要考慮哪些要留下,哪些要舍棄的問題。
          2. 反復生成快照帶來的副作用累積問題。由于每次都會使用當前頁面的DOM作為快照,但直接使用快照頁面再次加載時,JS邏輯會再次執行一邊,導致出現多套副作用累積,導致不可預期的bug。
          3. 緩存問題。如何保證代碼上線后,用戶不會仍然因為快照訪問到舊版本。如何保證不同參數進入頁面時,頁面數據的隔離性。
          4. 數據時效性問題。一些時效敏感數據,如何針對性地從緩存里去除,避免舊數據對用戶的誤導
          5. 前后的銜接問題。不像SSR有成熟的方案和框架支持,可以很順暢地從快照的DOM結構中注入JS交互邏輯。快照頁面通常只能重新生成一套DOM,完整替換原來的DOM,所以銜接的時機就很重要

          基于上述問題,離線包HTML快照能力做了一些針對性的方案。不過我們可以先看一下這個功能的運行流程:



          如圖所示,當用戶請求一個URL時,會先判斷是否命中離線包,如果命中,則檢查是否存在HTML快照,如果存在快照,那么使用快照HTML,否則使用離線包內附帶的HTML。當頁面完成加載后,會在頁面上調用一個JSB,用來存儲當前頁面的快照。

          關于快照,還設置了以下限制:

          1. 每個快照使用1次后即刪除,必須在加載快照的頁面上,調用JSB重新存儲當前頁面的快照,才能讓下一次頁面加載時使用。刪除的條件為:當前打開的容器,存續時間大于3秒,這個是為了防止用戶誤操作導致快照被刪的問題。
          2. 當離線包更新版本后,將不能使用舊版本離線包下創建的快照,即你需要將離線包版本當作一個key。這是為了保證代碼更新后,頁面可以正常更新。
          3. 快照需要有緩存key策略,允許前端根據url query、cookie、登錄狀態等參數區分頁面,防止頁面數據錯亂。
          4. 快照需要設置最大存儲時間和存儲隊列長度。最大存儲時間可以由jsb設置,但最長不超過5天,存儲隊列長度,每個url下(去query的)最大存儲20個HTML快照。整體快照大小不超過10MB

          除此之外,對于設置的HTML內容,還需要注意以下問題:

          1. 使用原始空殼HTML作為基礎,注入當前頁面#app節點內的結構。如果使用rem布局則追加設置rem的fontSize,整合后的HTML才用于設置快照
          2. 應用代碼在識別到當前使用快照時,先完整生成dom結構,再替換當前#app的內容
          3. 每個應用自行處理好時效敏感數據節點內的數據

          這樣基本可以解決上述提到的問題。HTML快照的技術基礎是在基于一個假設,即頁面DOM結構會與數據一一對應,所以相同的數據輸入,無論多少次渲染都應該輸出相同的DOM結構。那么只要提前緩存DOM結構,就可以在下次訪問時先展示舊頁面,再無縫切換到新頁面。

          這個假設雖然大部分情況下雖然成立,但卻仍舊缺乏業務實踐,具體在實踐中會遇到哪些問題,需要在生產應用后才能有相對靠譜的最佳實踐。這方面的實踐會在今年展開,歡迎持續關注與合作。


          AB實驗


          由于離線包會攔截入口HTML文件請求,在頁面做了大改版需要頁面級AB實驗的場合,就無法在服務端來進行分流決策。為此,我們使用網址Rewrite能力使得同樣一個URL在不同用戶側對應兩份不同的離線包。

          其大概流程如下:


          1. 在每次熱啟動請求離線包控制接口時,接口計算該用戶對應的AB實驗分組
          2. 根據對應的分組,下發url重寫規則,將地址重寫到對應版本離線包的URL
          3. 客戶端在打開頁面時,會根據映射表,將URL重映射到新的URL上,并且按照參數要求添加分組結果參數
          4. 最后按照修改后的url打開頁面,頁面上報URL參數里的分組結果

          上述方案可以實現頁面的AB能力,但由于AB分流決策與頁面打開的時機不同,所以無法上報進組數據,只能上報用戶實際進入頁面的數據,所以會影響部分數據的回收。



          新舊版本的配置差異如上圖所示。使用兩個不同的URL,就可以分別對應兩個不同的離線包,從而在不影響離線包本體設計的基礎上實現AB能力。


          適用場景與局限


          離線包提供的幾個核心能力,可以有效提升頁面的首屏速度,但仍然因為技術實現和架構的原因,存在一些限制:

          存在失效場景,需保留線上訪問能力

          在某些場景下,離線能力會暫時失效,從而會請求線上頁面。接入離線包的業務需要自行確保線上URL可訪問且功能正常,只能把離線包作為加速手段,而非最終部署目標。

          失效的場景包括:App版本過低、尚未更新到可用版本的用戶(新下載App用戶或長期低活躍用戶)、緊急下線或預約發布期間由業務觸發的暫時禁用

          增加上線復雜度

          接入離線包后,需要在上線前完成離線包功能驗證,且需要在線上頁面上線完成后,再操作離線包發版。會增加一定的上線復雜度。

          雖然離線包的資源預載和接口預載能力已經比較成熟,通常不會引入額外的問題,但仍然需要注意自測。

          上線鏈路復雜的問題,通過在公司前端統一發布平臺集成離線包發布流程來解決。

          帶來額外帶寬成本

          每一個上線發布的離線包,都會下發到絕大部分用戶的手機中,而很多用戶可能根本不是這個業務的目標用戶。同時下發的過程也會帶來額外的帶寬成本,尤其是在高峰期上線時。

          目前我們確實還沒有精細化提升帶寬使用率的能力,未來可能會看情況,采用人群包或其他方式,優化下發資源的精準度。

          Webview本身的限制

          目前由于IOS系統在使用https攔截能力時的一些Bug,需要格外注意這兩個問題:

          1. 不要在離線包的頁面內直接發起post請求,包括post數據或者圖片。如果有需要的話,可以使用基于JSAPI請求的公司內NPM包
          2. IOS離線包下,使用history.push,左滑時會退出整個webview容器,而非返回上一頁。目前技術無解,只能使用ability.openScheme打開新的容器來替代

          綜上,哪些業務適合接入離線包呢?離線包通常適合以下類型的業務:

          1. 有一定的用戶體量,例如日PV大于5萬的業務(不是硬性要求)。這一點主要是出于資源利用率考慮。
          2. 短時間內有大量用戶訪問的活動頁,或者有常駐入口的業務流程頁面
          3. 營收相關或者速度敏感的業務,提升用戶體驗可能對營收有一定的幫助
          4. 相對穩定,改動不頻繁的業務


          離線化的終點在哪里?


          當我們將越來越多資源提前緩存在用戶手機里,免去網絡IO這一最大的速度瓶頸,就會發現其實它跟客戶端界面業務差距并不會很大。這個存在形態位于Native和H5之間,它犧牲了一些發版的便利性,但獲得了更快的加載速度。雖然我們無法修改承載H5的Webview本身,也必須要承受Webview帶來的性能問題,但我們也很好奇如果將一個頁面優化到極致,是否真的可以讓用戶無法通過加載速度區分哪些頁面是Native實現,哪些頁面是H5實現呢?

          后續我們將在一些頁面上實驗這樣的速度優化策略:

          1. 所有代碼自身的資源離線化,這個目前就能實現
          2. 代碼通過script標簽引入的三方庫離線化,通過離線包平臺自動化管理三方庫的公共包,自動更新自動發布。保證頁面本身的代碼資源不再需要通過網絡加載
          3. 接口數據緩存和stale-while-revalidate策略組合。頁面預先使用上一次緩存的接口數據,等接口預載數據Ready后,替換成最新數據。(類似客戶端頁面進入后,先看到舊數據和loading標志,隨后刷新為新數據)
          4. HTML快照策略。充分利用HTML快照,在JS還未執行時就能看到首屏頁面
          5. 其他常規優化手段。包括優化第三方庫引用、優化代碼執行流、觀察代碼性能瓶頸并解決等。

          當這個實驗做完后,會再寫一篇文章,總結這樣綜合實踐方案下的經驗,為其他業務的優化提供參考。我們的目標是樹立起H5性能的新標桿,讓更多的業務可以使用H5來承載,釋放業務迭代的潛力!


          性能表現


          全局性能表現:

          Android:



          IOS:



          整體看,離線包對Android系統的頁面加載提升服務比較明顯,對IOS系統的提升相對幅度小一些,但也有可感知的速度提升。

          從單獨業務角度觀察,從番劇片單頁來看,離線包的速度提升效果如下:



          由于PV數較小的性能數據不可置信,因此看可置信數據而言,在onload事件的平均時間上,非離線包狀態的加載時間約為2700ms左右,簡單接入離線包后,加載時間約為2300ms,提升400ms。

          在經過對資源的詳細分析,將所有線上加載的js文件都打入離線包后,加載時間下降到2016ms,可以再提升300ms。總體而言,離線包可以為簡單H5頁面能提供700ms的加載性能提升(onLoad)。且在用戶觀感上,頁面基本可以在切換動畫完成前展現。



          配合頁面的FCP數據,可以看到在優化后,頁面的首屏渲染時間可以從1550下降到1230,能提升300ms,且基本接近“秒開”。

          在其他更復雜的頁面上(更多圖片、更多JS),離線包可以取得更好的性能提升效果。


          業界方案對比


          B站方案與業界方案的差異點


          綜合來看,B站的離線包方案有以下亮點:

          錯峰發布:通過錯峰發布,充分利用CDN閑時流量,可以用較低的成本下發資源

          版本控制策略:通過版本控制策略,避免舊版本滯留,符合前端業務一次發布所有用戶更新的常規心智,對于需要緊急更新或下線的場景比較重要

          調試流程優化:掃碼調試可以明顯提高接入效率,保證線上穩定性是性能優化的第一前提。

          HTML快照:利用上一次渲染結果優化下次進入的首屏速度,雖然有較多的調試成本,但也有一試的價值。這個方案在某些社區文章里有被簡單提到過。

          AB實驗能力:保證頁面重大改版期間性能不下降,且能回收AB實驗數據,兼顧用戶體驗和業務訴求


          同時,有些業界常見的方案我們選擇不去實現,例如:

          Webview預載

          提前初始化一個Webview掛在后臺,等需要的時候直接使用。這個方案在前期因為公司內各業務容器不統一而無法落地,現在統一后發現預載收益有限,B站App內的容器二次加載耗時基本在130ms以下,為了這點時間而長期占用較多的內存,在一個視頻觀看體驗優先的App上并不明智。而如果為提升首次初始化耗時而在App啟動時預載Webview,又會影響用戶的啟動速度,因此作罷。

          接口預請求

          同樣也是收益不夠高的問題。這里我們談的是在Webview初始化時提前發起接口請求的方案。在一個充分優化的離線包上,理論上在html和js加載后,會很快開始接口請求,所以有概率在請求還沒返回時,前端就開始從客戶端取數據,處理這個中間狀態有一定的成本,也會帶來額外的調試心智負擔。理論的性能優化表現應該在50-150ms左右,相對來說動力不足。

          對于像Feed流預加載后續文章接口的實現方案,由于通用性不足,不作為全公司級離線包需要考慮的方案,可以在各業務層自行實現。


          有一些技術是我們已經實現了,但其他人也都有類似實現的:

          公共包技術:集合公共JS資源,避免多份打包,同時利用平臺能力,自動完成公共資源更新

          增量包下發:對每個版本的包,都會生成這個包和前N個版本差分計算的增量包,根據客戶端當前版本下載增量包,降低CDN壓力

          熱更新推送:可以對某些高優先級業務,開啟熱更新推送,助力快速更新

          僅Wifi下載和版本限制:對下發條件做一定的限制,節約用戶流量


          Webview常駐方案


          簡介:對于Feed流或者頻繁打開同類頁面的場景,可以保持Webview常駐后臺,點擊鏈接時將Webview拉到前臺,并替換頁面的URL參數,頁面拉取接口數據并展現。

          差異點:



          Webview常駐方案

          B站離線包方案

          性能

          非常快。由于省去了容器初始化和頁面JS初始化的諸多流程,這個方案的性能優于離線包,配合feed流數據接口預取,可以讓用戶幾乎無法感知到加載過程。

          性能比較常規,在不使用HTML快照這樣的特殊方案時,性能與業界方案是類似的。

          方案通用性

          較差。適用場景相對局限,比較適合頻繁往返于列表頁到詳情頁的這類場景,例如新聞閱讀類應用、商城類應用等。

          通用性好,同樣的方案可以在B站絕大部分H5業務上使用。漫畫App、云視聽小電視、直播姬等App的也已低成本接入

          代碼侵入性

          強。需要代碼做針對性適配,以及客戶端配合才能完成一個特定場景下的落地

          侵入性低,除了需要注意Post請求問題和History API在IOS上無法記錄歷史記錄外,其他代碼與線上業務完全相同


          總體看,B站的離線包方案為了方案通用性,犧牲了一些性能表現,好處是可以比較簡單地應用在各業務場景上,帶來相對普遍的價值。


          貨拉拉的離線方案


          簡介:貨拉拉方案與本文提到的方案在某些方面是有共通之處的,在基礎加速層面上除了底層的技術實現思路采用了加載本地路徑的方案,其他包括資源下發、多層降級機制等方面基本類似。

          差異點:

          由于技術實現原理類似,因此在性能和通用性上,兩者差異較小。我們從另外的方面比較:



          貨拉拉方案

          B站離線包方案

          容器實現位置

          基于通用容器上的一個獨立增強容器,各業務容器基于此拓展

          直接放入基礎容器內,開箱即用

          兼容性

          存在跨域問題

          基于https攔截,與線上幾乎一致

          代碼侵入性

          有少量侵入性,需要對本地加載的路徑做一些適配

          侵入性低,除了需要注意Post請求問題和History API在IOS上無法記錄歷史記錄外,其他代碼與線上業務完全相同


          另外我們觀察到貨拉拉方案里離線包的URL映射是動態下發的,B站的離線包URL映射則是打包到包里的,另外通過一個動態接口控制當前可用的版本。

          這兩個方案都是可行的,總體來看,有這些差異:



          動態下發URL映射

          打包URL映射

          優勢

          方便靈活,可以很簡單地修改映射關系

          有利于這個包的邏輯獨立性,這也是掃碼調試功能好實現的一個因素

          缺點

          可追溯性差,配置不斷在變化,配置和包資源無法一一匹配時,可能導致過去歷史版本可能因為配置變更而不能再看

          靈活度差,修改URL映射需要發一個新的包


          支付寶的Nebula方案離線包能力


          支付寶的離線包的定位并非是加速線上業務的能力,而是更接近離線H5應用的模式。在離線包未下載的時候,他會請求提前部署的降級資源,也就是使用的業務并不需要自行部署一套頁面在線上,需要有一個域名和url,而是只需要使用它提供的方案就可以完成部署。

          其運行機制,類似貨拉拉方案,是用file協議加載,向H5頁面提供一個虛擬域名供識別。

          在API能力上,配備了一套JSAPI來滿足業務的使用,所以看起來比較接近小程序的實現思路。

          總體來講,支付寶因為業務都是以一個個獨立的H5App來承載的,H5 App之間彼此關聯和交互較少,所以方案會接近小程序,而后續支付寶也確實往前走了一步,實現了小程序的能力。

          B站由于頁面基本都是自有的,且Native跳H5、H5跳Native的場景很多,需要為用戶提供一個整體性的體驗,所以方案更偏向“加速”,而非獨立H5 App


          UC的NSR方案


          UC瀏覽器在新聞feed流頁面加載中采用了NSR(Native Side Rendering),首先在列表頁中加載離線頁面模板,通過Ajax預加載頁面數據,通過Native渲染生成Html數據并且緩存在客戶端。

          NSR本質是分布式SSR,將服務器的渲染工作放在了一個個獨立的移動設備中,實現了頁面的預加載,同時又不會增加額外的服務器壓力。

          下面看方案對比:



          NSR方案

          B站離線包方案

          性能

          更快,通過客戶端渲染,可以不依賴Webview生命周期就可以提前渲染出所需的HTML

          一般,仍然需要等待容器初始化+html加載+JS編譯執行+接口請求+框架渲染

          方案通用性

          相對一般,有一定的定制性

          比較通用,整體都是在Web標準上做的一些優化

          代碼侵入性

          較強侵入性。一個是代碼編寫接近常規帶SSR的頁面,另一方面NSR渲染結果和Webview內展現的過程是有一定邏輯的。

          較低侵入性。使用常規方案幾乎不需要改代碼。

          如需更快的性能,代碼快照會類似這套方案,只不過復用的是上一次渲染的結果。當然使用代碼快照也意味著帶來更強的侵入性。

          兼容性

          需要額外考慮在客戶端JS環境運行的兼容性

          較好,與線上頁面類似


          騰訊的VasSonic方案


          VasSonic除了常規的資源預載之外,還做了以下事情:

          • webview 初始化和通過客戶端代理資源請求并行
          • 流式攔截請求,邊加載邊渲染
          • 實現了動態緩存和增量更新

          VasSonic 的方案整體思路和效果非常不錯,特別是對于大部分 web 場景,通常我們的模板較少發生變化,大部分是數據部分變化,能夠很好的通過局部刷新做到秒開效果。對于首次加載而言,通過并發請求和 webview 創建帶來了不錯的性能提升,還能無縫的支持離線包策略。

          但是 VasSonic 定義了一套特殊的注釋標記及拓展了頭部,需要包括后臺在內的前后端進行改造,對 web 侵入性非常強,接入的工作量及維護成本會非常大。


          方案總結


          總的來講,離線包加速方案的各個技術決策其實就是在平衡通用性和性能。

          通用性越好,能做的事情越少,方案約需要遵循Web標準,當然性能的提升幅度也會小一些。

          拋開通用性,如果針對業務特定定制更多的優化方案,那么優化效果一定是可以做的更好的,當然也就更專用,難以大規模鋪開。


          B站的離線包在決策時更多考慮了通用性,因此得以在各業務線都能比較低成本地接入,有比較廣泛的使用群體。在此基礎上,例如電商業務也針對他們的業務特點做了更專用化的定制,例如Webview常駐方案,得到了更好的效果。所以可以認為離線包作為一個通用方案,提升了性能的下限,它并不與專用方案沖突,可以再額外使用專用方案來提升性能的上限。


          參考文章


          B站離線包在研發階段基本遵循凈室研發規則,除參考了業界方案的一些思路外,具體的方案設計和實現均為自研。

          在本篇文章的方案對比方面,主要參考了以下文章:

          【移動端h5秒開方案總結】https://blog.towavephone.com/mobile-h5-startup-way/

          【貨拉拉H5離線包原理與實踐】https://juejin.cn/post/7103348563479887885

          【CSR、SSR、NSR、ESR傻傻分不清楚,一文幫你理清前端渲染方案!】https://juejin.cn/post/6844904178519834638

          【離線化集成方案-安全設計與優化】https://research.szltech.com/?p=1935

          【WebView性能、體驗分析與優化】https://tech.meituan.com/2017/06/09/webviewperf.html

          【螞蟻金服金融科技產品手冊】https://docs-aliyun.cn-hangzhou.oss.aliyun-inc.com/assets/attach/87479/AntCloud_zh/1578025862524/10%20H5%20%E5%AE%B9%E5%99%A8%E5%92%8C%E7%A6%BB%E7%BA%BF%E5%8C%85%2020200101.pdf



          作者:大前端

          來源-微信公眾號:嗶哩嗶哩技術

          出處:https://mp.weixin.qq.com/s/WMf28adh30v67uuEn99F6Q


          主站蜘蛛池模板: 免费一区二区三区在线视频| 国产一区二区三区电影| 日本免费一区二区三区最新vr| 亚洲一区欧洲一区| 国产美女精品一区二区三区| 毛片一区二区三区无码| 少妇精品无码一区二区三区| 中文字幕一区在线播放| 亚洲国产激情一区二区三区| 精品免费国产一区二区三区 | 污污内射在线观看一区二区少妇| 国产精品高清一区二区三区| 精品国产一区二区三区香蕉| 午夜精品一区二区三区免费视频| 国产福利电影一区二区三区,日韩伦理电影在线福 | 午夜福利国产一区二区| 91福利视频一区| 一区二区在线播放视频| 中文激情在线一区二区| 无码AV天堂一区二区三区| 国产在线一区二区三区| 中文字幕一区二区视频| 国产一区在线视频观看| 爱爱帝国亚洲一区二区三区| 国产精品一区二区久久精品涩爱| 国产伦理一区二区| 人妻体内射精一区二区三四| 少妇无码AV无码一区| 一区二区三区无码视频免费福利| 国产精品福利区一区二区三区四区| 精品免费AV一区二区三区| 老鸭窝毛片一区二区三区| 日韩毛片一区视频免费| 久久久99精品一区二区| 亚洲综合一区二区精品导航| 亚洲av成人一区二区三区| 精品国产一区二区三区久| 精品国产日韩亚洲一区| 99热门精品一区二区三区无码 | 精品日韩一区二区| 国产精久久一区二区三区 |