Android PopupWindow Dialog 关于 is your activity running 崩溃详解

起因

对于 PopupWindow Dialog 需要 Activity 作为容器,并于其生命周期联系在一起.在Activity 还没有初始化完成时,此时我们调用 PopupWindow Dialogshow()方法就会抛出异常:

throw new WindowManager.BadTokenException("Unable to add window -- token " + attrs.token+ " is not valid; is your activity running?");

常见的崩溃日志如下:

android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@406a074 is not valid; is your activity running?
at android.view.ViewRoot.setView(ViewRoot.java:530)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:199)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:113)
at android.view.Window$LocalWindowManager.addView(Window.java:424)
at android.app.Dialog.show(Dialog.java:241)
at com.eleybourn.bookcatalogue.dialogs.StandardDialogs.goodreadsAuthAlert(StandardDialogs.java:261)
at com.eleybourn.bookcatalogue.goodreads.GoodreadsUtils$4$1.run(GoodreadsUtils.java:101)
at android.os.Handler.handleCallback(Handler.java:587)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:130)
at android.app.ActivityThread.main(ActivityThread.java:3687)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:507)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:878)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:636)
at dalvik.system.NativeStart.main(Native Method)

解决办法

if(!Activity.isFinishing()){
mPopupWindow.show(anchor);
}

如果页面结束时 PopupWindow Dialog 没有dismiss(),那么会出现内存泄漏,日志如下:

SpecialTopicActivity has leaked window android.widget.PopupWindow$PopupDecorView{dfa91cc V.E...... ........ 0,0-185,86} that was originally added here
at android.view.ViewRootImpl.<init>(ViewRootImpl.java:573)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:326)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:109)
at android.widget.PopupWindow.invokePopup(PopupWindow.java:1333)
at android.widget.PopupWindow.showAsDropDown(PopupWindow.java:1156)
at android.widget.PopupWindow.showAsDropDown(PopupWindow.java:1115)
at com.fanwe.customview.PopTipShare.show(PopTipShare.java:56)
at com.fanwe.seller.views.SpecialTopicActivity$4.run(SpecialTopicActivity.java:222)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:7224)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1230)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1120)
...

在Activity执行onDestroy()时 dismiss 掉 PopupWindow Dialog即可;

if (mPopupWindow!=null && mPopupWindow.isShowing()){
mPopupWindow.dismiss();
}

源码

需要涉及的类:

  • ViewManager : WindowManager 的父类.
  • WindowManager 及其实现类 WindowManagerImpl(@hide): 接口类与实现类,对用户开放.
  • ViewRootImpl(@hide) : WindowManager 的 View 的操作实现类.
  • WindowManagerGlobal(@hide) :WindowManagerImpl 类功能的执行者.

1. 在调用 Show() 方法时

注:以下情况都是对PopupWindow Dialog 适用的,现在不再指明PopupWindow Dialog,下面以PopupWindow为例一步步说明.

直接展示源码可能更容易说明问题,注释是关键点,以下源码展示以方法执行顺序进行.

public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
if (isShowing() || mContentView == null) {
return;
} TransitionManager.endTransitions(mDecorView);
attachToAnchor(anchor, xoff, yoff, gravity);
mIsShowing = true;
mIsDropdown = true;
final WindowManager.LayoutParams p = createPopupLayoutParams(anchor.getWindowToken());
preparePopup(p); final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff,
p.width, p.height, gravity);
updateAboveAnchor(aboveAnchor);
p.accessibilityIdOfAnchor = (anchor != null) ? anchor.getAccessibilityViewId() : -1; //以上都是准备 WindowManager.LayoutParams p;
invokePopup(p);
}
private void invokePopup(WindowManager.LayoutParams p) {
if (mContext != null) {
p.packageName = mContext.getPackageName();
} final PopupDecorView decorView = mDecorView;
decorView.setFitsSystemWindows(mLayoutInsetDecor); setLayoutDirectionFromAnchor(); //调用WindowManager.addView()方法
mWindowManager.addView(decorView, p); if (mEnterTransition != null) {
decorView.requestEnterTransition(mEnterTransition);
}
}

以上都是在PopupWindow,在调用WindowManager.addView()后进入WindowManager类,实际是其实现类WindowManagerImpl.

    @Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
//调用了 WindowManagerGlobal.addView()方法.
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

重点在这里 WindowManagerGlobal.addView()

    public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
// ...
// 检查一些状态
// ViewRootImpl root : 添加 View 最后一步由此 View 操作类的 setView() 完成.
ViewRootImpl root;
View panelParentView = null; //...
//准备需要的参数
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
} // do this last because it fires off messages to start doing things
try {
//ViewRootImpl类的 setView() 方法.
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// 异常在这里抛出
// BadTokenException or InvalidDisplayException, clean up.
synchronized (mLock) {
final int index = findViewLocked(view, false);
if (index >= 0) {
removeViewLocked(index, true);
}
}
throw e;
}
}

最后进入 ViewRootImpl 的 setView() 方法


/**
* We have one child
*/
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view; mAttachInfo.mDisplayState = mDisplay.getState();
mDisplayManager.registerDisplayListener(mDisplayListener, mHandler); mViewLayoutDirectionInitial = mView.getRawLayoutDirection();
//最终把 View 添加上去,如果异常了就传null.
mFallbackEventHandler.setView(view);
mWindowAttributes.copyFrom(attrs);
//...省略代码
if (DEBUG_LAYOUT) Log.v(mTag, "Added window " + mWindow);
if (res < WindowManagerGlobal.ADD_OKAY) {
mAttachInfo.mRootView = null;
mAdded = false;
mFallbackEventHandler.setView(null);
unscheduleTraversals();
setAccessibilityFocus(null, null);
switch (res) {
case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
//这里集中处理异常情况
//异常实际抛出的ADD_BAD_SUBWINDOW_TOKEN
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not valid; is your activity running?");
case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not for an application");
case WindowManagerGlobal.ADD_APP_EXITING:
throw new WindowManager.BadTokenException(
"Unable to add window -- app for token " + attrs.token
+ " is exiting");
case WindowManagerGlobal.ADD_DUPLICATE_ADD:
throw new WindowManager.BadTokenException(
"Unable to add window -- window " + mWindow
+ " has already been added");
case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED:
// Silently ignore -- we would have just removed it
// right away, anyway.
return;
case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON:
throw new WindowManager.BadTokenException("Unable to add window "
+ mWindow + " -- another window of type "
+ mWindowAttributes.type + " already exists");
case WindowManagerGlobal.ADD_PERMISSION_DENIED:
throw new WindowManager.BadTokenException("Unable to add window "
+ mWindow + " -- permission denied for window type "
+ mWindowAttributes.type);
case WindowManagerGlobal.ADD_INVALID_DISPLAY:
throw new WindowManager.InvalidDisplayException("Unable to add window "
+ mWindow + " -- the specified display can not be found");
case WindowManagerGlobal.ADD_INVALID_TYPE:
throw new WindowManager.InvalidDisplayException("Unable to add window "
+ mWindow + " -- the specified window type "
+ mWindowAttributes.type + " is not valid");
}
throw new RuntimeException(
"Unable to add window -- unknown error code " + res);
}
//....省略代码
}
}
}

从上面的代码中可以看出,判断异常类型的是一个int值res,现在看看res.

try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
// mWindowSession 的类型是 IWindowSession , mWindow 的类型是 IWindow.Stub .这行代码就是利用AIDL进行IPC, 实际被调用的是Session.addToDisplay()方法.
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
} catch {//...}

进一步调用Session.java中的addToDisplay方法:

    @Override
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
Rect outOutsets, InputChannel outInputChannel) {
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
outContentInsets, outStableInsets, outOutsets, outInputChannel);
}

mService是WindowManagerService,继续看 WmS 的 addWindow() 方法.

这是最核心的类,关于所有的Android addView 最后都是通过 WmS 的 addWindow()方法完成添加操作.

public int addWindow(Session session, IWindow client, int seq,
WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
InputChannel outInputChannel) {
int[] appOp = new int[1]; // 关于权限的检查,如果有使用 WindowManager 实现悬浮窗效果的对悬浮窗,关于 SYSTEM_ALERT_WINDOW 的申请问题肯定纠结过.
// 我在一篇文章说对于SDK 19以上,使用 WindowManager.LayoutParams.TYPE_TOAST,SDK 19 以下使用 WindowManager.LayoutParams.TYPE_PHONE
// 原因是:1.type为"TYPE_TOAST"在sdk19之前不接收事件,之后可以.
// 2.type为"TYPE_PHONE"需要"SYSTEM_ALERT_WINDOW"权限.在sdk19之前不可以直接申明使用,之后不能直接申明使用.
// 想知道为什么会这样,可以看看 mPolicy.checkAddPermission(attrs, appOp); 答案在这里面.(这里就不看了=.=)
int res = mPolicy.checkAddPermission(attrs, appOp);
if (res != WindowManagerGlobal.ADD_OKAY) {
return res;//如果权限不满足就不用继续了.
} boolean reportNewConfig = false;
WindowState attachedWindow = null;
long origId;
final int type = attrs.type; //...
//check something... boolean addToken = false; // mTokenMap 存储 WindowToken;
// 这里是取,后面在会执行 mTokenMap.put()方法,这样 token 就不为null了.
// Activity 的addWindow时会传入不为 null 的 token,然而 PopupWindow 和 Dialog 传入的是为 null 的token.
//final HashMap<IBinder, WindowToken> mTokenMap = new HashMap<>();
WindowToken token = mTokenMap.get(attrs.token); AppWindowToken atoken = null;
//...
//check something... if (addToken) {
//如果activity调用 WindowManager.addView(),token就会被 put 到 map 中.
mTokenMap.put(attrs.token, token);
}
win.attach();
mWindowMap.put(client.asBinder(), win);
//...省略 return res;
}

现在应该知道了在哪里抛出异常了,最后还有一点就是关于疑问的: 那 Activity 什么时候 才算 is running ?

Activity 什么时候 才算 is running ?

现在看看 ActivityThread类,这是 Activity 的管理类,分发Activity的生命周期等.

在 ActivityThread 的 handleResumeActivity() 方法中,有如下代码:

final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
//...
// performResumeActivity() 方法最后会调用 Activity 的 OnResume() 方法.
r = performResumeActivity(token, clearHide, reason); //....
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
// Normally the ViewRoot sets up callbacks with the Activity
// in addView->ViewRootImpl#setView. If we are instead reusing
// the decor view we have to notify the view root that the
// callbacks may have changed.
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient && !a.mWindowAdded) {
a.mWindowAdded = true;
//这里调用了 WindowManager 的 addView() 方法,最终会调用 WindowManagerService 的 addWindow() 方法.然后就是之前看到的源码内容了.其他的情况也类似这个流程,但是还是有很多细微区别,比如 WindowManager.LayoutParams 的 Type 与 Flag 等.
wm.addView(decor, l);
}
//...
}

在 Activity 的生命周期 onResume 执行后不久, Activity 的 token 随着 wm.addView(decor, l); 后就被 put 到 map 中,其后调用 PopupWindow 与 Dialog 的 Show() 方法后就不会出现 "is your activity running ?"这种异常了.由于token在 performResumeActivity() 后(从代码中可以看出,是在同一方法体中执行完两个操作),所以有人在 Activity 的 onResume() 方法中这样写道:

    @Override
protected void onResume() {
super.onResume();
mListview.postDelayed(new Runnable() {
@Override
public void run() {
mPopupWindow.show(anchor);
}
},100);
}

这种写法是不可取的.

在实际工作过程中,可能需要在界面展示后2s展示一个Dialog 或者 PopupWindow ,此时如果用户在2s内退出 Activity ,那么Runnable执行时无法使用一个Finished 的Activity 这里推荐的写法.

//第一种
mSomeView.postDelayed(new Runnable() {
@Override
public void run() {
if (!Activity.this.isFinishing()){
mPopupWindow.show();
}
}
},1000);
//第二种
mSomeView.post(new Runnable() {
@Override
public void run() {
if (!Activity.this.isFinishing()){
mPopupWindow.show();
}
}
});

Bugly社区整理一篇关于 Window 的文章总结的很不错,对于 Window 不是很了解的可以点此了解下. 我之前整理过PopupWindow 的实现过程,大概有一年了吧,现在竟然忘的差不多了,额....

剩余就是 Activity.isFinishing() 方法的具体调用与实现过程了. Google API 的说明是: Check to see whether this activity is in the process of finishing, either because you called finish on it or someone else has requested that it finished. This is often used in onPause to determine whether the activity is simply pausing or completely finishing. 检测 Activity 是否在 Process 或者 Finishing过程中.有空看看具体过程.

Android PopupWindow Dialog 关于 is your activity running 崩溃详解的更多相关文章

  1. Android中Intent传值与Bundle传值的区别详解

    Android中Intent传值与Bundle传值的区别详解 举个例子我现在要从A界面跳转到B界面或者C界面   这样的话 我就需要写2个Intent如果你还要涉及的传值的话 你的Intent就要写两 ...

  2. Android 高级UI设计笔记07:RecyclerView 的详解

    1. 使用RecyclerView       在 Android 应用程序中列表是一个非常重要的控件,适用场合非常多,如新闻列表.应用列表.消息列表等等,但是从Android 一出生到现在并没有非常 ...

  3. 转: Android 软件开发之如何使用Eclipse Debug调试程序详解(七)

    转自: http://www.uml.org.cn/mobiledev/201110092.asp Android 软件开发之如何使用Eclipse Debug调试程序详解(七)   发布于2011- ...

  4. Android为TV端助力 转载:Android绘图Canvas十八般武器之Shader详解及实战篇(上)

    前言 Android中绘图离不开的就是Canvas了,Canvas是一个庞大的知识体系,有Java层的,也有jni层深入到Framework.Canvas有许多的知识内容,构建了一个武器库一般,所谓十 ...

  5. Android为TV端助力 转载:Android绘图Canvas十八般武器之Shader详解及实战篇(下)

    LinearGradient 线性渐变渲染器 LinearGradient中文翻译过来就是线性渐变的意思.线性渐变通俗来讲就是给起点设置一个颜色值如#faf84d,终点设置一个颜色值如#CC423C, ...

  6. Android学习笔记:Activity生命周期详解

    进行android的开发,必须深入了解Activity的生命周期.而对这个讲述最权威.最好的莫过于google的开发文档了. 本文的讲述主要是对 http://developer.android.co ...

  7. Android之Activity启动流程详解(基于api28)

    前言 Activity作为Android四大组件之一,他的启动绝对没有那么简单.这里涉及到了系统服务进程,启动过程细节很多,这里我只展示主体流程.activity的启动流程随着版本的更替,代码细节一直 ...

  8. Activity生命周期详解

    http://blog.csdn.net/liuhe688/article/details/6733407 onPause 回到 onResume 的过程“在一般的开发中用不上”,但是作为开发者还是有 ...

  9. 【Android】时间选择器,选择日期DatePicker 简单详解demo及教程

    作者:程序员小冰,GitHub主页:https://github.com/QQ986945193 新浪微博:http://weibo.com/mcxiaobing 首先给大家看一下我们今天这个最终实现 ...

随机推荐

  1. .net点选验证码实现思路分享

    哈哈好久没冒泡了,最进看见点选验证码有点意思,所以想自己写一个. 先上效果图 如果你被这个效果吸引了就请继续看下去. 贴代码前先说点思路: 1.要有一个汉字库,并按字形分类.(我在数据库里是安部首分类 ...

  2. 【大型网站技术实践】初级篇:借助Nginx搭建反向代理服务器

    一.反向代理:Web服务器的“经纪人” 1.1 反向代理初印象 反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从 ...

  3. C语言 · 字符转对比

    问题描述 给定两个仅由大写字母或小写字母组成的字符串(长度介于1到10之间),它们之间的关系是以下4中情况之一: 1:两个字符串长度不等.比如 Beijing 和 Hebei 2:两个字符串不仅长度相 ...

  4. 使用AWS亚马逊云搭建Gmail转发服务(三)

    title: 使用AWS亚马逊云搭建Gmail转发服务(三) author:青南 date: 2015-01-02 15:42:22 categories: [Python] tags: [log,G ...

  5. MySQL 系列(一) 生产标准线上环境安装配置案例及棘手问题解决

    一.简介 MySQL是最流行的开放源码SQL数据库管理系统,它是由MySQL AB公司开发.发布并支持的.有以下特点: MySQL是一种数据库管理系统. MySQL是一种关联数据库管理系统. MySQ ...

  6. Android Studio开发RecyclerView遇到的各种问题以及解决(一)

    以前一直在用ListView,,,最近才看RecyclerView发现好强大.RecyclerView前提是Android版本在5.0以上,本人以前用的是eclipse只支持到4.4.索性就安装一个A ...

  7. Linux命令

    系统信息 arch 显示机器的处理器架构(1) uname -m 显示机器的处理器架构(2) uname -r 显示正在使用的内核版本 dmidecode -q 显示硬件系统部件 - (SMBIOS ...

  8. web安全浅析

    就之前本人主持开发的金融产品所遇到的安全问题,设计部分请参见:http://www.cnblogs.com/shenliang123/p/3835072.html 这里就部分web安全防护就简单的交流 ...

  9. 学习笔记:URL Protocol在浏览器中打开本地应用程序

    看到阿里的网站上可以通过点击卖家的旺旺图标从而调用本地的阿里旺旺程序,而且还可以传递当前浏览者需要咨询的商品.这是怎么实现的呢?是通过URLProtocol来完成. 原理还没有太清楚,即在系统里注册一 ...

  10. ASP.NET Core 中间件详解及项目实战

    前言 在上篇文章主要介绍了DotNetCore项目状况,本篇文章是我们在开发自己的项目中实际使用的,比较贴合实际应用,算是对中间件的一个深入使用了,不是简单的Hello World,如果你觉得本篇文章 ...