这是你们项目中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);
}
// 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的优异思路或技巧,咱们共同进步~