Android中Activity销毁后,页面中的dialog去哪了
问题的起源
在解决客户端中内存泄漏时,看到历史代码中有直接创建dialog并调用dialog.show()的代码存在,这让我对此产生了疑问:dialog会泄露吗?Activity销毁时他去哪里了?
来吧,debug试一下。
简单粗暴的测试代码(Activity中):
val builder = AlertDialog.Builder(this)
//五秒后开启弹窗
Handler().postDelayed(5000) {
builder.show()
}
//十秒后关闭Activity
Handler().postDelayed(10000) {
finish()
}
模拟器一走,5秒后,弹窗展示正常。可是10秒后,断点压根不经过!(正常back键是可以走到此处的)
但是dialog明明已经不见了,它去哪了呢?
一下午的功夫,翻起了源码
追根溯源
Dialog创建过程
dialog创建时,会同步创建一个window,用于dialog的视图管理。
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
...
//获取WindowManager
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
//创建PhoneWindow
final Window w = new PhoneWindow(mContext);
...
}
Dialog的展示过程
//核心代码
public void show() {
...
mDecor = mWindow.getDecorView();
mWindowManager.addView(mDecor, l);
...
mShowing = true;
sendShowMessage();
}
可以看到,dialog展示过程,实际上是使用WindowManager.addview的过程
那么addView里面具体是做了什么呢?
//WindowManagerImpl
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
mContext.getUserId());
}
可以看到,WindowManagerImpl的具体实现还是mGlobal去执行的。
ps:mGlobal是WindowManagerGlobal的单例
继续跟
//WindowManagerGlobal
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
}
...
ViewRootImpl root;
...
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
//被添加到了mViews中
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
}
代码中可以看到,dialog的decorView被添加到了WindowManagerGlobal的mViews中,并创建了ViewRootImpl,用于管理view的绘制过程
既然添加是在这里,那移除肯定也在WindowManagerGlobal中了。
带着信心看mRoot跟mViews修改的地方,看到了一个可疑函数:
void doRemoveView(ViewRootImpl root) {
boolean allViewsRemoved;
synchronized (mLock) {
final int index = mRoots.indexOf(root);
if (index >= 0) {
mRoots.remove(index);
mParams.remove(index);
final View view = mViews.remove(index);
mDyingViews.remove(view);
}
allViewsRemoved = mRoots.isEmpty();
}
...
}
直觉告诉我,这个地方肯定是dialog消失不见的元凶!
打个断点在这,跑一下试试。
a fewer minutes later
果不其然断点走到了这里两次,经过查看hash值确定,第一次是Activity的移除过程,第二次就是dialog的移除过程。直接上堆栈:
沿着堆栈,可以看到调用是来自于handler,跟踪message的what,找到了调用位置,是ViewRootImpl中die方法。
继续追源
原来在Activity结束的时候确实有涉及到dialog的操作。那我们看看都做了什么呢
这里明确说明,此时已经泄露了。不妨碍我们继续弄清真相,继续到WindowManagerGlobal中看代码:
总算是找到了!系统明确说明,这里泄露了!!
好奇心还是在作祟,ViewRootImpl是怎么找到的呢?token是怎么来的?
时间原因,直接说结论了,至于过程,大家有兴趣可以自行看看源码。
结论
- 关于WindowManager。
Activity重写了获取WindowManager的方法,使得通过Activity的context获取到的windowManager都是同一个。因此,Dialog的windowManager跟Activity的windowManager都是同一个。同一个WindowManager意味着在添加View的时候,token是同一个。区分开window不是同一个,window用于装载不同的视图。与此同时,decerView也不是同一个。 - 关于token。
Activity创建的时候ActivityThread会给他发放一个token,dialog在添加到window的时候,默认是没有token的,但是他有parentWindow,所以将parentWindow的token赋给了dialog的Window。token是管理视图的凭证 - 关于移除过程。
Activity结束时handleDestroyActivity,首先调用立即removeViewImmediate,把自身decorView移除。
同时会通过DecerView的token,调用WindowManagerGlobal的closeAll,异步查找token对应的ViewRoot,并执行移除–这时候已经是异步移除,已经泄露了。
再然后会通过当前Activity的token去调用gloabalemanage的closeAll(),会传递一个token过去,异步查找token对应的ViewRoot,并执行移除–这时候已经是异步移除,已经泄露了。