概述
WebView是一个基于webkit引擎、展现web页面的控件。Android WebView 在低版本和高版本采用了不同的 webkit 版本内核,在 4.4 版本后使用 Chrome 内核。参考 WebView for Android。
WebView
参考官方说明:WebView
WebView的状态
1 | // 激活WebView为活跃状态,能正常执行网页的响应 |
前进/后退网页
1 | // 是否可以后退 |
在不做任何处理前提下,浏览网页时点击系统的“Back”键时,整个 Browser 会调用 finish()而结束自身,因此需要在当前Activity中处理并消费掉该 Back 事件,当按下返回键时,调用goBack方法。
清除缓存数据
1 | // 清除网页访问留下的缓存 |
WebSettings
作用:对WebView进行配置和管理。可以参考官方说明:WebSettings
添加访问网络权限: android.permission.INTERNET
注意:从Android 9.0(API级别28)开始,默认情况下禁用明文支持,会显示 ERR_CLEARTEXT_NOT_PERMITTED
。因此http的url均无法在webview中加载,可以在manifest中application节点添加android:usesCleartextTraffic="true"
。
配置 WebSettings 子类
1 | // 声明WebSettings子类 |
WebViewClient
用来处理各种通知 & 请求事件,具体使用参考 官方文档。
shouldOverrideUrlLoading
在网页上的所有加载都会经过这个方法,因此可以使打开网页时不调用系统浏览器,而是在本 WebView 展示。
1 | webView.setWebViewClient(new WebViewClient(){ |
onPageStarted
开始载入页面时调用,可以在这里显示 loading 页面。
onPageFinished
在页面加载结束时调用,可以关闭 loading 条等。
onLoadResource
在加载页面资源时调用,每一个资源(如图片)的加载都会调用一次。
onReceivedError
加载出现错误时(如404)调用,可以展示错误页面。
onReceivedHttpError
加载资源时从服务器收到HTTP错误时调用。
onReceivedSslError
加载资源时发生 SSL 错误,需要调用 SslErrorHandler#cancel(默认) 或 SslErrorHandler#proceed 处理。
WebChromeClient
辅助 WebView 处理 Javascript 的对话框,网站图标,网站标题等等,具体使用参考 官方文档。
onProgressChanged
获取网页的加载进度。
onReceivedTitle
获取网页标题。
对话框
处理网页中的对话框弹出。
1 | mWebView.setWebChromeClient(new WebChromeClient() { |
客户端调用JS
概述
注意:JS代码要在onPageFinished回调之后才能调用,可以直接使用基础类型传递参数,也可以使用JSONObject。
WebView.loadUrl()
准备html文件,放到assets中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<html>
<head>
<meta charset="utf-8">
<title>测试</title>
<script>
function callJS(){
alert("Android调用了JS的callJS方法");
}
</script>
</head>
<body>
<h1>标题</h1>
</body>
</html>加载调用。
1
2
3
4mWebView.loadUrl("file:///android_asset/index.html");
// after onPageFinished
mWebView.loadUrl("javascript:callJS()");由于设置了alert,所以需要支持js对话框,内容的渲染需要使用webviewChromClient类去实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17mWebView.setWebChromeClient(new WebChromeClient() {
public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
AlertDialog.Builder b = new AlertDialog.Builder(MainActivity.this);
b.setTitle("Alert");
b.setMessage(message);
b.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
result.confirm();
}
});
b.setCancelable(false);
b.create().show();
return true;
}
});
WebView.evaluateJavascript()
优点:该方法比第一种方法效率更高、使用更简洁。
- 因为该方法的执行不会使页面刷新,而第一种方法(loadUrl)的执行则会。
- Android 4.4 后才可使用。
具体使用:
1 | // 只需要将第一种方法的loadUrl()换成下面该方法即可 |
JS调用客户端
概述
可以直接使用基础类型传递参数,也可以使用JSONObject。
WebView.addJavascriptInterface()
- 优点:使用简单,仅将Android对象和JS对象映射即可。
使用步骤:
定义一个与JS对象映射关系的Android类:AndroidtoJs。
1
2
3
4
5
6
7
8public class AndroidtoJs {
// 被JS调用的方法必须加入@JavascriptInterface注解
public void hello(String msg) {
Log.d(TAG, msg);
}
}准备html文件,放到assets中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<html>
<head>
<meta charset="utf-8">
<title>Carson</title>
<script>
function callAndroid(){
// 由于对象映射,所以调用test对象等于调用Android映射的对象
test.hello("js调用了android中的hello方法");
}
</script>
</head>
<body>
//点击按钮则调用callAndroid函数
<button type="button" onclick="callAndroid()"></button>
</body>
</html>定义映射,调用方法。
1
mWebView.addJavascriptInterface(new AndroidtoJs(), "test");
WebViewClient.shouldOverrideUrlLoading()
- 优点:不存在方式1的漏洞。
- 缺点:JS获取Android方法的返回值复杂。如果JS想要得到Android方法的返回值,只能通过 WebView 的 loadUrl() 去执行 JS 方法把返回值传递回去。
使用步骤:
- Android通过 WebViewClient 的回调方法shouldOverrideUrlLoading ()拦截 url;
- 解析该 url 的协议;
- 如果检测到是预先约定好的协议,就调用相应方法,即JS需要调用Android的方法。
缓存机制
到目前为止,H5的缓存机制一共有六种,分别是:
- 浏览器缓存机制
- Dom Storgage(Web Storage)存储机制
- Web SQL Database存储机制
- Indexed Database(IndexedDB)
- Application Cache(AppCache)机制
- File System API
参考: https://juejin.cn/post/6844903934004297736
WebView设置缓存
常见用法:设置WebView缓存,缓存模式如下:
- LOAD_CACHE_ONLY: 不使用网络,只读取本地缓存数据
- LOAD_DEFAULT: (默认)根据cache-control决定是否从网络上取数据。
- LOAD_NO_CACHE: 不使用缓存,只从网络获取数据.
- LOAD_CACHE_ELSE_NETWORK,只要本地有,无论是否过期,或者no-cache,都使用缓存中的数据。
当加载 html 页面时,WebView会在/data/data/包名目录下生成 database 与 cache 两个文件夹。请求的 URL记录保存在 WebViewCache.db,而 URL的内容是保存在 WebViewCache 文件夹下。设置是否启用缓存:
1 | //优先使用缓存: |
离线加载用法:
1 | if (NetStatusUtil.isConnected(getApplicationContext())) { |
注意: 每个 Application 只调用一次 WebSettings.setAppCachePath(),WebSettings.setAppCacheMaxSize()
浏览器缓存机制
浏览器缓存机制主要是根据HTTP协议头里的Cache-Control
、Expires
、Last-Modified
以及Etag
请求头控制缓存。浏览器缓存主要用于静态资源文件的存储,Webview会将访问的文件记录及文件内容存在当前app的data目录中。WebView内置自动实现,使用默认的CacheMode就可以实现。
浏览器缓存的优势在于支持Http协议层。不足之处有:
- 需要首次加载之后才能产生缓存文件;
- 终端设备缓存的空间有限,缓存有可能会被清除;
- 缓存使用缺乏校验,有可能被篡改;
Application Cache缓存机制
AppCache的缓存机制类似于浏览器的缓存(Cache-Control和Last-Modified)机制,都是以文件为单位进行缓存,且文件有一定更新机制。但AppCache是对浏览器缓存机制的补充,不是替代。
AppCache有两个关键点:manifest属性和manifest文件。在头中通过manifest属性引用manifest文件。
浏览器在首次加载时,会解析manifest属性,并读取manifest文件,获取Section:CACHE MANIFEST下要缓存的文件列表,再对文件缓存。AppCache也有更新机制。被缓存的文件如果要更新,需要更新manifest文件。发现有修改,就会重新获取manifest文件,manifest文件与缓存文件的检查更新也遵守浏览器缓存机制。用于存储静态文件(如JS、CSS、字体文件)。
AppCache已经不推荐使用了,标准也不会再支持。
使用方法:
1 | String path = getApplicationContext().getDir("cache", Context.MODE_PRIVATE).getPath(); |
Dom Storage存储机制
DOM Storage 是一套在 Web Applications 1.0 规范中首次引入的与存储相关的特性的总称,现在已经分离出来,单独发展成为独立的 W3C Web 存储规范。DOM存储被设计为用来提供一个更大存储量、更安全、更便捷的存储方法,从而可以代替掉将一些不需要让服务器知道的信息存储到 cookies里的这种传统方法。
Dom Storage机制类似Cookies,但有一些优势。Dom Storage是通过存储字符串的Key-Value对来提供的,Dom Storage存储的数据在本地,不像Cookies,每次请求一次页面,Cookies都会发送给服务器。
DOM Storage分为sessionStorage和localStorage,二者使用方法基本相同,区别在于作用范围不同:前者具有临时性,用来存储与页面相关的数据,它在页面关闭后无法使用,后者具备持久性,即保存的数据在页面关闭后也可以使用。
- sessionStorage是个全局对象,它维护着在页面会话(page session)期间有效的存储空间。只要浏览器开着,页面会话周期就会一直持续。当页面重新载入(reload)或者被恢复(restores)时,页面会话也是一直存在的。每在新标签或者新窗口中打开一个新页面,都会初始化一个新的会话。
- localStorage保存的数据是持久性的。当前PAGE关闭(Page Session结束后),保存的数据依然存在。重新打开PAGE,上次保存的数据可以获取到。另外,Local Storage 是全局性的,同时打开两个 PAGE 会共享一份存数据,在一个PAGE中修改数据,另一个 PAGE 中是可以感知到的。
Dom Storage的优势在于:存储空间(5M)大,远远大于Cookies(4KB),而且数据存储在本地无需经常和服务器进行交互,存储安全、便捷。可用于存储临时的简单数据。作用机制类似于SharedPreference。但是,如果要存储结构化的数据,可能要借助JSON了,将要存储的对象转为JSON 串。不太适合存储比较复杂或存储空间要求比较大的数据,也不适合存储静态的文件。
使用方法如下:
1 | webSettings.setDomStorageEnabled(true); |
Web SQL Database存储机制
Web SQL Database基于SQL的数据库存储机制,用于存储适合数据库的结构化数据,充分利用数据库的优势,存储适合数据库的结构化数据,Web SQL Database存储机制提供了一组可方便对数据进行增加、删除、修改、查询。
Web SQL Database存储机制就是通过提供一组API,借助浏览器的实现,将Native支持的数据库功能提供给了Web。
实现方法为:
1 | String cacheDirPath = context.getFilesDir().getAbsolutePath()+"cache/"; |
Web SQL Database存储机制官方已不再推荐使用,也已经停止了维护,取而代之的是IndexedDB缓存机制。
Indexed Database缓存机制
Indexed DB也是一种数据库的存储机制,但不同于已经不再支持 Web SQL Database缓存机制。IndexedDB不是传统的关系数据库,而是属于NoSQL数据库,通过存储字符串的Key-Value对来提供存储(类似于Dom Storage,但功能更强大,且存储空间更大)。其中Key是必需的,且唯一的,Key可以自己定义,也可由系统自动生成。Value也是必需的,但Value非常灵活,可以是任何类型的对象。一般Value通过Key来存取的。
IndexedDB提供了一组异步的API,可以进行数据存、取以及遍历。IndexedDB有个非常强大的功能:index(索引),它可对Value对象中任何属性生成索引,然后可以基于索引进行Value对象的快速查询。
IndexedDB集合了Dom Storage和Web SQL Database的优点,用于存储大块或复杂结构的数据,提供更大的存储空间,使用起来也比较简单。可以作为 Web SQL Database的替代。但是不太适合静态文件的缓存。
Android在4.4开始支持IndexedDB,只需要打开允许JS执行的开关就好了,开启方法如下:
1 | webSettings.setJavaScriptEnabled(true); |
File System
File System是H5新加入的存储机制。它为Web App提供了一个运行在沙盒中的虚拟的文件系统。不同WebApp的虚拟文件系统是互相隔离的,虚拟文件系统与本地文件系统也是互相隔离的。Web App在虚拟的文件系统中,通过File System API提供的一组文件与文件夹的操作接口进行文件(夹)的创建、读、写、删除、遍历等操作。
浏览器给虚拟文件系统提供了两种类型的存储空间:临时的和持久性的:
- 临时的存储空间是由浏览器自动分配的,但可能被浏览器回收;
- 持久性的存储空间需要显示的申请,申请时浏览器会给用户一提示,需要用户进行确认。持久性的存储空间是 WebApp 自己管理,浏览器不会回收,也不会清除内容。存储空间大小通过配额管理,首次申请时会一个初始的配额,配额用完需要再次申请。
File System的优势在于:
- 可存储数据体积较大的二进制数据
- 可预加载资源文件
- 可直接编辑文件
遗憾的是:由于File System是H5新加入的缓存机制,目前Android WebView暂时还不支持。
总结
名称 | 原理 | 优点 | 适用对象 | 说明 |
---|---|---|---|---|
浏览器缓存 | 使用HTTP协议头部字段进行缓存控制 | 支持HTTP协议层 | 存储静态资源 | Android默认实现 |
AppCache | 类似浏览器缓存,以文件为单位进行缓存 | 构建方便 | 离线缓存,存储静态资源 | 对浏览器缓存的补充 |
Dom Storage | 通过存储键值对实现 | 存储空间大,数据在本地,安全便捷 | 类似Cookies,存储临时的简单数据 | 类似Android中的SP |
Web SQL DataBase | 基于SQL | 利用数据库优势,增删改查方便 | 存储复杂、数据量大的结构化数据 | 不推荐使用,用IndexedDB替代 |
IndexedDB | 通过存储键值对实现(NoSQL) | 存储空间大、使用简单灵活 | 存储复杂、数据量大的结构化数据 | 集合Dom Storage和Web SQL DataBase的优点 |
File System | 提供一个虚拟的文件系统 | 可存储二进制数据、预加载资源和之间编辑文件 | 通过文件系统管理数据 | 目前Android不支持 |
域控制不严格漏洞
setAllowFileAccess
设置是否允许 WebView 使用 File 协议。使用 file 域加载的 js代码能够使用进行同源策略跨域访问(对私有目录文件进行访问),从而导致隐私信息泄露。
对于不需要使用 file 协议的应用,禁用 file 协议;
1 | setAllowFileAccess(false); |
对于需要使用 file 协议的应用,禁止 file 协议加载 JavaScript。
1 | if (url.startsWith("file://") { |
setAllowFileAccessFromFileURLs
设置是否允许通过 file url 加载的 Js代码读取其他的本地文件。
- 在Android 4.1前默认允许
- 在Android 4.1后默认禁止
setAllowUniversalAccessFromFileURLs
设置是否允许通过 file url 加载的 Javascript 可以访问其他的源(包括http、https等源)。
- 在Android 4.1前默认允许
- 在Android 4.1后默认禁止
setJavaScriptEnabled
通过此 API 可以设置是否允许 WebView 使用 JavaScript,默认是不允许,但很多应用,包括移动浏览器为了让 WebView 执行 http 协议中的 JavaScript,都会主动设置允许 WebView 执行 JavaScript,而又不会对不同的协议区别对待,比较安全的实现是如果加载的 url 是 http 或 https 协议,则启用 JavaScript,如果是其它危险协议,比如是 file 协议,则禁用 JavaScript。如果是 file 协议,禁用 javascript 可以很大程度上减小跨源漏洞对 WebView 的威胁,但是此时禁用 JavaScript 的执行并不能完全杜绝跨源文件泄露。
最终解决方案
对于不需要使用 file 协议的应用,禁用 file 协议;
1 | // 禁用 file 协议; |
对于需要使用 file 协议的应用,禁止 file 协议加载 JavaScript。
1 | // 需要使用 file 协议 |
内存泄漏
概述
一直听说 WebView 使用不当容易造成内存泄漏,网上有很多针对内存泄漏的解决方案,比较多的是在 Activity.onDestroy 的时候将 WebView 从 View 树中移除,然后再调用 WebView.destroy 方法:
1 | override fun onDestroy() { |
于是我写了一个简单的包含一个 WebView 的 Activity,然后在 Activity.onDestroy 中分别尝试 啥也不干 和 只调用 WebView.destroy 方法,接着项目里面集成了 leakcanary 用来检测内存泄漏,启动 App 后,反复横屏竖屏,发现 Activity.onDestroy 有被正常调用,但是 leakcanary 并没有提示有内存泄漏,因此猜想 WebView 高版本应该把这个问题修复了。我用的测试机是 Android 9 版本的,于是想着换个低版本的机型试试,就弄了个 Android 6 的手机一跑,发现还是没有发生内存泄漏,看了下网上这些讲 WebView 内存泄漏的文章,有的还是 2019 年的,既然都 2019 年了还在谈 WebView 会造成内存泄漏,那感觉 Android 6 的机型不应该表现正常呀,一脸懵逼。。。秉着不弄明白不罢休的原则,遇到这种问题好办,Read The Fucking Source Code
就完事了。
WebView销毁时做了什么
既然网上的解决方案说先调用 removeView 移除 WebView,然后再调用 WebView.destroy 方法,那想着内存泄漏应该可以从 onDetachedFromWindow(从 Window 中 detach) 和 destroy(销毁) 这两个逻辑里找原因,看一下 WebView 中的这两个方法:
1 | public void destroy() { |
一般而言 destroy 方法应该在 Activity.onDestroy 时手动调用,而 onDetachedFromWindowInternal 方法在 View detach 的时候会由系统回调。注意 onDestroy 的调用时机早于 onDetachedFromWindow,相关的源码可以参考 Android图形系统综述 中 View 系列的文章自行跟踪。
上面这两个方法都出现了一个叫 mProvider 的对象,这个对象是啥呢?在 WebView.java 中搜索了一下 mProvider =
发现只有一处赋值:
1 | private WebViewProvider mProvider; |
它是一个 WebViewProvider 类型的实例,接着看它是怎么被赋值的,首先看一看 getFactory 返回的工厂对象是什么:
1 | private static WebViewFactoryProvider getFactory() { |
上面的 WebViewFactory.getProvider() 方法看上去是通过调用 providerClass 中的 create 方法拿到了 sProviderInstance 实例,于是得继续看 getProviderClass 方法到底是返回了一个什么类型的类:
1 | private static Class<WebViewFactoryProvider> getProviderClass() { |
查看源码,可以发现 CHROMIUM_WEBVIEW_FACTORY 取值为 com.android.webview.chromium.WebViewChromiumFactoryProviderForP
,我查看的源码版本是 Android P 的,所以这里是 WebViewChromiumFactoryProviderForP,看了一下其它 Android 版本的源码,发现都有一个对应的 WebViewChromiumFactoryProviderForX 值。这个 WebViewChromiumFactoryProviderForP 类在 AOSP 中是没有的,那应该去哪里找呢?
参考 Chrome developer 的文档: WebView for Android,可以看到从 Android 4.4 开始,WebView 组件基于 Chromium open source project 项目,新的 Webview 与 Android 端的 Chrome 浏览器共享同样的渲染引擎,因此 WebView 和 Chrome 之间的渲染应该会更加一致。而从 Android 5.0(Lollipop) 版本开始将 WebView 迁移到了一个独立的 APK — Android System WebView,因此可以单独在 Android 平台更新。这个 APP 可以在应用管理中看到,看到这里我大概明白了之前为啥用 Android 6 的机器也没有测试出内存泄漏,猜想应该是它的 Android System WebView
应用版本已经把内存泄漏的问题解决了吧,看了一下其应用版本是 86.0.4240.198
(可以在应用管理中查看 Android System WebView
应用的版本,另外也可以在浏览器中打开这个 网址 也会显示版本)。于是我们验证一下这个猜想。
关于 Chromium open source project 的源码可以在这里查看: Chromium open source project Ref,在这里可以查看目标版本的源码,我选择 86.0.4240.198
版本的源码进行解析。接着上面的 WebViewChromiumFactoryProviderForP 开始:
1 | public class WebViewChromiumFactoryProviderForP extends WebViewChromiumFactoryProvider { |
可以看到返回了一个 WebViewChromiumFactoryProviderForP 实例,其 createWebView 方法在父类 WebViewChromiumFactoryProvider 中:
1 | public WebViewProvider createWebView(WebView webView, WebView.PrivateAccess privateAccess) { |
因此上面的 mProvider 是 WebViewChromium 实例,来看一下它的 onDetachedFromWindow 和 destroy 方法:
1 | public WebViewProvider.ViewDelegate getViewDelegate() { |
这俩都会调用到 AwContents 中对应的方法,所以上面 WebView 销毁的时候,其 destroy 和 onDetachedFromWindowInternal 方法最后会调用到 AwContents 中对应的方法,低版本的内存泄漏就发生在这里。
AwContents中的内存泄漏
我们先看一下 mAwContents 的创建:
1 | mAwContents = new AwContents(mFactory.getBrowserContextOnUiThread(), mWebView, mContext, ...); |
86.0.4240.198版本
首先看看 86.0.4240.198
版本中的 AwContents 类中的几个相关方法:
1 | public void destroy() { |
在 View attach 到 Window 中的时候会调用上面的 onAttachedToWindow 方法,在 View detach 的时候会调用到 onDetachedFromWindow 方法,这两个方法中调用了一个 registerComponentCallbacks 和 unregisterComponentCallbacks 函数分别注册和解注册了一个 Callback,低版本会发生内存泄漏的原因就在此!
所以我们再来看一下 ComponentCallbacks 相关的逻辑:
1 | // Context |
所以假设在 AwContents 中只调用了 registerComponentCallbacks 注册方法而没有调用 unregisterComponentCallbacks 方法来解注册,那么会出现什么情况呢?我们看一下这个 AwComponentCallbacks 类的实现,发现它是 AwContents 中的一个非静态内部类,因此它会持有外部 AwContents 实例的引用,而 AwContents 持有 WebView 的 Context 上下文,对于 xml 中的 WebView 布局而言,这个上下文就是其所在的 Activity,因此如果在 Activity 生命周期结束后没有调用 unregisterComponentCallbacks 方法解注册的话,便可能会发生内存泄漏。
在 86.0.4240.198
版本中,如果在 Activity.onDestroy 方法中啥也不干,那么在 View detach 的时候依旧会调用 unregisterComponentCallbacks 方法解注册;而如果在 Activity.onDestroy 方法中只手动调用了 WebView.destroy 方法,那么还是会先通过调用 onDetachedFromWindow 来解注册,此时的 if (isDestroyed(NO_WARN)) return;
判断是 false,可以正常执行到解注册的逻辑,然后才会标记为已销毁。
54.0.2805.1版本
接着我们再看一个旧版本 54.0.2805.1
中的 AwContents 这几个方法:
1 | public void destroy() { |
可以看到如果在 Activity.onDestroy 中只调用了 WebView.destroy 方法的话,那么此时还没有调用到 onDetachedFromWindow 方法去解注册,却已经将 mIsDestroyed 置为了 true,于是当 detach 的时候,onDetachedFromWindow 判断到 isDestroyed 为 true 则不会走接下来解注册的逻辑了,于是内存泄漏也随之而来。
而如果在 Activity.onDestroy 中不手动调用 WebView.destroy 的话,理论上在 WebView detach 的时候能调用 onDetachedFromWindow 方法解注册 Callback,那么这个内存泄漏问题应该不会发生,但是没有调用 WebView.destroy 方法的话,很可能会发生其它问题,比如说不会调用 mContentsClient.getCallbackHelper().removeCallbacksAndMessages()
去移除 pending 的消息,说不定又有新的内存泄漏之类的。。。
要测试低版本 Chromium 的内存泄漏,可以找一个低版本的 Android 手机,然后将其 Android System WebView
应用卸载到装机版本,然后查看对应版本的 AwContents 类源码,如果源码中有内存泄漏的可能的话就可以测试了。另外如果手里头有 Root 的手机,可以尝试将 Android System WebView
最新版卸载,然后在 apkmirror(要翻墙) 中下载一个低版本的 Android System WebView
APK 安装到手机上;或者直接从源码中编译出一个指定版本的 Android System WebView
应用,源码编译时间有限我也没试过,可以参考 build-instructions。
小结
WebView 中的内存泄漏其实与 Chromium 内核版本有关,在新版本的 Chromium 内核中内存泄漏问题已经被解决了,而且从 Android 5.0(Lollipop) 版本开始将 Chromium WebView 迁移到了一个独立的 APP – Android System WebView
,随着 Android System WebView
的独立发布,低版本 Android 系统(Android 5以上)上搭载的 Chromium 内核一般来说也不会太旧,所以出现内存泄漏的概率应该是比较小的。如果仍需要兼容这很小的一部分机型,可以通过文章开头的方式销毁 WebView,即先移除 WebView 组件,确保先调用到 onDetachedFromWindow 方法解注册,然后再通过 WebView.destroy 方法处理其它销毁逻辑。