这是你们项目中WebView的样子吗?
作者简介:Serpit,Android开发工程师,2023年加入37手游技术部,目前负责国内游戏发行 Android SDK 开发。
前言
开始前先问大家一个问题,你们项目中或者理想中的WebView的使用姿势是如何的?有哪些规范、功能?都可以在下方评论中说出来大家讨论一下。下面正式开始介绍我们对于一个WebView使用的一些理解。
可监控
可监控是线上项目很重要的一个功能,监控的东西可以是用户体验相关数据、加载情况数据、报错等。那么,如何监控呢?这里分两点提下。
加载时间
利用WebViewClient的onPageStarted
和onPageFinished
回调,但是注意,这两回调在某些情况下会回调多次,比如在发生重定向时候,会有多次的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等一系列回调,诸如onReceivedError
、onReceivedHttpError
和onReceivedSslError
这几个。只要在这几个方法中加上相应的埋点日志即可。但同时有注意的点是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不可以设置多个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);
}
@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 {
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();
}
@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的优秀思路或技巧,大家共同进步~
作者:37手游移动客户端团队
来源:juejin.cn/post/7316202809383321609