H5頁面承載了懂球帝文章、活動、廣告等核心業務場景,因此通過了長期的迭代以後,懂球帝客戶端H5相關的業務也很是複雜,這裏麪包含了分享、支付、用戶評論、點贊等交互,各類業務交織雜糅在一塊兒,致使這一塊的代碼難以維護。筆者對業務進行了全面的梳理,在重構這塊業務的過程當中也收穫了不少,同時考慮到不少產品都有類似的應用場景,分享出來但願對你們有幫助。 介紹下全文的結構和思路,首先筆者對現有業務進行抽象,提取出其中的調用關係和信息流,第二部分簡單地介紹了項目中以前採用的方案和缺點。第三部分着重介紹了新的設計架構。第四部分則是基於新的架構,實現了一些優化的功能,好比緩存和HttpDns的功能。javascript
首先咱們分析一下通常App中,H5相關的業務場景和這些業務場景中的信息流。H5業務場景裏面包含了幾個角色:WebView是顯示H5的控件;Activity/Fragment等嵌入有WebView的界面、H5頁面、JsBridge是H5和客戶端互相調用的橋樑,這裏面用戶直接打交道的是界面和H5頁面。基本上,大部分業務場景能夠概括爲如下幾種css
onPageStarted、onPageFinished
等方法,須要分別通知界面和H5進行處理業務邏輯多是以上四種業務場景之一,複雜的業務場景也多是這幾種業務場景的組合,好比用戶點擊H5中的點贊按鈕,這時候調用界面的登陸功能,登陸完成後,界面調用H5的功能執行刷新界面等操做。html
原有的實現邏輯也比較簡單粗暴,就是在界面和Webview中間增長一個WebviewManager,負責這二者之間的通訊。前端
在WebView和H5之間增長一個BrigeHelper負責管理JsBridge的交互。一開始可能沒什麼大問題,可是隨着業務代碼的積累,就會發現幾個問題:java
經過前面的業務介紹能夠發現,H5和native之間的交互多且雜。筆者在項目中也嘗試去使用Cordova的方案,可是Cordova過於龐大複雜,客戶端和前端要徹底遷移到這上面,特別是對於懂球帝這樣迭代了幾年的項目,成本太高。出於實際的考慮,筆者準備探尋其餘的解決方案。不過看了Cordova的源碼也給了我必定的思路。 經過進一步對業務場景的抽象,筆者初步傾向於把不少業務以及JsBridge都抽離出來互相獨立,好比分享的業務、支付的業務、用戶相關的業務等,這種情境就很是適合用策略模式,使用策略模式有幾個優勢:android
固然策略模式也有缺點,就是使用者必需要了解各個業務,並且策略模式會多出來不少類,目前看,應該是優勢大於缺點git
Activity/Fragment的生命週期能夠從界面的LifeCycle中得到,實現LifeCycleObserver就能夠了github
Webview的觸發事件定義爲IWebviewCallback,這裏麪包含策略裏面須要用到的主要方法web
public interface IWebviewCallback {
void onPageFinished();
void onPageStart();
boolean shouldOverrideUrlLoading(WebView webView, String url);
WebResourceResponse shouldInterceptRequest(WebView view, final WebResourceRequest webResourceRequest);
boolean onShowFileChooser(ValueCallback<Uri[]> filePathCallback,WebChromeClient.FileChooserParams fileChooserParams);
void onLoadResource(WebView webView, String url);
......
}
複製代碼
public interface IBridgeHandler {
/** * bridge名稱 * @return */
public String[] getName();
/** * Birdge觸發時調用調用 * @param jsonObject * @param callBackFunction */
public void onHandle(String functionName, JSONObject jsonObject, CallBackFunction callBackFunction);
}
複製代碼
public interface IPluginLifeCycle {
/** * 插件被添加的時候調用 */
void onPluginAdded();
/** * 插件被移除的時候調用 */
void onPluginRemoved();
}
複製代碼
接下來定義策略的接口IWebviewplugin繼承者四個接口apache
public abstract class IWebviewPlugin implements GenericLifecycleObserver, IPluginLifeCycle, IWebviewCallback, IBridgeHandler {
}
複製代碼
這樣策略的接口就定義好了,而且具有了處理來自WebView、界面生命週期、JsBridge以及自身生命週期的能力。經過這四個接口,咱們就能實現第一部分中介紹的業務場景的功能 接口定義好以後,整個框架就呼之欲出了:
除了以上介紹的幾個接口,咱們還須要幾個接口和類:
WebHostCallback
:由界面實現,能夠獲取WebView的狀態
PageInterface
: 因爲使用WebView的多是Activity,也有多是Fragment甚至是一個ViewGroup,因此須要對界面的功能進行抽象,提供一個可供WebView使用的一些功能,好比打開一個Activity等
PluginManager
:策略的管理類,能夠動態組合管理策略集合
WebviewWrapper
:WebView的包裝類,這裏採用抽象WebView的一些Api而後包裝的形式而不是繼承WebView來擴展功能,主要是考慮到使用者可能繼承WebView實現本身的一些功能,甚至是將來可能更換系統的WebView採用第三方的內核
PluginFactory
:生產策略的工廠
能夠看到,使用接口抽象後,WebView並不知道有多少個策略被實現了,它要作的只是調用IWebviewCallback接口,這樣就把WebView從業務中抽離出來,WebviewWrapper並不涉及到業務的代碼,只須要配置和管理好WebView就能夠了 經過PageInterface和WebviewHostCallback的接口,Webview也不須要關心本身是在Activity仍是Fragment裏面。 從使用者的角度來看,繼承IWebviewPlugin實現本身的策略就能夠了。 經過接口抽象後,解耦了原來各個部分強耦合的關係。
前面介紹了整個客戶端H5服務的架構,咱們就基於這個架構實現一個技術優化需求,那就是提高H5頁面的加載速度。咱們先來看下,H5頁面的網絡請求可能包括如下幾部分
在這些網絡請求中,咱們面臨一些問題:
要解決這些問題,咱們就須要實現由客戶端代理WebView的網絡請求,還須要實現WebView加載客戶端本地緩存的能力。 WebViewClient類中提供了shouldInterceptRequest的方法。
public WebResourceResponse shouldInterceptRequest(WebView view, final WebResourceRequest webResourceRequest) 複製代碼
經過複寫這個方法咱們能夠攔截瀏覽器的資源請求,返回指定的內容。這個接口在文檔中是這麼描述的
Notify the host application of a resource request and allow the application to return the data. If the return value is null, the WebView will continue to load the resource as usual. Otherwise, the return response and data will be used. This callback is invoked for a variety of URL schemes (e.g., http(s):, data:, file:, etc.), not only those schemes which send requests over the network. This is not called for javascript: URLs, blob: URLs, or for assets accessed via file:///android_asset/ or file:///android_res/ URLs. In the case of redirects, this is only called for the initial resource URL, not any subsequent redirect URLs.
這裏面有幾點須要注意的
1. 這個接口不僅是http或者https的請求才出發的,也有多是data或者file協議,可是不包括javascript、file:///android_asset或者file:///android_res。因此攔截的時候咱們要注意只攔截網絡請求。 2. 返回值爲空時瀏覽器會按原有的邏輯執行請求,不然就使用指定的數據。 3. 重定向的請求只有第一個連接會調用這個接口,後續跳轉連接不會。
那麼這個接口怎麼使用呢,看看這個接口的參數和返回值就知道了。這個接口的參數WebResourceRequest的定義:
public interface WebResourceRequest {
/** * 請求的url */
Uri getUrl();
/** * 返回該請求是否是主Frame發出的 */
boolean isForMainFrame();
/** * 該請求是不是服務端重定向的結果 */
boolean isRedirect();
/** * 該請求是否與用戶的手勢有關,好比點擊等 */
boolean hasGesture();
/** * 網絡的請求Method,好比GET或者POST */
String getMethod();
/** * 網絡請求的header */
Map<String, String> getRequestHeaders();
}
複製代碼
這裏麪包含了請求的基本信息,再來看看返回值WebResourceResponse的相關屬性
public class WebResourceResponse {
private String mMimeType;//資源的MIME類型,好比text/html
private String mEncoding;//response的編碼格式,好比utf-8
private int mStatusCode;//http狀態碼
private String mReasonPhrase;//狀態碼描述與,好比200對應的是「OK」,這個值不能爲空
private Map<String, String> mResponseHeaders;//response的header
private InputStream mInputStream;//輸入流
}
複製代碼
因爲國內Dns並非十分可靠,不少公司都會考慮接入HttpDns服務,咱們在網絡層中已經實現了HttpDns相關功能,是經過OkHttp的dns接口實現的,有興趣的能夠看一下這兩篇文章 OkHttp接入HttpDNS,最佳實踐 okhttp源碼解析(五):代理和DNS 相關原理在這裏就再也不贅述,咱們重點關心一下如何攔截WebView的請求,並使用OkHttp作網絡請求
Request.Builder builder = new Request.Builder();
//構造請求
builder.url(url).method(webResourceRequest.getMethod(), null);
Map<String, String> requestHeaders = webResourceRequest.getRequestHeaders();
if (Lang.isNotEmpty(requestHeaders)) {
for (Map.Entry<String, String> entry : requestHeaders.entrySet()) {
builder.addHeader(entry.getKey(), entry.getValue());
}
}
Call synCall = mClient.newCall(builder.build());
okhttp3.Response response = synCall.execute();
if (response.body() != null) {
ResponseBody body = response.body();
Map<String, String> map = new HashMap<>();
for (int i = 0; i < response.headers().size(); i++) {
//相應體的header
map.put(response.headers().name(i), response.headers().value(i));
}
//MIME類型
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(url));
String contentType = response.headers().get("Content-Type");
String encoding = "utf-8";
//獲取ContentType和編碼格式
if (contentType != null && !"".equals(contentType)) {
if (contentType.contains(";")) {
String[] args = contentType.split(";");
mimeType = args[0];
String[] args2 = args[1].trim().split("=");
if (args.length == 2 && args2[0].trim().toLowerCase().equals("charset")) {
encoding = args2[1].trim();
}
} else {
mimeType = contentType;
}
}
WebResourceResponse webResourceResponse = new WebResourceResponse(mimeType, encoding, body.byteStream());
String message = response.message();
int code = response.code();
if (TextUtils.isEmpty(message) && code == 200) {
//message不能爲空
message = "OK";
}
webResourceResponse.setStatusCodeAndReasonPhrase(code, message);
webResourceResponse.setResponseHeaders(map);
複製代碼
這裏面須要注意的是對重定向的處理,筆者在實際項目中使用時,若是有重定向的code返回,OkHttp中是能夠直接處理重定向的結果,而且返回重定向後網絡請求的結果。可是把這個結果返回給給WebView後,WebView並不知道通過302跳轉了,因此會將當前的響應與重定向前的url關聯,致使頁面加載時可能出現錯誤,因此須要把302的狀況交回給WebView進行處理。在OkHttp中把重定向的follow設置爲false:okClientBuilder.followRedirects(false);
這樣,就能夠代理網絡請求。結合咱們基於OkHttp作的HttpDns,就能夠解決WebView的請求Dns的問題。 此外,若有應用對Cookie有需求,能夠自行設置Cookie的請求,相關設置能夠參考有贊技術團隊的相關文章
前面介紹過咱們把H5頁面的資源分紅三個部分:
第3點咱們採用的是由客戶端對一些數據進行預緩存和預請求,在用戶打開頁面的時候,再將緩存數據經過jsBridge傳遞到H5中,提升頁面的加載速度。 對於1和2能夠由WebView自帶的緩存,可是缺點比較明顯,首先,客戶端很難操做WebView的緩存,沒辦經過WebView的緩存進行一些預下載和預更新的邏輯,形成首次進入時網絡請求的時間過長。其次,圖片在Native下載過,在WebView可能又得下載一遍,好比文章的封面圖,在列表中加載過一次,在H5中又須要下載一次。因此咱們考慮在Native中作代碼包管理和共享Native的圖片緩存。這些資源都以文件的形式存放在用戶的手機上。一樣經過shouldInterceptRequest接口實現:
fileInputStream = new FileInputStream(file);
String type = "text/html";
if (path.contains(".css")) {
type = "text/css";
} else if (path.contains(".js")) {
type = "application/javascript";
}
WebResourceResponse response = new WebResourceResponse(type, "utf-8", fileInputStream);
Map<String, String> map = new HashMap<>();
map.put("Content-Type", type);
//跨域響應頭,不然可能會有跨域沒法訪問的問題
map.put("Access-Control-Allow-Origin", "*");
response.setResponseHeaders(map);
return response;
複製代碼
把本地文件流做爲WebResourceResponse的輸入流,這樣就把網絡請求替代問本地文件。基於此,咱們也實現了本地包的管理和圖片的緩存。而這一切對於H5頁面來講甚至是毫無感知的。 優化完成後,懂球帝客戶端打開文章頁和加載封面圖,幾乎是秒開的速度
一、筆者首先對H5相關的業務進行了抽象,梳理了這其中的信息流和調用關係,採用策略模式重構了客戶端的WebView頁面,依賴於接口而不是原來的依賴於具體實現。
二、基於策略模式的接口,筆者實現了對於H5的網絡資源請求的代理,爲H5頁面的網絡請求實現HttpDns的功能,解決線上的DNS問題。
三、基於策略模式的接口,筆者實現H5包和圖片的本地緩存。
經過以上幾個優化,懂球帝的Android客戶端中與WebView相關的業務都獲得了極大的優化,十幾個頁面中重複的數千行代碼和邏輯獲得了從新的梳理,有些頁面甚至簡化了上千行代碼。基於策略模式,WebView的功能擴展也獲得了良好的擴展性和約束。至此咱們對於WebView頁面的優化也告一段落,固然後面咱們還會持續作一些優化,有幾個方向
一、目前包緩存只在部分頁面應用了,將來能夠擴展到更多的頁面和更細粒度的文件上
二、提升WebView的一致性,更換第三方內核
三、對WebView中的音視頻進行優化
......
【1】如何設計一個優雅健壯的Android WebView?(下)
【2】有贊webview加速平臺探索與建設(三)——html加速
【3】《研磨設計模式》,清華大學出版社,陳臣,王斌
【4】Android Developers WebResourceResponse
【5】Android:手把手教你構建 全面的WebView 緩存機制 & 資源加載方案
以上文獻對本文幫助甚多,十分感謝文獻做者的分享精神! 因爲本文準備比較倉促,文中有些地方可能有錯誤或者待提升的地方,敬請指出糾正,若有更多交流,能夠聯繫個人郵箱txlbupt@gmail.com
【本文做者】塗曉龍,曾就任於網易和美圖,現擔任懂球帝安卓研發工程師,致力於爲足球迷們打造一款更好用的App