这是你们项目中WebView的样子吗?

这是你们项目中WebView的样子吗?

这是你们项目中WebView的姿态吗?

作者简介:Serpit,Android开发工程师,2023年参加37手游技术部,目前担任国内游戏发行 Android SDK 开发。

前言

开端前先问咱们一个问题,你们项目中或许抱负中的WebView的运用姿态是怎么的?有哪些规范、功用?都能够在下方谈论中说出来咱们讨论一下。下面正式开端介绍咱们对于一个WebView运用的一些理解。

可监控

可监控是线上项目很重要的一个功用,监控的东西能够是用户体会相关数据、加载状况数据、报错等。那么,怎么监控呢?这儿分两点提下。

加载时间

运用WebViewClient的onPageStartedonPageFinished回调,可是留意,这两回调在某些状况下会回调多次,比方在发生重定向时分,会有多次的onPageStarted回调呈现,这就需求用一些符号位或许阻拦等手法来保证计算到最开端的onPageStarted和最终的onPageFinished之间的耗时。

这儿贴上一段伪代码

 @Override
    public void onPageStarted(WebView webView, String url, Bitmap favicon) {
        super.onPageStarted(webView, url, favicon);
        if (TextUtils.isEmpty(url)) {
            return;
        }
        consumeMap.put(getKey(url), SystemClock.uptimeMillis());
    }
    @Override
    public void onPageFinished(WebView webView, String url) {
        super.onPageFinished(webView, url);
        if (TextUtils.isEmpty(url)) {
            return;
        }
        Long loadConsuming = consumeMap.remove(getKey(url));
        if (loadConsuming == null) {
            return;
        }
        trackWebLoadFinish(url, SystemClock.uptimeMillis() - loadConsuming); //记载耗时,埋点
    }

报错监控

报错这一块能够运用WebViewClient等一系列回调,比方onReceivedErroronReceivedHttpErroronReceivedSslError这几个。只要在这几个办法中加上相应的埋点日志即可。但同时有留意的点是onReceivedError这个办法会有些报错是无用的,不影响用户运用,需求进行过滤。这儿能够参考网上的一些办法做以下处理

  • 加载失利的url跟WebView里的url不是同一个url,过滤
  • errorCode=-1,标明是ERROR_UNKNOWN的错误,为了保证不误判,过滤
  • failingUrl=null&errorCode=-12,由于错误的url是空而不是ERROR_BAD_URL,过滤

除了这些常规的,还有一个是运用onConsoleMessage,去监控前端的错误日志,发现到前端的一些报错~也是一个不错的方向。

与前端的交互

与前端的交互,这个方式信任在网上随意一搜“Android与JS彼此通讯”等要害词,就能够搜出一大堆。在Android侧需求注册一个JavascriptInterface,里边界说各个办法,然后前端就能够调用到。然后需求调用到前端的函数,则运用WebView的evaluateJavascript办法即可。这些都比较根底,这儿就不展开说,不清楚的同学能够用搜索引擎查找一下~

这儿想说的点其实是,在项目中如果简略界说JavascriptInterface或许会有“危险”。想象一个场景,界说了一个getToken办法,给到前端获取token办法,用于获取用户信息,这是一个很常见的办法。可是,重点来了,假如应用加载了一个外部的“恶意网站”,调用了这个getToken办法,就造成了信息走漏。在安全上就呈现了问题。

那么,有什么思路能够限制一下呢?有同学或许会想到,“混杂”把接口办法换成一些奇奇怪怪的办法,但这也太不利于代码可读性了吧。那这儿能够提供一个思路,便是“鉴权阻拦”。怎么做?

先上代码

private class WebViewJsInterface {
    @JavascriptInterface
    public void callAndroid(final String method, final String params) {
        boolean result = intercept(method, params); //阻拦办法
        if (!result){
            dispatcher.callAndroid(method, params);
        }
    }
}

这儿想说的是,能够在项目里边运用统一调度的方式,前端调用安卓的入口只有一个,然后经过传入办法名来分发到不同办法上,这样的优点是,能够做到统一,统一的意图也是为了做阻拦!在阻拦上,咱们就能够做许多文章,比方域名校验,白名单上的域名直接不让调用原生办法。这样能够添加必定的安全性。

关于WebView的一些运用封装思路

咱们知道WebView的灵魂其实有三个部分

  • WebView.getSetting()的设置
  • WebViewClient
  • WebChromeClient

咱们做的许多功用都是根据着这三者来完成,那么在代码中,许多项目或许同学都是直接封装一个BaseWebViewClient,然后在里边做一堆逻辑处理,比方上面说的监控办法,或许loading操作等。这么做没有说不好,可是会由于业务的日益拓宽,会使得这个“Base”日益臃肿,变得难以保护。在经历过这个阶段的咱们,也想出了一个办法去优化。那便是用阻拦器的思路,架构图如下:

这是你们项目中WebView的姿态吗?

这儿,由于WebView不能够设置多个Client,那么就运用阻拦器,将WebViewClient和WebChromeClient所有办法都封装起来,分发出去,每一个阻拦器都担任自己的功用即可。比方完成一个loading的逻辑:

public class ProgressWebHook extends WebHook {
    private final IWebViewLoading mWebViewLoading;
    public ProgressWebHook(IWebViewLoading loading) {
        this.mWebViewLoading = loading;
    }
    @Override
    public void onPageStarted(WebView webView, String url, Bitmap favicon) {
        super.onPageStarted(webView, url, favicon);
        startLoading();
    }
    @Override
    public void onPageFinished(WebView webView, String url) {
        super.onPageFinished(webView, url);
        stopLoading();
    }
    @Override
    public void onReceivedError(WebView webView, String url, int errorCode, String description) {
        super.onReceivedError(webView, url, errorCode, description);
        stopLoading();
    }
    @Override
    public void onProgressChanged(WebView webView, int newProgress) {
        super.onProgressChanged(webView, newProgress);
        mWebViewLoading.onProgress(getContext(), newProgress);
    }
}

这样是不是简洁许多。再比方监控的时分,也完成一个WebHook,只担任监控相关的办法重写即可。就能够将这些办法不同功用办法阻隔在不同的类中,便利解耦。至此,也会有同学想看下怎么完成的阻拦器,那这儿也简略给咱们看下。


public class BaseWebChromeClient extends WebChromeClient {
    private final WebHookDispatcher mWebHookDispatcher;
    public BaseWebChromeClient(WebHookDispatcher webHookDispatcher) {
        this.mWebHookDispatcher = webHookDispatcher;
    }
    @Override
    public void onPermissionRequest(PermissionRequest request) {
        mWebHookDispatcher.onPermissionRequest(request);
    }
    @Override
    public void onReceivedTitle(WebView view, String title) {
        super.onReceivedTitle(view, title);
        mWebHookDispatcher.onReceivedTitle(view, title);
    }
    @Override
    public void onProgressChanged(WebView view, int newProgress) {
        super.onProgressChanged(view, newProgress);
        mWebHookDispatcher.onProgressChanged(view, newProgress);
    }
    // For Android >= 5.0
    @Override
    public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,
        FileChooserParams fileChooserParams) {
        return mWebHookDispatcher.onShowFileChooser(webView, filePathCallback, fileChooserParams);
    }
    @Override
    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
        mWebHookDispatcher.onConsoleMessage(consoleMessage);
        return super.onConsoleMessage(consoleMessage);
    }
    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
        if (mWebHookDispatcher.onJsAlert(view, url, message, result)) {
            return true;
        }
        return super.onJsAlert(view, url, message, result);
    }
    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        if (mWebHookDispatcher.onJsPrompt(view, url, message, defaultValue, result)) {
            return true;
        }
        return super.onJsPrompt(view, url, message, defaultValue, result);
    }
    @Override
    public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) {
        if (mWebHookDispatcher.onJsBeforeUnload(view, url, message, result)) {
            return true;
        }
        return super.onJsBeforeUnload(view, url, message, result);
    }
    @Override
    public void onShowCustomView(View view, CustomViewCallback callback) {
        super.onShowCustomView(view, callback);
        mWebHookDispatcher.onShowCustomView(view, callback);
    }
    @Override
    public void onHideCustomView() {
        super.onHideCustomView();
        mWebHookDispatcher.onHideCustomView();
    }
}

阻拦分发代码如下:


public class WebHookDispatcher extends SimpleWebHook {
    /**
     * 由于shouldInterceptRequest是一个异步的回调,所以这个类需求加锁
     */
    private final List<WebHook> webHooks = new CopyOnWriteArrayList<>();
    public void addWebHook(WebHook webHook) {
        webHooks.add(webHook);
        if (hasInit) {
            webHook.onWebInit(mWebView);
        }
    }
    public void addWebHooks(Collection<WebHook> webHooks) {
        this.webHooks.addAll(webHooks);
        if (hasInit) {
            for (WebHook webHook : webHooks) {
                webHook.onWebInit(mWebView);
            }
        }
    }
    public void addWebHook(int position, WebHook webHook) {
        webHooks.add(position, webHook);
        if (hasInit) {
            webHook.onWebInit(mWebView);
        }
    }
    public void addWebHooks(int position, Collection<WebHook> webHooks) {
        this.webHooks.addAll(position, webHooks);
        if (hasInit) {
            for (WebHook webHook : webHooks) {
                webHook.onWebInit(mWebView);
            }
        }
    }
    @Nullable
    public WebHook findWebHookByClass(Class<? extends WebHook> clazz) {
        for (WebHook webHook : webHooks) {
            if (webHook.getClass().equals(clazz)) {
                return webHook;
            }
        }
        return null;
    }
    public void removeWebHook(WebHook webHook) {
        webHooks.remove(webHook);
    }
    @NonNull
    public List<WebHook> getWebHooks() {
        return webHooks;
    }
    public void clear() {
        webHooks.clear();
    }
    //dispatch method ----------------
    @Override
    public boolean shouldOverrideUrlLoading(WebView webView, String url) {
        for (WebHook webHook : webHooks) {
            if (webHook.shouldOverrideUrlLoading(webView, url)) {
                return true;
            }
        }
        return super.shouldOverrideUrlLoading(webView, url);
    }
    @Override
    public void onPageFinished(WebView webView, String url) {
        for (WebHook webHook : webHooks) {
            webHook.onPageFinished(webView, url);
        }
    }
    @Override
    public void onReceivedTitle(WebView webView, String title) {
        for (WebHook webHook : webHooks) {
            webHook.onReceivedTitle(webView, title);
        }
    }
    @Override
    public void onProgressChanged(WebView webView, int newProgress) {
        for (WebHook webHook : webHooks) {
            webHook.onProgressChanged(webView, newProgress);
        }
    }
    @Override
    public void onPageStarted(WebView webView, String url, Bitmap favicon) {
        for (WebHook webHook : webHooks) {
            webHook.onPageStarted(webView, url, favicon);
        }
    }
    @Override
    public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,
        FileChooserParams fileChooserParams) {
        for (WebHook webHook : webHooks) {
            if (webHook.onShowFileChooser(webView, filePathCallback, fileChooserParams)) {
                return true;
            }
        }
        return false;
    }
    @Override
    public boolean onActivityResult(int requestCode, int resultCode, Intent intent) {
        for (WebHook webHook : webHooks) {
            if (webHook.onActivityResult(requestCode, resultCode, intent)) {
                return true;
            }
        }
        return false;
    }
    @Override
    public void onReceivedError(WebView webView, WebResourceRequest request, WebResourceError error) {
        for (WebHook webHook : webHooks) {
            webHook.onReceivedError(webView, request, error);
        }
    }
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
        for (WebHook webHook : webHooks) {
            WebResourceResponse response = webHook.shouldInterceptRequest(view, request);
            if (response != null) {
                return response;
            }
        }
        return super.shouldInterceptRequest(view, request);
    }
    @Override
    public boolean onBackPressed() {
        for (WebHook webHook : webHooks) {
            if (webHook.onBackPressed()) {
                return true;
            }
        }
        return super.onBackPressed();
    }
    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        for (WebHook webHook : webHooks) {
            if (webHook.onKeyUp(keyCode, event)) {
                return true;
            }
        }
        return super.onKeyUp(keyCode, event);
    }
    @Override
    public void onConsoleMessage(ConsoleMessage consoleMessage) {
        for (WebHook webHook : webHooks) {
            webHook.onConsoleMessage(consoleMessage);
        }
    }
    @Override
    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
        for (WebHook webHook : webHooks) {
            webHook.onReceivedSslError(view, handler, error);
        }
        super.onReceivedSslError(view, handler, error);
    }
    @Override
    public void onReceivedError(WebView webView, String url, int errorCode, String description) {
        for (WebHook webHook : webHooks) {
            webHook.onReceivedError(webView, url, errorCode, description);
        }
        super.onReceivedError(webView, url, errorCode, description);
    }
    @Override
    public void onPermissionRequest(PermissionRequest request) {
        super.onPermissionRequest(request);
        for (WebHook webHook : webHooks) {
            webHook.onPermissionRequest(request);
        }
    }
    // ...其他回调代码省掉

总结

上述介绍了一些日常项目中的WebView运用思路介绍,希望能够对一些小伙伴有作用。欢迎小伙伴们能谈论,发下你们项目中的WebView的优异思路或技巧,咱们共同进步~