前段时间实现可一个录制语音等信息发送到后台的功能。
大家都知道,现在使用录音权限(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(); } }现在再来调用最开始的代码,异常确实不会再抛出了。问题算是解决了
下面继续深究下究竟是为什么会抛出这个异常:
先来看下源码中的区别:
@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 方法呢?
别急,继续往下看
首先我们得知道,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 的声明周期的。
就此,祝好。
