轉(zhuǎn)載請注明出處:
Webview秒開框架VasSonic源碼分析(一)
地址:http://www.lxweimin.com/p/802129f77c20
目錄
起初想把代碼貼上來,一點(diǎn)點(diǎn)分析。但是鑒于VasSoinc中Webview和SonicSession并行執(zhí)行協(xié)同交流時(shí),各狀態(tài)是不確定的,各因素排列組合下來有n多種情況。一個(gè)個(gè)分析讀者會(huì)被繞暈,結(jié)果反而不好。于是便基于源碼,總結(jié)性分析,這樣更好一些。
特別感謝一下騰訊VasSonic框架負(fù)責(zé)人之一的陳同學(xué)@lovekidchen,感謝他的幫助,幫我理解了不少問題。
1 先說幾個(gè)概念
說正文之前,先說下webview的幾個(gè)概念。方便下面的講解。
1.1 webviewClient#shouldInterceptRequest
public WebResourceResponse shouldInterceptRequest(WebView view,
String url) {
return null;
}
webview加載頁面整個(gè)過程中(包括點(diǎn)擊webview中的超鏈接),當(dāng)需要加載任何資源請求時(shí),可以通過這個(gè)方法攔截。通過返回WebResourceResponse,讓webview可以加載本地提供的資源,而不再需要webview自己加載url對(duì)應(yīng)的資源。里面的邏輯需要在非ui線程進(jìn)行。
這個(gè)方法在21之后被廢棄了,改為
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request)
不過使用前面那個(gè)也沒有問題。
1.2 webviewClient#onPageFinished
public void onPageFinished(WebView view, String url) {
''
'' }
這個(gè)方法在webview成功加載完成html后會(huì)被回調(diào)。
1.3 JS調(diào)用android代碼
todo
1.4 webview的初始化包括哪些
webview 的初始化不只是findViewById來獲取webview的實(shí)例。webview 的初始化包括webview實(shí)例化、設(shè)置webSettings、設(shè)置webViewClient等,如果是第一次初始化webview,那么時(shí)間消耗大約是大幾百毫秒。webview初始化示例:
// 實(shí)例化webview
WebView webView = (WebView) findViewById(R.id.webview);
// 設(shè)置webViewClient
webView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
//
}
@TargetApi(21)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
return shouldInterceptRequest(view, request.getUrl().toString());
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
if (sonicSession != null) {
//step 6: Call sessionClient.requestResource when host allow the application
// to return the local data .
return (WebResourceResponse) sonicSession.getSessionClient().requestResource(url);
}
return null;
}
});
//設(shè)置webSettings
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webView.removeJavascriptInterface("searchBoxJavaBridge_"); intent.putExtra(SonicJavaScriptInterface.PARAM_LOAD_URL_TIME, System.currentTimeMillis());
webView.addJavascriptInterface(new SonicJavaScriptInterface(sonicSessionClient, intent), "sonic");
webSettings.setAllowContentAccess(true);
webSettings.setDatabaseEnabled(true);
webSettings.setDomStorageEnabled(true);
webSettings.setAppCacheEnabled(true);
webSettings.setSavePassword(false);
webSettings.setSaveFormData(false);
webSettings.setUseWideViewPort(true);
webSettings.setLoadWithOverviewMode(true);
2 VasSonic特點(diǎn)
VasSonic是騰訊開源的、解決webview首屏提速的框架。這里先說下VasSonic的主要特點(diǎn):
- 分離webview初始化和web頁面主url(html)的請求。
- 主url(html)請求的數(shù)據(jù)進(jìn)行了本地緩存。除了緩存整個(gè)html,還對(duì)緩存進(jìn)行了更精細(xì)化的分割,分為:
模板(html string中不經(jīng)常變動(dòng)的字符串)和html string中經(jīng)常變動(dòng)的字符串。
首先這么分割的對(duì)象是主url對(duì)應(yīng)的html字符串,js/css/圖片資源還未請求加載,都以string的形式存在。分割時(shí)注意,不一定模板就是css,js,非模板就是數(shù)據(jù)。有些css和js也經(jīng)常變動(dòng),有些頁面數(shù)據(jù)也不會(huì)變。根據(jù)業(yè)務(wù)場景分割html 字符串。分割標(biāo)記需要前端來支持。如果請求時(shí)發(fā)現(xiàn)模板沒變,只是非模板的數(shù)據(jù)變了,那么可以實(shí)現(xiàn)頁面的局部刷新。局部刷新時(shí),需要js調(diào)android代碼。
3 web界面的兩種加載方式 :預(yù)加載和常規(guī)加載
說web界面進(jìn)行常規(guī)加載之前,先說下web界面的預(yù)加載模式。
3.1 web界面預(yù)加載方式
3.1.1 預(yù)加載整個(gè)html
預(yù)加載指開發(fā)者預(yù)先判斷哪些頁面可能會(huì)被用戶打開,預(yù)加載這些web頁面。比如貓眼資訊列表/頭條資訊列表等。預(yù)加載時(shí)先把html從服務(wù)器拉下來(都是文本,所以銷毀流量少,用戶可以接受)。然后等打開這個(gè)頁面時(shí),使用webview.loadDataWithBaseURL(html)加載頁面。之后進(jìn)行css、js、圖片加載時(shí),可以使用webviewClient#shouldInterceptRequest加載本地資源(apk asset目錄下可以預(yù)置一些資源)。
3.1.2 apk預(yù)置模板
如果業(yè)務(wù)形式像貓眼的feed流那樣,所有的資訊幾乎模樣都是一樣的。那么我們甚至可以把html的模板(概念等同于上面提到的模板)放到apk的asset目錄下。這樣預(yù)加載時(shí)只加載非模板部分(文字,圖片url,css等字符串),等用戶打開那一頁的時(shí)候,拼接非模板數(shù)據(jù)和模板為整個(gè)的html字符串。之后的流程和上面是一樣的,使用webview.loadDataWithBaseURL(html)加載頁面。之后進(jìn)行css、js、圖片加載時(shí),可以使用webviewClient#shouldInterceptRequest加載本地資源。
3.1.3 web界面采用預(yù)加載時(shí)VasSonic的使用方式
因?yàn)閂asSonic分離了webview初始化和主url(html)的請求。所以VasSonic本身支持預(yù)加載(調(diào)用sonicSession#preload()方法)。
不過,預(yù)加載不一定需要VasSonic的參與。即使你不使用VasSonic也可以進(jìn)行預(yù)加載。使用你app中的網(wǎng)絡(luò)框架將html加載下拉。然后等打開頁面時(shí),使用webview.loadDataWithBaseURL(html)進(jìn)行加載。
說下VasSonic的預(yù)加載過程。在沒打開頁面時(shí),使用sonnicSession#preload來加載數(shù)據(jù)。把html數(shù)據(jù)放到本地的某個(gè)位置,當(dāng)打開頁面后,使用webview.loadDataWithBaseURL(html)直接加載。(當(dāng)然html中的css ,js等url對(duì)應(yīng)的資源還沒有加載。VasSonic目前的版本并沒有對(duì)這些副資源進(jìn)行預(yù)加載或這些副資源的本地存儲(chǔ)好消息是下一個(gè)版本會(huì)支持。你可以自己預(yù)置一些css、js、圖片的資源到apk中,使用WebviewClient#shouldInterceptRequest自己攔截加載)。
3.2 web頁面的常規(guī)加載過程:
3.2.1 常規(guī)加載過程
當(dāng)我們不使用預(yù)加載時(shí),我們打開一個(gè)頁面,需要一下流程加載頁面:
初始化webview->webview進(jìn)行主框架url加載請求(html)->成功后,進(jìn)行css,js,圖片的請求(圖片可以等界面顯示時(shí)再加載)->顯示界面
(圖,這里應(yīng)該有個(gè) 圖,todo)
3.2.2 web頁面常規(guī)加載過程中的時(shí)間浪費(fèi)
我們看下可能存在的時(shí)間浪費(fèi):
- 首次初始化webview會(huì)銷毀幾百毫秒時(shí)間。webview初始化之前是不能進(jìn)行主html加載的。這個(gè)等待時(shí)間浪費(fèi)掉了。
- 主url(html)請求成功后,才能進(jìn)行css,js等資源的請求。因?yàn)橹挥械鹊絟tml獲取到以后,才知道css、js、圖片的url是什么。這個(gè)問題的優(yōu)化方案可以采用把資源放到本地。前面說了,目前的VasSonic沒有對(duì)js/css等副資源的加載、緩存做優(yōu)化。使用webviewClient#onIntercepterRequest來進(jìn)行本地資源加載。
綜上,我們接下來就看下VasSonic對(duì)第一個(gè)點(diǎn)是怎么優(yōu)化的。
3.2.3 webview的初始化和sonicSession的并發(fā)進(jìn)行
對(duì)于上面的第一個(gè)問題,VasSnoic是這么解決的:webview的初始化和主url(html)請求(之后統(tǒng)稱SonicSession)并發(fā)進(jìn)行。
3.2.3.1 webview初始化流程:
webview的初始化主要包括webview的實(shí)例化、webviewSetting的設(shè)置、webviewClient的設(shè)置。示例代碼前面已經(jīng)給出了。這里要說的是,在我們初始化完成后,要手動(dòng)調(diào)用(代碼中sonicSessionClient是webview的代理類。因?yàn)橛锌赡懿煌捻?xiàng)目用的webview不一樣)
if (sonicSessionClient != null) {
sonicSessionClient.bindWebView(webView);
sonicSessionClient.clientReady();
}
通知SonicSession webview已經(jīng)初始化好了。
3.2.3.2 SonicSession數(shù)據(jù)請求流程:
如果有緩存,先加載緩存(通知webview加載)。然后(不管有沒有緩存),從服務(wù)器獲取html。發(fā)送請求獲取服務(wù)端的html時(shí),VasSonic的第一個(gè)版本需要我們在請求中提供一些自定義的header(緩存html的摘要sha1,緩存html模板的摘要sha1)。這樣服務(wù)端根據(jù)這些摘要來判斷決定返回整個(gè)html或只返回html中非模板的部分(返回的整個(gè)html或部分html通知webview刷新)。第一個(gè)版本的VasSonic需要后臺(tái)的參與。為了讓框架對(duì)后臺(tái)透明,降低接入難度。VasSonic的2.0在版本中,不需要后臺(tái)的參與。自己模擬后臺(tái)的行為。即,在任何時(shí)候,都會(huì)從服務(wù)端拉取整個(gè)html,分割成模板和非模板。然后與本地的html的模板和非模板摘要對(duì)比。如果整個(gè)html都相同,那么什么都不做;如果模板相同,那么通知webview只刷新頁面中的非模板區(qū)域;如果都不相同,那么通知webview刷新整個(gè)頁面。
3.2.4 webview的初始化和sonicSession的協(xié)同合作
3.2.4.1 協(xié)同合作的通信機(jī)制和時(shí)機(jī)
既然webview的初始化和sonicSession請求數(shù)據(jù)并發(fā)進(jìn)行,那么肯定需要進(jìn)行通信交流來進(jìn)行協(xié)同合作。那使用什么機(jī)制什么時(shí)候來通知對(duì)方呢?
3.2.4.1.1 webview向SonicSession發(fā)送信號(hào)
- 前面提到了,當(dāng)webview初始化好了之后,會(huì)手動(dòng)調(diào)用sonicSessionClient#clientReady()來通知sonicSession webview已經(jīng)初始化好了。
- 如果沒有緩存時(shí),框架內(nèi)部調(diào)用webview.load(url)時(shí)(url為html對(duì)應(yīng)的url),webviewClient#shouldInterceptRequest會(huì)被觸發(fā),會(huì)通知sonicSession把已經(jīng)加載到內(nèi)存的資源傳遞給webview,讓webview加載。其實(shí)質(zhì)是”webviewClient#shouldInterceptRequest觸發(fā)后,中斷sonicSession的加載,把已經(jīng)解析到內(nèi)存的流和節(jié)點(diǎn)流包成一個(gè)自定義InputStream流“傳遞給webview,webview加載這個(gè)流時(shí)會(huì)先加載內(nèi)存中的流,再加載節(jié)點(diǎn)流。
- 1.0版本中,如果有緩存時(shí)先加載緩存。當(dāng)緩存頁面加載完成后,會(huì)觸發(fā)webviewClient#onPageFinished,繼而通知sonicSession中斷服務(wù)器端數(shù)據(jù)的加載,把已經(jīng)解析到內(nèi)存的流和節(jié)點(diǎn)流包成一個(gè)自定義InputStream流“傳遞給webview,webview加載這個(gè)流時(shí)會(huì)先加載內(nèi)存中的流,再加載節(jié)點(diǎn)流。不過,在2.0版本中,如果有緩存時(shí),也是先加載緩存。然后啟動(dòng)服務(wù)端的數(shù)據(jù)加載,一直加載直到html加載完成,不會(huì)受緩存頁面加載成功信號(hào)的中斷。多說一句,為什么這里會(huì)一直加載html直到結(jié)束呢?因?yàn)?.0版本進(jìn)行了后臺(tái)模擬的工作,拉取服務(wù)端完整的html和緩存中的html做etag,templetag對(duì)比,自己模擬來使response返回相應(yīng)的header。這樣就不需要后臺(tái)的參與了。也因?yàn)檫@個(gè),所以需要一直加載服務(wù)端html直到html加載完成。
上面三點(diǎn)講的是webview向SonicSession發(fā)送信號(hào)。簡單來說就是webview初始化好的時(shí)候、webviewClient#shouldInterceptRequest被觸發(fā)時(shí)、webviewClient#onPageFinished被觸發(fā)時(shí)。
3.2.4.1.2 SonicSession向webview發(fā)送信號(hào)
那么SonicSession在加載過程中,也會(huì)發(fā)送信號(hào)給webview,讓其加載數(shù)據(jù)。通過前面的SonicSession工作過程分析,知道sonicSession會(huì)先進(jìn)行緩存加載,然后進(jìn)行服務(wù)端html加載。服務(wù)端html加載時(shí)(1.0)/加載后(2.0),分析webview需要首次加載還是非模板數(shù)據(jù)更新還是模板跟新,數(shù)據(jù)加載和分析邏輯在VasSonic中對(duì)應(yīng):
protected abstract void handleFlow_FirstLoad();
protected abstract void handleFlow_DataUpdate(String serverRsp);
protected abstract void handleFlow_TemplateChange(String newHtml);
獲取到相應(yīng)的數(shù)據(jù)后,就需要發(fā)送信號(hào)通知webview進(jìn)行加載。因?yàn)镾onicSeesion的加載數(shù)據(jù)過程是在異步線程,webview加載數(shù)據(jù)在主線程。所以需要handler 發(fā)送message來通知。我們看一眼VasSonic 的handler處理邏輯:
public boolean handleMessage(Message msg) {
// fix issue[https://github.com/Tencent/VasSonic/issues/89]
if (super.handleMessage(msg)) {
return true; // handled by super class
}
if (CLIENT_CORE_MSG_BEGIN < msg.what && msg.what < CLIENT_CORE_MSG_END && !clientIsReady.get()) {
pendingClientCoreMessage = Message.obtain(msg);
SonicUtils.log(TAG, Log.INFO, "session(" + sId + ") handleMessage: client not ready, core msg = " + msg.what + ".");
return true;
}
switch (msg.what) {
case CLIENT_CORE_MSG_PRE_LOAD:
handleClientCoreMessage_PreLoad(msg);
break;
case CLIENT_CORE_MSG_FIRST_LOAD:
handleClientCoreMessage_FirstLoad(msg);
break;
case CLIENT_CORE_MSG_CONNECTION_ERROR:
handleClientCoreMessage_ConnectionError(msg);
break;
case CLIENT_CORE_MSG_SERVICE_UNAVAILABLE:
handleClientCoreMessage_ServiceUnavailable(msg);
break;
case CLIENT_CORE_MSG_DATA_UPDATE:
handleClientCoreMessage_DataUpdate(msg);
break;
case CLIENT_CORE_MSG_TEMPLATE_CHANGE:
handleClientCoreMessage_TemplateChange(msg);
break;
case CLIENT_MSG_NOTIFY_RESULT:
setResult(msg.arg1, msg.arg2, true);
break;
case CLIENT_MSG_ON_WEB_READY: {
diffDataCallback = (SonicDiffDataCallback) msg.obj;
setResult(srcResultCode, finalResultCode, true);
break;
}
default: {
if (SonicUtils.shouldLog(Log.DEBUG)) {
SonicUtils.log(TAG, Log.DEBUG, "session(" + sId + ") can not recognize refresh type: " + msg.what);
}
return false;
}
}
return true;
}
處理的情況還是不少的。因?yàn)椴淮_定因素導(dǎo)致協(xié)同合作是一個(gè)動(dòng)態(tài)的過程。所以協(xié)同起來還是很繁瑣的。
3.2.4.2 協(xié)同合作的動(dòng)態(tài)性
上面講的是什么時(shí)候發(fā)送信號(hào),但發(fā)送信號(hào)后的處理是不確定的。舉個(gè)例子,當(dāng)webview初始化完成時(shí),SonicSession可能已經(jīng)加載好了緩存數(shù)據(jù),但可能沒加載好網(wǎng)絡(luò)數(shù)據(jù),也可能是其他情況。反過來sonicSession下載好了數(shù)據(jù)讓webview進(jìn)行加載緩存/刷新模板/模板更新/第一次加載時(shí),webview初始化有么有完成不確定。或者讓webview進(jìn)行模板更新時(shí),webview這時(shí)候初始化完成情況不確定,有沒有加載緩存不確定。這些不確定因素導(dǎo)致,整個(gè)協(xié)同工作是一個(gè)排列組合多因素動(dòng)態(tài)的過程。所以
如果在這里如果每一種情況都進(jìn)行分析,顯然是不現(xiàn)實(shí)的。但是,通讀完他們的代碼,我有這么一個(gè)感覺:他們把能減少的時(shí)間等待都利用了,用來做一些其他的事情,或者想辦法減少這種等待時(shí)間。所以對(duì)于協(xié)同合作的動(dòng)態(tài)性,我們也可以有個(gè)整體的把握。說到這里,其實(shí)我在擼他們代碼的時(shí)候,就越發(fā)的佩服騰訊團(tuán)隊(duì)做出的這個(gè)框架。配合他們的耐心與做事情追求極致的精神。
3.2.4.3 協(xié)同合作情況舉例
接下來就分析webview和sonicSession通信協(xié)同合作的兩種常見情況,其他情況可以直接閱讀源碼進(jìn)行分析。
既然是常見情況,那么我們做以下兩點(diǎn)假設(shè):
- SonicSession緩存加載時(shí)間比webview的初始化時(shí)間要長。
- SonicSession服務(wù)器加載html的時(shí)間比SonicSession緩存加載時(shí)間要長。
3.2.4.3.1 本地有緩存時(shí)
當(dāng)本地有緩存時(shí),那么我們分析一下這種情況。
- webview初始化與SonicSession同時(shí)進(jìn)行。
- SonicSession進(jìn)行緩存加載,加載好緩存數(shù)據(jù)以后,封裝成message通過handler傳遞主線程looper 隊(duì)列中。即,通知webview加載緩存數(shù)據(jù)。webview這時(shí)候還沒初始化好,那么會(huì)把之前的message再封裝成pendingClientCoreMessage存起來。等到webview初始化好以后,會(huì)再把這個(gè)message傳遞到主線程looper隊(duì)列中等待webview加載。
- SonicSession開始從服務(wù)端加載html數(shù)據(jù)。如果是2.0的情況并且我們設(shè)置使用純終端模式,那么SonicSession會(huì)把服務(wù)端的整個(gè)html數(shù)據(jù)全部加載下來(此過程不會(huì)被外界中斷)。然后模擬得到etag,templeTag等response 的header值。然后通過handler通知webview進(jìn)行局部刷新/模板更新。這里的具體情況可以看我寫的 Webview秒開框架VasSonic源碼分析(二)-1.0與2.0版本的不同 。
3.2.4.3.2 本地沒有緩存時(shí)-“截流加載”
如果本地沒有緩存時(shí),我們分析一下這種情況。
webview初始化與SonicSession同時(shí)進(jìn)行。
SonicSession從服務(wù)端加載html。
-
webview初始化好了,這時(shí)候服務(wù)端的html數(shù)據(jù)還沒加載完成。如果等到html加載完成以后再讓webview進(jìn)行加載,那么這里就有一個(gè)等待情況。為了避免這種等待浪費(fèi)。VasSonic是這么做的:webview初始化好時(shí),會(huì)發(fā)送一個(gè)信號(hào),讓SonicSession中斷其網(wǎng)絡(luò)加載。中斷時(shí),會(huì)把已經(jīng)加載的數(shù)據(jù)放到內(nèi)存的outputSream流和serverRsp 字符串中,然后把內(nèi)存outputSream流和未加載的節(jié)點(diǎn)流包成SonicSessionStream返回。sonicInputstream#read時(shí)會(huì)先讀取內(nèi)存中的流,再讀取節(jié)點(diǎn)流中的數(shù)據(jù):
public synchronized InputStream getResponseStream(AtomicBoolean breakConditions) { if (readServerResponse(breakConditions)) { BufferedInputStream netStream = !TextUtils.isEmpty(serverRsp) ? null : connectionImpl.getResponseStream(); return new SonicSessionStream(this, outputStream, netStream); } else { return null; } }
代碼中的breakConditions就是外界的中斷信號(hào)。當(dāng)內(nèi)部有調(diào)用webview.load(url)時(shí),webviewClient#shouldInterceptRequest會(huì)被觸發(fā),進(jìn)而把sonicInputstream傳遞給webview進(jìn)行解析渲染。
4 如果前端不介入,使用VasSonic框架的好處
2.0版本不需要后臺(tái)介入,但需要前端配合來進(jìn)行局部刷新。如果前端不介入。那么優(yōu)點(diǎn)就是
- 首次加載,利用webview初始化的時(shí)間進(jìn)行服務(wù)端html的加載,然后webview初始化后,webview“截流加載”。
- 如果有緩存,那么webview先加載緩存。再進(jìn)行模板更新,因?yàn)闆]有前端配合,沒有模板概念,也沒有局部刷新。這時(shí)候加載的是新的完整的html。