導語

本文從實際需求出發,通過分析Android端的Webview加載流程以及加載過程中可以優化的耗時點,分階段優化加載速度,最終實現在一秒內加載H5頁面,希望對有此需求的開發者有所啓發和幫助

背景

目前58商家通對58來說是一個連接B端平臺,對於商家來說是一個運營管理工具,商家可以在58商家通上進行商業服務(精準、置頂)、信息溝通、帖子管理等基本運營操作而獲取服務保障、訪客足跡、會員權益等服務。由於58商家通是一個平臺軟件,隨着規模的擴大,接入兄弟部門服務也越來越多,如推薦有獎,服務保障,放心服務,到家精選,福利商城等,而接入兄弟部門的服務都是通過H5的形式接入,因此,58商家通上的H5頁面比例已經超過了Native 頁面的比例,由於H5頁面的加載效率遠遠低於Native的加載效率,所以對於58商家通的H5加載效率成爲了重中之重的問題,優化這個問題,首先可提高用戶體驗和APP的活躍度、流量,其次接入各個服務之後能夠提高用戶的體驗意願,對於新服務在58商家通的推廣也有很大的意義,故此將優化過程中遇到的問題及解決方案跟大家分享一下,希望能給大家一些幫助。

webview默認加載流程分析

1、 webview默認加載流程

在優化webview加載H5頁面之前我們需要了解默認webview加載H5頁面的流程,並針對特定的耗時流程做出符合我們開發技術的方案。首先,webview首次加載流程大概分爲以下幾個階段:

A、webview的初始化

  B、瀏覽器內核初始化(全部webview共享,第一次初始化)

C、請求html頁面,並對頁面進行解析

D、下載解析過程中需要下載的js,css,圖片等資源文件

E、生成h5的domTree

F、根據上文的domTree渲染頁面

2、 webview優化過程分析

分析之前,先解釋下文中的一個名詞(usdt服務:是58自主開發的一個管理web資源版本的scf服務,其主要功能是對資源文件通過添加版本後綴來實現版本管理。主要實現方式:上線資源時,給資源文件自動化加個時間戳後綴,主要實現如下:

String resultUrl = jsurl.replace(suffix, "_v" + version + suffix);//替換前端文件後綴,拼成html

//舉例用法:getVersion("https://test.58.com/test.js", ".js");

//https://test.58.com/test_v2019555555555.js

然後我們看下之前說的流程中,其中A-D步驟都是頁面白屏,本文中的測試頁面是我們58商家通中相對資源比較多的頁面,所以首次加載耗時相當嚴重,白屏超過3秒,二次加載也需要大概2秒多,所以這對於使用者是難以忍受的,優化過程大致分爲以下幾個階段:

A、Webview緩存的優化,使用自定義緩存替代webview自帶緩存

webview默認加載過程中不可避免的需要使用到緩存,由於我們h5頁面的開發行爲以及使用到的一些技術,如果使用webview自帶的緩存api去實現緩存邏輯,將會有以下一些問題:

  • API固定,依賴系統自帶的API,如果需要擴展,如果系統不支持,很難二次開發。

  • 由於我們的h5頁面圖片都已經使用了cdn服務,而我們默認配置了8臺服務器,所以相同的資源文件在客戶端可能重複下載多次,造成流量和存儲的浪費。

  • 現在大多數公司都會對資源文件做版本控制(爲了解決資源文件內容修改之後,前端不能及時更新資源的問題),其中我們58就使用了相應的usdt服務,對於js,css文件使用具有usdt相同功能的服務,自帶版本號,如果使用默認webview的緩存,新版本更新之後不能及時刪除老版本的js,css文件問題。

  • 默認緩存,對於緩存策略和文件的操作都不可擴展,對於我們來說是個黑盒子。

由於以上等原因我們做了第一階段的優化,使用自定義緩存替代webview自帶的緩存。
B、預先初始化Webview,可提前初始化瀏覽器內核,並預加載頁面,在父頁面提前根據策略加載需要加載的頁面。
APP啓動之後就定義一個全局的webview對象實例,因爲在我們第一次使用webview初始化過程中,需要初始化瀏覽器內核大概500ms左右,可以對此進行優化,其次,在我們第一次進入某頁面之後,在頁面初始化View組件的同時,使用默認的webview將資源文件緩存到本地,等到View組件初始好之後,可以直接使用本地緩存的資源進行渲染,以提高頁面加載速度。並且使用該全局webview對象在父頁面提前緩存子頁面,這樣在加載子頁面的時候提前從本地緩存加載頁面。減少第一次下載頁面資源以及解析生成中間數據的時間。
C、特定的場景使用H5離線包,首次請求進行下載,二次進入場景如果需要顯示直接從本地顯示。
特定的場景:例如,顯示推廣活動的H5頁面,定期宣傳的服務H5頁面等等,
服務器會提供相關接口告知APP,APP在首次啓動的時候下載資源,當下載完成之後,App第二次進入時候,直接從本地加載。
優化之後的整體架構如下:

下一章節將具體說明優化方案的全過程。

Webview加載優化方案實踐過程

1、默認加載速度測試過程

由於第一章節說了使用webview自帶緩存的問題,所以默認測試速度的時候是禁用系統的緩存的,並且加載了的測試頁面(58商家通中的商學院頁面),關鍵代碼如下:

webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);

webView.loadUrl("https://hyapp.58.com/app/school/open/articles/tohome");

然後測試10次,並記錄每次各項數據如下:

分析:
創建頁面時間:onCreate()開始時間
頁面加載時間:onPageStarted()開始時間
頁面加載完成時間:onPageFinished()開始時間
初始化耗時:從onCreate()到onPageStarted的時間
加載資源耗時:從onPageStarted()到onPageFinished()的時間
總耗時:從onCreate()到onPageFinished()的時間
由數據可以看出:

  • 第一次資源緩存時間大概3.1s左右,第二次資源緩存大概2s左右

  • 第一次總耗時大概3.9s,第二次大概2.3s左右

  • 第一次初始化耗時大概800ms,第二次初始化耗時300ms

所以爲了縮短總耗時,首先需要優化緩存這個最耗時的步驟,下面我們將說明緩存優化的過程。

2、 緩存優化的過程

首頁我們來開看緩存優化的切入點以及具體的流程如下圖所示:

A、切入點:當webview 需要加載資源的時候,會使用下面兩個api進行攔截。

/**

* 發生資源加載,攔截順序

*

* 此方法添加於API21,調用於非UI線程,攔截資源請求並返回數據,返回null時WebView將繼續加載資源

* @param view

* @param request

* @return

*/

public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request)



/**

* 此方法廢棄於API21,調用於非UI線程攔截資源請求並返回響應數據,返回null時WebView將繼續加載資源

* @param view

* @param url

* @return

*/

public WebResourceResponse shouldInterceptRequest(WebView view, String url)


B、當攔截到需要下載網頁資源的url後,我們需要以下幾點需要明確:

  • 哪些url文件需要緩存?

  • 對於js,css文件等如何更新?

  • 多臺cdn服務圖片如何只下載一份?

解決這些問題之前:首先我們內部開發約定如下:

  • js,css文件上線需要繼承usdt等相近的服務。

  • 圖片資源應上傳至cdn服務器,從cdn服務器應用。

之後,開始解決上面的問題,首先我們app端只緩存了符合我們規定的資源文件
大概佔95%以上,對於不符合規定的資源文件依舊是從網絡獲取,來保證我們的頁面的正確性。所以我們只針對具有版本號的js,css文件,已經cdn服務器的圖片進行緩存。其次來看看文件的更新策略,由於我們需要緩存的js,css文件都是攜帶版本號的(https://j1.58cdn.com.cn/shangjiatong/sdk/sj_app_v20190327110116.js)我們會以攜帶的版本號來判斷文件是否需要更新,如果需要更新,則直接異步緩存文件,並刪除之前的舊版本,並同時讓webview從網略加載需要更新的資源。最後對於多臺cdn服務器緩存的同一個圖片資源的url是不同的例如:

https://pic1.58cdn.com.cn/nowater/sjtnw/n_v2d2dd3ffb95d84cc8ae2dad24e8bd4a5b.jpg

https://pic2.58cdn.com.cn/nowater/sjtnw/n_v2d2dd3ffb95d84cc8ae2dad24e8bd4a5b.jpg

https://pic3.58cdn.com.cn/nowater/sjtnw/n_v2d2dd3ffb95d84cc8ae2dad24e8bd4a5b.jpg

所以可以根據這個特點,做細節處理,對於後綴相同的圖片值緩存一次,避免重複下載。後期服務端會做路由和分發,可以直接避免此問題。我們在優化之後使用相同的手機和相同的頁面進行測試,測試結果如下:

由測試結果可以看出,第一次加載資源耗時因爲是從網絡緩存文件1709ms,第二次由於直接從緩存獲取已經降低到了416ms。而頁面初始化的時間第一次依舊需要800ms左右,第二次需要300ms左右,但可以看出如果第二次初始化都是300ms,比第一次少了500ms左右,這因爲第二次加載頁面首先不需要初始化瀏覽器內核,第二是第一次加載頁面之後,會對一些臨時、簡單數據進行緩存,Cookies的擴展。具體的API如下

webView.getSettings().setDomStorageEnabled(true);

正因爲第二次比第一次加載明顯變快,所以能不能將第一次加載也做成是第二次加載呢?因此帶着這個問題我們進入了下一個流程的優化。

3、 初始化全局webview階段,並提前預加載頁面

我們在APP使用Webview加載一個頁面總感覺比在手機瀏覽器中打開同一個頁面會慢,這主要是因爲當我們在手機瀏覽器中打開頁面之前,我們已經打開了手機瀏覽器這個APP,打開完成之後,它已經對瀏覽器內核進行了初始化。而當我們打開自己的APP去加載頁面時候,當加載頁面的時候纔會去初始化webview,然後第一個初始化webview 就會初始化瀏覽器內核,所以會比瀏覽器慢,爲了解決這個問題,我們可以如下優化。
在App啓動之後,定義了一個全局的WebviewProxy單例對象,它會持有一個webview對象,首先,在初始化它的時候,會初始化瀏覽器內核,下次進入頁面初始化webview時會更快,由之前的數據可以看出大概會提高500ms;其次,通過這個webview對象可以在父頁面提前緩存子頁面,這樣加載子頁面時候可以快速顯示子頁面。
針對上面的方案,實踐過程中需要注意以下幾個問題?

  • 初始化webview持有上下文環境?

  • 當父頁面加載子頁面還沒有完成時,點擊子頁面如何處理?

  • 父頁面爲H5頁面,子頁面很多,如何動態配置加載子頁面?

  • 啓動APP的時候如何將本地文件加載到內存?

首先,webview持有的上下文環境如果直接傳當前頁面的上下文環境,如果當前需要退出,由於被全局webview對象持有,所以會導致內存泄漏,如果使用MutableContextWrapper類去持有當前頁面Context,需要在銷燬頁面時候去主動調用setBaseContext()方法去釋放當前Context,由於我們的webview本來就是全局唯一的單例對象,所以我們爲其分配了Applciation對象作爲Context對象。
具體的定義全部的webview的代理對象:

public class WebviewProxy implements IWebviewProxy{

private WebView webView;

private static volatile WebviewProxy INSTANCE;

private WebviewProxy(){

webView = new WebView(MyApplication.getInstance());

initWebView();

}

public static WebviewProxy getInstatnce(){

if(INSTANCE == null){

synchronized (WebviewProxy.class){

if(INSTANCE == null){

INSTANCE = new WebviewProxy();

}

}

}

return INSTANCE;

}

@Override

public void load(String url) {

webView.loadUrl(url);

}

...

}

其中在需要提前加載頁面時可以通過load方法,提前緩存頁面以及生成domTree,如下所示。

private void initWebview() {

WebviewProxy.getInstatnce().load("https://hyapp.58.com/app/school/open/articles/tohome");

}

第二個問題當父頁面加載子頁面還沒有完成時,點擊子頁面時如何處理,這裏主要關注的點是緩存資源可能存在重複下載的問題,所以在做此處的時候需要對上面的下載組件進行了重構,加入了任務隊列模塊,所以,在點擊子頁面的時候,如果已經緩存了則直接從緩存獲取,如果沒有,則判斷是否在緩存隊列,如果在,則不需要重新緩存,如果不在,纔會下載,這樣就避免了同一資源重複下載的問題。
第三個問題父頁面爲H5頁面,如何動態加載子頁面,對於這個問題,我們對H5頁面提供了jsbridge協議,當父頁面需要緩存時,直接調用Native提供的協議即可,這樣H5開發過程中,會自己根據判斷是否需要加載子頁面,而動態的調用協議去緩存。
第四個問題,啓動的時候如何將本地文件加載到內存中,如果將本地的緩存文件全部加載到內存中,如果緩存文件過多,太消耗內存,所以我們做了動態配置,以及優先級等策略,首先,文件保存到本地會設置文件優先級,核心頁面爲1-10,普通一級頁面爲10-100,二級頁面爲100-1000,其次,配置加載的內存大小,所以,首次啓動APP之後,我們按優先級最高的一個一個文件加載到內存中,並判斷是否到達最大內存限制,對需要初始化的資源進行管控。
最後還是使用我們商學院的的頁面做測試,我們再首頁啓動的時候就預先使用全局的WebviewProxy這個對象去加載商學院url,然後,點擊商學院頁面,進入商學院頁面,對其進行了測試,數據結果如下:

可以看出初始化全局webview並且預加載頁面之後,我們的58商家通中資源消耗最多的商學院頁面加載速度也達到了1秒以內,並且第二次和第一次加載耗時基本相近。

4、 特定場景下,支持離線包

之前的流程在一般場景下都是可以適用的,最後針對我們特定的業務需求,又做了離線包加載模塊,首先,來看下場景:我們需要動態的在APP啓動的時候顯示可配置的H5廣告,因爲是APP啓動,所以應該以最快速度顯示頁面,所以需要將所有的H5頁面以及資源打包,當我們啓動的時候,首次先請求接口,如果有需要展示的廣告,先下載本地並解壓,之後啓動APP的時候判斷如果還需要顯示,就直接從本地加載,這樣可以以最快速度加載我們的H5頁面,而且是否顯示,顯示的廣告內容,都是服務器可配置的,方便產品做運營推廣,這一節其實只是一個場景的補充,與上面幾部的優化沒有什麼關聯,但是提供了另一種優化方案(APP提供瀏覽器的殼,所有的資源可動態加載到本地,之後直接加載本地頁面和資源)。具體流程如下圖所示:

持續優化計劃

上文優化過程中還有許多需要後期優化的點,後期持續優化計劃如下:
首先會對緩存文件的初始化邏輯優化,緩存策略的優化,減少運行過程中對File的操作,能直接命中內存中的緩存。其次是對架構中各模塊的封裝,降低各模塊之間的耦合性,最後,希望能夠封裝成sdk,直接在其他項目中集成使用。

總結

技術服務於業務需求,由於58商家通中的H5頁面比例的增多,所以對於我們58商家通平臺來說優化H5頁面的加載速度顯得格外重要,所以經過一系列的實踐和方案的實施,使得我們58商家通APP的H5頁面加載到達了秒級顯示,因此性能優化在實踐中得到了驗證,所以出此文章供大家參考。我也一直會繼續努力優化我們的58商家通這款App,後期計劃的優化點還有許多,使得商家能夠簡單高效的在我們58上做生意,提高我們產品的體驗度,對此如果大家還有別的方案,也希望能夠多多交流,以後,爭取能夠發表更多關於前端Android技術的分享。溫故而知新,希望這次總結,也能對自己有所幫助。


作者簡介

趙兵, 58商家通全棧開發工程師,主要負責58商家通前後端業務開發、性能優化、架構設計、重點項目和版本迭代,長期參與58商家通需求開發迭代,並偶爾參與本部門其他服務端開發如推薦有獎、廣告平臺、企管家等服務。

閱讀推薦

相關文章