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左右。


image.png

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


image.png

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


image.png

如果大家有兴趣,也可以自行测试,如果每次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的互调

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

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

<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的图片选择了。效果如下:

image.png

八、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;
        }
    }
});
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容