一少梁、混合開發(fā)的優(yōu)勢與缺陷
在混合開發(fā)大行其道的今天,很多頁面和功能都轉(zhuǎn)由前端實(shí)現(xiàn)考杉,客戶端只要在APP中嵌入一個(gè)WebView即可,同時(shí)前端開發(fā)的頁面對于Android和iOS端的效果是統(tǒng)一的舰始,省去了適配的困擾崇棠。
適合前端開發(fā)的界面主要有以下兩種:
1、新聞咨詢類頁面丸卷,這類頁面布局比較復(fù)雜枕稀,通過前端實(shí)現(xiàn)相對原生更為簡單。
2谜嫉、運(yùn)營活動類界面萎坷,這類頁面更新較為頻繁,前端迭代后可以直接上線沐兰,跳過了客戶端的發(fā)版流程哆档。
前端開發(fā)的優(yōu)勢顯而易見:開發(fā)敏捷、發(fā)版靈活住闯。但是它的缺點(diǎn)同樣明顯瓜浸,我認(rèn)為主要有以下3點(diǎn)。
第一個(gè)問題就是性能問題比原,冷啟APP后第一次新建WebView的耗時(shí)會讓用戶感受到明顯的卡頓插佛,而WebView.loadUrl(String url)
的耗時(shí)更為嚴(yán)重,很容易出現(xiàn)白屏的現(xiàn)象春寿。
第二個(gè)問題是浪費(fèi)流量朗涩,雖然WebView內(nèi)核有緩存機(jī)制(例如兩次打開相同的頁面,第二次會快很多)绑改,但是對于新聞咨詢頁面來說谢床,很多css、js甚至圖片資源都是重復(fù)的厘线,但是內(nèi)核的緩存機(jī)制不一定能準(zhǔn)確地復(fù)用這些資源识腿。
第三個(gè)問題就是安全問題,部分前端頁面需要通過JSBridge調(diào)用原生API造壮,如果JavaScriptInterface不對調(diào)用方的域名進(jìn)行限制渡讼,某些惡意網(wǎng)址就可能調(diào)用原生的方法進(jìn)行入侵。
安全問題是依賴業(yè)務(wù)去解決的耳璧,如果沒有暴露相應(yīng)的方法成箫,惡意網(wǎng)址也就無從下手。而性能問題與流量問題則會影響WebView的加載速度旨枯,比較影響用戶體驗(yàn)蹬昌,下面從WebView的加載流程來看我們應(yīng)該如何解決這兩個(gè)問題來提高加載速度。
二攀隔、WebView加載頁面的流程
假設(shè)WebView嵌入在Activity中皂贩,從Activity啟動到顯示出前端頁面的第一幀栖榨,大致要經(jīng)歷以下階段。
① Activity啟動
② WebView新建與初始化
③ WebView.loadUrl(String url)
而當(dāng)調(diào)用WebView.loadUrl(String url)之后明刷,所有的資源請求都會經(jīng)過WebViewClient的shouldInterceptRequest(...)
方法婴栽,這些資源請求包括但不局限于主html內(nèi)容、css辈末、js和圖片資源等愚争,應(yīng)用可以在該方法攔截資源請求并加載別的內(nèi)容。
可以看到加載頁面的流程是串行的本冲,主要分為兩個(gè)部分准脂,一是WebView的新建與初始化,二是WebView.loadUrl(String url)
檬洞,那么只要我們縮短其中任一階段的耗時(shí)狸膏,就可以達(dá)到縮短耗時(shí)的目的。
三添怔、WebView加載速度優(yōu)化方案
這里以新聞資訊類頁面為例湾戳,目前這類頁面加載最快的就是頭條系A(chǔ)PP,例如“懂車帝”广料,在信息流內(nèi)點(diǎn)擊咨詢跳轉(zhuǎn)后直接展示咨詢詳情砾脑,不會像其他APP那樣出現(xiàn)過渡界面或白屏。
那么懂車帝究竟做了什么才能讓W(xué)ebView加載地如此之快艾杏?
首先對于新聞咨詢頁面來說韧衣,不同的頁面之間有很多重復(fù)的資源,這些資源可以直接保存在本地復(fù)用购桑。反編譯懂車帝APK之后會發(fā)現(xiàn)Assets中有很多的css畅铭、js和圖片文件,應(yīng)用可以攔截這部分的請求轉(zhuǎn)而加載這些文件勃蜘。
其次硕噩,WebView的新建與初始化也是比較耗時(shí)的,因此可以使用WebView緩存池進(jìn)行復(fù)用缭贡。
當(dāng)然還有最重要的一點(diǎn)就是預(yù)加載炉擅,在懂車帝信息流內(nèi),即使你斷網(wǎng)后點(diǎn)開一條咨詢阳惹,你會發(fā)現(xiàn)文字內(nèi)容還是正常加載谍失。也就是說,在信息流內(nèi)時(shí)莹汤,url的主html內(nèi)容已經(jīng)被下載到了內(nèi)存中快鱼,可以直接通過WebView.loadDataWithBaseUrl(...)
加載。
接下來介紹這3種方案的具體實(shí)現(xiàn)以及對加載速度的影響,我在懂車帝中挑選了7篇咨詢攒巍,編寫Demo并統(tǒng)計(jì)其加載時(shí)間(這里統(tǒng)計(jì)的是點(diǎn)擊item到WebView加載進(jìn)度為100所需的時(shí)間),在沒有使用任何優(yōu)化的情況下荒勇,平均加載時(shí)間為1203ms柒莉。
3.1 資源緩存
上面提到,WebView 請求任何的資源時(shí)都會回調(diào)shouldInterceptRequest()方法沽翔,此時(shí)可以將 WebView 需要請求的資源替換為本地資源以提升加載速度兢孝。
本地資源可以保存在Assets或文件系統(tǒng)中,如果保存在Assets中仅偎,文件的安全性會得到保證跨蟹,沒有出錯(cuò)或者被修改的風(fēng)險(xiǎn),但是無法實(shí)時(shí)更新橘沥,只能依賴客戶端發(fā)版更新文件窗轩;如果保存在文件系統(tǒng)中,可以做到實(shí)時(shí)更新座咆,但是下載文件時(shí)可能出錯(cuò)痢艺,使用時(shí)需要對文件進(jìn)行校驗(yàn)。
這里以保存在Assets中為例介陶,當(dāng)WebView回調(diào)shouldInterceptRequest(...)時(shí)堤舒,如果發(fā)現(xiàn)當(dāng)前的文件可以使用Assets中的緩存,即可將其包裝成WebResourceResponse哺呜,如下所示舌缤。
mWebView.setWebViewClient(new WebViewClient() {
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
// 根據(jù)url得到文件名
String fileName = getFileNameByUrl(request.getUrl().toString());
if (!fileInCache(fileName)) {
// 如果當(dāng)前文件不在緩存列表中, 不使用緩存
return super.shouldInterceptRequest(view, request);
} else {
// 當(dāng)前文件可以使用緩存, 根據(jù)后綴判斷 mimetype
InputStream inputStream = null;
String mimeType = null;
if (fileName.endsWith("css")) {
mimeType = "text/css";
} else if (fileName.endsWith("js")) {
mimeType = "text/javascript";
} else if (fileName.endsWith("png")) {
mimeType = "image/png";
}
if (mimeType == null) {
return null;
}
try {
inputStream = App.getContext().getAssets().open(fileName);
} catch (IOException e) {
Log.e(LOG_TAG, "read file IOException: " + e.getMessage());
}
if (inputStream != null) {
WebResourceResponse response = new WebResourceResponse(
mimeType, "utf-8", inputStream);
// 解決css、js的跨域問題
Map<String, String> headers = new HashMap<>();
headers.put("Access-Control-Allow-Origin", "*");
headers.put("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
headers.put("Access-Control-Max-Age", "3600");
headers.put("Access-Control-Allow-Headers", "Content-Type,Access-Token,Authorization");
response.setResponseHeaders(headers);
return response;
}
return null;
}
}
});
使用資源緩存后某残,平均的加載時(shí)間為1001ms国撵,相較于初始時(shí)提升了200ms左右。
3.2 WebView緩存池
平時(shí)使用WebView的時(shí)候我們都是動態(tài)新建并添加到ViewGroup中驾锰,新建時(shí)就傳入了Context卸留,那如果使用緩存池,Context該怎么傳呢椭豫?
一種方案是直接用ApplicationContext新建WebView耻瑟;另一種方案是使用MutableContextWrapper,如果在某個(gè)Activity中被使用了就改為該Activity的Context赏酥,回收時(shí)改為ApplicationContext喳整。一個(gè)簡易版的WebView緩存池實(shí)現(xiàn)如下。
注意在APP啟動時(shí)應(yīng)該就要調(diào)用WebViewPool的初始化方法新建一個(gè)WebView裸扶,不然啟動第一個(gè)包含WebView的頁面時(shí)耗時(shí)也會很久框都。
public class WebViewPool {
private List<DetailWebView> mIdleWebViewList;
private List<DetailWebView> mUsingWebViewList;
private static class Holder {
private static WebViewPool sInstance = new WebViewPool();
}
public static WebViewPool getInstance() {
return Holder.sInstance;
}
private WebViewPool() {
}
/**
* 在 APP 啟動時(shí)調(diào)用, 直接新建一個(gè)備用的WebView
*/
public void init() {
mIdleWebViewList = new CopyOnWriteArrayList<>();
mUsingWebViewList = new ArrayList<>();
MutableContextWrapper contextWrapper = new MutableContextWrapper(App.getContext());
DetailWebView detailWebView = new DetailWebView(contextWrapper);
mIdleWebViewList.add(detailWebView);
}
public DetailWebView acquireWebView(Context context) {
if (mIdleWebViewList != null && mIdleWebViewList.size() > 0) {
DetailWebView webView = mIdleWebViewList.remove(0);
MutableContextWrapper contextWrapper = (MutableContextWrapper) webView.getContext();
contextWrapper.setBaseContext(context);
mUsingWebViewList.add(webView);
return webView;
} else {
MutableContextWrapper contextWrapper = new MutableContextWrapper(context);
DetailWebView webView = new DetailWebView(contextWrapper);
mUsingWebViewList.add(webView);
return webView;
}
}
public void recycleWebView(DetailWebView webView) {
if (webView == null) {
return;
}
ViewGroup viewParent = (ViewGroup) webView.getParent();
if (viewParent != null) {
viewParent.removeView(webView);
}
webView.loadUrl("about:blank");
if (mUsingWebViewList != null && mUsingWebViewList.contains(webView)) {
mUsingWebViewList.remove(webView);
MutableContextWrapper contextWrapper = (MutableContextWrapper) webView.getContext();
contextWrapper.setBaseContext(App.getContext());
webView.setWebViewClient(null);
webView.setWebChromeClient(null);
mIdleWebViewList.add(webView);
} else {
webView.clearHistory();
webView.destroy();
}
}
}
在使用資源緩存的基礎(chǔ)上再使用WebView緩存池,平均的加載時(shí)間為918ms呵晨。
3.3 預(yù)加載
預(yù)加載主體的實(shí)現(xiàn)比較簡單魏保,在信息流的RecyclerView進(jìn)入IDLE狀態(tài)時(shí)熬尺,判斷當(dāng)前有多少個(gè)條目曝光,然后啟動對應(yīng)數(shù)量的子線程開始下載數(shù)據(jù)為String谓罗,用戶進(jìn)入某個(gè)條目時(shí)粱哼,如果數(shù)據(jù)下載完畢就可以直接通過WebView.loadDataWithBaseURL(mUrl, data, "text/html", "utf-8", null)
展示數(shù)據(jù)。
應(yīng)用對預(yù)加載下載的數(shù)據(jù)是需要統(tǒng)一管理的檩咱,可以考慮使用LRU緩存揭措。不過由于是多線程下載數(shù)據(jù),需要對在對LRU緩存讀取時(shí)加鎖保證線程安全刻蚯,這里的鎖可以選擇讀寫鎖绊含。
通過實(shí)驗(yàn),使用預(yù)加載之后的平均加載時(shí)間為698ms炊汹。
3.4 總結(jié)
這里的Demo對于加載時(shí)間的統(tǒng)計(jì)其實(shí)是存在誤差的躬充,首先選取的咨詢較少;其次前端頁面的加載時(shí)間受網(wǎng)速影響是最大的讨便。所以這個(gè)統(tǒng)計(jì)僅供參考麻裳,但是也能看出來每個(gè)優(yōu)化方法都是有作用的,在實(shí)際項(xiàng)目中可以根據(jù)項(xiàng)目實(shí)際情況使用器钟。
四津坑、其余優(yōu)化方案
騰訊有一個(gè)WebView加載優(yōu)化的方案VasSonic,將WebView初始化和WebView加載數(shù)據(jù)這兩個(gè)操作由原本的串行改為并行傲霸,縮短整體的加載時(shí)間疆瑰。
VasSonic大概的流程為:WebView開始初始化時(shí)啟動一個(gè)子線程去下載html數(shù)據(jù),當(dāng)WebView初始化完的時(shí)候通知子線程已經(jīng)初始化完畢昙啄,此時(shí)數(shù)據(jù)下載有3種情況:1穆役、子線程還沒開始下載數(shù)據(jù);2梳凛、子線程下載了一部分?jǐn)?shù)據(jù)耿币;3、子線程已經(jīng)下載完了數(shù)據(jù)韧拒。收到WebView初始化完的消息后淹接,子線程將已經(jīng)下載的數(shù)據(jù)和沒有下載的數(shù)據(jù)拼接為橋接流返回給內(nèi)核渲染。
當(dāng)然這個(gè)框架還有很多功能叛溢,例如將html內(nèi)容分為模板和數(shù)據(jù)塑悼,并且為模板和數(shù)據(jù)分別提供更新的功能,適合用于更新頻繁的運(yùn)營類界面楷掉,具體可見參考3厢蒜。