(這篇文章寫得有點晚,請諒解~)javascript
在上文《如何設計一個優雅健壯的Android WebView?(上)》中,筆者分析了國內WebView的現狀,以及在WebView開發過程當中所遇到的一些坑。在踩坑的基礎上,本文着重介紹WebView在開發過程當中所須要注意的問題,這些問題大部分在網上找不到標準答案,但倒是WebView開發過程當中幾乎都會遇到的。此外還會淺談WebView優化,旨在給用戶帶來更好的WebView體驗。css
WebView在使用過程當中會遇到各類各樣的問題,下面針對幾個在生產環境中使用的WebView可能出現的問題進行探討。html
也許大部分的開發者針對要打開一個網頁這一個Action,會停留在下面這段代碼:前端
WebView webview = new WebView(context);
webview.loadUrl(url);
複製代碼
這應該是打開一個正常網頁最簡短的代碼了。但大多數狀況下,咱們須要作一些額外的配置,例如縮放支持、Cookie管理、密碼存儲、DOM存儲等,這些配置大部分在WebSettings
裏,具體配置的內容在上文中已有說起,本文再也不具體講解。java
接下來,試想若是訪問的網頁返回的請求是30X,如使用http訪問百度的連接(www.baidu.com),那麼這時候頁面就是空白一片,GG了。爲何呢?由於WebView只加載了第一個網頁,接下來的事情就無論了。爲了解決這個問題,咱們須要一個WebViewClient
讓系統幫咱們處理重定向問題。android
webview.setWebViewClient(new WebViewClient());
複製代碼
除了處理重定向,咱們還能夠覆寫WebViewClient
中的方法,方法有:git
public boolean shouldOverrideUrlLoading(WebView view, String url)
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request)
public void onPageStarted(WebView view, String url, Bitmap favicon)
public void onPageFinished(WebView view, String url)
public void onLoadResource(WebView view, String url)
public void onPageCommitVisible(WebView view, String url)
public WebResourceResponse shouldInterceptRequest(WebView view, String url)
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request)
public void onTooManyRedirects(WebView view, Message cancelMsg, Message continueMsg)
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl)
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error)
public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse)
public void onFormResubmission(WebView view, Message dontResend, Message resend)
public void doUpdateVisitedHistory(WebView view, String url, boolean isReload)
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error)
public void onReceivedClientCertRequest(WebView view, ClientCertRequest request)
public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm)
public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event)
public void onUnhandledKeyEvent(WebView view, KeyEvent event)
public void onScaleChanged(WebView view, float oldScale, float newScale)
public void onReceivedLoginRequest(WebView view, String realm, String account, String args)
public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail)
複製代碼
這些方法具體介紹能夠參考文章《WebView使用詳解(二)——WebViewClient與經常使用事件監聽》。有幾個方法是有必要覆寫來處理一些客戶端邏輯的,後面遇到會詳細介紹。程序員
另外,WebView的標題不是一成不變的,加載的網頁不同,標題也不同。在WebView中,加載的網頁的標題會回調WebChromeClient.onReceivedTitle()
方法,給開發者設置標題。所以,設置一個WebChromeClient
也是有必要的。github
webview.setWebChromeClient(new WebChromeClient());
複製代碼
一樣,咱們還能夠覆寫WebChromeClient
中的方法,方法有:web
public void onProgressChanged(WebView view, int newProgress)
public void onReceivedTitle(WebView view, String title)
public void onReceivedIcon(WebView view, Bitmap icon)
public void onReceivedTouchIconUrl(WebView view, String url, boolean precomposed)
public void onShowCustomView(View view, int requestedOrientation, CustomViewCallback callback)
public void onHideCustomView()
public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg)
public void onRequestFocus(WebView view)
public void onCloseWindow(WebView window)
public boolean onJsAlert(WebView view, String url, String message, JsResult result)
public boolean onJsConfirm(WebView view, String url, String message, JsResult result)
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result)
public void onExceededDatabaseQuota(String url, String databaseIdentifier, long quota, long estimatedDatabaseSize, long totalQuota, WebStorage.QuotaUpdater quotaUpdater)
public void onReachedMaxAppCacheSize(long requiredStorage, long quota, WebStorage.QuotaUpdater quotaUpdater)
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback)
public void onGeolocationPermissionsHidePrompt()
public void onPermissionRequest(PermissionRequest request)
public void onPermissionRequestCanceled(PermissionRequest request)
public boolean onJsTimeout()
public void onConsoleMessage(String message, int lineNumber, String sourceID)
public boolean onConsoleMessage(ConsoleMessage consoleMessage)
public Bitmap getDefaultVideoPoster()
public void getVisitedHistory(ValueCallback<String[]> callback)
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams)
public void openFileChooser(ValueCallback<Uri> uploadFile, String acceptType, String capture)
public void setupAutoFill(Message msg)
複製代碼
這些方法具體介紹能夠參考文章《WebView使用詳解(三)——WebChromeClient與LoadData補充》。除了接收標題之外,進度條的改變,WebView請求本地文件、請求地理位置權限等,都是經過WebChromeClient
的回調實現的。
在初始化階段,若是啓用了Javascript
,那麼須要移除相關的安全漏洞,這在上一篇文章中也有所說起。最後,在考拉KaolaWebView.init()
方法中,執行了以下操做:
protected void init() {
mContext = getContext();
mWebJsManager = new WebJsManager(); // 初始化Js管理器
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// 根據本地調試開關打開Chrome調試
WebView.setWebContentsDebuggingEnabled(WebSwitchManager.isDebugEnable());
}
// WebSettings配置
WebViewSettings.setDefaultWebSettings(this);
// 獲取deviceId列表,安全相關
WebViewHelper.requestNeedDeviceIdUrlList(null);
// 設置下載的監聽器
setDownloadListener(this);
// 前端控制回退棧,默認回退1。
mBackStep = 1;
// 重定向保護,防止空白頁
mRedirectProtected = true;
// 截圖使用
setDrawingCacheEnabled(true);
// 初始化具體的Jsbridge類
enableJsApiInternal();
// 初始化WebCache,用於加載靜態資源
initWebCache();
// 初始化WebChromeClient,覆寫其中一部分方法
super.setWebChromeClient(mChromeClient);
// 初始化WebViewClient,覆寫其中一部分方法
super.setWebViewClient(mWebViewClient);
}
複製代碼
若是說加載一個網頁只須要調用WebView.loadUrl(url)
這麼簡單,那確定沒程序員啥事兒了。每每事情沒有這麼簡單。加載網頁是一個複雜的過程,在這個過程當中,咱們可能須要執行一些操做,包括:
mTouchByUser
)、前端控制的回退棧(mBackStep
)等,業務狀態包括進度條、當前頁的分享內容、分享按鈕的顯示隱藏等。WebViewClient.onPageStarted(webview, url, favicon)
。在此方法中,能夠重置重定向保護的變量(mRedirectProtected
),固然也能夠在頁面加載前重置,因爲歷史遺留代碼問題,此處還沒有省去優化。WebChromeClient.onReceivedTitle(webview, title)
,用來設置標題。須要注意的是,在部分Android系統版本中可能會回調屢次這個方法,並且有時候回調的title是一個url,客戶端能夠針對這種狀況進行特殊處理,避免在標題欄顯示沒必要要的連接。WebChromeClient.onProgressChanged(webview, progress)
,根據這個回調,能夠控制進度條的進度(包括顯示與隱藏)。通常狀況下,想要達到100%的進度須要的時間較長(特別是首次加載),用戶長時間等待進度條不消失一定會感到焦慮,影響體驗。其實當progress達到80的時候,加載出來的頁面已經基本可用了。事實上,國內廠商大部分都會提早隱藏進度條,讓用戶覺得網頁加載很快。WebViewClient.shouldInterceptRequest(webview, request)
,不管是普通的頁面請求(使用GET/POST),仍是頁面中的異步請求,或者頁面中的資源請求,都會回調這個方法,給開發一次攔截請求的機會。在這個方法中,咱們能夠進行靜態資源的攔截並使用緩存數據代替,也能夠攔截頁面,使用本身的網絡框架來請求數據。包括後面介紹的WebView免流方案,也和此方法有關。WebViewClient.shouldOverrideUrlLoading(webview, request)
,若是遇到了重定向,或者點擊了頁面中的a標籤實現頁面跳轉,那麼會回調這個方法。能夠說這個是WebView裏面最重要的回調之一,後面WebView與Native頁面交互
一節將會詳細介紹這個方法。WebViewClient.onReceived**Error(webview, handler, error)
,加載頁面的過程當中發生了錯誤,會回調這個方法。主要是http錯誤以及ssl錯誤。在這兩個回調中,咱們能夠進行異常上報,監控異常頁面、過時頁面,及時反饋給運營或前端修改。在處理ssl錯誤時,遇到不信任的證書能夠進行特殊處理,例如對域名進行判斷,針對本身公司的域名「放行」,防止進入醜陋的錯誤證書頁面。也能夠與Chrome同樣,彈出ssl證書疑問彈窗,給用戶選擇的餘地。WebViewClient.onPageFinished(webview, url)
。這時候能夠根據回退棧的狀況判斷是否顯示關閉WebView按鈕。經過mActivityWeb.canGoBackOrForward(-1)
判斷是否能夠回退。Android WebView與JavaScript的通訊方案,目前業界已經有比較成熟的方案了。常見的有lzyzsd/JsBridge、pengwei1024/JsBridge等,詳見此連接。
一般,Java調用js方法有兩種:
WebView.loadUrl("javascript:" + javascript);
WebView.evaluateJavascript(javascript, callbacck);
第一種方式已經不推薦使用了,第二種方式不只更方便,也提供告終果的回調,但僅支持API 19之後的系統。
js調用Java的方法有四種,分別是:
這四種方式再也不一一介紹,掘金上的這篇文章已經講得很詳細。
下面來介紹一下考拉使用的JsBridge方案。Java調用js方法沒必要多說,根據Android系統版本不一樣分別調用第一個方法和第二個方法。在js調用Java方法上,考拉使用的是第四種方案,即侵入WebChromeClient.onJsPrompt(webview, url, message, defaultValue, result)
實現通訊。
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue,
JsPromptResult result) {
if (!ActivityUtils.activityIsAlive(mContext)) {//頁面關閉後,直接返回
try {
result.cancel();
} catch (Exception ignored) {
}
return true;
}
if (mJsApi != null && mJsApi.hijackJsPrompt(message)) {
result.confirm();
return true;
}
return super.onJsPrompt(view, url, message, defaultValue, result);
}
複製代碼
onJsPrompt
方法最終是在主線程中回調,判斷一下WebView所在容器的生命週期是有必要的。js與Java的方法調用主要在mJsApi.hijackJsPrompt(message)
中。
public boolean hijackJsPrompt(String message) {
if (TextUtils.isEmpty(message)) {
return false;
}
boolean handle = message.startsWith(YIXIN_JSBRIDGE);
if (handle) {
call(message);
}
return handle;
}
複製代碼
首先判斷該信息是否應該攔截,若是容許攔截的話,則取出js傳過來的方法和參數,經過Handler把消息拋給業務層處理。
private void call(String message) {
// PREFIX
message = message.substring(KaolaJsApi.YIXIN_JSBRIDGE.length());
// BASE64
message = new String(Base64.decode(message));
JSONObject json = JSONObject.parseObject(message);
String method = json.getString("method");
String params = json.getString("params");
String version = json.getString("jsonrpc");
if ("2.0".equals(version)) {
int id = json.containsKey("id") ? json.getIntValue("id") : -1;
call(id, method, params);
}
callJS("window.jsonRPC.invokeFinish()");
}
private void call(int id, String method, String params) {
Message msg = Message.obtain();
msg.what = MsgWhat.JSCall;
msg.obj = new KaolaJSMessage(id, method, params);
// 經過handler把消息發出去,待接收方處理。
if (handler != null) {
handler.sendMessage(msg);
}
}
複製代碼
jsbridge中,實現了一個存儲jsbridge指令的隊列CommandQueue,每次須要調用jsbridge時,只須要入隊便可。
function CommandQueue() {
this.backQueue = [];
this.queue = [];
};
CommandQueue.prototype.dequeue = function() {
if(this.queue.length <=0 && this.backQueue.length > 0) {
this.queue = this.backQueue.reverse();
this.backQueue = [];
}
return this.queue.pop();
};
CommandQueue.prototype.enqueue = function(item) {
this.backQueue.push(item);
};
Object.defineProperty(CommandQueue.prototype, 'length',
{get: function() {return this.queue.length + this.backQueue.length; }});
var commandQueue = new CommandQueue();
function _nativeExec(){
var command = commandQueue.dequeue();
if(command) {
nativeReady = false;
var jsoncommand = JSON.stringify(command);
var _temp = prompt(jsoncommand,'');
return true;
} else {
return false;
}
}
複製代碼
上面的代碼有所刪減,若須要執行完整的jsbridge功能,還須要作一些額外的配置。例如告知前端這段js代碼已經注入成功的標記。
若是作過WebView開發,而且須要和js交互的同窗,大部分都會認爲js在WebViewClient.onPageFinished()
方法中注入最合適,此時dom樹已經構建完成,頁面已經徹底展示出來^1^3。但若是作過頁面加載速度的測試,會發現WebViewClient.onPageFinished()
方法一般須要等待好久纔會回調(首次加載一般超過3s),這是由於WebView須要加載完一個網頁裏主文檔和全部的資源纔會回調這個方法。能不能在WebViewClient.onPageStarted()
中注入呢?答案是不肯定。通過測試,有些機型能夠,有些機型不行。在WebViewClient.onPageStarted()
中注入還有一個致命的問題——這個方法可能會回調屢次,會形成js代碼的屢次注入。
另外一方面,從7.0開始,WebView加載js方式發生了一些小改變,官方建議把js注入的時機放在頁面開始加載以後。援引官方的文檔^4:
Javascript run before page load
Starting with apps targeting Android 7.0, the Javascript context will be reset when a new page is loaded. Currently, the context is carried over for the first page loaded in a new WebView instance.
Developers looking to inject Javascript into the WebView should execute the script after the page has started to load.
在這篇文章中也說起了js注入的時機能夠在多個回調裏實現,包括:
儘管文章做者已經作了測試證實以上時機注入是可行的,但他不能徹底保證沒有問題。事實也是,這些回調裏有多個是會回調屢次的,不能保證一次注入成功。
WebViewClient.onPageStarted()
太早,WebViewClient.onPageFinished()
又太遲,究竟有沒有比較合適的注入時機呢?試試WebViewClient.onProgressChanged()
?這個方法在dom樹渲染的過程當中會回調屢次,每次都會告訴咱們當前加載的進度。這不正是告訴咱們頁面已經開始加載了嗎?考拉正是使用了WebViewClient.onProgressChanged()
方法來注入js代碼。
@Override
public void onProgressChanged(WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
if (null != mIWebViewClient) {
mIWebViewClient.onProgressChanged(view, newProgress);
}
if (mCallProgressCallback && newProgress >= mProgressFinishThreshold) {
DebugLog.d("WebView", "onProgressChanged: " + newProgress);
mCallProgressCallback = false;
// mJsApi不爲null且容許注入js的狀況下,開始注入js代碼。
if (mJsApi != null && WebJsManager.enableJs(view.getUrl())) {
mJsApi.loadLocalJsCode();
}
if (mIWebViewClient != null) {
mIWebViewClient.onPageFinished(view, newProgress);
}
}
}
複製代碼
能夠看到,咱們使用了mProgressFinishThreshold
這個變量控制注入時機,這與前面說起的當progress達到80的時候,加載出來的頁面已經基本可用了
是相呼應的。
達到80%很容易,達到100%卻很難。
正是由於這個緣由,頁面的進度加載到80%的時候,實際上dom樹已經渲染得差很少了,代表WebView已經解析了<html>標籤,這時候注入必定是成功的。在WebViewClient.onProgressChanged()
實現js注入有幾個須要注意的地方:
H5頁面、Weex頁面與Native頁面的交互是經過URL攔截實現的。在WebView中,WebViewClient.shouldOverrideUrlLoading()
方法可以獲取到當前加載的URL,而後把URL傳遞給考拉路由框架,即可以判斷URL是否可以跳轉到其餘非H5頁面,考拉路由框架在《考拉Android客戶端路由總線設計》一文中有詳細介紹,但當時未引入Weex頁面,關於如何整合三者的通訊,後續文章會有詳細介紹。
在WebViewClient.shouldOverrideUrlLoading()
中,根據URL類型作了判斷:
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (StringUtils.isNotBlank(url) && url.equals("about:blank")) { //js調用reload刷新頁面時候,個別機型跳到空頁面問題修復
url = getUrl();
}
url = WebViewUtils.removeBlank(url);
mCallProgressCallback = true;
//容許啓動第三方應用客戶端
if (WebViewUtils.canHandleUrl(url)) {
boolean handleByCaller = false;
// 若是不是用戶觸發的操做,就沒有必要交給上層處理了,直接走url攔截規則。
if (null != mIWebViewClient && isTouchByUser()) {
// 先交給業務層攔截處理
handleByCaller = mIWebViewClient.shouldOverrideUrlLoading(view, url);
}
if (!handleByCaller) {
// 業務層不攔截,走通用路由總線規則
handleByCaller = handleOverrideUrl(url);
}
mRedirectProtected = true;
return handleByCaller || super.shouldOverrideUrlLoading(view, url);
} else {
try {
notifyBeforeLoadUrl(url);
// https://sumile.cn/archives/1223.html
Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setComponent(null);
intent.setSelector(null);
mContext.startActivity(intent);
if (!mIsBlankPageRedirect) {
back();
}
} catch (Exception e) {
ExceptionUtils.printExceptionTrace(e);
}
return true;
}
}
private boolean handleOverrideUrl(final String url) {
RouterResult result = WebActivityRouter.startFromWeb(
new IntentBuilder(mContext, url).setRouterActivityResult(new RouterActivityResult() {
@Override
public void onActivityFound() {
if (!mIsBlankPageRedirect) {
// 路由攔截成功之後,爲防止首次進入WebView產生白屏,所以加了保護機制
back();
}
}
@Override
public void onActivityNotFound() {
}
}));
return result.isSuccess();
}
複製代碼
代碼裏寫了註釋,就不一一解釋了。
因爲考拉使用的下拉刷新跟Material Design所使用的下拉刷新樣式不一致,所以不能直接套用SwipeRefreshLayout
。考拉使用的是一套改造過的Android-PullToRefresh,WebView的下拉刷新,正是繼承自PullToRefreshBase
來實現的。
/**
* 建立者:Square Xu
* 日期:2017/2/23
* 功能模塊:webview下拉刷新組件
*/
public class PullToRefreshWebView extends PullToRefreshBase<KaolaWebview> {
public PullToRefreshWebView(Context context) {
super(context);
}
public PullToRefreshWebView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PullToRefreshWebView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs);
}
public PullToRefreshWebView(Context context, Mode mode) {
super(context, mode);
}
public PullToRefreshWebView(Context context, Mode mode, AnimationStyle animStyle) {
super(context, mode, animStyle);
}
@Override
public Orientation getPullToRefreshScrollDirection() {
return Orientation.VERTICAL;
}
@Override
protected KaolaWebview createRefreshableView(Context context, AttributeSet attrs) {
KaolaWebview kaolaWebview = new KaolaWebview(context, attrs);
//解決鍵盤彈起時候閃動的問題
setGravity(AXIS_PULL_BEFORE);
return kaolaWebview;
}
@Override
protected boolean isReadyForPullEnd() {
return false;
}
@Override
protected boolean isReadyForPullStart() {
return getRefreshableView().getScrollY() == 0;
}
}
複製代碼
考拉使用了全屏模式實現沉浸式狀態欄及滑動返回,全屏模式和WebView下拉刷新相結合對鍵盤的彈起產生了閃動效果,通過組內大神的研究與屢次調試(感謝@俊俊),發現setGravity(AXIS_PULL_BEFORE)
可以解決閃動的問題。
對於WebView加載一個網頁過程當中所產生的錯誤回調,大體有三種:
WebViewClient.onReceivedHttpError(webView, webResourceRequest, webResourceResponse)
任何HTTP請求產生的錯誤都會回調這個方法,包括主頁面的html文檔請求,iframe、圖片等資源請求。在這個回調中,因爲混雜了不少請求,不適合用來展現加載錯誤的頁面,而適合作監控報警。當某個URL,或者某個資源收到大量報警時,說明頁面或資源可能存在問題,這時候可讓相關運營及時響應修改。
WebViewClient.onReceivedSslError(webview, sslErrorHandler, sslError)
任何HTTPS請求,遇到SSL錯誤時都會回調這個方法。比較正確的作法是讓用戶選擇是否信任這個網站,這時候能夠彈出信任選擇框供用戶選擇(大部分正規瀏覽器是這麼作的)。但人都是有私心的,況且是遇到自家的網站時。咱們可讓一些特定的網站,無論其證書是否存在問題,都讓用戶信任它。在這一點上,分享一個小坑。考拉的SSL證書使用的是GeoTrust的GeoTrust SSL CA - G3
,可是在某些機型上,打開考拉的頁面都會提示證書錯誤。這時候就不得不使用「絕招」——讓考拉的全部二級域都是可信任的。
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
if (UrlUtils.isKaolaHost(getUrl())) {
handler.proceed();
} else {
super.onReceivedSslError(view, handler, error);
}
}
複製代碼
WebViewClient.onReceivedError(webView, webResourceRequest, webResourceError)
只有在主頁面加載出現錯誤時,纔會回調這個方法。這正是展現加載錯誤頁面最合適的方法。然鵝,若是無論三七二十一直接展現錯誤頁面的話,那頗有可能會誤判,給用戶形成常常加載頁面失敗的錯覺。因爲不一樣的WebView實現可能不同,因此咱們首先須要排除幾種誤判的例子:
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);
// -12 == EventHandle.ERROR_BAD_URL, a hide return code inside android.net.http package
if ((failingUrl != null && !failingUrl.equals(view.getUrl()) && !failingUrl.equals(view.getOriginalUrl())) /* not subresource error*/
|| (failingUrl == null && errorCode != -12) /*not bad url*/
|| errorCode == -1) { //當 errorCode = -1 且錯誤信息爲 net::ERR_CACHE_MISS
return;
}
if (!TextUtils.isEmpty(failingUrl)) {
if (failingUrl.equals(view.getUrl())) {
if (null != mIWebViewClient) {
mIWebViewClient.onReceivedError(view);
}
}
}
}
複製代碼
Cookie默認狀況下是不須要作處理的,若是有特殊需求,如針對某個頁面設置額外的Cookie字段,能夠經過代碼來控制。下面列出幾個有用的接口:
CookieManager.getInstance().getCookie(url)
CookieManager.getInstance().acceptCookie()
CookieManager.getInstance().removeSessionCookies(ValueCallback<Boolean> callback)
CookieManager.getInstance().removeAllCookies(ValueCallback<Boolean> callback)
CookieManager.getInstance().flush()
CookieManager.getInstance().setCookie(String url, String value)
在Android 4.4版本之後,可使用Chrome開發者工具調試WebView內容^5。調試須要在代碼裏設置打開調試開關。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WebView.setWebContentsDebuggingEnabled(true);
}
複製代碼
開啓後,使用USB鏈接電腦,加載URL時,打開Chrome開發者工具,在瀏覽器輸入
chrome://inspect
複製代碼
能夠看到當前正在瀏覽的頁面,點擊inspect
便可看到WebView加載的內容。
除了上面提到的基本操做用來實現一個完整的瀏覽器功能外,WebView的加載速度、穩定性和安全性是能夠進一步增強和提升的。下面從幾個方面介紹一下WebView的優化方案,這些方案可能並非都適用於全部場景,但思路是能夠借鑑的。
咱們知道,在加載頁面的過程當中,js、css和圖片資源佔用了大量的流量,若是這些資源一開始就放在本地,或者只須要下載一次,後面重複利用,豈不美哉。儘管WebView也有幾套緩存方案^6,可是整體而言效果不理想。基於自建緩存系統的思路,由網易杭研研發的CandyWebCache項目應運而生。CandyWebCache是一套支持離線緩存WebView資源並實時更新遠程資源的解決方案,支持打母包時下載當前最新的資源文件集成到apk中,也支持在線實時更新資源。在WebView中,咱們須要攔截WebViewClient.shouldInterceptRequest()
方法,檢測緩存是否存在,存在則直接取本地緩存數據,減小網絡請求產生的流量。
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
if (WebSwitchManager.isWebCacheEnabled()) {
try {
WebResourceResponse resourceResponse = CandyWebCache.getsInstance().getResponse(view, request);
return WebViewUtils.handleResponseHeader(resourceResponse);
} catch (Throwable e) {
ExceptionUtils.uploadCatchedException(e);
}
}
return super.shouldInterceptRequest(view, request);
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
if (WebSwitchManager.isWebCacheEnabled()) {
try {
WebResourceResponse resourceResponse = CandyWebCache.getsInstance().getResponse(view, url);
return WebViewUtils.handleResponseHeader(resourceResponse);
} catch (Throwable e) {
ExceptionUtils.uploadCatchedException(e);
}
}
return super.shouldInterceptRequest(view, url);
}
複製代碼
除了上述緩存方案外,騰訊的QQ會員團隊也推出了開源的解決方案VasSonic,旨在提高H5的頁面訪問體驗,但最好由先後端一塊兒配合改造。這套總體的解決方案有不少借鑑意義,考拉也在學習中。
將http請求切換爲https請求,能夠下降運營商網絡劫持(js劫持、圖片劫持等)的機率,特別是使用了http2後,可以大幅提高web性能,減小網絡延遲,減小請求的流量。
HttpDns,使用http協議向特定的DNS服務器進行域名解析請求,代替基於DNS協議向運營商的Local DNS發起解析請求,能夠下降運營商DNS劫持帶來的訪問失敗。目前在WebView上使用HttpDns尚存在必定問題,網上也沒有較好的解決方案(阿里雲Android WebView+HttpDns最佳實踐、騰訊雲HttpDns SDK接入、webview接入HttpDNS實踐),所以還在調研中。
另外一方面,能夠把靜態資源部署到多路CDN,直接經過CDN地址訪問,減小網絡延遲,多路CDN保障單個CDN大面積節點訪問失敗時可切換到備用的CDN上。
WebView實例在Android7.0系統之後,已經能夠選擇運行在一個獨立進程上^7;8.0之後默認就是運行在獨立的沙盒進程中^8,將來Google也在朝這個方向發展,具體的WebView歷史能夠參考上一篇文章《如何設計一個優雅健壯的Android WebView?(上)》第一小節。
Android7.0系統之後,WebView相對來講是比較穩定的,不管承載WebView的容器是否在主進程,都不須要擔憂WebView崩潰致使應用也跟着崩潰。而後7.0如下的系統就沒有這麼幸運了,特別是低版本的WebView。考慮應用的穩定性,咱們能夠把7.0如下系統的WebView使用一個獨立進程的Activity來包裝,這樣即便WebView崩潰了,也只是WebView所在的進程發生了崩潰,主進程仍是不受影響的。
public static Intent getWebViewIntent(Context context) {
Intent intent;
if (isWebInMainProcess()) {
intent = new Intent(context, MainWebviewActivity.class);
} else {
intent = new Intent(context, WebviewActivity.class);
}
return intent;
}
public static boolean isWebInMainProcess() {
return android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N;
}
複製代碼
從去年開始,市場上出現了一批互聯網套餐卡,如騰訊王卡、螞蟻寶卡、京東強卡、阿里魚卡、網易白金卡等,這些互聯網套餐相比傳統的運營商套餐來講,資費便宜,流量多,甚至某些卡還擁有特殊權限——對某些應用免流。如網易白金卡,對於網易系與百度系的部分應用實現免流。
市面上常見的免流應用,原理無非就是走「特殊通道」,讓這一部分的流量不計入運營商的流量統計平臺中。Android中要實現這種「特殊通道」,有幾種方案。
對於上面提到的幾種方案,native頁面全部的請求都是應用層發起的,實際上都比較好實現,但WebView的頁面和資源請求是經過JNI發起的,想要攔截請求的話,須要一些功夫。網羅網上的全部方案,目前以爲可行的有兩種,分別是全局代理和攔截WebViewClient.shouldInterceptRequest()
。
因爲WebView並無提供接口針對具體的WebView實例設置代理,因此咱們只能進行全局代理。設置全局代理時,須要通知系統代理環境發生了改變,不幸地是,Android並無提供公開的接口,這就致使了咱們只能hook系統接口,根據不一樣的系統版原本實現通知的目的^9、^10。6.0之後的系統,還沒有嘗試是否可行,根據公司同事的反饋,和5.0系統的方案是一致的。
/**
* Set Proxy for Android 4.1 - 4.3.
*/
@SuppressWarnings("all")
private static boolean setProxyJB(WebView webview, String host, int port) {
Log.d(LOG_TAG, "Setting proxy with 4.1 - 4.3 API.");
try {
Class wvcClass = Class.forName("android.webkit.WebViewClassic");
Class wvParams[] = new Class[1];
wvParams[0] = Class.forName("android.webkit.WebView");
Method fromWebView = wvcClass.getDeclaredMethod("fromWebView", wvParams);
Object webViewClassic = fromWebView.invoke(null, webview);
Class wv = Class.forName("android.webkit.WebViewClassic");
Field mWebViewCoreField = wv.getDeclaredField("mWebViewCore");
Object mWebViewCoreFieldInstance = getFieldValueSafely(mWebViewCoreField, webViewClassic);
Class wvc = Class.forName("android.webkit.WebViewCore");
Field mBrowserFrameField = wvc.getDeclaredField("mBrowserFrame");
Object mBrowserFrame = getFieldValueSafely(mBrowserFrameField, mWebViewCoreFieldInstance);
Class bf = Class.forName("android.webkit.BrowserFrame");
Field sJavaBridgeField = bf.getDeclaredField("sJavaBridge");
Object sJavaBridge = getFieldValueSafely(sJavaBridgeField, mBrowserFrame);
Class ppclass = Class.forName("android.net.ProxyProperties");
Class pparams[] = new Class[3];
pparams[0] = String.class;
pparams[1] = int.class;
pparams[2] = String.class;
Constructor ppcont = ppclass.getConstructor(pparams);
Class jwcjb = Class.forName("android.webkit.JWebCoreJavaBridge");
Class params[] = new Class[1];
params[0] = Class.forName("android.net.ProxyProperties");
Method updateProxyInstance = jwcjb.getDeclaredMethod("updateProxy", params);
updateProxyInstance.invoke(sJavaBridge, ppcont.newInstance(host, port, null));
} catch (Exception ex) {
Log.e(LOG_TAG, "Setting proxy with >= 4.1 API failed with error: " + ex.getMessage());
return false;
}
Log.d(LOG_TAG, "Setting proxy with 4.1 - 4.3 API successful!");
return true;
}
/**
* Set Proxy for Android 5.0.
*/
public static void setWebViewProxyL(Context context, String host, int port) {
System.setProperty("http.proxyHost", host);
System.setProperty("http.proxyPort", port + "");
try {
Context appContext = context.getApplicationContext();
Class applictionClass = Class.forName("android.app.Application");
Field mLoadedApkField = applictionClass.getDeclaredField("mLoadedApk");
mLoadedApkField.setAccessible(true);
Object mloadedApk = mLoadedApkField.get(appContext);
Class loadedApkClass = Class.forName("android.app.LoadedApk");
Field mReceiversField = loadedApkClass.getDeclaredField("mReceivers");
mReceiversField.setAccessible(true);
ArrayMap receivers = (ArrayMap) mReceiversField.get(mloadedApk);
for (Object receiverMap : receivers.values()) {
for (Object receiver : ((ArrayMap) receiverMap).keySet()) {
Class clazz = receiver.getClass();
if (clazz.getName().contains("ProxyChangeListener")) {
Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class, Intent.class);
Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);
onReceiveMethod.invoke(receiver, appContext, intent);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
複製代碼
須要注意的是,在WebView退出時,須要重置代理。
WebViewClient.shouldInterceptRequest()
攔截WebViewClient.shouldInterceptRequest()
的目的是使用免流的第三種方案——IP替換。直接看代碼。
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
WebResourceResponse resourceResponse = CandyWebCache.getsInstance().getResponse(view, request);
if (request.getUrl() != null && request.getMethod().equalsIgnoreCase("get")) {
Uri uri = request.getUrl();
String url = uri.toString();
String scheme = uri.getScheme().trim();
String host = uri.getHost();
String path = uri.getPath();
if (TextUtils.isEmpty(path) || TextUtils.isEmpty(host)) {
return null;
}
// HttpDns解析css文件的網絡請求及圖片請求
if ((scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https")) && (path.endsWith(".css")
|| path.endsWith(".png")
|| path.endsWith(".jpg")
|| path.endsWith(".gif")
|| path.endsWith(".js"))) {
try {
URL oldUrl = new URL(uri.toString());
URLConnection connection;
// 獲取HttpDns域名解析結果
List<String> ips = HttpDnsManager.getInstance().getIPListByHostAsync(host);
if (!ListUtils.isEmpty(ips)) {
String ip = ips.get(0);
String newUrl = url.replaceFirst(oldUrl.getHost(), ip);
connection = new URL(newUrl).openConnection(); // 設置HTTP請求頭Host域
connection.setRequestProperty("Host", oldUrl.getHost());
} else {
connection = new URL(url).openConnection(); // 設置HTTP請求頭Host域
}
String fileExtension = MimeTypeMap.getFileExtensionFromUrl(url);
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension);
return new WebResourceResponse(mimeType, "UTF-8", connection.getInputStream());
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return super.shouldInterceptRequest(view, request);
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
if (!TextUtils.isEmpty(url) && Uri.parse(url).getScheme() != null) {
Uri uri = Uri.parse(url);
String scheme = uri.getScheme().trim();
String host = uri.getHost();
String path = uri.getPath();
if (TextUtils.isEmpty(path) || TextUtils.isEmpty(host)) {
return null;
}
// HttpDns解析css文件的網絡請求及圖片請求
if ((scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https")) && (path.endsWith(".css")
|| path.endsWith(".png")
|| path.endsWith(".jpg")
|| path.endsWith(".gif")
|| path.endsWith(".js"))) {
try {
URL oldUrl = new URL(uri.toString());
URLConnection connection;
// 獲取HttpDns域名解析結果
List<String> ips = HttpDnsManager.getInstance().getIPListByHostAsync(host);
if (!ListUtils.isEmpty(ips)) {
String ip = ips.get(0);
String newUrl = url.replaceFirst(oldUrl.getHost(), ip);
connection = new URL(newUrl).openConnection(); // 設置HTTP請求頭Host域
connection.setRequestProperty("Host", oldUrl.getHost());
} else {
connection = new URL(url).openConnection(); // 設置HTTP請求頭Host域
}
String fileExtension = MimeTypeMap.getFileExtensionFromUrl(url);
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension);
return new WebResourceResponse(mimeType, "UTF-8", connection.getInputStream());
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return super.shouldInterceptRequest(view, url);
}
複製代碼
使用此種方案,還能夠把WebView網絡請求與native網絡請求使用的框架統一塊兒來,方便管理。
本文介紹了WebView在開發中的一些實踐經驗和優化流程。爲了知足業務需求,WebView着實提供了很是豐富的接口供應用層處理業務邏輯。針對WebView的二次開發,本文介紹了一些經常使用的回調處理邏輯以及開發過程當中總結下的經驗。因爲是經驗,不必定是準確的,如有錯誤的地方,敬請指出糾正,不勝感激!