当前位置:网站首页>WebView的优化与常见问题解决方案

WebView的优化与常见问题解决方案

2022-08-10 12:38:00 锐湃

Android-WebView的优化与常见问题

其实关于Android的WebView大家使用起来应该都是有过封装,网上林林总总的分析与封装也不少。

我知道只要讲 WebView 一定有同学会说,原生WebView垃圾,我们都用的是腾讯X5 WebView 之类的。但是我们研发的是海外项目,只能使用原生的WebView,所以这里不涉及到TBS服务相关的点。

每一个人的封装可能都不一样,看我抛砖引玉,希望大家可以互相交流学习。

一、自定义WebView

我们需要一个统一管理的WebView,那么我们需要继承WebView,并内部对一些属性开启,对JS的支持,对加载过程与状态的监听,对文件操作的回调等。

public class MyWebView extends WebView {

    private WebSettings mWebSettings;
    private boolean isNeedExe = true;

    public MyWebView(Context context) {
        super(context);
        initView();
    }

    public MyWebView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public MyWebView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    @SuppressLint({"ObsoleteSdkInt", "SetJavaScriptEnabled"})
    private void initView() {

        mWebSettings = getSettings();
        mWebSettings.setSupportZoom(false);
        mWebSettings.setBuiltInZoomControls(false);
        mWebSettings.setDefaultTextEncodingName("utf-8");
        mWebSettings.setJavaScriptEnabled(true);
        mWebSettings.setDefaultFontSize(16);
        mWebSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);
        mWebSettings.setGeolocationEnabled(true);   //允许访问地址

        //允许访问多媒体
        mWebSettings.setAllowFileAccess(true);
        mWebSettings.setAllowFileAccessFromFileURLs(true);
        mWebSettings.setAllowUniversalAccessFromFileURLs(true);

        setVerticalScrollBarEnabled(false);
        setVerticalScrollbarOverlay(false);
        setHorizontalScrollBarEnabled(false);
        setHorizontalScrollbarOverlay(false);
        setOverScrollMode(OVER_SCROLL_NEVER);
        setFocusable(true);
        setHorizontalScrollBarEnabled(false);
        setDrawingCacheEnabled(true);

        //加载https的兼容
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            //两者都可以
            mWebSettings.setMixedContentMode(mWebSettings.getMixedContentMode());
            //mWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
        }


        //先加载页面再加载图片,这里先禁止图片加载
        if (Build.VERSION.SDK_INT >= 19) {
            mWebSettings.setLoadsImagesAutomatically(true);
        } else {
            mWebSettings.setLoadsImagesAutomatically(false);
        }


        setWebViewClient(mWebViewClient);
        setWebChromeClient(mWebChromeClient);
    }


    WebViewClient mWebViewClient = new WebViewClient() {
        //https ssl证书问题,如果没有https的问题可以注释掉
         @Override
        public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
            // 接受所有网站的证书,Google不通过
            //使用下面的兼容写法
            final SslErrorHandler mHandler;
            mHandler= handler;
            AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
            builder.setMessage("SSL validation failed");
            builder.setPositiveButton("Continue", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    mHandler.proceed();
                }
            });
            builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    mHandler.cancel();
                }
            });
            builder.setOnKeyListener(new DialogInterface.OnKeyListener() {
                @Override
                public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
                    if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
                        mHandler.cancel();
                        dialog.dismiss();
                        return true;
                    }
                    return false;
                }
            });
            AlertDialog dialog = builder.create();
            dialog.show();

        }

        //页面加载完成,展示图片
        @Override
        public void onPageFinished(WebView view, String url) {
            if (!mWebSettings.getLoadsImagesAutomatically()) {
                mWebSettings.setLoadsImagesAutomatically(true);
            }
        }

        //在当前的webview中跳转到新的url
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            if (mListener != null) mListener.onInnerLinkChecked();

            if (Build.VERSION.SDK_INT < 26) {
                if (!TextUtils.isEmpty(url)) {
                    view.loadUrl(url);
                }
                return true;
            }
            return false;
        }

        //WebView加载错误的回调
        @Override
        public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
            super.onReceivedError(view, request, error);
            if (mListener != null) mListener.onWebLoadError();
        }

        //拦截WebView中的网络请求
        @Nullable
        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
            return super.shouldInterceptRequest(view, request);
        }

    };


    WebChromeClient mWebChromeClient = new WebChromeClient() {
        //获取html的title标签
        @Override
        public void onReceivedTitle(WebView view, String title) {
            if (mListener != null) mListener.titleChange(title);
            super.onReceivedTitle(view, title);
        }

        //获取页面加载的进度
        @Override
        public void onProgressChanged(WebView view, int newProgress) {
            if (mListener != null) mListener.progressChange(newProgress);
            super.onProgressChanged(view, newProgress);

            if (newProgress > 95 && isNeedExe) {
                isNeedExe = !isNeedExe;

                if (newProgress == 100) {
                    //注入js代码测量webview高度
                    loadUrl("javascript:App.resize(document.body.getBoundingClientRect().height)");
                }
            }

        }

        // 指定源的网页内容在没有设置权限状态下尝试使用地理位置API。
        @Override
        public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
            boolean allow = true;   // 是否允许origin使用定位API
            boolean retain = false; // 内核是否记住这次制授权
            callback.invoke(origin, true, false);
        }

        // 之前调用 onGeolocationPermissionsShowPrompt() 申请的授权被取消时,隐藏相关的UI。
        @Override
        public void onGeolocationPermissionsHidePrompt() {
        }

        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Override
        public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
            //启动系统相册
            YYLogUtils.w("网页尝试调取Android相机相册");

            CommUtils.getHandler().post(() -> {
                if (mFilesListener != null) mFilesListener.onWebFileSelect(filePathCallback);
            });

            return true;
        }

    };

    //网页状态的回调相关处理
    private OnWebChangeListener mListener;

    public interface OnWebChangeListener {
        void titleChange(String title);

        void progressChange(int progress);

        void onInnerLinkChecked();

        void onWebLoadError();
    }

    public void setOnWebChangeListener(OnWebChangeListener listener) {
        mListener = listener;
    }

    //网页选择图片文件的回调相关处理
    private OnWebChooseFileListener mFilesListener;

    public interface OnWebChooseFileListener {

        void onWebFileSelect(ValueCallback<Uri[]> callback);
    }

    public void setOnWebChooseFileListener(OnWebChooseFileListener listener) {
        mFilesListener = listener;
    }


    /**
     * 暴露方法,是否滑动到底部
     */
    public boolean isScrollBottom() {
        if (getContentHeight() * getScale() == (getHeight() + getScrollY())) {
            //说明已经到底了
            return true;
        } else {
            return false;
        }
    }

}
复制代码

都是比较基础的代码,涉及到属性的开启,与监听和回调大家应该都能看懂,下面就是看如何使用了。

    private fun initWeb() {
        val params = FrameLayout.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT
        )
        mWebView = MyWebView(applicationContext)
        mWebView.layoutParams = params

        mWebView.setOnWebChangeListener(object : MyWebView.OnWebChangeListener {
            override fun titleChange(title: String) {
                if (CheckUtil.isEmpty(mWebtitle)) {
                    easy_title.setTitle(mWebtitle)
                }
            }

            override fun progressChange(progress: Int) {
                var newProgress = progress
                if (newProgress == 100) {
                    pb_web_view.setProgress(100)
                    CommUtils.getHandler()
                        .postDelayed({ pb_web_view.visibility = View.GONE }, 200)//0.2秒后隐藏进度条
                } else if (pb_web_view.visibility == View.GONE) {
                    pb_web_view.visibility = View.VISIBLE
                }
                //设置初始进度10,这样会显得效果真一点,总不能从1开始吧
                if (newProgress < 10) {
                    newProgress = 10
                }
                //不断更新进度
                pb_web_view.setProgress(newProgress)
            }

            override fun onInnerLinkChecked() {

            }

            override fun onWebLoadError() {
                toast("Load Error")
            }
        })

        if (!TextUtils.isEmpty(mWeburl))
            mWebView.loadUrl(mWeburl!!)

        fl_content.addView(mWebView)

    }

    override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
        if (keyCode == KeyEvent.KEYCODE_BACK && mWebView!!.canGoBack()) {
            mWebView!!.goBack()
            return true
        }
        return super.onKeyDown(keyCode, event)
    }

    override fun onPause() {
        super.onPause()
        mWebView?.onPause()
        
    }

     override fun onResume() {
        super.onResume()
        mWebView?.onResume()
        
    }

    override fun onDestroy() {
        super.onDestroy()
        if (mWebView != null) {
            mWebView?.clearCache(true) //清空缓存
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {

                fl_content.removeView(mWebView)

                mWebView?.removeAllViews()
                mWebView?.destroy()
            } else {
                mWebView?.removeAllViews()
                mWebView?.destroy()

                fl_content.removeView(mWebView)

            }
            mWebView = null
        }

    }
复制代码

大家大致上应该都是这么使用了,为了优化内存我们手动创建WebView,初始化并lodUrl之后,我们加入到容器中,在销毁的时候我们销毁WebView,并移除掉。

其实老玩家都知道,就算如此还是会有内存泄露与开销的,那么大家使用多进程的方案,让WebView运行在一个单独的进程中,不影响当前进程的内存。

大家可以试试如果是使用这种方式,那么每次退出Web页面,在进入Web,再退出,是可以看到内存是慢慢在涨的。大概一次能涨个2M左右。

二、WebView的缓存

其实我们就可以换一个思路,如果说WebView的销毁会内存泄露,那么我们不销毁不就行了吗?我们把WebView缓存起来。每次使用的时候去缓存里面拿,然后销毁的时候回收,这样不就不会内存泄露了吗?

网上找的一个WebViewCacheManager:

/**
 * WebView的缓存容器
 * obtail获取对象
 * recycle回收对象
 */
object WebViewManager {

    private val webViewCache: MutableList<MyWebView> = ArrayList(1)

    private fun create(context: Context): MyWebView {
        return MyWebView(context)
    }

    /**
     * 初始化
     */
    @JvmStatic
    fun prepare(context: Context) {
        if (webViewCache.isEmpty()) {
            Looper.myQueue().addIdleHandler {
                webViewCache.add(create(MutableContextWrapper(context)))
                false
            }
        }
    }

    /**
     * 获取WebView
     */
    @JvmStatic
    fun obtain(context: Context): MyWebView {

        if (webViewCache.isEmpty()) {
            webViewCache.add(create(MutableContextWrapper(context)))
        }
        val webView = webViewCache.removeFirst()
        val contextWrapper = webView.context as MutableContextWrapper
        contextWrapper.baseContext = context
        webView.clearHistory()
        webView.resumeTimers()
        return webView
    }

    /**
     * 回收资源
     */
    @JvmStatic
    fun recycle(webView: MyWebView) {
        try {
            webView.stopLoading()
            webView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null)
            webView.clearHistory()
            webView.pauseTimers()
            webView.clearFormData()
            webView.removeJavascriptInterface("webkit")

            val parent = webView.parent
            if (parent != null) {
                (parent as ViewGroup).removeView(webView)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            if (!webViewCache.contains(webView)) {
                webViewCache.add(webView)
            }
        }
    }

    /**
     * 销毁资源
     */
    @JvmStatic
    fun destroy() {
        try {
            webViewCache.forEach {
                it.removeAllViews()
                it.destroy()
                webViewCache.remove(it)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

}
复制代码

网上很多的这种管理类,原理都大致差不多,这样管理了WebView之后还有一个好处是可以优化启动速度,无需每次New一个WebView然后初始化内核之类的耗时了。

使用之前我们需要初始化


open class BaseApplication : Application() {

  override fun onCreate() {
    super.onCreate()
    
   //空闲的时候初始化WebView容器
   Looper.myQueue().addIdleHandler {
      //初始化WebView缓存容器
      WebViewManager.prepare(this)
      false
    }
  }
}
复制代码

初始化完成之后,如果要使用工具类,我们这样修改WebView的使用:

    private fun initWeb() {
        val params = FrameLayout.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT
        )
        mWebView = WebViewManager.obtain(this)  //管理类获取对象
        mWebView.layoutParams = params

        mWeburl?.let { mWebView.loadUrl(it) }

        mWebView.addJavascriptInterface(H5CallBackAndroid(), "webkit")

        mBinding.flContent.addView(mWebView)

    }

    override fun onPause() {
        super.onPause()
        mWebView.onPause()
    }

    override fun onResume() {
        super.onResume()
        mWebView.onResume()
    }

    override fun onDestroy() {
        super.onDestroy()
        WebViewManager.recycle(mWebView)
    }

    override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
        if (keyCode == KeyEvent.KEYCODE_BACK && mWebView!!.canGoBack()) {
            mWebView!!.goBack()
            return true
        }
        return super.onKeyDown(keyCode, event)
    }
复制代码

可以看到我们只是修改了WebView的创建于销毁,这么做的好处是当销毁的时候不会泄露内存了。

例如我跳转Web之前的页面-占内存为160左右

跳转到一个Web,内存飙升至190左右 

返回之前的页面-占用内存依旧是160左右 

如果大家有兴趣,也可以自行测试,如果每次New WebView 再 destory () 那么内存是慢慢上涨的,如果使用WebView缓存之后内存并不会上涨。

三、WebView的返回问题

但是这么做有一个很大的坑,就是每次销毁的时候它的Url并没有清除,我们又不能使用webView的destory方法,那么我们第一个启动Web并返回是正常的,第二次再启动再返回,此时使用的是缓存WebView,是无法一次返回的。

因为之前的WebView已经有一个Url了,因为加载的网页可能是任意网址,我们无法判断,那么我们在回收的方法中手动的设置了指定的url

webView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null)

那么这样的效果还是有问题,之前我们还需要按2次返回键才能返回Web页面,而现在我们加载了一个空视图之后,现在在Web的栈顶,按一次返回键会返回一个空白的页面,再按返回才能返回,还是需要二次返回。

解决办法是,我们在返回的时候判断一下,上一个url是不是空白的不就行了吗?

我们通过 copyBackForwardList 可以拿到WebView的全部栈顶,和当前的栈索引,我们加上一点判断,就可以正常的返回了。

override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
        val webBackForwardList = mWebView.copyBackForwardList()
        val historyOneOriginalUrl = webBackForwardList.getItemAtIndex(0)?.originalUrl
        val curIndex = webBackForwardList.currentIndex

        return if (keyCode == KeyEvent.KEYCODE_BACK && mWebView.canGoBack()) {

            //判断是否是缓存的WebView
            if (historyOneOriginalUrl?.contains("data:text/html;charset=utf-8") == true) {
                //说明是缓存复用的的WebView
                if (curIndex > 1) {
                    //内部跳转到另外的页面了,可以返回的
                    mWebView.goBack()
                    true
                } else {
                    //等于1的时候就要Finish页面了
                    super.onKeyDown(keyCode, event)
                }
            } else {
                //如果不是缓存复用的WebView,可以直接返回
                mWebView.goBack()
                true
            }
        } else {
            super.onKeyDown(keyCode, event)
        }

    }
复制代码

配合返回的完善,缓存的WebView是实战中的一大利器,大大的优化了启动速度,与性能开销。

四、WebView中JS的注入和Java的互调

其实这已经不算优化的点了,但是是我们常用互调的方法,这里就简单说明一下。

当然了很多人喜欢用框架来实现,每个框架的实现步骤不同,这里我不使用框架,用原生的实现。

4.1 Java中调用JS定义的方法

    <script>
        function changeContent(data){
            document.getElementById('content').innerHTML=data;
        }
    </script>
复制代码

有两种方法调用JS:

webView.loadUrl("javascript:changeContent('<p>我是HTML</p>')");
复制代码
webView.evaluateJavascript("javascript:changeContent('<p>我是HTML</p>')");
复制代码

4.2 JS调用Java的方法

比如JS代码如下:

    function isAndroid_ios() {
		var u = navigator.userAgent,
		app = navigator.appVersion;
		var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1; //android终端或者uc浏览器
				var isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端
		return isAndroid == true ? true : false;
	}

	function checkImage() {
		if (!window.isClick) {
		   window.isClick = true;
		   if (isAndroid_ios()) {
			 window.webkit.clickImage(null);
			} else {
			   window.webkit.messageHandlers.clickImage.postMessage(null);
			}
		}

	}
复制代码

在网页中我们定义了Android iOS的回调之后,它的回调方法名是 clickImage 作用域是 webkit ,那么我们在WebView中定义就行了

  mWebView.addJavascriptInterface(H5CallBackAndroid(), "webkit")

  inner class H5CallBackAndroid {

        //图片的点击
        @JavascriptInterface
        fun clickImage(obj: String) {
          
        }

  }

复制代码

4.3 JS的手动注入

比如前端工程师没有写一些方法,那要他何用,自己动手,丰衣足食,我们自己手写JS注入到前端代码中,然后自己调用自己的JS。

例如一个前端的网页是新闻展示,我们需要获取新闻的全部图片,而前端代码中并没有定义这样的方法给我们调用

             //js注入调用
                view.loadUrl("javascript:function myFunction(){ var imgs = document.getElementsByTagName(\"img\");\n" +
                        "    var imgurls = new Array();\n" +
                        "    for (var i = 0; i < imgs.length; i++) {\n" +
                        "        imgs[i].style.marginTop = '10px';\n" +
                        "        imgs[i].style.marginBottom = '10px';\n" +
                        "        var imgurl = imgs[i].src;\n" +
                        "        if (imgurl.length > 50) {\n" +
                        "            imgurls[i] = imgurl;\n" +
                        "        } else {\n" +
                        "            imgs[i].remove();\n" +
                        "            continue;\n" +
                        "        }\n" +
                        "        (function (e) {\n" +
                        "            imgs[e].onclick = function () {\n" +
                        "                window.App.showImgFromPosition(e);\n" +
                        "            };\n" +
                        "        })(i)\n" +
                        "    }\n" +
                        "    var imgs = function () {\n" +
                        "        window.webkit.getAllImgs(imgurls);\n" +
                        "\n" +
                        "    };\n" +
                        "    imgs();\n" +
                        "    document.getElementsByTagName(\"aside\")[0].remove();\n" +
                        "    document.getElementsByTagName(\"time\")[0].remove();\n" +
                        "    document.getElementsByClassName('art_title_op')[0].height = '0px';\n" +
                        "    document.getElementsByClassName('art_title_op')[0].lineHeight = '0px';\n" +
                        "    document.getElementsByClassName('art_title_op')[0].remove();\n" +
                        "    var ps = document.getElementsByTagName(\"p\");\n" +
                        "    for (var i = 0; i < ps.length; i++) {\n" +
                        "        var p_text = $(ps[i]).text();\n" +
                        "        if (p_text != null && p_text != undefined && p_text != \"\" && p_text.length > 0) {\n" +
                        "            var pp = function () {\n" +
                        "                window.App.getFirstContent(p_text);\n" +
                        "            };\n" +
                        "            pp();\n" +
                        "            break;\n" +
                        "        }\n" +
                        "    }\n" +
                        "    for (var i = 0; i < ps.length; i++) {\n" +
                        "        ps[i].style.fontSize = '16px';\n" +
                        "        ps[i].style.lineHeight = '1.8';\n" +
                        "    }\n" +
                        "    document.getElementsByTagName(\"body\")[0].style.padding = '10px';\n" +
                        "    document.getElementsByTagName(\"body\")[0].style.background = '#fff'; }");

                //注入完成顺便执行注入的JS
                view.loadUrl("javascript:myFunction()");

复制代码

注入了JS之后,我们调用我们注入的JS,注入的JS会回调到Java中来,代码如下:

    @JavascriptInterface
    public void getAllImgs(String[] imgs) {
        mAllImgs.clear();
        for (int i = 0; i < imgs.length; i++) {
            mAllImgs.add(imgs[i]);
        }
    }
复制代码

当然了我们这么玩的机会还是比较少的,因为这种问题一般都是找前端去改的。这里也只是给大家扩展一下思路。

五、WebView中Cookie的管理

Cookie我们用的也是比较少,一般都是特殊场景下才需要使用到,webkit自带的CookieManager管理

下面是常用的几种方法

// 设置接收第三方Cookie
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    CookieManager.getInstance().setAcceptThirdPartyCookies(vWeb, true);
}


// 获取指定url关联的所有Cookie
// 返回值使用"Cookie"请求头格式:"name=value; name2=value2; name3=value3"
CookieManager.getInstance().getCookie(url);

// 为指定的url设置一个Cookie
// 参数value使用"Set-Cookie"响应头格式,参考:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Set-Cookie
CookieManager.getInstance().setCookie(url, value);

// 移除指定url下的指定Cookie
CookieManager.getInstance().setCookie(url, cookieName + "=");

复制代码

Cookie的工具类:

public class WebkitCookieUtil { 
    // 移除指定url关联的所有cookie
    public static void remove(String url) {
        CookieManager cm = CookieManager.getInstance();
        for (String cookie : cm.getCookie(url).split("; ")) {
            cm.setCookie(url, cookie.split("=")[0] + "=");
        }
        flush();
    }
    // sessionOnly 为true表示移除所有会话cookie,否则移除所有cookie
    public static void remove(boolean sessionOnly) {
        CookieManager cm = CookieManager.getInstance();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            if (sessionOnly) {
                cm.removeSessionCookies(null);
            } else {
                cm.removeAllCookies(null);
            }
        } else {
            if (sessionOnly) {
                cm.removeSessionCookie();
            } else {
                cm.removeAllCookie();
            }
        }
        flush();
    }
    // 写入磁盘
    public static void flush() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            CookieManager.getInstance().flush();
        } else {
            CookieSyncManager.getInstance().sync();
        }
    }
}
复制代码

同步Cookie


// 将系统级Cookie(比如`new URL(...).openConnection()`的Cookie) 同步到 WebView
public class WebkitCookieHandler extends CookieHandler {
    private static final String TAG = WebkitCookieHandler.class.getSimpleName();
    private CookieManager wcm;
    public WebkitCookieHandler() {
        this.wcm = CookieManager.getInstance();
    }
    @Override
    public void put(URI uri, Map<String, List<String>> headers) throws IOException {
        if ((uri == null) || (headers == null)) {
            return;
        }
        String url = uri.toString();
        for (String headerKey : headers.keySet()) {
            if ((headerKey == null) || !(headerKey.equalsIgnoreCase("set-cookie2") || headerKey.equalsIgnoreCase("set-cookie"))) {
                continue;
            }
            for (String headerValue : headers.get(headerKey)) {
                Log.e(TAG, headerKey + ": " + headerValue);
                this.wcm.setCookie(url, headerValue);
            }
        }
    }
    @Override
    public Map<String, List<String>> get(URI uri, Map<String, List<String>> headers) throws IOException {
        if ((uri == null) || (headers == null)) {
            throw new IllegalArgumentException("Argument is null");
        }
        String url = uri.toString();
        String cookie = this.wcm.getCookie(url);
        Log.e(TAG, "cookie: " + cookie);
        if (cookie != null) {
            return Collections.singletonMap("Cookie", Arrays.asList(cookie));
        } else {
            return Collections.emptyMap();
        }
    }
}

复制代码

六、WebView中定位操作

一些Web需要定位的时候,需要我们App提供他们服务,此时需要用到一些权限申请和处理

先需要配置权限

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
复制代码

设置WebView的服务可用

settings.setGeolocationEnabled(true);
复制代码
//申请权限时的回调
@Override
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
    boolean allow = true;   // 是否允许origin使用定位API
    boolean retain = false; // 内核是否记住这次制授权
    callback.invoke(origin, true, false);
}
// 申请的授权被取消时,隐藏相关的UI。
@Override
public void onGeolocationPermissionsHidePrompt() {
}
复制代码

当然我们App也是授权给Web,定位的操作还是在Web那边的 Geolocation API,如果想通过App来定位,也是可以的,我们可以通过App的定位完成之后直接把经纬度传递给Web。

七、WebView中图片与文件的获取

首先我们需要定义权限

    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission
        android:name="android.permission.READ_EXTERNAL_STORAGE"
        tools:remove="android:maxSdkVersion" />
    <uses-permission
        android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        tools:ignore="ScopedStorage"
        tools:remove="android:maxSdkVersion" />
复制代码

设置WebView的支持

    //允许访问多媒体
    mWebSettings.setAllowFileAccess(true);
    mWebSettings.setAllowFileAccessFromFileURLs(true);
    mWebSettings.setAllowUniversalAccessFromFileURLs(true);
复制代码

在设置的 WebChromeClient 方法中重写此回调

       @Override
        public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
            //启动相册
            YYLogUtils.w("网页尝试调取Android相机相册");

            if (mFilesListener != null) mFilesListener.onWebFileSelect(filePathCallback);
            

            return true;
        }


     //网页选择图片文件的回调相关处理
    private OnWebChooseFileListener mFilesListener;

    public interface OnWebChooseFileListener {

        void onWebFileSelect(ValueCallback<Uri[]> callback);
    }

    public void setOnWebChooseFileListener(OnWebChooseFileListener listener) {
        mFilesListener = listener;
    }       
复制代码

上面是使用了一个回调,让具体的页面来实现具体的需求,我们只需要注意参数 ValueCallback 就行了,我们获取到的图片文件数据通过 ValueCallback 回调给Web。

下面看看如何具体使用


  private ValueCallback<Uri[]> filePathCallback1;

       //文件与图片的选择回调
        mWebView.setOnWebChooseFileListener(new MyWebView.OnWebChooseFileListener() {
            @Override
            public void onWebFileSelect(ValueCallback<Uri[]> callback) {
                filePathCallback1 = callback;
                showPickDialog();
            }
        });


    /**
     * 相机相册的选择
     */
    private void showPickDialog() {

        PickPhotoDialog photoDialog = new PickPhotoDialog(mActivity);
        photoDialog.SetOnChooseClickListener(new PickPhotoDialog.OnChooseClickListener() {
            @Override
            public void chooseCamera() {
                startCamera();
            }

            @Override
            public void chooseAlbum() {
                startAlbum();
            }
        });
        photoDialog.setCancelable(false);
        photoDialog.show();
        photoDialog.setOnDismissListener(dialog -> {
            cancelFilePick();
        });
    }    
复制代码

开启相机或者相册大家可以具体的实现,每个人用的框架不同,这里就不做推荐了。

 //选择相册
    private void startAlbum() {
       //自行实现选择相册
       ...
      handlePath(xxx);
    }

    //选择相机
    private void startCamera() {
      //自行实现选择相机
       ...
      handlePath(xxx);
    }

    /**
     * 处理图片-转换图片-返回给Web
     */
    private void handlePath(List<String> result) {
        YYLogUtils.w("处理图片-转换图片-返回给Web");

        if (!CheckUtil.isEmpty(result)) {

            String path = result.get(0);

            Uri fileUri = UriExtKt.getFileUri(this, new File(path));
        

            if (filePathCallback1 != null) {
                //回调给Web
                filePathCallback1.onReceiveValue(new Uri[]{fileUri});
                filePathCallback1 = null;
            }
        }
    }

    //取消图片的选择
    private void cancelFilePick() {
        if (filePathCallback1 != null) {
            YYLogUtils.w("取消图片的选择");
            filePathCallback1.onReceiveValue(null);
            filePathCallback1 = null;
        }
    
    }

复制代码

到处就完成了Web的图片选择了。效果如下:

八、WebView中网络拦截

原理为 WebView内核的 shouldInterceptRequest 回调,拦截资源请求由客户端进行下载,并以管道方式填充到内核的 WebResourceResponse中。

使用场景是,我们使用Web之前我们已经通过网络把一些JS CSS 图片等资源放入了本地存储,那么我们Web使用的时候就判断如果本地已经有资源了,我们就从本地拿,如果没有我们就使用OkHttp下载到本地再使用。

在 WebView 的 WebViewClient 中我们加入如下拦截

      @Nullable
        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {

            if (view != null && request != null) {
            if(canCacheResource(request)){
                return cacheResourceRequest(view.context, request)
            }
        }
        return super.shouldInterceptRequest(view, request)

复制代码

具体的判断与

private fun canCacheResource(webRequest: WebResourceRequest): Boolean {

        val url = webRequest.url.toString()
         val extension = getExtensionFromUrl(url)

        //当资源是这些后缀的时候我们都需要拦截
         return extension == "gif"
            || extension == "jpeg" || extension == "jpg" || extension == "png"
            || extension == "svg" || extension == "webp" || extension == "css"
            || extension == "js" || extension == "json" || extension == "eot"
            || extension == "otf" || extension == "ttf"

        }

}

private fun cacheResourceRequest(context: Context,  webRequest: WebResourceRequest): WebResourceResponse? {

    try {
        val url = webRequest.url.toString()
        val cachePath = CacheUtils.getCacheDirPath(context, "web_cache")
        val filePathName = cachePath + File.separator + url.encodeUtf8().md5().hex()
        val file = File(filePathName)

        //如果文件不存在,下载到本地
        if (!file.exists() || !file.isFile) {
            runBlocking {
                // 使用工具类下载资源
                download(HttpRequest(url).apply {
                    webRequest.requestHeaders.forEach { putHeader(it.key, it.value) }
                }, filePathName)
            }
        }
 
        //文件存在或下载完成,我们使用管道传递给Web
        if (file.exists() && file.isFile) {
            val webResourceResponse = WebResourceResponse()
            webResourceResponse.mimeType = getMimeTypeFromUrl(url)
            webResourceResponse.encoding = "UTF-8"
            webResourceResponse.data = file.inputStream()
            webResourceResponse.responseHeaders = mapOf("access-control-allow-origin" to "*")
            return webResourceResponse
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
    return null
}
  
复制代码

这么做可以大大的提升页面的加载速度,特别适用于一些固定样式的页面,如文章的详情之类。但是需要注意的是注意本地磁盘缓存的大小限制,最好是做限时存储(时间戳)或者限量存储(LRUCache)。

九、WebView中点击事件

WebView中图片的点击,或者其他控件的点击我们之前可以通过JS互调的方式来手动的定义,也可以通过WebView自带的一些类型的点击监听。

9.1 使用JS方法自定义

    function isAndroid_ios() {
		var u = navigator.userAgent,
		app = navigator.appVersion;
		var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1; //android终端或者uc浏览器
				var isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端
		return isAndroid == true ? true : false;
	}

	function longClickImage(url) {
		if (!window.isClick) {
		   window.isClick = true;
		   if (isAndroid_ios()) {
			 window.webkit.longClickImage(url);
			} else {
			   window.webkit.messageHandlers.longClickImage.postMessage(url);
			}
		}

	}
复制代码

使用

  mWebView.addJavascriptInterface(H5CallBackAndroid(), "webkit")

  inner class H5CallBackAndroid {

        //图片的点击
        @JavascriptInterface
        fun longClickImage(url: String) {
            Intent i = new Intent(MainActivity.this, ImageActivity.class);
            i.putExtra("imgUrl", url);
            startActivity(i);
        }

  }

复制代码

9.2 使用WebView的点击监听

mWebView.setOnLongClickListener(new View.OnLongClickListener() {
    @Override
    public boolean onLongClick(View v) {
        WebView.HitTestResult result = ((WebView)v).getHitTestResult();
        if (null == result)
            return false;
        int type = result.getType();
        if (type == WebView.HitTestResult.UNKNOWN_TYPE)
            return false;
        // 这里可以拦截很多类型,我们只处理图片类型就可以了
        switch (type) {
            case WebView.HitTestResult.PHONE_TYPE: // 处理拨号
                break;
            case WebView.HitTestResult.EMAIL_TYPE: // 处理Email
                break;
            case WebView.HitTestResult.GEO_TYPE: // 地图类型
                break;
            case WebView.HitTestResult.SRC_ANCHOR_TYPE: // 超链接
                break;
            case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
                break;
            case WebView.HitTestResult.IMAGE_TYPE: // 处理长按图片的菜单项
                // 获取图片的路径
                String saveImgUrl = result.getExtra();
                // 跳转到图片详情页,显示图片
                Intent i = new Intent(MainActivity.this, ImageActivity.class);
                i.putExtra("imgUrl", saveImgUrl);
                startActivity(i);
                break;
            default:
                break;
        }
    }
});
复制代码

总结

其实WebView的细节还是蛮多的,我已经尽量缩减了,但是不知不觉都这么长了,基本的使用应是差不多了。

当然 WebView 还能继续优化,比如使用模板,后端直出等等,如果需要更进一步优化启动速度,还需要前端、后端和我们移动端的配合了,单独我们移动端能优化的点就以上这些了。

本文的部分代码有一些是思路之类的,代码不全,大家可以自行实现,比较基本的封装代码都已经在本文贴出了,大家可以自取。


作者:newki
链接:https://juejin.cn/post/7125680139551113252
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

原网站

版权声明
本文为[锐湃]所创,转载请带上原文链接,感谢
https://blog.csdn.net/chuyouyinghe/article/details/126260613