这篇文章是根据项目中遇到的真实情况归纳整理而成的。整个项目组的研发人员从发现 BUG 那一刻开始,到彻底解决这个问题耗费了很长的时间,中间填了很多坑,甚至否定了刚接入没多久的 TBS。不过,好在解决了这个由 Android P 行为变更引发的错误,使得错误率降低到了现在可接受的 0.1113%,下面让我们开始复盘吧!
1. 奇怪的 WebView 错误
开始之前先看一张后台 BUG 统计图,这个 BUG 从 2019/3/30 开始爆发,并且无一例外的都发生在了 Android 9.0 的系统上,到 4 月中旬大面积爆发。当时已经查找了和 WebView 相关的 BUG,并且在云真机上也针对出错机型进行了测试,并没有出现问题。后台收集到的错误日志如下:
1 | android.view.InflateException: Binary XML file line #13: Binary XML file line #56: Error inflating class android.webkit.WebView |
后来就开始追查这个版本上线的时候新增了哪些功能,结果发现集成了腾讯的 TBS,并且用 TBS X5 内核的 WebView 替换了系统原生 WebVieW。到这看起来是找到了原因,所以去除 TBS 组件后,紧急升级了一个版本并发布上线。观察一段时间后,发现错误还在,并且错误率还在增加。这下,才意识到这个 BUG 仿佛不可控了,后来经过一位研发人员不懈的尝试,终于将这个 BUG 与 Android 的行为变更联系了起来,庆幸解决问题的方向是正确的。
2. Android Pie 行为变更
Android Pie ( API 级别 28 ) 是谷歌于 2018 年 8 月 7 日发布的正式版系统。和以往一样,每个重大版本的发布都伴随着一系列的安全策略和性能优化方面的行为变更,Android Pie 向 Android 系统引入了多项行为变更,这些行为变更仅影响以 API 28 或更高级别为目标的应用。将 targetSdkVersion 设为 API 28 或更高级别的应用必须进行修改,以便正确支持这些行为。
Android Pie 的行为变更包括:前台服务、隐私权变更、框架安全性变更、连接变更、界面变更。其中,在框架安全性变更中包含了 WebView 的行为变更。以下内容来自官方文档对 “按进程分设基于网络的数据目录” 的描述。
为改善 Android 9 中的应用稳定性和数据完整性,应用无法再让多个进程共用同一 WebView 数据目录。 此类数据目录一般存储 Cookie、HTTP 缓存以及其他与网络浏览有关的持久性和临时性存储。
在大多数情况下,您的应用只应在一个进程中使用 android.webkit 软件包中的类,例如 WebView 和 CookieManager。 例如,您应该将所有使用 WebView 的 Activity 对象移入同一进程。 您可以通过在应用的其他进程中调用 disableWebView(),更严格地执行"仅限一个进程”规则。该调用可防止 WebView 在这些其他进程中被错误地初始化,即使是从依赖内容库进行的调用也能防止。
如果您的应用必须在多个进程中使用 WebView 的实例,则必须先利用 WebView.setDataDirectorySuffix() 函数为每个进程指定唯一的数据目录后缀,然后再在该进程中使用 WebView 的给定实例。 该函数会将每个进程的网络数据放入其在应用数据目录内自己的目录中。
注意:即使您使用
setDataDirectorySuffix()
,系统也不会跨应用的进程界限共享 Cookie 以及其他网络数据。 如果应用中的多个进程需要访问同一网络数据,您需要自行在这些进程之间复制数据。 例如,您可以调用 getCookie() 和 setCookie(),在不同进程之间手动传输 Cookie 数据。
3. Google 的渐进式策略
其实 Google 早就为 Android P 的行为变更做了准备,并且是渐进执行的,还记得开始的错误日志吗?如果没注意,不必重新回去看了,我将日志的关键部分放到了下面:
1 | Caused by: java.lang.RuntimeException: Using WebView from more than one process at once with the same data directory is not supported. https://crbug.com/558377 |
这个异常是从系统 WebView 的 AwBrowserProcess 文件抛出的,这个异常的意思是:来自不同进程的 WebView 不允许共享数据目录。并且还给出了一个网址供参考,事实上不论你是否科学上网,这个地址都无法访问!当时在查找问题的时候已经定位到了这个文件,可惜没有深入下去查看这个文件的提交日志,所以错失了解决问题的最佳时机,现在在复盘的时候才注意到,为了保持文章的完整性,也为以后解决问题提供一个思路,所以将查看提交日志的过程也写了进来。
为了看着直接明了,我将提交日志按时间先后筛了出来,整理到一个表格中,从这你基本就能看出 Google 的程序员是如何渐进的为 Android 行为变更做准备的。
标识 | 描述 | 时间 |
---|---|---|
7259fe14 | Inline isAtLeastP and targetAtLeastP. | Tue Oct 23 19:31:33 2018 |
8902e04 | Relax WebView data dir check to only fire on P. | Fri Mar 09 16:45:32 2018 |
160a68e | Support defining a different data dir for WebView. | Thu Dec 21 20:23:15 2017 |
2017 年 12 月为 WebView 定义不同的数据目录提供支持;2018 年 3 月放松 WebView 数据目录检查,只在 Android P 上触发;2018 年 10 月 Android P 已发布,内联 isAtLeastP 和 targetAtLeastP,正式对 WebView 的行为变更生效。然后,各个厂商基于 Android P 进行二次开发,开发者如果没有针对这次行为变更做出对应的适配,就会在多进程 WebView 使用的时候出现错误。
4. 模拟 WebView 错误
想要模拟 WebView 的错误,首先,需要将应用的 targetSdkVersion 的 API 级别设为 28;然后,通过两个处于不同进程的 Activity 分别使用 WebView 加网页实现。为 Activity 设置进程需要在 AndroidManifest.xml
文件中对应的 Activity 节点下增加 process
属性,并设置进程名。大概像这样:
1 |
|
注意:process 的命名不能以数字开头,否正会提示 Install failed。
详细的示例请代码访问 WebView@Github,下载完成并在 Android P 的模拟器或真机上运行,就可以重现之前提到的错误。如果没有重现,请将清单文件中 Application 节点下的 name 属性注释后重试。
5. 修复 WebView 错误
经过上面的模拟,现在可以确定 WebView 的报错就是由多进程导致的。但是,仔细查了清单文件,发现项目中以新进程运行的只有消息推送,唯一的可能就是消息推送进程内打开了带有 WebView 的页面。从业务需求角度来说,收到消息推送,点击打开带有 WebView 的页面是不可避免的。所以,解决问题只能从兼容 Android P 的角度去考虑。
重新回到 Android P 的行为变更,我们可以为每个进程指定唯一的数据目录后缀,然后再在该进程中使用 WebView 的给定实例。什么时候指定数据目录最合适?Application 初始化的时候。因为每新开一个进程,Application 的 onCreate()
方法都会重新执行一次。我们可以在新开进程的时候,拿到当前进程名,然后和我们的主进程名进行对比,如果不一致,我们就为该进程指定对应的 WebView 数据目录后缀,代码如下:
1 | public class WebApplication extends Application { |
为新进程指定 WebView 数据目录后,重新在 Android P 的模拟器或真机上运行,并进行页面跳转测试,一切正常如初!这时,打开 APP 对应的安装路径会发现多了一个对应进程名后缀的 webview 目录,这证明我们的设置是生效的。至此,我们的适配工作也就完成了。