在native与网页相结合开发的过程中,难免会遇到关于WebView一些共通的问题。就我目前开发过程中遇到的问题以及最后得到的优化方案都将在这里列举出来。有些是老生常谈,有些则是个人摸索得出解决方法。
默认情况html代码下载到WebView后,webkit开始解析网页各个节点,发现有外部样式文件或者外部脚本文件时,会异步发起网络请求下载文件,但如果在这之前也有解析到image节点,那势必也会发起网络请求下载相应的图片。在网络情况较差的情况下,过多的网络请求就会造成带宽紧张,影响到css或js文件加载完成的时间,造成页面空白loading过久。解决的方法就是告诉WebView先不要自动加载图片,等页面finish后再发起图片加载。
故在WebView初始化时设置如下代码:
public void int () { if(Build.VERSION.SDK_INT >= 19) { webView.getSettings().setLoadsImagesAutomatically(true); } else { webView.getSettings().setLoadsImagesAutomatically(false); } }
同时在WebView的WebViewClient实例中的onPageFinished()方法添加如下代码:
@Override public void onPageFinished(WebView view, String url) { if(!webView.getSettings().getLoadsImagesAutomatically()) { webView.getSettings().setLoadsImagesAutomatically(true); } }
从上面的代码,可以看出我们对系统API在19以上的版本作了兼容。因为4.4以上系统在onPageFinished时再恢复图片加载时,如果存在多张图片引用的是相同的src时,会只有一个image标签得到加载,因而对于这样的系统我们就先直接加载。
当WebView加载页面出错时(一般为404 NOT FOUND),安卓WebView会默认显示一个卖萌的出错界面。但我们怎么能让用户发现原来我使用的是网页应用呢,我们期望的是用户在网页上得到是如原生般应用的体验,那就先要从干掉这个默认出错页面开始。当WebView加载出错时,我们会在WebViewClient实例中的onReceivedError()方法接收到错误,我们就在这里做些手脚:
@Override public void onReceivedError (WebView view, int errorCode, String description, String failingUrl) { super.onReceivedError(view, errorCode, description, failingUrl); loadDataWithBaseURL(null, "", "text/html", "utf-8", null); mErrorFrame.setVisibility(View.VISIBLE); }
从上面可以看出,我们先使用loadDataWithBaseURL清除掉默认错误页内容,再让我们自定义的View得到显示(mErrorFrame为蒙在WebView之上的一个LinearLayout布局,默认为View.GONE)。
当我们做类似上拉加载下一页这样的功能的时候,页面初始的时候需要知道当前WebView是否存在纵向滚动条,如果有则不加载下一页,如果没有则加载下一页直到其出现纵向滚动条。首先继承WebView类,在子类添加下面的代码:
public boolean existVerticalScrollbar () { return computeVerticalScrollRange() > computeVerticalScrollExtent(); }
computeVerticalScrollRange得到的是可滑动的最大高度,computeVerticalScrollExtent得到的是滚动把手自身的高,当不存在滚动条时,两者的值是相等的。当有滚动条时前者一定是大于后者的。
同样我们在做上拉加载下一页这样的功能时,也需要知道当前页面滚动条所处的状态,如果快到底部,则要发起网络请求数据更新网页。同样继承WebView类,在子类覆盖onScrollChanged方法,具体如下:
@Override protected void onScrollChanged(int newX, int newY, int oldX, int oldY) { super.onScrollChanged(newX, newY, oldX, oldY); if (newY != oldY) { float contentHeight = getContentHeight() * getScale(); // 当前内容高度下从未触发过, 浏览器存在滚动条且滑动到将抵底部位置 if (mCurrContentHeight != contentHeight && newY > 0 && contentHeight <= newY + getHeight() + mThreshold) { // TODO Something... mCurrContentHeight = contentHeight; } } }
上面mCurrContentHeight用于记录上次触发时的网页高度,用来防止在网页总高度未发生变化而目标区域发生连续滚动时会多次触发TODO,mThreshold是一个阈值,当页面底部距离滚动条底部的高度差<=这个值时会触发TODO。
当我们在WebView中加载出从web服务器上拿取的内容时,是无法访问本地资源的,如assets目录下的图片资源,因为这样的行为属于跨域行为(Cross-Domain),而WebView是禁止的。解决这个问题的方案是把html内容先下载到本地,然后使用loadDataWithBaseURL加载html。这样就可以在html中使用 file:///android_asset/xxx.png 的链接来引用包里面assets下的资源了。示例如下:
private void loadWithAccessLocal(final String htmlUrl) { new Thread(new Runnable() { public void run() { try { final String htmlStr = NetService.fetchHtml(htmlUrl); if (htmlStr != null) { TaskExecutor.runTaskOnUiThread(new Runnable() { @Override public void run() { loadDataWithBaseURL(htmlUrl, htmlStr, "text/html", "UTF-8", ""); } }); return; } } catch (Exception e) { Log.e("Exception:" + e.getMessage()); } TaskExecutor.runTaskOnUiThread(new Runnable() { @Override public void run() { onPageLoadedError(-1, "fetch html failed"); } }); } }).start(); }
上面有几点需要注意:
如果你的多个WebView是放在ViewPager里一个个加载出来的,那么就会遇到这样的问题。ViewPager首屏WebView的创建是在前台,点击时没有问题;而其他非首屏的WebView是在后台创建,滑动到它后点击页面会出现如下错误日志:
20955-20968/xx.xxx.xxx E/webcoreglue﹕ Should not happen: no rect-based-test nodes found
解决这个问题的办法是继承WebView类,在子类覆盖onTouchEvent方法,填入如下代码:
@Override public boolean onTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onScrollChanged(getScrollX(), getScrollY(), getScrollX(), getScrollY()); } return super.onTouchEvent(ev); }
该方法的最先提出在WebView in ViewPager not receive user inputs。
4.0以上的系统我们开启硬件加速后,WebView渲染页面更加快速,拖动也更加顺滑。但有个副作用就是,当WebView视图被整体遮住一块,然后突然恢复时(比如使用SlideMenu将WebView从侧边滑出来时),这个过渡期会出现白块同时界面闪烁。解决这个问题的方法是在过渡期前将WebView的硬件加速临时关闭,过渡期后再开启,代码如下:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null); }
使用开源项目Safe Java-JS WebView Bridge可以很好替代addJavaScriptInterface方法,同时增加了异步回调等支持,并且不存在了安全风险。
在开发过程中你可能会遇到这样一种情况。端里面使用ViewPager嵌套了多个WebView页面,同时某一个WebView中的页面元素需要响应TouchMove事件。详细解决方案请移步:http://www.pedant.cn/2014/09/23/webview-touch-conflict
在开发过程中你可能会遇到这样一种情况。端里面使用ViewPager嵌套了多个WebView页面,同时某一个WebView中的页面元素需要响应TouchMove事件。详细解决方案请移步:http://www.pedant.cn/2014/09/23/webview-touch-conflict
webView.clearCache(true); webView.clearHistory();
CookieSyncManager.createInstance(this); CookieSyncManager.getInstance().startSync(); CookieManager.getInstance().removeSessionCookie();
有时候需要加上请求头,但是非超链接的请求,没有办法再shouldOverrinding中拦截并用webView.loadUrl(String url,HashMap headers)方法添加请求头目前用了一个临时的办法解决:
首先需要在url中加特殊标记/协议, 如在onWebViewResource方法中拦截对应的请求,然后将要添加的请求头,以get形式拼接到url末尾
在shouldInterceptRequest()方法中,可以拦截到所有的网页中资源请求,比如加载JS,图片以及Ajax请求等等
Ex:
@SuppressLint("NewApi") @Override public WebResourceResponse shouldInterceptRequest(WebView view,String url) { // 非超链接(如Ajax)请求无法直接添加请求头,现拼接到url末尾,这里拼接一个imei作为示例 String ajaxUrl = url; // 如标识:req=ajax if (url.contains("req=ajax")) { ajaxUrl += "&imei=" + imei; } return super.shouldInterceptRequest(view, ajaxUrl); }
需要在Activity的onDestory()中调用
webView.destroy();但是直接调用可能会引起如下错误:
10-10 15:01:11.402: E/ViewRootImpl(7502): sendUserActionEvent() mView == null 10-10 15:01:26.818: E/webview(7502): java.lang.Throwable: Error: WebView.destroy() called while still attached! 10-10 15:01:26.818: E/webview(7502): at android.webkit.WebViewClassic.destroy(WebViewClassic.java:4142) 10-10 15:01:26.818: E/webview(7502): at android.webkit.WebView.destroy(WebView.java:707) 10-10 15:01:26.818: E/webview(7502): at com.didi.taxi.ui.webview.OperatingWebViewActivity.onDestroy(OperatingWebViewActivity.java:236) 10-10 15:01:26.818: E/webview(7502): at android.app.Activity.performDestroy(Activity.java:5543) 10-10 15:01:26.818: E/webview(7502): at android.app.Instrumentation.callActivityOnDestroy(Instrumentation.java:1134) 10-10 15:01:26.818: E/webview(7502): at android.app.ActivityThread.performDestroyActivity(ActivityThread.java:3619) 10-10 15:01:26.818: E/webview(7502): at android.app.ActivityThread.handleDestroyActivity(ActivityThread.java:3654) 10-10 15:01:26.818: E/webview(7502): at android.app.ActivityThread.access$1300(ActivityThread.java:159) 10-10 15:01:26.818: E/webview(7502): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1369) 10-10 15:01:26.818: E/webview(7502): at android.os.Handler.dispatchMessage(Handler.java:99) 10-10 15:01:26.818: E/webview(7502): at android.os.Looper.loop(Looper.java:137) 10-10 15:01:26.818: E/webview(7502): at android.app.ActivityThread.main(ActivityThread.java:5419) 10-10 15:01:26.818: E/webview(7502): at java.lang.reflect.Method.invokeNative(Native Method) 10-10 15:01:26.818: E/webview(7502): at java.lang.reflect.Method.invoke(Method.java:525) 10-10 15:01:26.818: E/webview(7502): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1187) 10-10 15:01:26.818: E/webview(7502): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003) 10-10 15:01:26.818: E/webview(7502): at dalvik.system.NativeStart.main(Native Method)
如上所示,webview调用destory时,webview仍绑定在Activity上。这是由于自定义webview构建时传入了该Activity的context对象,因此需要先从父容器中移除webview,然后再销毁webview:
rootLayout.removeView(webView); webView.destroy();
这个功能首先可以从两方面完成:
(1) 在js中完成:
处理android.selection.longTouch
这里推荐一个开源项目进行参考,:
https://github.com/btate/BTAndroidWebViewSelection
(2) android层处理:
首先使用OnTouchListener实现长按实现监听,然后实现WebView的Context menu,最后调用webview中的emulateShiftHeld(),为了适配安卓不同版本,最好使用反射方式调用。