本文源码基于安卓9.0
1、什么情况下会“泄漏”window
当主动finish一个activity的时候,如果此时没有关闭dialog,此时会出现如下所示提示
看上去在DialogTestActivity中有一个window“泄漏”了,而且还将显示的位置栈信息打印出来了,我这个是做了延迟自动关闭的,居然还能显示之前的堆栈信息,很神奇。
2、报错原因
如下图所示,报错地址在WindowManagerGlobal的closeAllExceptView方法中
之所以报错是此时view为空且mParams.get(i).token == token为true,此时who就是dialog所在activity的全路径类名
2.1、显示dialog的堆栈信息是怎么来的
当我们需要打印当前调用堆栈时,可以这么写
val excep = StackTraceException("just stack trace")
Log.e(TAG,msg,excep)
而当前代码是这么写的
WindowLeaked leak = new WindowLeaked(
what + " " + who + " has leaked window "
+ root.getView() + " that was originally added here");
leak.setStackTrace(root.getLocation().getStackTrace());
Log.e(TAG, "", leak);
root.getLocation()调用返回的是WindowLeaked的一个实例
final WindowLeaked getLocation() {
return mLocation;
}
而mLocation是在ViewRootImpl初始化的时候创建的,保存了调用堆栈信息
public ViewRootImpl(Context context, Display display) {
mContext = context;
mWindowSession = WindowManagerGlobal.getWindowSession();
mDisplay = display;
mBasePackageName = context.getBasePackageName();
mThread = Thread.currentThread();
mLocation = new WindowLeaked(null);
...
}
ViewRootImpl初始化过程如下所示,SprdViewRootImpl继承于ViewRootImpl
这么一来就保存了显示dialog的堆栈信息,方便后面查找具体dialog,调用leak.setStackTrace(root.getLocation().getStackTrace())则将前面保存的堆栈信息设置给了leak,再调用Log.e则可以打印之前的堆栈
2.2 mParams.get(i).token是怎么回事
token是每个窗口的一个标识,每个窗口都有一个。那么它是在哪里赋值的呢?查看了上述的调用流程,并且断点验证发现是在Window的adjustLayoutParamsForSubWindow方法中赋值的,如下图所示
void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
...
if (wp.token == null) {
wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
}
...
在调用WindowManagerGlobal的addView方法时会调用该方法,使用parentWindow来调整子窗口的LayoutParams,将parentWindow的mAppToken传递给LayoutParams的token。
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
}
...
}
这个parentWindow看名字大概是当前window的parent,是在WindowManagerImpl初始化的时候传进去的
private WindowManagerImpl(Context context, Window parentWindow) {
mContext = context;
mParentWindow = parentWindow;
}
WindowManagerImpl初始化是在Window的setWindowManager方法中执行的,该方法将PhoneWindow传了过去,将自己PhoneWindow设为自己的parent window,什么鬼。。。而且mAppToken也是在这里赋值的,调用的地方是dialog初始化的时候,而且传过来是null,那么这个appToken是哪里赋值的呢?
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
...
w.setWindowManager(mWindowManager, null, null);//调用setWindowManager的三个参数的方法,appToken为null
...
}
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
mAppToken = appToken;
mAppName = appName;
mHardwareAccelerated = hardwareAccelerated
|| SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
if (wm == null) {
wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
}
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);//this是当前PhoneWindow实例
}
通过断点调试发现,在wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;这步中,mContainer == null为true,activity的mAppToken跟dialog的是同一个,而代码看不出来哪里设置了,想了一段时间,应该是某些地方漏了。找啊找,最终在Activity中发现这么一段代码
public Object getSystemService(@ServiceName @NonNull String name) {
...
if (WINDOW_SERVICE.equals(name)) {
return mWindowManager;
}
...
return super.getSystemService(name);
}
而mWindowManager是在attach初始化的
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
...
mWindowManager = mWindow.getWindowManager();
...
}
这个mWindowManager跟context.getSystemService(Context.WINDOW_SERVICE)返回的WindowManager已经不是同一个了,这个是在前面setWindowManager创建的
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);//这里的this是activity的PhoneWindow实例
}
如上,在activity调用该方法的时候this指的是在它的attach方法中初始化的PhoneWindow,当我们调用dialog的show方法时,会调用WindowManager的addView方法,其实是调用了activity中的在PhoneWindow初始化的WindowManagerImpl,而它持有的parentWindow就是activity中的PhoneWindow,这样一来就理解了,mAppToken用的都是attach传过来的token。