Hybrid开发之WebView使用方法及注意事项
工作这么长时间,细细想来有很长时间都在与WebView打交道。在Hybrid App的开发中,积累了一定的经验,在此做一个简单的工作总结。
Hybrid开发中最常用的组件就是WebView了。WebView不仅用来展示Web页面,更是Web页面和安卓手机Native之间沟通的桥梁。但由于历史原因,android不同版本之间WebView不同,存在一些兼容性问题。
注:鉴于在市场上Android 4.0及以后的系统占90%之上,较多的开源工程的minSdkVersion为14。本人参与开发的app也只关注Android 4.0及以上系统的兼容性。
WebView
在Android 4.4系统之前,WebView一直采用WebKit内核;而在Android 4.4及以后google采用了chromium内核。二者的API变化不大,但是在某些场景下的表现有些微差异。整体来说chromium内核更为高效,支持V8引擎解析Javascript。更重要的是Chromium支持远程调试。
开发中web页面可使用console.log打印控制台日志。此时可通过Android Studio的Logcat查看到打印信息。在4.4系统之前,WebView相关日志tag是webkit;而在4.4及之后,tag是chromium。日志中包括Javascript运行中打印的日志,错误信息等,有助于分析Hybrid开发中遇到的问题。
I/chromium: [INFO:CONSOLE(1)] "The key "target-densitydpi" is not supported.", source: file:///data/user/0/com.tfzq.gcs.dev/files/www/m_tf/trade/indexTota.js?v=0.4672763997119571 (1)
创建WebView包括两种方法,一是在Xml中配置;二是直接使用new动态创建。推荐使用第二种方式进行开发。WebView可以使用loadUrl加载本地或线上的Web页面,执行javascript语句;也可以使用loadData直接加载html数据。另一方法loadDataWithBaseUrl在加载页面中有本地图片时可以使用。
使用WebView之前需要通过WebSettings进行一定的配置。
WebSettings settings = getSettings();
//默认是false 设置true允许和js交互
settings.setJavaScriptEnabled(true);
// WebSettings.LOAD_DEFAULT 如果本地缓存可用且没有过期则使用本地缓存,否加载网络数据 默认值
// WebSettings.LOAD_CACHE_ELSE_NETWORK 优先加载本地缓存数据,无论缓存是否过期
// WebSettings.LOAD_NO_CACHE 只加载网络数据,不加载本地缓存
// WebSettings.LOAD_CACHE_ONLY 只加载缓存数据,不加载网络数据
//Tips:有网络可以使用LOAD_DEFAULT 没有网时用LOAD_CACHE_ELSE_NETWORK
settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
//开启 DOM storage API 功能 较大存储空间,使用简单
settings.setDomStorageEnabled(true);
//设置数据库缓存路径 存储管理复杂数据 方便对数据进行增加、删除、修改、查询 不推荐使用
settings.setDatabaseEnabled(true);
final String dbPath = context.getApplicationContext().getDir("db", Context.MODE_PRIVATE).getPath();
settings.setDatabasePath(dbPath);
//开启 Application Caches 功能 方便构建离线APP 不推荐使用
settings.setAppCacheEnabled(true);
final String cachePath = context.getApplicationContext().getDir("cache", Context.MODE_PRIVATE).getPath();
settings.setAppCachePath(cachePath);
settings.setAppCacheMaxSize(5 * 1024 * 1024);
WebChromeClient
WebChromeClient常用以下几个回调方法
-
WebChromeClient. onProgressChanged
页面加载进度回调,progress从0-100。可用来实现自定义加载进度条。 -
WebChromeClient. onReceivedTitle
可用来接收当前页面的标题,实现本地标题栏的变化。 -
WebChromeClient.onJsPrompt
可用来实现自定义的弹窗,也可用来实现安全的JSBridge。phonegap混合开发框架即使用onJsPrompt实现在Android 4.2以下系统的安全性。类似的方法还有onJsConfirm,onJsAlert。
在开发中遇到过界面相关问题,恍惚记得WebView使用chromium内核之后,onJsPrompt方法运行线程不在是主线程导致。年代久远,有兴趣的读者可自行验证。
WebViewClient
WebViewClient常用以下几个回调方法。
-
WebViewClient.shouldOverrideUrlLoading
在WebView加载Url前调用,app可拦截该方法来自己处理本次url的加载。该方法返回true代表app自己处理url;返回false代表WebView处理url。该方法可配合自定义的协议头来区分url是事件或普通的web连接。JSBridge框架主要依赖此方法实现JS事件的回调。 -
WebViewClient.shouldInterceptRequest
可拦截WebView对页面中资源的加载,使用本地资源代替。曾经在一项目中用该方法实现本地缓存。 -
WebViewClient.onPageStarted
该方法表示页面开始加载,理论上是只调用一次。但是在WebKit内核开发时,遇到过调用次数超过一次的情况。 -
WebViewClient.onReceivedError
页面加载出现错误时调用该方法。可用来定制错误页面。 -
WebViewClient.onPageFinished
该方法一般用来处理页面加载完成时的一些操作。比如注入JSBridge框架代码建立JS-Native通信通道。但是在我早期的开发经验中,4.*的系统上低概率出现onPageFinished方法未回调的问题。在开发测试中需要关注。Web页面中很容易有一些自动跳转逻辑,这时onPageFinished会被调用多次,当然url的参数不同。可参考How to listen for a Webview finishing loading a URL in Android? -
WebViewClient
Web与Native通信
关于Web与Native之间的通信,可参考我的文章。介绍了基本的通信方法,及开源库JsBridge的使用。
JSbridge系列解析(一):JS-Native调用方法
JSbridge系列解析(二):lzyzsd/JsBridge使用方法
JSbridge系列解析(三):lzyzsd/JsBridge源码解析
JSbridge系列解析(四):Web端发消息给Native代码流程具体分析
WebView内存泄漏
网上搜索Webview,可以说最多的就是内存泄漏相关的介绍,如说某篇文章中出现的下段。文章指出不要在xml中直接使用webview,否则可能出现webview所在activity的内存泄漏。
webview的创建也是有技巧的,最好不要在layout.xml中使用webview,可以通过一个viewgroup容器,使用代码动态往容器里addview(webview),这样可以在onDestory()里销毁掉webview及时清理内存,另外需要注意创建webview需要使用applicationContext而不是activity的context,销毁时不再占有activity对象,这个大家应该都知道了,最后离开的时候需要及时销毁webview,onDestory()中应该先从viewgroup中remove掉webview,再调用webview.removeAllViews();webview.destory();
最初做hybrid相关开发时,4.4及以上系统的手机较少,确实存在webview.destroy()后无法释放内存的问题。但现在6.0系统已经成了主流,为了进一步验证,我用红米Note 4,Android 6.0系统进行了测试。
代码如下,webview直接定义在xml文件中,且onDestroy中没有针对webview的destroy操作。通过模拟器验证Android 5.0表现与6.0一致,back退出后内存顺利回收;但4.4系统就会出现内存泄漏。
public class WebViewTestActivity extends Activity {
private WebView webview;
@Override
public void onCreate(Bundle saveInstance){
super.onCreate(saveInstance);
setContentView(R.layout.activity_webview_test);
webview = findViewById(R.id.webview);
webview.loadUrl("https://www.baidu.com");
//部分页面,如百度主页,如果不设置setJavaScriptEnabled为true,则显示白屏
WebSettings webSettings = webview.getSettings();
webSettings.setJavaScriptEnabled(true);
}
@Override
public void onDestroy(){
super.onDestroy();
}
}
多次进入主界面后按Back键退出,通过执行adb shell dumpsys meminfo 包名命令,监控发现WebView及对应Activity的资源释放了。这也说明google官方做了一定的修复。但为了兼容低版本,仍建议通过ViewGroup容器动态添加WebView,使用完成后进行清理操作。
public class WebViewTestActivity extends Activity {
private WebView webview;
private FrameLayout webviewContainer;
@Override
public void onCreate(Bundle saveInstance){
super.onCreate(saveInstance);
setContentView(R.layout.activity_webview_test);
webviewContainer = findViewById(R.id.webview_container);
webview = new WebView(this);
webviewContainer.addView(webview);
webview.loadUrl("https://www.baidu.com");
//部分页面,如百度主页,如果不设置setJavaScriptEnabled为true,则显示白屏
WebSettings webSettings = webview.getSettings();
webSettings.setJavaScriptEnabled(true);
}
@Override
public void onDestroy(){
super.onDestroy();
if(webview != null){
ViewGroup parent = (ViewGroup)webview.getParent();
if (parent != null){
parent.removeView(webview);
}
webview.removeAllViews();
webview.destroy();
}
}
}
4.4的模拟器使用上面改进的代码,但仍出现内存泄漏,不过是InputMethodManager泄漏引起。后面有时间再研究这个。
网上另一种解决内存泄漏的方法,是在创建WebView时使用Application的Context。但是如果Web页面中出现Video时,会出现如果崩溃。Web页面弹框的场景未测试。
W/System.err: android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?
W/System.err: at android.app.ContextImpl.startActivity(ContextImpl.java:1034)
W/System.err: at android.app.ContextImpl.startActivity(ContextImpl.java:1021)
W/System.err: at android.content.ContextWrapper.startActivity(ContextWrapper.java:311)
W/System.err: at com.android.webview.chromium.WebViewContentsClientAdapter$NullWebViewClient.shouldOverrideUrlLoading(WebViewContentsClientAdapter.java:196)
W/System.err: at com.android.webview.chromium.WebViewContentsClientAdapter.shouldOverrideUrlLoading(WebViewContentsClientAdapter.java:293)
W/System.err: at com.android.org.chromium.android_webview.AwContentsClientBridge.shouldOverrideUrlLoading(AwContentsClientBridge.java:96)
W/System.err: at com.android.org.chromium.base.SystemMessageHandler.nativeDoRunLoopOnce(Native Method)
W/System.err: at com.android.org.chromium.base.SystemMessageHandler.handleMessage(SystemMessageHandler.java:27)
W/System.err: at android.os.Handler.dispatchMessage(Handler.java:102)
W/System.err: at android.os.Looper.loop(Looper.java:136)
W/System.err: at android.app.ActivityThread.main(ActivityThread.java:5017)
W/System.err: at java.lang.reflect.Method.invokeNative(Native Method)
W/System.err: at java.lang.reflect.Method.invoke(Method.java:515)
W/System.err: at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:779)
W/System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:595)
W/System.err: at dalvik.system.NativeStart.main(Native Method)
A/libc: Fatal signal 6 (SIGABRT) at 0x0000084d (code=-6), thread 2125 (n.myapplication)
WebView如果执行destory操作,则后续不能再进行loadUrl的操作,否则会出现白屏。这一点在webView的复用时需要考虑。同时WebView复用时,
实际测试WebView复用的场景,将WebView置为static变量,下列代码仅有示范的作用,不具实际的意义。目前我仍未想到WebView复用的意义所在。注意在界面销毁时需要将WebView从父布局中remove,避免持有父布局引用导致当前界面的内存泄漏。但是仍无办法解决WebView创建时持有的Activity的内存泄漏。最初我怀疑用复用的WebVIew播放视频会出现问题,毕竟其持有的Context已不可见,但实际测试运行视频OK。
public class WebViewTestActivity extends Activity {
private static WebView webview;
private FrameLayout webviewContainer;
@Override
public void onCreate(Bundle saveInstance){
super.onCreate(saveInstance);
setContentView(R.layout.activity_webview_test);
webviewContainer = findViewById(R.id.webview_container);
if (webview == null) {
webview = new WebView(this);
}
webviewContainer.addView(webview);
webview.loadUrl("https://www.baidu.com");
//部分页面,如百度主页,如果不设置setJavaScriptEnabled为true,则显示白屏
WebSettings webSettings = webview.getSettings();
webSettings.setJavaScriptEnabled(true);
}
@Override
public void onDestroy(){
super.onDestroy();
if(webview != null){
ViewGroup parent = (ViewGroup)webview.getParent();
if (parent != null){
parent.removeView(webview);
}
webview.removeAllViews();
// webview.destroy();
}
}
}
关于内存泄漏的另一种解决方案,即将显示WebView的activity运行在另外的进程。这样在WebView界面关闭时将该进程直接kill,避免对主程序的影响。但是该方法对多WebView的程序不太适合,毕竟跨进程通信成本较大,容易出现各种问题。该方案只适应一定场景,了解即可。
参考:
Android Webview的一些使用总结和遇到过得坑
Android:你不知道的 WebView 使用漏洞
Android 各个版本WebView
安卓webview的一些坑
H5 缓存机制浅析 移动端 Web 加载性能优化