WebView套壳实战

前言

GitHub
市面上大多理论只是的讲解,在此我不在过多的话语,我更希望的是直接按照实战来给大家伙讲解我在使用WebVIew作为壳嵌套WebApp的一些经验之谈。

WebView的初级使用

WebView对象的创建

		if (webView == null) {
            	webView = new WebView(context);
           }
        LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        webView.setLayoutParams(layoutParams);
        dynamicContainer.addView(webView);

没有直接把WebView放在布局中是为了防止内存泄漏,下文想详细讲解这一块。

WebSettings我们需要设置些什么

WebSettings是什么

WebSettings是WebView管理设置状态的类,WebVIew初始化后会创建WebSettings设置一些默认的状态,我们需要修改需要通过WebView.getSettings()获取到;如果Web View被干掉之后还继续调用WebSettings会导致崩溃。

WebSettings有那些需要关注的项目

  1. WebSettings.setBuiltInZoomControls: 支持内置缩放机制,默认为false 官方推荐设置为true
  2. WebSettings.setUserAgentString (String userAgent):设置用户代理为了不影响默认的用户代理,建议取出来用“;”隔开添加
  3. WebSettings.setUseWideViewPort:设置WebView是否支持meta标签,true 为支持,因为前端适配各种设备需要使用这个标签,所以建议打开
  4. WebSettings.setSupportMultipleWindows:设置是否支持多屏应用,默认为false,建议不适配多屏多不开启
  5. WebSettings.setLoadWithOverviewMode:是否启用概览模式,即缩小内容宽度以适应屏幕
  6. WebSettings.setJavaScriptEnabled:启用JS支持,这个必须设置True,否则无法使用JS
  7. WebSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN):设置底层布局算法,建议使用WebSettings.LayoutAlgorithm.SINGLE_COLUMN为自适应屏幕,默认为WebSettings.LayoutAlgorithm.NORMAL不做任何渲染处理
  8. WebSettings.setSupportZoom:是否支持手势屏幕的缩放,建议设置为false
  9. WebSettings.setDomStorageEnabled:开启DomStorage缓存
  10. WebSettings.setCacheMode(WebSettings.LOAD_DEFAULT):设置缓存类型,推荐使用 WebSettings.LOAD_DEFAULT,前端通过cache-control控制缓存时间。

代码例子为:

		WebSettings webSettings = webView.getSettings();
        if (Build.VERSION.SDK_INT >= 19) {
            webSettings.setLoadsImagesAutomatically(true);//图片自动缩放 打开
        } else {
            webSettings.setLoadsImagesAutomatically(false);//图片自动缩放 关闭
        }
        webSettings.setDefaultTextEncodingName("utf-8");
        webSettings.setBuiltInZoomControls(true);
        String ua = webSettings.getUserAgentString();
        webSettings.setUserAgentString(ua + ";webview");
        webSettings.setUseWideViewPort(true);
        webSettings.setLoadWithOverviewMode(true);
        webSettings.setJavaScriptEnabled(true);
        webSettings.setBuiltInZoomControls(true);
        webSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);//自适应屏幕
        webSettings.setSupportZoom(false);
        webSettings.supportMultipleWindows();
        webSettings.setAllowFileAccess(true);
        webSettings.setNeedInitialFocus(true);
        webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
        //开启DOM stoare
        webSettings.setDomStorageEnabled(true);
        //为了迎合浏览器缓存 设置有缓存 缓存本地的、无缓存访问网络的缓存模式
        webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);``

基础WebViewClient我们需要关注那些问题

WebViewClient是WebVIew用于监听WebView加载URL 的过程,比如开启加载url、加载url过程中、结束加载url等过程.

WebViewClient正确使用姿势

  • 创建继承WebViewClient的派生类
  • webview.setWebViewClient(webviewclient)

WebVIewClient相关类

  • public void onPageStarted(WebView view, String url, Bitmap favicon):页面开始加载监听,其中view为设置WebViewClient对应的WebView,url为加载的资源路径,favicon是前端设置的图标
  • public boolean shouldOverrideUrlLoading(WebView view, String url):判断在手机APP内打开网页的方式,系统默认返回false,跳转到手机浏览器,返回true则不触发后续跳转到手机浏览器的方式
  • public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request):API大于21
  • public void onPageFinished(WebView view, String url) :为页面加载完毕回调
  • public void onReceivedError(WebView view, int errorCode, String description, String failingUrl):前端获取资源或者加载Url报错的时候回调,适用于少于API23的设备,其中errorCode为错误码,description为错误描述,failingUrl为访问失败的URL
  • public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error):出错时回调,适用于API大于23的设备
  • doUpdateVisitedHistory:用于清理清空历史栈
	public static final String DISCONNECT_ERROR_MSG = "net::ERR_NAME_NOT_RESOLVED";
    public static final String DISCONNECT_EMPTY_ERROR_MSG = "net::ERR_EMPTY_RESPONSE";
    //兼容OPPO 8.0超时问题
    public static final String DISCONNECT_CONNECTION_TIMED_OUT_MSG = "net::ERR_CONNECTION_TIMED_OUT";
    //华为超时兼容
    public static final String HUIWEI_TIMED_OUT = "net::ERR_TIMED_OUT";
	private WebViewClient webViewClient = new WebViewClient() {
        private boolean isLoadingHttp = false;

        @Override
        public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) {
             super.doUpdateVisitedHistory(view, url, isReload);
        }

        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            return super.shouldOverrideUrlLoading(view, url);
        }



        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
            return super.shouldOverrideUrlLoading(view, request);
        }

        @Override
        public void onPageStarted(WebView view, String url, Bitmap favicon) {
            super.onPageStarted(view, url, favicon);
            loadingLayout.setVisibility(View.VISIBLE);
        }

        @Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            loadingLayout.setVisibility(View.GONE);
        }

        @Override
        public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
            super.onReceivedError(view, request, error);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                if (!android.text.TextUtils.isEmpty(request.getUrl().getPath())) {
                    noNetHandle(request.getUrl().getPath(), error.getDescription().toString(), 100);
                }

            }
        }

        private void noNetHandle(String url, String des, int errorCode) {

            if (url == null) {
                return;
            }

            boolean isExistingQuery = url.contains("?");
            int startIndex = url.lastIndexOf("/") + 1;
            int endIndex = url.length();
            String endFile;
            if (isExistingQuery) {
                endIndex = url.lastIndexOf("?");
            }
            endFile = url.substring(startIndex, endIndex);
            if (!endFile.contains(".") || "html".equalsIgnoreCase(endFile.substring(endFile.indexOf(".") + 1)) || isLoadingHttp) {

                if (isLoadingHttp) {
                    isLoadingHttp = false;
                }

                if (DISCONNECT_ERROR_MSG.equalsIgnoreCase(des)
                        || DISCONNECT_EMPTY_ERROR_MSG.equalsIgnoreCase(des)
                        || DISCONNECT_CONNECTION_TIMED_OUT_MSG.equalsIgnoreCase(des)
                        || HUIWEI_TIMED_OUT.equalsIgnoreCase(des)
                        || errorCode == ERROR_HOST_LOOKUP
                        || errorCode == ERROR_CONNECT
                        || errorCode == ERROR_TIMEOUT) {
                    netError();
                }
            }
        }

        @Override
        public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
            super.onReceivedError(view, errorCode, description, failingUrl);
            if (!android.text.TextUtils.isEmpty(failingUrl)) {
                noNetHandle(failingUrl, description, errorCode);
            }

        }


    };

基础WebChromeClient我们需要关注那些问题

WebChromeClient提供了拦截页面加载进度、JS提示框、确认框、警告框、标题等功能。
onJsPrompt、onJsAlert、onJsConfirm分别表示拦截JS的提示框、警告框和确认框,一般不做任何处理。

  • onProgressChanged(WebView view, int newProgress):回调页面加载网页的进度条进度。

加载远程Url

	String url="https://www.hao123.com/";
	webView.loadUrl(url );

加载本地的Url

	webView.loadUrl("file:///android_asset/index.html");

使用WebView关注的一些问题

如何避免WebView内存泄漏

  • 采用New的方式创建WebView而不是通过XML
  • 显示调用WebView.destroy()

WebView是Android很容易导致内存泄漏的组件,究其原理主要是因为以下方面导致的一些问题:

  • WebView注册的事件没有反注册导致一些问题使得WebView引用无法释放,Activity 也没有办法释放

采用New的方式创建WebView而不是通过XML

		if (webView == null)  {
            webView = new WebView(context);
        }
        LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        webView.setLayoutParams(layoutParams);
        dynamicContainer.addView(webView);

显示调用WebView.destroy()

		if( webView!=null) {

            // 如果先调用destroy()方法,则会命中if (isDestroyed()) return;这一行代码,需要先onDetachedFromWindow(),再
            // destory()
            ViewParent parent = webView.getParent();
            if (parent != null) {
                ((ViewGroup) parent).removeView(webView);
            }

            webView.stopLoading();
            // 退出时调用此方法,移除绑定的服务,否则某些特定系统会报错
            webView.getSettings().setJavaScriptEnabled(false);
            webView.clearHistory();
            webView.clearView();
            webView.removeAllViews();
            webView.destroy();
            webView=null;
        }

我们应当如何选择较好的JS和Android通信的方案

因为前端有很多硬件功能是无法直接调用实现的,所以有部分功能需要JS发送指令给Android端,然后Android端调用原生API,最后Android完成后发送结果信息给JS呈现,JS和Android 通信是WebView壳最重要的部分,主要根据两个方向整理

JS调用Android的方法

Js调用Android的方式有如下几个方法:

  • 拦截shouldOverrideUrlLoading方法
  • 拦截alert、prompt、confirm处理方法
  • addJavascriptInterface注册Android对象供H5调用
拦截shouldOverrideUrlLoading方法

H5进行window.location.href调用对应的url,会调用WebViewClient中的这个方法,所以我们可以规定相应的协议进行处理,一般我的Scheme格式如下:{公司缩写}?/na.api/{功能块}?action={具体行为}&param={参数} ,
比如:lyc://na.api/test_mobile_func?item=xxx&param=xxx
定义好协议后,我们来看看处理shouldOverrideUrlLoading拦截的办法:

public boolean shouldOverrideUrlLoading(WebView view, String url) {
            Log.e("cloudy", "shouldOverrideUrlLoading, url: " + url);
            if (判断Scheme头是否是我们的协议头) {
            	//解析出功能块和具体参数,做对象的操作
                return true;
            } 
        }
拦截alert、prompt、confirm处理方法

和拦截shouldOverrideUrlLoading方法类似,在WebChromeClient中回调,这里不在说明使用方法。

addJavascriptInterface注册Android对象供H5调用

WebView可以通过对Web页面注入一个全局对象的方式,对H5进行注入方法,让H5直接调用
WebView注入对象:

        webView.addJavascriptInterface(new JavaScriptInterface(), "JSAndroid");

JavaScriptInterface中可以直接编写方法:

public class JavaScriptInterface {

    @android.webkit.JavascriptInterface
    public boolean test(String msg) {
        Toast.makeText(AppManager.getApp(), "JS Callback", Toast.LENGTH_SHORT).show();
        return isCheckOk;
    }
}

H5想要调用原生的test方法时,可以这样调用:

window.JSAndroid.test(“我是H5来的提示”)

尽管这个方法很简单,同时也可以设置很好的扩展性,但是Android4.2以前这是存在安全漏洞的,JS拿到JSAndroid对象后,很容易侵入一些异常操作,So我们需要解决这个问题:

  • 因为在低于API17的WebView上默认添加"searchBoxJavaBridge_"到JavaScriptObjects中使用,removeJavascriptInterface(“searchBoxJavaBridge_”)移除searchBoxJavaBridge_对象。
  • 对方法的参数进行验证处理
  • 使用shouldOverrideUrlLoading替代方案

Android给JS反馈结果回调方法

Android 调用JS的方法有两种方法:

  • 通过WebView.loadUrl(“javascript:方法名(‘参数’)”)
  • Android4.4以上还可以webview.evaluateJavascript(js,callBack),js为"javascript:方法名(‘参数’)",好处是能获取到H5的return

当我们使Webview.loadUrl的时候,我们肯定不会直接这样使用,这样太麻烦了,我们可以做类似这样的封装:

其中,callback为调用的js方法,query、value做为返回值传回去

public void setCheckResult(String query, String value, String callback) {
        try {
            JSONObject json = new JSONObject();
            json.put(query, value);
           if (entryWebView != null) {
                entryWebView.loadUrl("javascript:" + callback + "('" + 功能标示 + "'," + json.toString() + ")");
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

WebView使用的维护Cookie的算法讲解

WebView通过对Cookie写入文件来对Cookie进行持久化缓存,当WebView检测到服务端反馈的Cookie字段后会显示到缓存头中,在前端调试工具我们也能具体的看到这些字段,然后WebView会把字段缓存到 datd/data/{包名}/app_webview/中到Cookies文件中,这个过程是异步的,所以有时候我们会发现,当我们杀死APP进程后,再开启有几率会无法获取到之前最新的Cookie值,这就是因为Cookie异步写入Cookies文件持久化这个步骤被中断了,假如我们想要确保Cookie正确更新,应该使用如下代码:

			CookieManager cookieManager = CookieManager.getInstance();
            String cookiesStr = cookieManager.getCookie(url);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                cookieManager.flush();
            }

缓存策略的我们应该如何选择

WebView缓存策略需要开启有如下几个方面:

  • 浏览器支持的缓存策略,使用webSettings.setCacheMode(缓存类型)开启,前端再配置Cache-Control和Last-Modified即可,也叫协议缓存
  • Application Cache 缓存机制
  • Dom Storage 缓存机制
  • Indexed Database 缓存机制
  • Web SQL Database 缓存机制

根据业务修改开启各类缓存,但是个人建议除了使用浏览器缓存外,其他一般不使用,这里就不讲太多,只详细讲解浏览器缓存。

浏览器缓存

浏览器缓存是浏览器自带的一种缓存机制,因为默认情况下WebView的CacheMode为WebSettings.LOAD_DEFAULT,已经是我们最优解,所以Android这边不用做任何配置,但是前端服务器需要相应的设置两个字段:

  • Cache-Control:用于控制文件在本地缓存有效时长。
  • Last-Modified:标识文件在服务器上的最新更新时间

该模式下应用场景:
缓存静态文件,比如图片、CSS、JS、字体等

优点:

  • 支持 Http协议层

缺点:

  • 缓存文件需要首次加载后才会产生
  • 浏览器缓存的存储空间有限,缓存有被清除的可能
  • 缓存的文件没有校验

深入学习WebViewClient

深入学习WebChromeClient

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页