从一个异常认识Android中的 commit() 和 commitAllowingStateLoss()
文章目录
一、重现以及解决
前段时间实现可一个录制语音等信息发送到后台的功能。
大家都知道,现在使用录音权限(Manifest.permission.RECORD_AUDIO)
是需要动态申请权限的,项目中使用的权限管理的是 RxPermission
这个开源库。
之前项目中大部分的权限申请基本都是在 Activity
中申请的,申请完之后的操作一般是打开新的 Activity、定位等操作。
现在的需求是申请完权限后打开一个弹窗,让用户输入信息、录制语音等,所有就有了下面的代码:
getRxPermissions().request(Manifest.permission.RECORD_AUDIO)
.subscribe(new Action1<Boolean>() {
@Override
public void call(Boolean aBoolean) {
if (aBoolean) {
LaunchAssistDialog.newBuilder()
.setSize((int) (ScreenUtils.getScreenWidth(getContext()) * 0.85), WRAP_CONTENT)
.setAnimation(R.style.DialogAnimFromCenter)
.setGravity(CENTER)
.setMtId(String.valueOf(bean1.getId()))
.build()
.setIRequestSuccess(new LaunchAssistDialog.IRequestSuccess() {
@Override
public void isSuccess(boolean isSuccess) {
if (isSuccess) {
getMtList();
}
}
})
.show(getSupportFragmentManager(), "launchAssistDialog");
} else {
ToastUtil.toastError(getContext(), ResourceUtils.getString(mContext, R.string.need_record_permission_string));
}
}
});
看上去没啥问题,先申请权限,得到 aBoolean :
- 如果是
true
弹出弹窗 - 如果是
false
使用Toast 提示
但是,就是这么看起来很正常的代码,却在第一次申请权限的时候,会抛出异常
Caused by: rx.exceptions.OnErrorNotImplementedException: Can not perform this action after onSaveInstanceState
字面意思就是:不能在 onSaveInstanceState 之后执行这个动作(commit()方法)。
带着疑问去 RxPermission
的 Issues
中寻找问题的答案,看到有人说把 FragmentTransaction
的 commit()
方法 换成 commitAllowingStateLoss()
就可以解决问题,
但是我的 LaunchAssistDialog
是我封装好的,并且直接调用的 DialogFragment
的 show()
方法弹出的。
也就是说我要重写 DialogFragment
的 show()
方法。
DialogFragment
的 show()
方法源码:
public void show(FragmentManager manager, String tag) {
mDismissed = false;
mShownByMe = true;
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commit();
}
可以看到,除了正常使用 Fragment 需要开启事务管理,再提交的流程外,还需要把 mDismissed 置为 false; mShownByMe 置为 true;
这就比较尴尬了,不得已我们只能使用反射来解决。
下面是我重写的 show() 方法:
public void show(android.support.v4.app.FragmentManager manager, String tag) {
setBooleanField("mDismissed", false);
setBooleanField("mShownByMe", true);
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commitAllowingStateLoss();
}
private void setBooleanField(String fieldName, boolean value) {
try {
Field field = DialogFragment.class.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(this, value);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
现在再来调用最开始的代码,异常确实不会再抛出了。问题算是解决了
下面继续深究下究竟是为什么会抛出这个异常:
二、原因分析
2.1 commitAllowingStateLoss 与 commit 的区别
先来看下源码中的区别:
@Override
public int commit() {
return commitInternal(false);
}
@Override
public int commitAllowingStateLoss() {
return commitInternal(true);
}
看来都调用了 commitInternal
方法,只是参数不同,来看下 commitInternal 方法。
int commitInternal(boolean allowStateLoss) {
if (mCommitted) throw new IllegalStateException("commit already called");
if (FragmentManagerImpl.DEBUG) {
Log.v(TAG, "Commit: " + this);
LogWriter logw = new LogWriter(TAG);
PrintWriter pw = new PrintWriter(logw);
dump(" ", null, pw, null);
pw.close();
}
mCommitted = true;
if (mAddToBackStack) {
mIndex = mManager.allocBackStackIndex(this);
} else {
mIndex = -1;
}
mManager.enqueueAction(this, allowStateLoss);
return mIndex;
}
主要看下形参:allowStateLoss在哪使用:
mManager.enqueueAction(this, allowStateLoss);
public void enqueueAction(OpGenerator action, boolean allowStateLoss) {
if (!allowStateLoss) {
checkStateLoss();
}
synchronized (this) {
if (mDestroyed || mHost == null) {
throw new IllegalStateException("Activity has been destroyed");
}
if (mPendingActions == null) {
mPendingActions = new ArrayList<>();
}
mPendingActions.add(action);
scheduleCommit();
}
}
一路看下来,看到了最终这个参数是用来判断是不是执行 checkStateLoss() 方法的,
- commit 传入的参数是 false ,要执行 checkStateLoss()
- commitAllowingStateLoss 传入的参数是 true ,不执行 checkStateLoss()
再来看下 checkStateLoss() :
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);
}
}
是不是看到了我们刚才看到的异常 Can not perform this action after onSaveInstanceState。
不会,抛出这个异常还有个前提条件,mStateSaved == true,字面意思是 状态有没有被保存。
来看下官方的定义:
可以看到对于 commitAllowingStateLoss 的解释就是,类似于 commit ,但是它允许在 Activity 状态被保存了之后被执行。也就是说在 activity 调用了 onSaveInstanceState() 之后,再 commit 一个事务就会出现该异常,使用 commitAllowingStateLoss 却不会出现该异常。
但是后面也说了,这是一个危险的操作,因为如果 Activity 恢复了 ,那么可能导致 commit 的内容丢失,所以 commitAllowingStateLoss 适应于 UI 的变化对用户来说是可接受的。
那么问题来了?为什么我就申请个权限,却会调用 Activity 的 onSaveInstanceState 方法呢?
别急,继续往下看
2.2 Android 6.0 权限申请
首先我们得知道,Google 为什么提供了权限申请的方法,只是用起来比较复杂,所以我们使用 RxPermission 来进行权限申请。
RxPermission 作为一个权限申请框架,必然是对系统提供方法的封装。
不信?继续看 RxPermission 的源码
我们的 rxPermissions 的 request 方法最终调用的是下面这行代码:
@TargetApi(Build.VERSION_CODES.M)
void requestPermissionsFromFragment(String[] permissions) {
mRxPermissionsFragment.log("requestPermissionsFromFragment " + TextUtils.join(", ", permissions));
mRxPermissionsFragment.requestPermissions(permissions);
}
这里又调用了 RxPermissionsFragment 的 requestPermissions 方法,来看下:
@TargetApi(Build.VERSION_CODES.M)
void requestPermissions(@NonNull String[] permissions) {
requestPermissions(permissions, PERMISSIONS_REQUEST_CODE);
}
public final void requestPermissions(@NonNull String[] permissions, int requestCode) {
if (mHost == null) {
throw new IllegalStateException("Fragment " + this + " not attached to Activity");
}
mHost.onRequestPermissionsFromFragment(this, permissions,requestCode);
}
看到最终调用的是 mHost 的 onRequestPermissionsFromFragment 方法,关键就在于 mHost,我们知道 Fragment 是的宿主是 Android 四大组件之一的 Activity,所以这里的 mHost 指的就是 Activity,来看下 Activity 的 onRequestPermissionsFromFragment 方法:
@Override
public void onRequestPermissionsFromFragment(Fragment fragment, String[] permissions,
int requestCode) {
String who = REQUEST_PERMISSIONS_WHO_PREFIX + fragment.mWho;
Intent intent = getPackageManager().buildRequestPermissionsIntent(permissions);
startActivityForResult(who, intent, requestCode, null);
}
看到,我们首先得到一个 Intent,我们通过源码查看,得到了下面这个返回 Intent 的方法:
public Intent buildRequestPermissionsIntent(@NonNull String[] permissions) {
if (ArrayUtils.isEmpty(permissions)) {
throw new IllegalArgumentException("permission cannot be null or empty");
}
Intent intent = new Intent(ACTION_REQUEST_PERMISSIONS);
intent.putExtra(EXTRA_REQUEST_PERMISSIONS_NAMES, permissions);
intent.setPackage(getPermissionControllerPackageName());
return intent;
}
/**
* The action used to request that the user approve a permission request
* from the application.
*
* @hide
*/
@SystemApi
public static final String ACTION_REQUEST_PERMISSIONS =
"android.content.pm.action.REQUEST_PERMISSIONS";
可以看到,通过隐式启动 Activity 的方法去启动了一个系统提供的 Activity,所以这个时候,我们申请权限的 Activity 可能被 kill 掉,所以执行了 onPause 方法 和 onSaveInstanceState 方法。
所以这个时候我们前面提到的 mStateSaved 这个变量被置为了 true,所以如果使用 commit 的话,就会执行 checkStateLoss(); 方法,进而抛出异常,如果使用 commitAllowingStateLoss 的话,就不会执行 checkStateLoss(); 方法,不会抛出异常。
到现在,总算搞清楚到底是怎么回事了,是不是有一种豁然开朗的感觉呢?
三、总结
对于我这次出现的异常,知道了是因为 申请权限的时候,调用了系统的 Activity ,导致宿主 Activity 执行了 onSaveInstanceState 方法,让 mStateSaved 变为 true,调用 commit() 的时候,执行 checkStateLoss() 检查,抛出了异常。
解决方法就是重写 DialogFragment 的 show() 方法,把 commit() 变为 commitAllowingStateLoss() 方法。
对于日常开发来说:
- 如果强制 Fragment 一定要显示,即使让程序 Crash 也要显示的,使用 commit() ,比如用户比较关心的数据:金融相关等
- 如果要显示 Fragment 消失对用户没有特别大的影响,建议使用 commitAllowingStateLoss() ,能在一定程度上保证程序的稳定性。
此外,还要尽量避免在异步的回调方法中使用 commit() ,因为此时是感受不到 Activity 的声明周期的。
就此,祝好。