返回

Booster 系列之——为系统 Bug 兜底

Android



## 缘起:排查一个 Toast 引发的异常

### 现象

最近在开发过程中遇到了一个奇怪的问题:在 Android 系统中使用 Toast 时,偶尔会出现 WindowManager$BadTokenException 异常。这个问题让人很头疼,因为 Toast 是 Android 开发中经常使用的一种轻量级提示方式,如果它不稳定,将会给开发带来很大的困扰。

经过排查,我们发现这个问题只在某些特定的场景下才会出现,比如当 Activity 处于后台时,或者当 Toast 在 Activity 的 onDestroy() 方法中显示时。

### 问题根源

为了找到问题的根源,我们深入研究了 Toast 的实现原理。Toast 是通过 WindowManager 来显示的,而 WindowManager 是 Android 系统中负责管理窗口和视图的一个重要组件。

当我们调用 Toast.show() 方法时,实际上是向 WindowManager 发送了一个请求,要求它创建一个新的窗口来显示 Toast。WindowManager 会根据请求创建一个新的窗口,并将其添加到当前 Activity 的窗口栈中。

如果在 Toast.show() 被调用时,Activity 已经处于后台,或者正在执行 onDestroy() 方法,那么 Activity 的窗口栈可能已经销毁或正在销毁。这时,WindowManager 就会抛出 WindowManager$BadTokenException 异常,因为它是试图向一个已经不存在的窗口栈添加一个新的窗口。

## 探索解决方案

既然我们已经找到了问题的根源,接下来就是探索解决方案了。

### 直接解决问题

最直接的解决方案是修改 Toast 的显示时机,避免在 Activity 处于后台或正在执行 onDestroy() 方法时显示 Toast。然而,这种解决方案过于保守,可能会限制 Toast 的使用场景。

### 优雅解决问题

我们希望找到一种更优雅的解决方案,既能解决问题,又不会限制 Toast 的使用场景。经过一番思考,我们想到了使用动态代理技术来解决这个问题。

动态代理是一种面向对象编程技术,它允许我们创建一个类的代理对象,该代理对象可以拦截对原始对象的方法调用并执行一些额外的操作。

在我们的场景中,我们可以创建一个 Toast 的代理对象,在代理对象中拦截 Toast.show() 方法的调用。如果在 Toast.show() 被调用时,Activity 处于后台或正在执行 onDestroy() 方法,那么代理对象就会阻止 Toast 的显示,从而避免异常的发生。

## BoosterKit 方案

我们将动态代理技术封装成了一个名为 BoosterKit 的库,供其他开发者使用。BoosterKit 库提供了一个简单的 API,可以轻松地为任何类创建代理对象。

```java
// 创建 Toast 的代理对象
Toast toast = BoosterKit.createProxy(Toast.class);

// 显示 Toast
toast.show();

如果在 Toast.show() 被调用时,Activity 处于后台或正在执行 onDestroy() 方法,那么 BoosterKit 就会阻止 Toast 的显示。

总结

通过使用 BoosterKit 库,我们可以优雅地解决 Toast 引发的 WindowManager$BadTokenException 异常问题,同时又不限制 Toast 的使用场景。BoosterKit 库是一个通用的动态代理库,可以用于解决各种各样的 Android 开发问题。

附录

除了 Toast 引发的 WindowManager$BadTokenException 问题外,在 Activity 的生命周期回调中也经历了同样的问题。究其根本,都是因为第三方库的引入导致了问题的发生。

在我们的案例中,引入了一个第三方库,该库在 Activity 的 onCreate() 方法中创建了一个新的窗口。当 Activity 处于后台时,这个窗口仍然存在,导致了 WindowManager$BadTokenException 异常的发生。

通过分析第三方库的源码,我们找到了问题的根源,并提出了相应的解决方案。我们修改了第三方库的代码,使其在 Activity 处于后台时销毁窗口,从而解决了问题。

这个案例再次说明了第三方库引入的风险。在使用第三方库时,我们必须仔细审查其源码,确保其不会对我们的应用程序造成负面影响。