从一个异常认识Android中的 commit() 和 commitAllowingStateLoss()

从一个异常认识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()方法)。

带着疑问去 RxPermissionIssues 中寻找问题的答案,看到有人说把 FragmentTransactioncommit() 方法 换成 commitAllowingStateLoss() 就可以解决问题,

但是我的 LaunchAssistDialog 是我封装好的,并且直接调用的 DialogFragmentshow() 方法弹出的。

扫描二维码关注公众号,回复: 2657017 查看本文章

也就是说我要重写 DialogFragmentshow() 方法。

DialogFragmentshow() 方法源码:

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 的声明周期的。

就此,祝好。

猜你喜欢

转载自blog.csdn.net/Sean_css/article/details/79868946