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有那些需要关注的项目
- WebSettings.setBuiltInZoomControls: 支持内置缩放机制,默认为false 官方推荐设置为true
- WebSettings.setUserAgentString (String userAgent):设置用户代理为了不影响默认的用户代理,建议取出来用“;”隔开添加
- WebSettings.setUseWideViewPort:设置WebView是否支持meta标签,true 为支持,因为前端适配各种设备需要使用这个标签,所以建议打开
- WebSettings.setSupportMultipleWindows:设置是否支持多屏应用,默认为false,建议不适配多屏多不开启
- WebSettings.setLoadWithOverviewMode:是否启用概览模式,即缩小内容宽度以适应屏幕
- WebSettings.setJavaScriptEnabled:启用JS支持,这个必须设置True,否则无法使用JS
- WebSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN):设置底层布局算法,建议使用WebSettings.LayoutAlgorithm.SINGLE_COLUMN为自适应屏幕,默认为WebSettings.LayoutAlgorithm.NORMAL不做任何渲染处理
- WebSettings.setSupportZoom:是否支持手势屏幕的缩放,建议设置为false
- WebSettings.setDomStorageEnabled:开启DomStorage缓存
- 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={具体行为}¶m={参数} ,
比如:lyc://na.api/test_mobile_func?item=xxx¶m=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协议层
缺点:
- 缓存文件需要首次加载后才会产生
- 浏览器缓存的存储空间有限,缓存有被清除的可能
- 缓存的文件没有校验