补丁冷启动模式_Bilibili 移动端组件化实践中的冷启动优化
原標題:Bilibili 移動端組件化實踐中的冷啟動優化
編者按:移動互聯網的火熱改變著人們的生活方式,從移動社交到移動購物再到如今迅猛發展的互娛行業。移動端的力量不可忽視,Bilibili 的移動端項目目前已經進行了接近一年多的組件化實踐,其中部分方案是基于優化應用的冷啟動速度,本次演講的內容主要包括進程冷啟動優化和 H5 首屏優化的一些具體方案,以及方案落實過程中所遇到的一些問題以及應對措施。以下為此次演講的整理。
謝曉楓 Bilibili 安卓端高級開發工程師
嘉賓介紹:2012 年開始接觸安卓開發,參與過手機 YY 客戶端、嗶哩嗶哩客戶端項目的研發。 目前主要負責項目框架層類庫的開發, 經常實踐 TDD 保證代碼質量,技術實踐內容不僅局限于某一單一功能的開發,而是某一整體方案的落地,比較注重自動化、持續集成。不僅僅專注與 Android 開發,在前端、服務器以及腳本構建上也有相關研究。
|移動架構與性能優化
移動架構與性能調優,這個課題是必須涵蓋架構與性能,我們部門是剛好進行了一個一年多的組件化的實踐,有涉及到性能優化,特別是冷啟動優化的,這個可以講一下。
今天演講涵蓋以下的三點:
組件化方案
進程冷啟動優化
H5 首屏優化
這邊會分享一下項目中的組件化方案。性能調優方面會挑冷啟動優化來講。H5 是承載移動端業務非常重要的容器之一,因此會講 web 首屏優化的知識點。
在開始進行這個組件化演講之前的,我覺得是有必要說一下組件的概念。在以往的概念中,組件是指 Component, 它指的是項目里面框架型層部分的一個構建、沒有業務代碼的組件。比如有一個沒有業務代碼邏輯的圖片加載器,可以很方便移植到其他項目里,這個是比較傳統的組件。而最近一兩年組件化是一個新的概念,它比較像安卓里面的 Bundle, 主要是從業務的角度是來闡述這個問題,指的是多個業務方開發自己的組件,由各個組件組成一個完整客戶端項目的方案,所以大家不要把這個組件的概念搞混了,本次課題特指后者。
|組件化
圖 1
先看一下我們項目組件化前的結構(圖 1)。看上去也不是很混亂,而且是還包括了幾個應用。但是也暴露一些問題,依賴雖然比較清晰,但是庫的層次比較復雜。我理想的項目結構其實應該是一個三角加一個倒三角,頂端是一個應用層,中間是業務模塊,業務模塊一起依賴一個共同的框架層。
圖 2
再加上遠程庫之后就更復雜了(圖 2)。而且這還不是最糟糕的情況,我剛進來寫代碼的時候,大概還知道從哪個來入手,但是隨著人員增加,參與這個項目的小伙伴越來越多,這種項目結構肯定是不能滿足開發需求的。
因此剛剛展示的兩個圖有以下的問題:
項目模塊層級混亂,模塊耦合力度大,導致業務分離難度巨大
模塊依賴關系混亂,存在重復依賴、循環依賴、依賴不同版本問題
業務并行開發合并代碼困難,API錯誤使用問題顯著
無法并行貢獻代碼
層級比較混亂,因為這個依賴庫以及依賴不同的版本的問題非常的嚴重,會出現不知道依賴的庫是否正確,所以需要把這個項目的層級理一下。而且在這樣的結構下業務并行的開發是不可能的。這些種種的問題,都可以用一句話來總結,各個業務小組間沒有辦法并行地貢獻代碼。
我們需要的是通過組件化的這個方案,達到一個各個業務組之間一起貢獻代碼的狀態,那怎么做呢?
圖 3
首先,剛剛那個混亂的圖可以簡化成圖 3 ,它的問題是層級不明顯,我做的一個工作就是理清楚層級。我們分為這三層,右邊最下面這一次層是基礎庫(Foundations),這一層主要存放項目里面通用的一些庫,例如分享庫或者是圖片加載庫等。左邊這一層是屬于垂直的,貫穿到整個項目,所有的模塊都可以依賴它。右邊上面是應用層(APP),這一層層我們暫時先畫這么大,大家如果是接觸過比較大的安卓的項目,它們會有一個明顯的特點是 APP 的模塊非常臃腫,因為現在安卓的客戶端一方面是業務多,迭代周期也比較短,所以都是把代碼寫在一個模塊里面,一開始就解耦 APP 模塊是不容易的,我們先別管。
圖 4
接下來,我們在 APP 層逐步的進行業務解耦(圖 4)。一層一層的組件庫分離,這一層的組件層,各個組件層的代碼互相獨立,互不依賴。如果組件需要通信,只能通過路由(Route)進行通信。隨著 APP 層,越來越小后,大概變成這樣的圖 4 右邊一樣。當 APP 層是變成足夠小的話,就會沒有任何的業務代碼的,它只包括一些入口 api 或者配置清單之類的輕量文件。所有的業務模塊,都下移到組件層。
這里有一個細節,可能各位看的不是很清晰,圖里是有虛線和實線的區別,虛線表示的是遠程的依賴。本地的客戶端模塊只包含了 APP 模塊這個殼,沒有其他本地的模塊,所以這時候我們可以把 APP 模塊改稱為 SHELL 模塊了。
圖 5
到了這個階段,已經對這個項目進行拆分了,也就是做到項目級別的代碼分離。左邊這個是一個基礎庫的項目(圖 5),基礎庫是獨立維護的,每次基礎庫有更改都需重新發版,而且都有對應版本號。其他組件只能依賴于某個特并版本號的基礎庫進行開發。而右邊這是一個組件的項目,做到了不僅僅可以在客戶端項目貢獻代碼,也可以從組件項目上貢獻代碼。
這樣組件化的后,還有一個好處。不再需要臃腫客戶端來調試組件代碼,我只需要創建一個自己的組件項目,在這個組件項目的模塊里面寫相應的業務代碼。如果需要客戶端其他的業務功能,可以直接遠程依賴聯調,或者創建一個其他業務功能的一個 Mock 實現。講的比較通俗一點,登錄界面是非常華麗的,但是在我組件項目我完全不需要,我只需要提供兩個輸入框,讓我輸入賬號密碼就可以了。各個組件之間的代碼貢獻就可以做到并行化。
到最后,再把所有的組件打包在客戶端里面,再加上一個殼就可以了。而且如果我是直播,打包直播的組件,加上一個直播的殼,就可以打包成為一個直播項目。
剛才也有提到,我們還有插件項目。就是說我給他的一個插件的殼,那就變成一個插件了。這就組件化項目的好處和目的。
圖 6
剛才也有提到 Router,Router 是組件之間通信用的。那到底是怎么工作的呢?簡單來說,不允許直接調用組件的 api ,通過協議表示當前組件登錄的界面,如果是在另外一個界面是需要打開登錄界面,只需要是一個層級的 Router 。同樣的,不僅是可以實現 activity 的跳轉,也可以是實現別的組件的跳轉,甚至可以加上跨進程的框架,進行跨進程的數據傳輸。
Router的工作原理如圖 6,包括兩部分,一個庫,它是用來解析路由協議與獲取目標組件服務的框架,另一部分是注解處理工具(APT),功能是給組件生成對應的路由表。我們每一個組件都有一個路由表,每一個路由表表示組件的入口,可以通過路由表來查找路由提供的功能。這就是我們項目組件化的一個大致情況。
組件化,其實還涉及到一個動態的概念。這些組件,這里我們剛剛講到的都是一個靜態集成方案,其實還有一個動態的。例如這個組件可能并不是放在項目里一起編譯的,需要動態加載或升降級。或者是這個組件,可以運行時直接給他取消掉,不用它了,這個是插件化的范疇了。如果是有興趣,可以跟我私下談。
|進程冷啟動
進程冷啟動分析
性能指標
載業務模塊太多
Application 濫用
Support Multidex 耗時
系統兼容
剛才簡單的介紹了組件化的優化,給接下來進程冷啟動優化做一個鋪墊。說到進程冷啟動,也有一個故事了,去年收到投訴說,安卓的打開很慢。但是快和慢的主觀感受占的成份比較多,很難去描述。所以先定了一些性能指標,比如收集了一些項目初始化用了多少時間,每個業務模塊初始化用多少時間。還收集在初始化完到內容加載完,占了多少時間等等。如果沒有性能指標,沒有辦法是有確認這個是好是壞。通過收集后,發現冷啟動的卡頓主要是在 Application類,因為現在業務太多了,每個同學都是在 Application里面寫初始化邏輯,導致了這個進程啟動的時候,過來了太多不需要過載的模塊,還有一個是 Application 的被濫用。雖然說谷歌官方推薦不要在這個 Application 的里面寫任何業務代碼,但是我們發現有一些同學還是喜歡寫單例的,他就把單例的這些業務接口,寫到 Application 里面,而這個功能,給 Application 增加一些額外的負擔。
這里又有另外的一個問題,初步統計 Application 造成的負擔應用的啟動時間增加了1-2秒,這個也不算太嚴重。但是數據中心給我們反饋啟動的時間是 8 秒。后來跟數據中心核對后才發現,數據中心用的是一個叫 8 分位的指標, 8 分位指的是如果是有十個設備,前面的7 臺是 1 秒,后面的 3 臺是 10 秒,這個指標就是 10 秒。那我看了一下我們落在八分位的設備,都剛好是安卓4.3、4.1的設備,這些設備冷啟動的耗時往往是非常壯觀的,這個是問題的所在。接下來,還有是系統兼容的問題,比如我發現有一個問題,在某個ROM版本打 log 響應的時間是 50 毫秒-100 毫秒,而我印象中log只需要很短的時間損耗,這樣容易造成時間損耗。
圖 7
回到 Application 濫用及業務太多的問題,我們做了什么工作呢?
配合剛才的組件化,我們做了以下工作(圖 7),我們先把這個 Application 鎖死,我們不允許寫入任何業務代碼。我們把 Application 從項目里面移走,由 APT 具來生成 Application 類。由 APT 工程生成的 Application 類什么都不做,只做調用 MultiProcess,根據當前進程的名字,調用不同的初始化入口。通過剛才說的路由表,在這里做初始化,這樣的好處是可以只對應的組件進行掛載就可以。其實這里還帶來另外一個好處是經過這樣的處理,Application 會變得非常薄。這意味著什么,就是在你應用啟動的時候,Application足夠小,可以及時加載熱修復的補丁,一啟動就加載這個補丁,這樣的話就能很大程度的發揮這個補丁的作用。
但是還有一個問題,安卓開發有一個壞習慣,盡管谷歌推薦 Application 不寫業務代碼,但是還是有很多人喜歡在 Application 上創建一個靜態的接口用于獲取 Application 實例,我們的業務代碼也有類似的情況,我這樣一搞的話,他們的代碼都通通都掛了。我想了一個辦法,就是通過 hock 的手段。因為應用進程啟動的時候肯定會有 Application 實例,只不過不知道怎么來獲取它,通過研究框架層的代碼,初始化的時候會保留一個 APPLICATION 的實例,我們通過一些非正規的手段獲取實例,這樣就可以通過全局的 Application 去訪問 Application 實例。
圖 8
接下來還有一個關于 Multidex 的問題,我簡單的解釋一下 Multidex 做的工作,在安卓的 5.0 之前,對 dex 文件做優化處理,變成本地文件(odex)。這個過程中是大概是這樣,啟動-解壓,這個時間比較耗時,再接下來是執行,把這些 dex 優化成本地的代碼。這個過程中,也是很耗時,比較差的設備,需要 6-7 秒。我的模擬器,大概要 500 毫秒。第二次啟動的時候,就比較快的,因為上一次啟動的時候有本地緩存,因此我們需要解決第一次啟動時候的問題。剛好我在做 APK 的更新,這時忽然產生了一個靈感,在第一次啟動的時候比較耗時,我們能否在靜默更新的時候做一些工作,保存到指定的路徑下。用戶在啟動的時候,過一遍有沒有啟動工作。有的話,我直接把這個優化,或者是同命名到對方指定的這個路徑上,再進一步的執行。當時只是一個簡單的想法但是我實現了之后,還是挺有意思的,米 1 的設備,本來這個模塊有 7-8 秒,但是做了這樣的一個優化,大概只有 500 毫秒了,差不多是第二次冷啟動的時間,同時也擔心會不會帶來額外的風險。所以也加入了安全模式,當發現用戶啟動之后出現類加載問題,就會給他清空,繼續走原來的邏輯。經過這樣的處理之后,發現效果還是很明顯的,原本是需要 7-8 秒的時間,現在就只需要 500 毫秒。
|H5 首屏優化
圖 9
剛才是講的冷啟動的優化,另外還有一個比較大的問題-H5 首屏的問題。我們的前端跟我們說,打開的比較慢,需要知道慢在哪里。如圖 9 有幾個性能指標,白屏時間指的是用戶點擊到看到這個內容的時間。DOM 內容加載時間。首屏指的是 WEB 頁打開最慢加載好所有圖片的時間,一般是背景大圖是加載的最慢的,我們是認為加載完大圖就是首屏的時間。根據這個生命周期,前后部分是客戶端行為,我們是能夠做優化空間很少。中間的過程,我們能夠做哪些優化呢?
一開始不太熟悉 H5 開發的一些技術,也沒有更好的辦法,只能讓前端的同事打開網頁的時候,加入 LODING 交互 ,讓用戶覺得沒有那么多的白屏時間。后來回想起在之前的工作經驗中采用 WEB 來動態更新UI,但是WEB 更新也會有一些問題,網絡一旦出問題,就什么都不顯示了,因此需要做優化,我們在網絡加載失敗的時候,把URL重定向到內置資源件上面去,就解決了這個頁面空白的問題。這個時候我想了這跟我們的需求有一點相似的地方,如果我避開這個網絡請求,而是直接使用本地離線的,大大縮減這個時間,因此做了以下工作。
首先重寫 API ,設計優先使用本地的緩存,如果是有緩存,這個頁面的刷新時間非常的快。改變這種邏輯還需要機制配合,離線包更新邏輯在定期的時間拉起服務器離線包的配置,再判斷這個是否要更新,就會形成一個整體的邏輯。比如前段時間,上新了一些活動包,客戶端用戶收到了這些信息的話,就把它下載下來保存本地,保持最新的一個版本,等他打開活動的時候,先命中他的緩存,避免耗時,達到一個加速的效果。
除此之外還有一些問題-到達率太低,很多用戶在還沒緩存完前就已經打開了,另外現在為了避免一些泄露,我們都把 WEB 的業務放在一個獨立的進程里面,但是這個是主進程的更新需要知道進程同步的問題,在我更新的離線包的時候是否已經更新了。而且這個還帶來了一個壓力,這個滑動的時效性要求非常高。比如說今天上線,換了一個版本,用戶再拉,會導致沒有那么的及時。考慮的一個工作是做一個增量更新的一個操作。
通過這樣的一個操作之后(圖 11),我們發現,基本上能夠只要是命中,就達到一個秒開的功能。平均的這個首屏的時間,大概是能夠減少 30% 的時間。我們認為他的效果還是很顯著的。
|問題與優化方案
HTTPS問題
到達率低
后端依賴嚴重
HTML 文本拓撲方案
WebView 預熱方案
但是這里也有還一些問題要來處理,比如說 HTTPS 的問題,熟悉這個協議的人都知道,避免不了證書校驗這個問題,無法確保本地離線文件是安全的。 WEB 有一些問題,還是沒有辦法命中離線緩存的問題。到達率低,無論我們怎么做增量更新優化,用戶到達率達到都沒有達到一個滿意的效果。還有一個更嚴重的問題,這個功能有很嚴重的后端依賴問題。比如我開發了這個 WEB 的容器,但是我要配套的更新離線包的服務,必須要求有一個服務器接口來做離線包更新。
而且做性能優化的工作,很難爭取到充足的資源去做推進,所以這個時候,我們在想有沒有辦法來解決這個問題。現在在實現的是 HTML 文件拓撲的方案,我們希望,繞開這個后端接口。我們想到了這樣的辦法,用戶在訪問我們列表頁的時候,我先預判,再通過 HTML 文本分析的其中較大的圖片,較大的資源,給他預先下載下來,預熱一下,達到一個命中緩存的效果,還有是WEBVIEW,我先判斷,用戶是要進入哪一個,使用一個隱藏的 WebView 實例去加載這個頁面,等用戶打開的時候,我再把這個展現出來,下面的方案,比上面的更明顯更直接一點。但是,可能是帶來內容泄露的風險,這個我們目前還沒有上線。
以上是此次分享的內容。返回搜狐,查看更多
責任編輯:
總結
以上是生活随笔為你收集整理的补丁冷启动模式_Bilibili 移动端组件化实践中的冷启动优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring Cloud 与 Dubbo
- 下一篇: 男人的责任思考