如何設計一個優雅健壯的Android WebView?(下)

轉:如何設計一個優雅健壯的Android WebView?(下)javascript

前言

在上文《如何設計一個優雅健壯的Android WebView?(上)》中,筆者分析了國內WebView的現狀,以及在WebView開發過程當中所遇到的一些坑。在踩坑的基礎上,本文着重介紹WebView在開發過程當中所須要注意的問題,這些問題大部分在網上找不到標準答案,但倒是WebView開發過程當中幾乎都會遇到的。此外還會淺談WebView優化,旨在給用戶帶來更好的WebView體驗。css

WebView實戰操做

WebView在使用過程當中會遇到各類各樣的問題,下面針對幾個在生產環境中使用的WebView可能出現的問題進行探討。html

WebView初始化

也許大部分的開發者針對要打開一個網頁這一個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加載一個網頁的過程當中該作些什麼?

若是說加載一個網頁只須要調用WebView.loadUrl(url)這麼簡單,那確定沒程序員啥事兒了。每每事情沒有這麼簡單。加載網頁是一個複雜的過程,在這個過程當中,咱們可能須要執行一些操做,包括:

  1. 加載網頁前,重置WebView狀態以及與業務綁定的變量狀態。WebView狀態包括重定向狀態(mTouchByUser)、前端控制的回退棧(mBackStep)等,業務狀態包括進度條、當前頁的分享內容、分享按鈕的顯示隱藏等。
  2. 加載網頁前,根據不一樣的域拼接本地客戶端的參數,包括基本的機型信息、版本信息、登陸信息以及埋點使用的Refer信息等,有時候涉及交易、財產等還須要作額外的配置。
  3. 開始執行頁面加載操做時,會回調WebViewClient.onPageStarted(webview, url, favicon)。在此方法中,能夠重置重定向保護的變量(mRedirectProtected),固然也能夠在頁面加載前重置,因爲歷史遺留代碼問題,此處還沒有省去優化。
  4. 加載頁面的過程當中,WebView會回調幾個方法。
    • WebChromeClient.onReceivedTitle(webview, title),用來設置標題。須要注意的是,在部分Android系統版本中可能會回調屢次這個方法,並且有時候回調的title是一個url,客戶端能夠針對這種狀況進行特殊處理,避免在標題欄顯示沒必要要的連接。
    • WebChromeClient.onProgressChanged(webview, progress),根據這個回調,能夠控制進度條的進度(包括顯示與隱藏)。通常狀況下,想要達到100%的進度須要的時間較長(特別是首次加載),用戶長時間等待進度條不消失一定會感到焦慮,影響體驗。其實當progress達到80的時候,加載出來的頁面已經基本可用了。所以,能夠投機取巧,達到80%之後即可以認爲進度條到100%了,事實上,國內廠商大部分都會提早隱藏進度條,讓用戶覺得網頁加載很快。
    • 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證書疑問彈窗,給用戶選擇的餘地。
  5. 頁面加載結束後,會回調WebViewClient.onPageFinished(webview, url)。這時候能夠根據回退棧的狀況判斷是否顯示關閉WebView按鈕。經過mActivityWeb.canGoBackOrForward(-1)判斷是否能夠回退。

WebView與JavaScript交互——JsBridge

Android WebView與JavaScript的通訊方案,目前業界已經有比較成熟的方案了。常見的有lzyzsd/JsBridgepengwei1024/JsBridge等,詳見此連接

一般,Java調用js方法有兩種:

  • WebView.loadUrl("javascript:" + javascript);
  • WebView.evaluateJavascript(javascript, callbacck);

第一種方式已經不推薦使用了,第二種方式不只更方便,也提供告終果的回調,但僅支持API 19之後的系統。

js調用Java的方法有四種,分別是:

  • JavascriptInterface
  • WebViewClient.shouldOverrideUrlLoading()
  • WebChromeClient.onConsoleMessage()
  • WebChromeClient.onJsPrompt()

這四種方式再也不一一介紹,掘金上的這篇文章已經講得很詳細。

下面來介紹一下考拉使用的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方法不肯定是在何時回調,官方文檔也沒有說明這個方法是在主線程調用仍是異步線程,所以判斷一下Activity的生命週期是有必要的。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 filterObj(obj){ for(var i in obj){ if (obj.hasOwnProperty(i)) { if(typeof obj[i] == 'string'){ obj[i] = obj[i].replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, ''); } } } return obj; } function _nativeExec(){ var command = commandQueue.dequeue(); if(command) { nativeReady = false; var jsoncommand = JSON.stringify(command); // 作了base64轉換。 var _temp = prompt(YIXIN_JSBRIDGE + base64encode(UTF8.encode(jsoncommand)),''); return true; } else { return false; } } 

前端真正須要調用Java方法時,執行window.WeiXinJSBridge.call方法。

function doCall(request, success_cb, error_cb) { if (jsonRPCIdTag in request && typeof success_cb !== 'undefined') { _callbacks[request.id] = { success_cb: success_cb, error_cb: error_cb }; } commandQueue.enqueue(request); if(nativeReady) { _nativeExec(); } } jsonRPC.call = function(method, params, success_cb, error_cb) { var request = { jsonrpc : jsonRPCVer, method : method, params : params, id : _current_id++ }; doCall(request, success_cb, error_cb); }; jsonRPC.notify = function(method, params) { var request = { jsonrpc : jsonRPCVer, method : method, params : params, }; doCall(request, null, null); }; jsonRPC.ready = function() { jsonRPC.nativeEvent.on('NativeReady', function(e) { nativeReady = false; if(!_nativeExec()) { nativeReady = true; } }); jsonRPC.nativeEvent.Trigger('WeixinJSBridgeReady'); }; jsonRPC.invokeFinish = function() { nativeReady = true; _nativeExec(); }; jsonRPC.nativeEvent = {}; jsonRPC.nativeEvent.Trigger = function(type, detail) { var ev = YixinEvent(type,detail); document.dispatchEvent(ev); }; var nativeEvent = {}; var doc = document; window.WeixinJSBridge = {}; window.jsonRPC = jsonRPC; window.WeixinJSBridge.call = jsonRPC.notify; })(); 

注意,上面的代碼有所刪減,若須要執行完整的jsbridge功能,還須要作一些額外的配置。例如告知前端這段js代碼已經注入成功的標記。

何時注入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注入的時機能夠在多個回調裏實現,包括:

  • onLoadResource
  • doUpdateVisitedHistory
  • onPageStarted
  • onPageFinished
  • onReceivedTitle
  • onProgressChanged

儘管文章做者已經作了測試證實以上時機注入是可行的,但他不能徹底保證沒有問題。事實也是,這些回調裏有多個是會回調屢次的,不能保證一次注入成功。

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注入有幾個須要注意的地方:

  1. 上文提到的屢次注入控制,咱們使用了mCallProgressCallback變量控制
  2. 從新加載一個URL以前,須要重置mCallProgressCallback,讓從新加載後的頁面再次注入js
  3. 注入的進度閾值能夠自由定製,理論上10%-100%都是合理的,咱們使用了80%。

H5頁面、Weex頁面與Native頁面交互——KaolaRouter

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(); } 

代碼裏寫了註釋,就不一一解釋了。

WebView下拉刷新實現

因爲考拉使用的下拉刷新跟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)可以解決閃動的問題。

如何處理加載錯誤(Http、SSL、Resource)?

對於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實現可能不同,因此咱們首先須要排除幾種誤判的例子:

  1. 加載失敗的url跟WebView裏的url不是同一個url,排除;
  2. errorCode=-1,代表是ERROR_UNKNOWN的錯誤,爲了保證不誤判,排除
  3. failingUrl=null&errorCode=-12,因爲錯誤的url是空而不是ERROR_BAD_URL,排除
@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默認狀況下是不須要作處理的,若是有特殊需求,如針對某個頁面設置額外的Cookie字段,能夠經過代碼來控制。下面列出幾個有用的接口:

  • 獲取某個url下的全部Cookie:CookieManager.getInstance().getCookie(url)
  • 判斷WebView是否接受Cookie:CookieManager.getInstance().acceptCookie()
  • 清除Session Cookie:CookieManager.getInstance().removeSessionCookies(ValueCallback<Boolean> callback)
  • 清除全部Cookie:CookieManager.getInstance().removeAllCookies(ValueCallback<Boolean> callback)
  • Cookie持久化:CookieManager.getInstance().flush()
  • 針對某個主機設置Cookie:CookieManager.getInstance().setCookie(String url, String value)

下面是一個給考拉M站設置Cookie的例子:

public static void setBoundCookies() { CookieSyncManager.createInstance(HTApplication.getInstance()); long expiredTime = System.currentTimeMillis() + 10 * 60 * 1000; CookieManager cookieManager = CookieManager.getInstance(); cookieManager.setAcceptCookie(true); cookieManager.setCookie(NetConfig.KAOLA_M_HOST, String.format("Expires=%s; domain=.kaola.com; path=/", expiredTime)); cookieManager.setCookie(NetConfig.KAOLA_M_HOST, "KAOLA_CLEAR_RELATION=1; domain=.kaola.com; path=/"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { CookieManager.getInstance().flush(); } else { CookieSyncManager.getInstance().sync(); } } 

如何調試WebView加載的頁面?

在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的加載速度、穩定性和安全性是能夠進一步增強和提升的。下面從幾個方面介紹一下WebView的優化方案,這些方案可能並非都適用於全部場景,但思路是能夠借鑑的。

CandyWebCache

咱們知道,在加載頁面的過程當中,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的頁面訪問體驗,但最好由先後端一塊兒配合改造。這套總體的解決方案有不少借鑑意義,考拉也在學習中。

Https、HttpDns、CDN

將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獨立進程

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; } 

WebView免流實現(待實現)

  1. 全局代理
  2. WebViewClient.shouldInterceptRequest(),IP替換

做者:網易考拉移動端團隊
連接:https://juejin.im/post/5a94fb046fb9a0635865a2d6
來源:掘金
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
做者:網易考拉移動端團隊 連接:https://juejin.im/post/5a94fb046fb9a0635865a2d6 來源:掘金 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
相關文章
相關標籤/搜索