最近我的个人应用KeepassA在bugly上看到一个很奇怪的问题,activity在给fragment传参时,使用了赋值的方式,如下:

val f = FragmentA()
f.b = "sss"

某些情况下,b的属性死活拿不到,导致程序出现空指针异常。

一、原因分析

1.1 androidx 版本:1.1.0

查看了FragmentActivity的代码,

protected void onCreate(@Nullable Bundle savedInstanceState) {
mFragments.attachHost(null /*parent*/);
if (savedInstanceState != null) {
Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
mFragments.restoreSaveState(p);
......
}
.....

}

savedInstanceState不为空时(如屏幕旋转,fragment实例被销毁响应onDetach),fragment会清空状态,一路跟踪代码下去,发现在FragmentManagerImplrestoreSaveState中,发现Fragment竟然被重建了!!!

Fragment f = fs.instantiate(mHost.getContext().getClassLoader(), getFragmentFactory());

查看fragment创建的逻辑,发现原来Fragment的属性并不会赋值到新创建的fragmen中,在Fragment中唯一恢复数据的只有setArguments,这也是为什么官方建议我们使用setArguments传参的原因。

public Fragment instantiate(@NonNull ClassLoader classLoader,
@NonNull FragmentFactory factory) {
if (mInstance == null) {
if (mArguments != null) {
mArguments.setClassLoader(classLoader);
}
mInstance = factory.instantiate(classLoader, mClassName);
mInstance.setArguments(mArguments); // fragment唯一恢复数据的操作
if (mSavedFragmentState != null) {
mSavedFragmentState.setClassLoader(classLoader);
mInstance.mSavedFragmentState = mSavedFragmentState;
} else {
// When restoring a Fragment, always ensure we have a
// non-null Bundle so that developers have a signal for
// when the Fragment is being restored
mInstance.mSavedFragmentState = new Bundle();
}
mInstance.mWho = mWho;
mInstance.mFromLayout = mFromLayout;
mInstance.mRestored = true;
mInstance.mFragmentId = mFragmentId;
mInstance.mContainerId = mContainerId;
mInstance.mTag = mTag;
mInstance.mRetainInstance = mRetainInstance;
mInstance.mRemoving = mRemoving;
mInstance.mDetached = mDetached;
mInstance.mHidden = mHidden;
mInstance.mMaxState = Lifecycle.State.values()[mMaxLifecycleState];
if (FragmentManagerImpl.DEBUG) {
Log.v(FragmentManagerImpl.TAG, "Instantiated fragment " + mInstance);
}
}
return mInstance;
}

1.2 androidx 1.2.4

恢复时:
首先从FragmentManagerViewModel查找fragment是否有实例。

如果有实例,则恢复FragmentState,这种情况下,我们的fragment的属性还是存在着的。

如果没有实例,则通过反射重新创建Fragment,并且给新的Fragment设置保存的 mArguments

void restoreSaveState(@Nullable Parcelable state) {
// If there is no saved state at all, then there's nothing else to do
if (state == null) return;
FragmentManagerState fms = (FragmentManagerState) state;
if (fms.mActive == null) return;

// Build the full list of active fragments, instantiating them from
// their saved state.
mFragmentStore.resetActiveFragments();
for (FragmentState fs : fms.mActive) {
if (fs != null) {
FragmentStateManager fragmentStateManager;
Fragment retainedFragment = mNonConfig.findRetainedFragmentByWho(fs.mWho);
if (retainedFragment != null) {
if (isLoggingEnabled(Log.VERBOSE)) {
Log.v(TAG, "restoreSaveState: re-attaching retained "
+ retainedFragment);
}
//
fragmentStateManager = new FragmentStateManager(mLifecycleCallbacksDispatcher,
retainedFragment, fs);
} else {
// 在这重新创建了Fragment
fragmentStateManager = new FragmentStateManager(mLifecycleCallbacksDispatcher,
mHost.getContext().getClassLoader(), getFragmentFactory(), fs);
}
Fragment f = fragmentStateManager.getFragment();
f.mFragmentManager = this;
if (isLoggingEnabled(Log.VERBOSE)) {
Log.v(TAG, "restoreSaveState: active (" + f.mWho + "): " + f);
}
fragmentStateManager.restoreState(mHost.getContext().getClassLoader());
mFragmentStore.makeActive(fragmentStateManager);
// Catch the FragmentStateManager up to our current state
// In almost all cases, this is Fragment.INITIALIZING, but just in
// case a FragmentController does something...unique, let's do this anyways.
fragmentStateManager.setFragmentManagerState(mCurState);
}
}

........
}

将mArguments赋值到Fragment中

FragmentStateManager(@NonNull FragmentLifecycleCallbacksDispatcher dispatcher,
@NonNull ClassLoader classLoader, @NonNull FragmentFactory fragmentFactory,
@NonNull FragmentState fs) {
mDispatcher = dispatcher;
// 通过反射重新创建fragment
mFragment = fragmentFactory.instantiate(classLoader, fs.mClassName);
if (fs.mArguments != null) {
fs.mArguments.setClassLoader(classLoader);
}
mFragment.setArguments(fs.mArguments);
.....
}

二、解决:

知道了出现的原因,那么就很好解决了

val f = FragmentA()
f.arguments = Bundle().apply {
putString("sk", "bbbb")
}

三、使用kotlin优化代码

使用扩展函数 + 委托属性可更加优雅

fun <T> Fragment.putArgument(
key: String,
value: T
) {
if (this.arguments == null) {
this.arguments = Bundle()
}

when (value) {
is Int -> this.requireArguments().putInt(key, value)
is Boolean -> this.requireArguments().putBoolean(key, value)
is String -> this.requireArguments().putString(key, value)
is CharSequence -> this.requireArguments().putCharSequence(key, value)
is Float -> this.requireArguments().putFloat(key, value)
is Long -> this.requireArguments().putLong(key, value)
is Bundle -> this.requireArguments().putBundle(key, value)
is Serializable -> this.requireArguments().putSerializable(key, value)
is Parcelable -> this.requireArguments().putParcelable(key, value)
is Char -> this.requireArguments().putChar(key, value)
is Byte -> this.requireArguments().putByte(key, value)
else -> error("不支持的类型, $value")
}
}

fun <T> Fragment.getArgument(key: String): T {
return this.arguments?.get(key) as T
}

使用

// 设置数据
class ActivityA :Activity{
fun startFragment(){
val f = FragmentA()
f.putArgument("book","小黄书")
}
}


class FragmentA: Fragment{
val bk: String by lazy {getArgument<String>("book")} // 使用懒加载方式,保证只会获取一次数据
}