那些年在WebView上踩过的坑

2022-06-22 10:30:04 浏览数 (1)

之前我在Android中使用WebView与JS交互全解析一文中,介绍了通过Webview和JS的交互方式,但Webview这个控件简直是让人又爱又恨,各种你想不到的错误在各种奇怪的手机上,各种不一样的版本里,所以我想通过这篇博客总结Webview开发中的不得不注意的一些坑。

1.WebView的内存泄露问题 问题描述: webview内存泄露的情况还是很严重的,尤其是当你加载的页面比较庞大的时候。 解决方案: 1) 展示webview的activity可以另开一个进程,这样就能和我们app的主进程分开了,即使webview产生了oom崩溃等问题也不会影响到主程序,如何实现呢,其实很简单,在Androidmanifest.xml的activity标签里加上Android:process=”packagename.web”就可以了,并且当这个 进程结束时,请手动调用System.exit(0)。

2) 如果实在不想用开额外进程的方式解决webview 内存泄露的问题,那么下面的方法很大程度上可以避免这种情况

代码语言:javascript复制
public void releaseAllWebViewCallback() {
         if (android.os.Build.VERSION.SDK_INT < 16) {
             try {
                 Field field = WebView.class.getDeclaredField("mWebViewCore");
                 field = field.getType().getDeclaredField("mBrowserFrame");
                 field = field.getType().getDeclaredField("sConfigCallback");
                 field.setAccessible(true);
                 field.set(null, null);
             } catch (NoSuchFieldException e) {
                 if (BuildConfig.DEBUG) {
                     e.printStackTrace();
                 }
             } catch (IllegalAccessException e) {
                 if (BuildConfig.DEBUG) {
                     e.printStackTrace();
                 }
             }
         } else {
             try {
                 Field sConfigCallback = Class.forName("android.webkit.BrowserFrame").getDeclaredField("sConfigCallback");
                 if (sConfigCallback != null) {
                     sConfigCallback.setAccessible(true);
                     sConfigCallback.set(null, null);
                 }
             } catch (NoSuchFieldException e) {
                 if (BuildConfig.DEBUG) {
                     e.printStackTrace();
                 }
             } catch (ClassNotFoundException e) {
                 if (BuildConfig.DEBUG) {
                     e.printStackTrace();
                 }
             } catch (IllegalAccessException e) {
                 if (BuildConfig.DEBUG) {
                     e.printStackTrace();
                 }
             }
         }
     }

在webview的 destroy方法里 调用这个方法就行了。

2.慎重在shouldoverrideurlloading中返回true

当设置了WebviewClient时,在shouldoverrideurlloading中如果不需要对url进行拦截做处理,而是简单的继续加载此网址,则建议采用返回false的方式而不是loadUrl的方式进行加载网址。 1) 当请求的方式是”POST”方式时这个回调是不会通知的。 2) 因为如果采用loadUrl的方式进行加载,那么对于加载有跳转的网址时,进行webview.goBack就会特别麻烦。 例如加载链接如下: A1->(A2->A3->A4)->A5 括号内为跳转 如果采用return false的方式,那么在goBack的时候,可以从第二步直接回到A1网页。从A5回到A1只需要执行两次goBack 而如果采用的是loadUrl,则没办法直接从第二步回到A网页。因为loadUrl把第二步的每个跳转都认为是一个新的网页加载,因此从A5回到A1需要执行四次goBack

只有当不需要加载网址而是拦截做其他处理,如拦截tel:xxx等特殊url做拨号处理的时候,才应该返回true。

3.getSettings().setBuiltInZoomControls(true) 引发的crash。 问题描述: 这个方法调用以后 如果你触摸屏幕 弹出的提示框还没消失的时候 你如果activity结束了 就会报错了。3.0以上 4.4以下很多手机会出现这种情况

解决方案: 在activity的onDestroy方法里手动的将webiew设置成 setVisibility(View.GONE)

4.onPageFinished 函数的问题 问题描述: 你永远无法确定当WebView调用这个方法的时候,网页内容是否真的加载完毕了。当前正在加载的网页产生跳转的时候这个方法可能会被多次调用,多数开发者都是参考的http://stackoverflow.com/questions/3149216/how-to-listen-for-a-webview-finishing-loading-a-url-in-android 这个上面的高票答案,但其中列举的解决方法并不完美。

解决方案: 当你的WebView需要加载各种各样的网页并且需要在页面加载完成时采取一些操作的话,可能WebChromeClient.onProgressChanged()比WebViewClient.onPageFinished()都要靠谱一些。如果哪位大神有更好的解决方法,欢迎留言。

5.WebView后台耗电问题。

问题描述: 当你的程序调用了WebView加载网页,WebView会自己开启一些线程,如果你没有正确地将WebView销毁的话,这些残余的线程会一直在后台运行,由此导致你的应用程序耗电量居高不下。

解决方案: 在Activity.onDestroy()中直接调用System.exit(0),使得应用程序完全被移出虚拟机,这样就不会有任何问题了。

6.后台无法释放js 导致耗电

问题描述: 在有的手机里,你如果webview加载的html里 有一些js 一直在执行比如动画之类的东西,如果此刻webview 挂在了后台,这些资源是不会被释放 用户也无法感知,导致一直占有cpu 耗电特别快。

解决方案: 在Activity的onstop和onresume里分别把setJavaScriptEnabled();给设置成false和true。

7.怎么用网页的标题来设置自己的标题栏?

代码语言:javascript复制
WebChromeClient mWebChromeClient = new WebChromeClient() {    
    @Override    
    public void onReceivedTitle(WebView view, String title) {    
        super.onReceivedTitle(view, title);    
        txtTitle.setText(title);    
    }    
};  

mWedView.setWebChromeClient(mWebChromeClient());

但是发现在部分的手机上,当通过webview.goBack()回退的时候,并没有触发onReceiveTitle(),这样会导致标题仍然是之前子页面的标题,没有切换回来.

这里可以分两种情况去处理: 1) 可以确定webview中子页面只有二级页面,没有更深的层次,这里只需要判断当前页面是否为初始的主页面,可以goBack的话,只要将标题设置回来即可. 2)webview中可能有多级页面或者以后可能增加多级页面,这种情况处理起来要复杂一些: 因为正常顺序加载的情况onReceiveTitle是一定会触发的,所以就需要自己来维护webview loading的一个url栈及url与title的映射关系。那么就需要一个ArrayList来保持加载过的url,一个HashMap保存url及对应的title。 正常顺序加载时,将url和对应的title保存起来,webview回退时,移除当前url并取出将要回退到的web 页的url,找到对应的title进行设置即可。

这里还要说一点,当加载出错的时候,比如无网络,这时onReceiveTitle中获取的标题为 找不到该网页,因此建议当触发onReceiveError时,不要使用获取到的title.

8.怎么隐藏缩放控件?

代码语言:javascript复制
if (DeviceUtils.hasHoneycomb()) {
      mWebView.getSettings().setDisplayZoomControls(false);  
}
最好把这两句加上
mWebView.getSettings().setSupportZoom(true);  
mWebView.getSettings().setBuiltInZoomControls(true);

9.怎么知道WebView是否已经滚动到页面底端?

代码语言:javascript复制
if (mWebView.getContentHeight() * mWebView.getScale()  
    == (mWebView.getHeight()   mWebView.getScrollY())) {
    }

附上官网对几个方法的注释

代码语言:javascript复制
getContentHeight() @return the height of the HTML content
getScale() @return the current scale
getHeight() @return The height of your view
getScrollY() @return The top edge of the displayed part of your view, in pixels.

10.怎么清理cache 和历史记录?

代码语言:javascript复制
mWebView.clearCache(true);   
mWebView.clearHistory();

11.怎么清理Cookie?

代码语言:javascript复制
CookieSyncManager.createInstance(this);   
CookieSyncManager.getInstance().startSync();   
CookieManager.getInstance().removeSessionCookie();

12.为什么打包之后JS调用失败? 原因:没在proguard-rules.pro中添加混淆吧

代码语言:javascript复制
-keep class con.demo.activity.web.utils.JsShareInterface {
   public void share(java.lang.String);
}

13.WebView页面中播放了音频,退出Activity后音频仍然在播放

需要在Activity的onDestory()中调用以下方法

代码语言:javascript复制
1.  webView.destroy();  

但可能会出现报错:

代码语言:javascript复制
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:

代码语言:javascript复制
rootLayout.removeView(webView);  
webView.destroy(); 

14.处理WebView中的非超链接请求(如Ajax请求) 有时候需要加上请求头,但是非超链接的请求,没有办法再shouldOverrinding中拦截并用webView.loadUrl(String url,HashMap headers)方法添加请求头 目前用了一个临时的办法解决: 首先需要在url中加特殊标记/协议, 如在onWebViewResource方法中拦截对应的请求,然后将要添加的请求头,以get形式拼接到url末尾 在shouldInterceptRequest()方法中,可以拦截到所有的网页中资源请求,比如加载JS,图片以及Ajax请求等等

代码语言:javascript复制
    @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);  

    }  

15.屏蔽webview长按事件,因为webview长按时将会调用系统的复制控件

代码语言:javascript复制
mWebView.setOnLongClickListener(new OnLongClickListener() {  

          @Override  
          public boolean onLongClick(View v) {  
              return true;  
          }  
      });  

16.在页面中先显示图片

代码语言:javascript复制
@Override  
public void onLoadResource(WebView view, String url) {  
  mEventListener.onWebViewEvent(CustomWebView.this, OnWebViewEventListener.EVENT_ON_LOAD_RESOURCE, url);  
    if (url.indexOf(".jpg") > 0) {  
     hideProgress(); //请求图片时即显示页面  
     mEventListener.onWebViewEvent(CustomWebView.this, OnWebViewEventListener.EVENT_ON_HIDE_PROGRESS, view.getUrl());  
     }  
    super.onLoadResource(view, url);  
}  

17.为WebView自定义错误显示界面 覆写WebViewClient中的onReceivedError()方法:

代码语言:javascript复制
/** 
 * 显示自定义错误提示页面,用一个View覆盖在WebView 
 */  
protected void showErrorPage() {  
    LinearLayout webParentView = (LinearLayout)mWebView.getParent();  

    initErrorPage();  
    while (webParentView.getChildCount() > 1) {  
        webParentView.removeViewAt(0);  
    }  
    LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LayoutParams.FILL_PARENT,LayoutParams.FILL_PARENT);  
    webParentView.addView(mErrorView, 0, lp);  
    mIsErrorPage = true;  
}  
protected void hideErrorPage() {  
    LinearLayout webParentView = (LinearLayout)mWebView.getParent();  

    mIsErrorPage = false;  
    while (webParentView.getChildCount() > 1) {  
        webParentView.removeViewAt(0);  
    }  
}  


   protected void initErrorPage() {  
    if (mErrorView == null) {  
        mErrorView = View.inflate(this, R.layout.online_error, null);  
        Button button = (Button)mErrorView.findViewById(R.id.online_error_btn_retry);  
        button.setOnClickListener(new OnClickListener() {  
            public void onClick(View v) {  
                mWebView.reload();  
            }  
        });  
        mErrorView.setOnClickListener(null);  
    }  
}  

@Override  
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {  
<span style="white-space:pre">                  </span>  
<span style="white-space:pre">          </span>mErrorView.setVisibility(View.VISIBLE);  
<span style="white-space:pre">          </span>super.onReceivedError(view, errorCode, description, failingUrl);  
}          

最后提一下WebView的一些小技巧: 1.webview的创建也是有技巧的,最好不要在layout.xml中使用webview,可以通过一个viewgroup容器,使用代码动态往容器里addview(webview),这样可以在onDestory()里销毁掉webview及时清理内存,另外需要注意创建webview需要使用applicationContext而不是activity的context,销毁时不再占有activity对象,这个大家应该都知道了,最后离开的时候需要及时销毁webview,onDestory()中应该先从viewgroup中remove掉webview,再调用webview.removeAllViews();webview.destory();

创建

代码语言:javascript复制
ll = new LinearLayout(getApplicationContext());   
ll.setOrientation(LinearLayout.VERTICAL);  
wv = new WebView(getApplicationContext());  

销毁

代码语言:javascript复制
@Override  
rotected void onDestroy() {  
   ll.removeAllViews();  
   wv.stopLoading();  
   wv.removeAllViews();  
   wv.destroy();  
   wv = null;  
   ll = null;  
   super.onDestroy();  

2.另外很多人 不知道webview 实际上有自己一套完整的cookie机制的,利用好这个 可以大大增加对客户端的访问速度。

实际上cookie就是存放在这个表里的。 很多人都想要一个效果:网页更新cookie 设置完cookie以后 不刷新页面即可生效。这个在2.3以下和2.3以上要实现的方法不太一样,不过现在的安卓版本已经基本没有2.3的啦

代码语言:javascript复制
public void updateCookies(String url, String value) {
         if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) { // 2.3及以下
             CookieSyncManager.createInstance(getContext().getApplicationContext());
         }
         CookieManager cookieManager = CookieManager.getInstance();
         cookieManager.setAcceptCookie(true);
         cookieManager.setCookie(url, value);
         if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) {
             CookieSyncManager.getInstance().sync();
         }
     }

3.进一步的优化,activity被动被杀之后,最好能够保存webview状态,这样用户下次打开时就看到之前的状态了,嗯,就这么干,webview支持saveState(bundle)和restoreState(bundle)方法,所以就简单了,看看代码吧: 保存状态:

代码语言:javascript复制
@Override  
protected void onSaveInstanceState(Bundle outState) {  
super.onSaveInstanceState(outState);  
wv.saveState(outState);  
Log.e(TAG, "save state...");  
}  

恢复状态: 在activity的onCreate(bundle savedInstanceState)里,这么调用:

代码语言:javascript复制
if(null!=savedInstanceState){  
    wv.restoreState(savedInstanceState);  
    Log.i(TAG, "restore state");  
}else{  
    wv.loadUrl("http://3g.cn");  
}  

4.WebView 图片延迟加载: 有些页面如果包含网络图片,在移动设备上我们等待加载图片的时间可能会很长,所以我们需要让图片延时加载,这样不影响我们加载页面的速度: 定义变量:

代码语言:javascript复制
boolean blockLoadingNetworkImage=false;

在WebView初始化的时候设置:

代码语言:javascript复制
blockLoadingNetworkImage = true;

webView.setWebViewClient(new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                //返回值是true的时候控制去WebView打开,为false调用系统浏览器或第三方浏览器
                return true;
            }

            @Override
            public void onPageStarted(WebView view, String url, Bitmap favicon) {
                super.onPageStarted(view, url, favicon);
                if (!blockLoadingNetworkImage){
                    webView.getSettings().setBlockNetworkImage(true);
                }
            }

            @Override
            public void onPageFinished(WebView view, String url) {
                super.onPageFinished(view, url);
                if (blockLoadingNetworkImage){
                    webView.getSettings().setBlockNetworkImage(false);
                }
            }
        });

0 人点赞