前言
Android應用層的開發有幾大模塊,其中WebView是最重要的模塊之一。網上可以搜索到的WebView資料可謂寥寥,Github上的開源項目也不是不少,更別提有一個現成封裝好的WebView容器直接用於生產環境了。本文僅當記錄在使用WebView實現業務需求時所踩下的一些坑,並提供一些解決思路,避免遇到相同問題的朋友再次踩坑。須要說明的是,本文僅提供解決思路,不提供源碼。css
WebView現狀
Android系統的WebView發展歷史可謂一波三折,系統WebView開發者確定費勁心思才換取了今天的局面——應用裏的WebView和Chrome表現一致。對於Android初學者,或者剛要開始接觸WebView的開發來講,WebView是有點難以適應,甚至是有一些害怕的。開源社區對於WebView的改造和包裝很是少,須要開發者查找大量資料去理解WebView。html
WebView Changelog
在Android4.4(API level 19)系統之前,Android使用了原生自帶的Android Webkit內核,這個內核對HTML5的支持不是很好,如今使用4.4如下機子的也很少了,就不對這個內核作過多介紹了,有興趣能夠看下這篇文章。前端
從Android4.4系統開始,Chromium內核取代了Webkit內核,正式地接管了WebView的渲染工做。Chromium是一個開源的瀏覽器內核項目,基於Chromium開源項目修改實現的瀏覽器很是多,包括最著名的Chrome瀏覽器,以及一衆國內瀏覽器(360瀏覽器、QQ瀏覽器等)。其中Chromium在Android上面的實現是Android System WebView
^1。java
從Android5.0系統開始,WebView移植成了一個獨立的apk,能夠不依賴系統而獨立存在和更新,咱們能夠在系統->設置->Android System WebView
看到WebView的當前版本。android
從Android7.0系統開始,若是系統安裝了Chrome (version>51),那麼Chrome將會直接爲應用的WebView提供渲染,WebView版本會隨着Chrome的更新而更新,用戶也能夠選擇WebView的服務提供方(在開發者選項->WebView Implementation裏),WebView能夠脫離應用,在一個獨立的沙盒進程中渲染頁面(須要在開發者選項裏打開)^2。git
從Android8.0系統開始,默認開啓WebView多進程模式,即WebView運行在獨立的沙盒進程中^3。程序員
爲何WebView那麼難搞?
儘管應用開發者使用WebView和使用普通的View同樣簡單,只須要在xml裏定義或者直接實例化出來便可使用,但WebView是至關難搞的。爲何呢?如下有幾個可能的因素。github
- 繁雜的WebView配置
WebView在初始化的時候就提供了默認配置WebSettings
,可是不少默認配置是不可以知足業務需求的,還須要進行二次配置,例如考拉App在默認配置基礎作了以下修改:web
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
public static void
setDefaultWebSettings(WebView webView) {
WebSettings webSettings = webView.getSettings
();
//5.0以上開啓混合模式加載
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
webSettings.
setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
webSettings.
setLoadWithOverviewMode(true);
webSettings.
setUseWideViewPort(true);
//容許js代碼
webSettings.
setJavaScriptEnabled(true);
//容許SessionStorage/LocalStorage存儲
webSettings.
setDomStorageEnabled(true);
//禁用放縮
webSettings.
setDisplayZoomControls(false);
webSettings.
setBuiltInZoomControls(false);
//禁用文字縮放
webSettings.
setTextZoom(100);
//10M緩存,api 18後,系統自動管理。
webSettings.
setAppCacheMaxSize(10 * 1024 * 1024);
//容許緩存,設置緩存位置
webSettings.
setAppCacheEnabled(true);
webSettings.
setAppCachePath(context.getDir("appcache", 0).getPath());
//容許WebView使用File協議
webSettings.
setAllowFileAccess(true);
//不保存密碼
webSettings.
setSavePassword(false);
//設置UA
webSettings.
setUserAgentString(webSettings.getUserAgentString() + " kaolaApp/" + AppUtils.getVersionName());
//移除部分系統JavaScript接口
KaolaWebViewSecurity.removeJavascriptInterfaces
(webView);
//自動加載圖片
webSettings.
setLoadsImagesAutomatically(true);
}
|
除此以外,使用方還須要根據業務需求實現WebViewClient
和WebChromeClient
,這兩個類所須要覆寫的方法更多,用來實現標題定製、加載進度條控制、jsbridge交互、url攔截、錯誤處理(包括http、資源、網絡)等不少與業務相關的功能。
- 複雜的前端環境
現在,萬維網的核心語言,超文本標記語言已經發展到了HTML5,隨之而來的是html、css、js相應的升級與更新。高版本的語法沒法在低版本的內核上識別和渲染,業務上須要使用到新的特性時,開發不得不面對後向兼容的問題。互聯網的連接千千萬萬,使用哪些語言特性不是WebView能決定的,要求WebView適配全部頁面幾乎是不可能的事情。
- 版本間差別
WebView不一樣的版本方法的實現是有可能不同的,而前端通常狀況下只會調用系統的api來實現功能,這就會致使Android不一樣的系統、不一樣的WebView版本表現不一致的狀況。一個典型的例子是下面即將描述的WebView中的文件上傳功能,當咱們在Web頁面上點擊選擇文件的控件(<input type="file">
)時,會產生不一樣的回調方法。除了文件上傳功能,版本間的差別還有不少不少,好比緩存機制的版本差別,js安全漏洞的屏蔽,cookie管理等。Google也在想辦法解決這些差別給開發者帶來的適配壓力,例如Webkit內核到Chromium內核的切換對開發者是透明的,底層的API徹底沒有改變,這也是好的設計模式帶來的益處。
- 國內ROM、瀏覽器對WebView內核的魔改
國產手機的廠商基本在出廠時都自帶了瀏覽器,查看系統應用時,發現並無內置com.android.webview
或者com.google.android.webview
包,這些瀏覽器並非簡單地套了一層WebView的殼,而是直接使用了Chromium內核,至於有沒有魔改過內核源碼,不得而知。國產出品的瀏覽器,如360瀏覽器、QQ瀏覽器、UC瀏覽器,幾乎都魔改了內核。值得一提的是,騰訊出品的X5內核,號稱頁面渲染流暢度高於原生內核,客戶端減小了WebView帶來坑的同時,增長了前端適配的難度,功能實現上須要有更多地考慮。
- 須要必定的Web知識
若是僅僅會使用WebView.loadUrl()
來加載一個網頁而不瞭解底層到底發生了什麼,那麼url發生錯誤、url中的某些內容加載不出來、url裏的內容點擊無效、支付寶支付浮層彈不起來、與前端沒法溝通等等問題就會接踵而至。要開發好一個功能完整的WebView,須要對Web知識(html、js、css)有必定了解,知道loadUrl,WebView在後臺請求這個url之後,服務器作了哪些響應,又下發了哪些資源,這些資源的做用是怎麼樣的。
爲何Github上的WebView項目不適用?
從上面的連接能夠看到,Github上面star過千的WebView項目主要是FinestWebView-Android和Android-AdvancedWebView。看過源碼的話應該知道,第一個項目偏向於實現一個瀏覽器,第二個項目提供的接口太少,而且一些坑並未填完。陸續看過幾個別的開源實現,發現並不理想。後來想一想,很難不依賴於業務而單獨實現一個WebView,特別是與前端約定了jsbridge接口,須要處理頁面關閉、全屏、url攔截、登陸、分享等一系列功能,即使是接入了開源平臺的WebView,也須要作大量的擴展才有可能徹底知足需求。與其如此,每一個電商平臺都有本身一套規則,基於電商的業務需求來本身擴展WebView是比較合理的。
WebView踩坑歷程
能夠說,若是是初次接觸WebView,不踩坑幾乎是不可能的。筆者在接觸到前人留下來的WebView代碼時,有些地方寫的很trickey,若是不仔細閱讀,或者翻閱資料,頗有可能就會掉進坑裏。下面介紹幾個曾經遇到過的坑。
WebSettings.setJavaScriptEnabled
我相信99%的應用都會調用下面這句
1
|
WebSettings.setJavaScriptEnabled(true)
;
|
在Android 4.3版本調用WebSettings.setJavaScriptEnabled()
方法時會調用一下reload方法,同時會回調屢次WebChromeClient.onJsPrompt()
。若是有業務邏輯依賴於這兩個方法,就須要注意判斷回調屢次是否會帶來影響了。
同時,若是啓用了JavaScript,務必作好安全措施,防止遠程執行漏洞^5。
1
2
3
4
5
6
7
8
9
10
11
12
|
private static final void removeJavascriptInterfaces(WebView webView) {
try {
if (Build.VERSION.SDK_INT >= 11 && Build.VERSION.SDK_INT < 17) {
webView.removeJavascriptInterface(
"searchBoxJavaBridge_");
webView.removeJavascriptInterface(
"accessibility");
webView.removeJavascriptInterface(
"accessibilityTraversal");
}
}
catch (Throwable tr) {
tr.printStackTrace();
}
}
|
301/302重定向問題
WebView的301/302重定向問題,絕對在踩坑排行榜里名列前茅。。。隨便搜了幾個解決方案,要麼不能知足業務需求,要麼清一色沒有完全解決問題。
https://stackoverflow.com/questions/4066438/android-webview-how-to-handle-redirects-in-app-instead-of-opening-a-browser
http://blog.csdn.net/jdsjlzx/article/details/51698250
http://www.cnblogs.com/pedro-neer/p/5318354.html
http://www.jianshu.com/p/c01769ababfa
301/302業務場景及白屏問題
先來分析一下業務場景。對於須要對url進行攔截以及在url中須要拼接特定參數的WebView來講,301和302發生的情景主要有如下幾種:
- 首次進入,有重定向,而後直接加載H5頁面,如http跳轉https
- 首次進入,有重定向,而後跳轉到native頁面,如掃一掃短鏈,而後跳轉到native
- 二次加載,有重定向,跳轉到native頁面
- 對於考拉業務來講,還有相似登陸後跳轉到某個頁面的需求。如個人拼團,未登陸狀態下點擊個人拼團跳轉到登陸頁面,登陸完成後再加載個人拼團頁。
第一種狀況屬於正常狀況,暫時沒遇到什麼坑。
第二種狀況,會遇到WebView空白頁問題,屬於原始url不能攔截到native頁面,但301/302後的url攔截到native頁面的狀況,當遇到這種狀況時,須要把WebView對應的Activity結束,不然當用戶從攔截後的頁面返回上一個頁面時,是一個WebView空白頁。
第三種狀況,也會遇到WebView空白頁問題,緣由在於加載的第一個頁面發生了重定向到了第二個頁面,第二個頁面被客戶端攔截跳轉到native頁面,那麼WebView就停留在第一個頁面的狀態了,第一個頁面顯然是空白頁。
第四種狀況,會遇到無限加載登陸頁面的問題。考拉的登陸連接是相似下面這種格式:
1
|
https://m.kaola.com/login.html?target=登陸後跳轉的url
|
若是登陸成功後還從新加載這個url,那麼就會循環跳轉到登陸頁面。第四點解決起來比較簡單,登陸成功之後拿到target後的跳轉url再從新加載便可。
301/302回退棧問題
不管是哪一種重定向場景,都不可避免地會遇到回退棧的處理問題,若是處理不當,用戶按返回鍵的時候不必定能回到重定向以前的那個頁面。不少開發者在覆寫WebViewClient.shouldOverrideUrlLoading()
方法時,會簡單地使用如下方式粗暴處理:
1
2
3
4
5
6
7
8
|
WebView.setWebViewClient(
new WebViewClient() {
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
...
)
|
這種方法最致命的弱點就是若是不通過特殊處理,那麼按返回鍵是沒有效果的,還會停留在302以前的頁面。現有的解決方案無非就幾種:
- 手動管理回退棧,遇到重定向時回退兩次^6。
- 經過
HitTestResult
判斷是不是重定向,從而決定是否本身加載url^7。 - 經過設置標記位,在
onPageStarted
和onPageFinished
分別標記變量避免重定向^9。
能夠說,這幾種解決方案都不是完美的,都有缺陷。
301/302較優解決方案
解決301/302回退棧問題
可否結合上面的幾種方案,來更加準確地判斷301/302的狀況呢?下面說一下本文的解決思路。在提供解決方案以前,咱們須要瞭解一下shouldOverrideUrlLoading
方法的返回值表明什麼意思。
Give the host application a chance to take over the control when a new url is about to be loaded in the current WebView. If WebViewClient is not provided, by default WebView will ask Activity Manager to choose the proper handler for the url. If WebViewClient is provided, return true means the host application handles the url, while return false means the current WebView handles the url.
簡單地說,就是返回true,那麼url就已經由客戶端處理了,WebView就無論了,若是返回false,那麼當前的WebView實現就會去處理這個url。
WebView可否知道某個url是否是301/302呢?固然知道,WebView可以拿到url的請求信息和響應信息,根據header裏的code很輕鬆就能夠實現,事實正是如此,交給WebView來處理重定向(return false
),這時候按返回鍵,是能夠正常地回到重定向以前的那個頁面的。(PS:從上面的章節可知,WebView在5.0之後是一個獨立的apk,能夠單獨升級,新版本的WebView實現確定處理了重定向問題)
可是,業務對url攔截有需求,確定不能把全部的狀況都交給系統WebView處理。爲了解決url攔截問題,本文引入了另外一種思想——經過用戶的touch事件來判斷重定向。下面經過代碼來講明。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
|
/**
* WebView基礎類,處理一些基礎的公有操做
*
*
@author xingli
*
@time 2017-12-06
*/
public class BaseWebView extends WebView {
private boolean mTouchByUser;
public BaseWebView(Context context) {
super(context);
}
public BaseWebView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public BaseWebView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public final void loadUrl(String url, Map<String, String> additionalHttpHeaders) {
super.loadUrl(url, additionalHttpHeaders);
resetAllStateInternal(url);
}
public void loadUrl(String url) {
super.loadUrl(url);
resetAllStateInternal(url);
}
public final void postUrl(String url, byte[] postData) {
super.postUrl(url, postData);
resetAllStateInternal(url);
}
public final void loadData(String data, String mimeType, String encoding) {
super.loadData(data, mimeType, encoding);
resetAllStateInternal(getUrl());
}
public final void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding,
String historyUrl) {
super.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl);
resetAllStateInternal(getUrl());
}
public void reload() {
super.reload();
resetAllStateInternal(getUrl());
}
public boolean isTouchByUser() {
return mTouchByUser;
}
private void resetAllStateInternal(String url) {
if (!TextUtils.isEmpty(url) && url.startsWith("javascript:")) {
return;
}
resetAllState();
}
// 加載url時重置touch狀態
protected void resetAllState() {
mTouchByUser =
false;
}
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//用戶按下到下一個連接加載以前,置爲
true
mTouchByUser =
true;
break;
}
return super.onTouchEvent(event);
}
public void setWebViewClient(final WebViewClient client) {
super.setWebViewClient(new WebViewClient() {
public boolean shouldOverrideUrlLoading(WebView view, String url) {
boolean handleByChild = null != client && client.shouldOverrideUrlLoading(view, url);
if (handleByChild) {
// 開放client接口給上層業務調用,若是返回true,表示業務已處理。
return true;
}
else if (!isTouchByUser()) {
// 若是業務沒有處理,而且在加載過程當中用戶沒有再次觸摸屏幕,認爲是301/302事件,直接交由系統處理。
return super.shouldOverrideUrlLoading(view, url);
}
else {
//不然,屬於二次加載某個連接的狀況,爲了解決拼接參數丟失問題,從新調用loadUrl方法添加固有參數。
loadUrl(url);
return true;
}
}
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
boolean handleByChild = null != client && client.shouldOverrideUrlLoading(view, request);
if (handleByChild) {
return true;
}
else if (!isTouchByUser()) {
return super.shouldOverrideUrlLoading(view, request);
}
else {
loadUrl(request.getUrl().toString());
return true;
}
}
});
}
}
|
上述代碼解決了正常狀況下的回退棧問題。
解決業務白屏問題
爲了解決白屏問題,考拉目前的解決思路和上面的回退棧問題思路有些相似,經過監聽touch
事件分發以及onPageFinished
事件來判斷是否產生白屏,代碼以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
|
public class KaolaWebview extends BaseWebView implements DownloadListener, Lifeful, OnActivityResultListener {
private boolean mIsBlankPageRedirect; //是否因重定向致使的空白頁面。
public KaolaWebview(Context context) {
super(context);
init();
}
public KaolaWebview(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public KaolaWebview(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
protected void back() {
if (mBackStep < 1) {
mJsApi.trigger2(
"kaolaGoback");
}
else {
realBack();
}
}
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_UP) {
mIsBlankPageRedirect =
true;
}
return super.dispatchTouchEvent(ev);
}
private WebViewClient mWebViewClient = new WebViewClient() {
public boolean shouldOverrideUrlLoading(WebView view, String url) {
url = WebViewUtils.removeBlank(url);
//容許啓動第三方應用客戶端
if (WebViewUtils.canHandleUrl(url)) {
boolean handleByCaller = false;
// 若是不是用戶觸發的操做,就沒有必要交給上層處理了,直接走url攔截規則。
if (null != mIWebViewClient && isTouchByUser()) {
handleByCaller = mIWebViewClient.shouldOverrideUrlLoading(view, url);
}
if (!handleByCaller) {
handleByCaller = handleOverrideUrl(url);
}
return handleByCaller || super.shouldOverrideUrlLoading(view, url);
}
else {
try {
notifyBeforeLoadUrl(url);
Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
intent.addCategory(Intent.CATEGORY_BROWSABLE);
mContext.startActivity(intent);
if (!mIsBlankPageRedirect) {
// 若是遇到白屏問題,手動後退
back();
}
}
catch (Exception e) {
ExceptionUtils.printExceptionTrace(e);
}
return true;
}
}
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
return shouldOverrideUrlLoading(view, request.getUrl().toString());
}
private boolean handleOverrideUrl(final String url) {
RouterResult result = WebActivityRouter.startFromWeb(
new IntentBuilder(mContext, url).setRouterActivityResult(new RouterActivityResult() {
public void onActivityFound() {
if (!mIsBlankPageRedirect) {
// 路由已經攔截到跳轉到native頁面,但此時可能發生了
// 301/302跳轉,那麼執行後退動做,防止白屏。
back();
}
}
public void onActivityNotFound() {
if (mIWebViewClient != null) {
mIWebViewClient.onActivityNotFound();
}
}
}));
return result.isSuccess();
}
};
public void onPageFinished(WebView view, String url) {
mIsBlankPageRedirect =
true;
if (null != mIWebViewClient) {
mIWebViewClient.onPageReallyFinish(view, url);
}
super.onPageFinished(view, url);
}
}
|
原本上面的兩個問題能夠用同一個變量控制解決的,但因爲歷史代碼遺留問題,目前尚未時間優化測試,這也是代碼暫不公佈的緣由之一(代碼太醜陋:(
)。
url參數拼接問題
通常狀況下,WebView會拼接一些本地參數做爲識別碼傳給前端,如app版本號,網絡狀態等,例如須要加載的url是
1
|
http://m.kaola.com?platform=android
|
假設咱們拼接appVersion和network,則拼接後url變成:
1
|
http://m.kaola.com?platform=android&appVersion=3.10.0&network=4g
|
使用WebView.loadUrl()
加載上面拼接好的url,隨意點擊這個頁面上的某個連接跳轉到別的頁面,本地拼接的參數是不會自動帶過去的。若是須要前端處理參數問題,那麼若是是同域,能夠經過cookie傳遞。非同域的話,仍是須要客戶端拼接參數帶過去。
部分機型沒有WebView,應用直接崩潰
在Crash平臺上面發現有部分機型會存在下面這個崩潰,這些機型都是7.0系統及以上的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
android
.util.AndroidRuntimeException: android.webkit.WebViewFactory$MissingWebViewPackageException: Failed to load WebView provider: No WebView installed
at android
.webkit.WebViewFactory.getProviderClass(WebViewFactory.java:371)
at android
.webkit.WebViewFactory.getProvider(WebViewFactory.java:194)
at android
.webkit.WebView.getFactory(WebView.java:2325)
at android
.webkit.WebView.ensureProviderCreated(WebView.java:2320)
at android
.webkit.WebView.setOverScrollMode(WebView.java:2379)
at android
.view.View.(View.java:4015)
at android
.view.View.(View.java:4132)
at android
.view.ViewGroup.(ViewGroup.java:578)
at android
.widget.AbsoluteLayout.(AbsoluteLayout.java:55)
at android
.webkit.WebView.(WebView.java:627)
at android
.webkit.WebView.(WebView.java:572)
at android
.webkit.WebView.(WebView.java:555)
at android
.webkit.WebView.(WebView.java:542)
at com
.kaola.modules.webview.BaseWebView.void (android.content.Context)(Unknown Source)
|
通過測試發現,普通用戶是沒有辦法卸載WebView的(即便能卸載,也只是把更新卸載了,原始版本的WebView仍是存在的),因此理論上不會存在異常……但既然發生而且上傳上來了,那麼就須要細細分析一下緣由了。跟着代碼WebViewFactory.getProvider()
走,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
static WebViewFactoryProvider getProvider() {
synchronized (sProviderLock) {
//
For now the main purpose of this function (and the factory abstraction) is to keep
// us honest
and minimize usage of WebView internals when binding the proxy.
if (sProviderInstance != null) return sProviderInstance;
final int uid = android.os.Process.myUid();
if (uid == android.os.Process.ROOT_UID || uid == android.os.Process.SYSTEM_UID
|| uid == android.os.Process.PHONE_UID || uid == android.os.Process.NFC_UID
|| uid == android.os.Process.BLUETOOTH_UID) {
throw new UnsupportedOperationException(
"For security reasons, WebView is not allowed in privileged processes");
}
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW,
"WebViewFactory.getProvider()");
try {
Class<WebViewFactoryProvider> providerClass = getProviderClass();
Method staticFactory = null;
try {
staticFactory = providerClass.getMethod(
CHROMIUM_WEBVIEW_FACTORY_METHOD, WebViewDelegate.class);
}
catch (Exception e) {
if (DEBUG) {
Log.w(LOGTAG, "error instantiating provider with static factory method", e);
}
}
Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW,
"WebViewFactoryProvider invocation");
try {
sProviderInstance = (WebViewFactoryProvider)
staticFactory.invoke(
null, new WebViewDelegate());
if (DEBUG) Log.v(LOGTAG, "Loaded provider: " + sProviderInstance);
return sProviderInstance;
}
catch (Exception e) {
Log.e(LOGTAG, "error instantiating provider", e);
throw new AndroidRuntimeException(e);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
StrictMode.setThreadPolicy(oldPolicy);
}
}
}
|
能夠看到,獲取WebView的實例,就是先拿到WebViewFactoryProvider
這個工廠類,經過WebViewFactoryProvider
工廠類裏的靜態方法CHROMIUM_WEBVIEW_FACTORY_METHOD
建立一個WebViewFactoryProvider
,接着,調用WebViewFactoryProvider.createWebView()
建立一個WebViewProvider
(至關於WebView的代理類),後面WebView的方法都是經過代理類來實現的。
在第一步獲取WebVIewFactoryProvider
類的過程當中,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
private static Class<WebViewFactoryProvider> getProviderClass() {
Context webViewContext =
null;
Application initialApplication = AppGlobals.getInitialApplication();
try {
//獲取WebView上下文並設置provider
webViewContext = getWebViewContextAndSetProvider();
}
finally {
Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
}
代碼省略...
}
}
private static Context getWebViewContextAndSetProvider() {
Application initialApplication = AppGlobals.getInitialApplication();
WebViewProviderResponse response =
null;
Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW,
"WebViewUpdateService.waitForAndGetProvider()");
try {
response = getUpdateService().waitForAndGetProvider();
}
finally {
Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
}
if (response.status != LIBLOAD_SUCCESS
&& response.status != LIBLOAD_FAILED_WAITING_FOR_RELRO) {
// 崩潰就發生在這裏。
throw new MissingWebViewPackageException("Failed to load WebView provider: "
+ getWebViewPreparationErrorReason(response.status));
}
}
|
能夠發現,在與WebView包通訊的過程當中,so庫並無加載成功,最後代碼到了native層,沒有繼續跟下去了。
對於這種問題,解決方案有兩種,一種是判斷包名,若是檢測到系統包名裏不包含com.google.android.webview
或者com.android.webview
,則認爲用戶手機裏的WebView不可用;另一種是經過try/catch判斷WebView實例化是否成功,若是拋出了WebViewFactory$MissingWebViewPackageException
異常,則認爲用戶的WebView不可用。
須要說明的是,第一種解決方案是不可靠的,由於國內的廠商基於Chromium的WebView實現有不少種,頗有可能包名就被換了,好比MiWebView,包名是com.mi.webkit.core
。
WebView中的POST請求
在WebView中,若是前端使用POST方式向後端發起一個請求,那麼這個請求是不會走到WebViewClient.shouldOverrideUrlLoading()
方法裏的^10。網上有一些解決方案,例如android-post-webview,經過js判斷是不是post請求,若是是的話,在WebViewClient.shouldInterceptRequest()
方法裏本身創建鏈接,並拿到對應的頁面信息,返回給WebResourceResponse
。總之,儘可能避免Web頁面使用POST請求,不然會帶來很大沒必要要的麻煩。
WebView文件上傳功能
WebView中的文件上傳功能,當咱們在Web頁面上點擊選擇文件的控件(<input type="file">
)時,會產生不一樣的回調方法:^4
void openFileChooser(ValueCallback uploadMsg) works on Android 2.2 (API level 8) up to Android 2.3 (API level 10)
openFileChooser(ValueCallback uploadMsg, String acceptType) works on Android 3.0 (API level 11) up to Android 4.0 (API level 15)
openFileChooser(ValueCallback uploadMsg, String acceptType, String capture) works on Android 4.1 (API level 16) up to Android 4.3 (API level 18)
onShowFileChooser(WebView webView, ValueCallback<uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) works on Android 5.0 (API level 21) and above
最坑的點是在Android4.4系統上沒有回調,這將致使功能的不完整,須要前端去作兼容。解決方案就是和前端另外約定一個jsbridge來解決此類問題。
總結
限於篇幅,《如何設計一個優雅健壯的Android WebView?(上)》先介紹到這裏。本文介紹了目前Android裏的WebView現狀,以及因爲現狀的不可改變致使遺留下的一些坑。所幸,世界上沒有什麼代碼問題是一個程序員不能解決的,若是有,那就用兩個程序員解決。既然咱們已經把前人留下的一些坑填了,那麼是時候構造一個能夠用於生產環境的WebView了!《如何設計一個優雅健壯的Android WebView?(下)》將會介紹如何打造WebView的實戰操做,以及爲了用戶更好的體驗,提出的一些WebView優化策略,敬請期待。
參考連接
- https://developer.chrome.com/multidevice/webview/overview
- https://developer.android.com/about/versions/nougat/android-7.0.html#webview
- https://developer.android.com/about/versions/oreo/android-8.0-changes.html#o-sec
- https://stackoverflow.com/questions/30078217/why-openfilechooser-in-webchromeclient-is-hidden-from-the-docs-is-it-safe-to-us
- http://blog.csdn.net/self_study/article/details/55046348
- http://qbeenslee.com/article/android-webview-302-redirect/
- https://juejin.im/entry/5977598d51882548c0045bde
- http://www.cnblogs.com/zimengfang/p/6183869.html
- http://blog.csdn.net/dg_summer/article/details/78105582
- https://issuetracker.google.com/issues/36918490