转:如何设计一个优雅健壮的Android WebView?(下)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的时候,加载出来的页面已经基本可用了。所以,能够投机取巧,达到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证书疑问弹窗,给用户选择的余地。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
方法不肯定是在何时回调,官方文档也没有说明这个方法是在主线程调用仍是异步线程,所以判断一下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代码已经注入成功的标记。
若是作过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)
下面是一个给考拉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(); } }
在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; }
WebViewClient.shouldInterceptRequest()
,IP替换