异步回调中操作UI的参见异常

在异步回调中操作UI时一定要检查回调时的页面状态,比如用 Activity.isFinishing 检测 Activity 的生命周期是否已经 destory; 用 Fragment.isAdded 检查是否还绑定在Activity;在涉及到fragment的操作时还需要注意Activity state loss的问题。

1. Unable to add window – token null is not valid; is your activity running?

android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
at android.view.ViewRootImpl.setView(ViewRootImpl.java:580)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:259)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)
at android.widget.PopupWindow.invokePopup(PopupWindow.java:1019)
at android.widget.PopupWindow.showAtLocation(PopupWindow.java:850)
at android.widget.PopupWindow.showAtLocation(PopupWindow.java:814)

这个exception经常出现在异步回调中显示Dialog,PopupWindow时,原因就是在长时间的异步回调操作结束后,Activity已经退出生命周期了。

对应相关的生命周期有两个相关方法可以判断:

isDestroyed()
Returns true if the final onDestroy() call has been made on the Activity, so this instance is now dead.
isFinishing()
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.

回忆一下Activity的生命周期:用户点击back或Activity显式的调用finish(),这个时候Activity的生命周期:onPause()->onStop()-> onDestroy(),一系列调用完毕后Activity instance就不再可用。

另外当系统旋转屏幕时,Activity也会Destroy,但是此时会自动重建。

这两者的区别在于前一种情况isFinishing()返回true,而后一种情况返回false。

Google 自家应用的incallui.InCallPresenter source code中封装一个方法isActivityStarted。(大部分时候仅仅使用isFinishing()判断就足够了)

public boolean isActivityStarted() {
return (mInCallActivity != null &&
!mInCallActivity.isDestroyed() &&
!mInCallActivity.isFinishing());
}

拿过来直接用的话,可以考虑先判断isActivityStarted再去弹出Dialog、PopupWindow。

需要注意的是,isDestoryed()必须在api level 17以上才能用,Override一下做一下版本兼容。

@Override
public boolean isDestroyed() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
return super.isDestroyed();
} else {
return mIsDestoryed;// 在onDestroy中设置true
}
}

2. Can not perform this action after onSaveInstanceState

Fatal Exception: java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at android.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java)
at android.app.FragmentManagerImpl.popBackStackImmediate(FragmentManager.java)
......
......
$FrameDisplayEventReceiver.run(Choreographer.java)
at android.os.Handler.handleCallback(Handler.java)
at android.os.Handler.dispatchMessage(Handler.java)
at android.os.Looper.loop(Looper.java)
at android.app.ActivityThread.main(ActivityThread.java)
at java.lang.reflect.Method.invokeNative(Method.java)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java)
at dalvik.system.NativeStart.main(NativeStart.java)

这个异常常发生于在Activity已经调用了onSaveInstanceState后,又尝试提交FragmentTransaction.commit()

那为啥这个时候会抛异常呢?

先了解下onSaveInstanceState()的调用。framework调用该函数时,会对当前的Activity存一个快照,并把Dialog、fragment、和view的状态存储到一个Bundle object,并通过Binder interface传到系统层,系统负责持久化保存起来;当Activity重建时,Bundle又会被重新取出来用于还原之前的状态。

那么在Activity 调用了onSaveInstanceState后,如果又提交了transaction,用户就会发现这次transaction的状态没有被记录下来。这个现象也称为Activity state loss。为了防止这种情况发生,Android就简单的抛了这个
IllegalStateException异常。

在Android 系统版本3.0(Honeycomb)之前,系统保证在onPause return之前不会强制kill进程,onSaveInstanceState会被保证在onPause之前调用。然而在3.0之后,系统保证在onStop return之前不会kill进程,onSaveInstanceState会被保证在onStop之前调用。

这样导致的一个结果就是在老的手机上,如果每次onPause之后的commit就抛一个exception,就会太过严格了。作为妥协,Android决定onPause() and onStop()之间的commit出现的 state loss 是可接受的。然而在3.0之后的系统上,如果你在save state之后commit,就一定会抛出异常。

OP pre-Honeycomb post-Honeycomb
commit() before onPause() OK OK
commit() between onPause() and onStop() STATE LOSS OK
commit() after onStop() EXCEPTION EXCEPTION

接下来看看具体的代码,这个异常抛出的地方。

在FragmentManager的实现类
FragmentManagerImpl.java 里面

private void checkStateLoss() {
if (mStateSaved) {
throw new IllegalStateException(
"Can not perform this action after onSaveInstanceState");
}
if (mNoTransactionsBecause != null) {
throw new IllegalStateException(
"Can not perform this action inside of " + mNoTransactionsBecause);
}
}

checkStateLoss又是在哪里调用的呢?
还是在FragmentManagerImpl.java中,有两个地方调用了checkStateLoss。popBackStackImmediateenqueueAction。先看popBackStackImmediate:

@Override
public boolean popBackStackImmediate() {
checkStateLoss();
executePendingTransactions();
return popBackStackState(mActivity.mHandler, null, -1, 0);
}

@Override
public void popBackStack(final String name, final int flags) {
enqueueAction(new Runnable() {
@Override public void run() {
popBackStackState(mActivity.mHandler, name, -1, flags);
}
}, false);
}

顺藤摸瓜

找到 Activity.java,在 onBackPressed 中调用了popBackStackImmediate。

public void onBackPressed() {
if (mActionBar != null && mActionBar.collapseActionView()) {
return;
}

if (!mFragments.popBackStackImmediate()) {
finishAfterTransition();
}
}

这里似乎没有什么可以做的

接下来看FragmentManagerImpl#enqueueAction,

   
public void enqueueAction(Runnable action, boolean allowStateLoss) {
if (!allowStateLoss) {
checkStateLoss();
}
synchronized (this) {
if (mDestroyed || mActivity == null) {
throw new IllegalStateException("Activity has been destroyed");
}
if (mPendingActions == null) {
mPendingActions = new ArrayList<Runnable>();
}
mPendingActions.add(action);
if (mPendingActions.size() == 1) {
mActivity.mHandler.removeCallbacks(mExecCommit);
mActivity.mHandler.post(mExecCommit);
}
}
}

enqueueAction又是什么时候调用呢?

FragmentTransaction的实现类 BackStackRecord中有


public int commit() {
return commitInternal(false);
}

public int commitAllowingStateLoss() {
return commitInternal(true);
}

int commitInternal(boolean allowStateLoss) {
if (mCommitted) {
throw new IllegalStateException("commit already called");
}
if (FragmentManagerImpl.DEBUG) {
Log.v(TAG, "Commit: " + this);
LogWriter logw = new LogWriter(Log.VERBOSE, TAG);
PrintWriter pw = new FastPrintWriter(logw, false, 1024);
dump(" ", null, pw, null);
pw.flush();
}
mCommitted = true;
if (mAddToBackStack) {
mIndex = mManager.allocBackStackIndex(this);
} else {
mIndex = -1;
}
mManager.enqueueAction(this, allowStateLoss);
return mIndex;
}

可以看到使用transaction.commitAllowingStateLoss();可以解决在commit时抛出的异常。

网上许多文章都说transaction.commitAllowingStateLoss();可以解决这类异常,实际上这个只覆盖了enqueueAction的那一部分。onBackPressed 那里还是会有exception的可能。从上面贴的stack trace 可以看到异常就是从popBackStackImmediate();抛出的,所以实际并没有完全解决问题。

其实出现这个问题大部分还是使用姿势错误,正确的使用方式如下:

  1. 不要在onActivityResult(), onStart(), and onResume()里面commit(), 而是在 FragmentActivity#onResumeFragments() or Activity#onPostResume(),后两个回调接口会保证在restore了state之后调用。

  2. 不要在异步回调里commit transactions,可能onStop调用之后,回调才开始commit。

  3. 使用commitAllowingStateLoss 作为最后的选择。

介绍个一种暴力的解决方法,后果自己负责:
注意到checkStateLoss 会去检查一个mStateSaved的类变量。
对于popBackStackImmediate 抛出的异常,可以在onBackPressed()里通过反射修改mStateSaved = false;

Class clazz = getSupportFragmentManager().getClass();
Field field = clazz.getDeclaredField("mStateSaved");
if(!field.isAccessible()) {
field.setAccessible(true);
}
field.set(getSupportFragmentManager(), false);`

或者反射调用noteStateNotSaved

public void noteStateNotSaved() {
mStateSaved = false;
}

对于enqueueAction抛出的异常,老老实实使用commitAllowingStateLoss吧,先保证引用不挂,才能考虑用户体验吧。

参考文档:

Activity官方文档
Activity lifecycle explained in details
fragment-transaction-commit-state-loss
FragmentActivity官方文档