Fragment经常遇到的坑及解决方案
先规定一个术语:“内存重启”,就是app运行在后台的时候,系统资源紧张的时候导致把app的资源全部回收(杀死app的进程),这时再把app从后台返回到前台时,app会重启。
第一个坑
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at android.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1331)
at android.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1349)
at android.app.BackStackRecord.commitInternal(BackStackRecord.java:735)
at android.app.BackStackRecord.commit(BackStackRecord.java:711)
at com.dighammer.kisson.testfragment.MainActivity$1.run(MainActivity.java:30)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5273)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:908)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:703)
遇到的崩溃信息,下面代码能够复现:
public class MainActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
FragmentTransaction transaction = getFragmentManager().beginTransaction();
TestFragment fragment = new TestFragment();
transaction.add(R.id.container, fragment);
transaction.commit();
}
}, 5000);
}
当程序界面显示出来后,按home键,过接近5秒后,就会出现该奔溃日志。原因是FragmentTransaction中的commit方法必须在onSaveInstanceState之前调用。那么如何解决了?在FragmentTransaction中提供了commitAllowingStateLoss方法,通过调用该方法就不用关心Activity的状态是否保存。
第二个坑
代码如下:
public class MainActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FragmentTransaction transaction = getFragmentManager().beginTransaction();
TestFragment fragment = new TestFragment();
transaction.add(R.id.container, fragment);
transaction.commit();
fragment.setText("kisson chan");
}
}
运行后,会出现如下的崩溃信息:
java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.TextView.setText(java.lang.CharSequence)' on a null object reference
at com.dighammer.kisson.testfragment.TestFragment.setText(TestFragment.java:53)
at com.dighammer.kisson.testfragment.MainActivity.onCreate(MainActivity.java:18)
at android.app.Activity.performCreate(Activity.java:6041)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1109)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2283)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2392)
at android.app.ActivityThread.access$800(ActivityThread.java:154)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1308)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5273)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:908)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:703)
这里是空指针异常,TestFragment中的TextView还未被初始化。但是,如果我们通过Activity布局中声明Fragment则不会报错,代码如下。
public class MainActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TestFragment fragment = (TestFragment)getFragmentManager().findFragmentById(R.id.test);
fragment.setText("kisson");
}
}
原因其实很简单,通过Activity布局中声明Fragment,Fragment的初始化以及其布局中控件的初始化都发生在Activity的setContentView方法中。所以在setContentView方法调用之后,我们可以直接操作Fragment中的控件。但是通过代码将Fragment添加到ViewGroup的方式,并不能在Activity的onCreate方法中去操作Fragment中的控件,只能等到onCreate完成之后调用。如果必须要在Activity的onCreate方法中操作Fragment中的控件,这里我提供一个参考办法。
public class TestFragment extends Fragment{
private View mRootView;
private TextView mTextView;
private String mTextStr;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mRootView = inflater.inflate(R.layout.fragment_test,container,false);
return mRootView;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mTextView = (TextView)mRootView.findViewById(R.id.text);
if (!TextUtils.isEmpty(mTextStr)){
mTextView.setText(mTextStr);
}
}
/**
* 如果mTextView为空,即mTextView还未被初始化,那么将text赋值给mTextStr,等到onActivityCreated调用后再setText
* 如果mTextView不为空,则直接调用TextView的setText方法进行赋值
* */
public void setText(String text){
if (mTextView == null){
mTextStr = text;
}
else {
mTextView.setText(text);
}
}
}
第三个坑
代码如下:
public class TestFragment extends Fragment{
private View mRootView;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mRootView = inflater.inflate(R.layout.fragment_test,container,false);
return mRootView;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
((MainActivity) (getActivity())).print();
}
},5000);
}
}
public class MainActivity extends FragmentActivity {
private static final String TAG = "KissonChan";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FragmentTransaction transaction = getFragmentManager().beginTransaction();
TestFragment fragment = new TestFragment();
transaction.add(R.id.container,fragment);
transaction.commit();
}
public void print() {
Log.d(TAG, "KissonChan");
}
}
当程序Activity显示出来后,接着按下返回键结束Activity,静待五秒后,出现以下崩溃日志。
Process: com.dighammer.kisson.testfragment, PID: 15870
java.lang.NullPointerException: Attempt to invoke virtual method 'void com.dighammer.kisson.testfragment.MainActivity.print()' on a null object reference
at com.dighammer.kisson.testfragment.TestFragment$1.run(TestFragment.java:41)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5273)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:908)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:703)
因为MainActivity已经被销毁,五秒钟后在TestFragment实例中会调用MainActivity的print方法,所以抛出空指针异常。这里很奇怪,Activity已经被销毁了,为什么Fragment还能继续苟活。这是因为Fragment并不会随着Activity的回收而被系统回收,所有被创建的Fragment都会保存在一个Bundle里面。所以会导致以上崩溃。所以在一些延时操作或者线程操作中调用getActivity最好做下为空判断!
第四个坑
有时你使用getActivity()直接得到null,或者平时运行没啥问题,但是发生”内存重启”后,就会崩溃,然后会发现getActivity()变成null了,报空指针异常。
大多数情况下的原因:你在调用了getActivity()时,当前的Fragment已经onDetach()了宿主Activity。
比如:你在pop了Fragment之后,该Fragment的异步任务仍然在执行,并且在执行完成后调用了getActivity()方法,这样就会空指针。
解决方法:
在Fragment基类里设置一个Activity mActivity的全局变量,在onAttach(Activity activity)里赋值,使用mActivity代替getActivity(),保证Fragment即使在onDetach后,仍持有Activity的引用(有引起内存泄露的风险,但是异步任务没停止的情况下,本身就可能已内存泄漏,相比Crash,这种做法“安全”些),即:
protected Activity mActivity;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
this.mActivity = activity;
}
/**
* 如果你用了support 23的库,上面的方法会提示过时,有强迫症的小伙伴,可以用下面的方法代替
*/
@Override
public void onAttach(Context context) {
super.onAttach(context);
this.mActivity = (Activity)context;
}
第五个坑
有时运行代码会报这个异常:Can not perform this action after onSaveInstanceState
在你离开当前Activity等情况下,系统会调用onSaveInstanceState()帮你保存当前Activity的状态、数据等,直到再回到该Activity之前(onResume()之前),你使用commit()提交了Fragment事务,就会抛出该异常!
解决方案:
在重新回到该Activity的时候(onResumeFragments()或onPostResume()),再执行该事务!
第六个坑
有时使用fragment会发生重叠现象
如果你add()了几个Fragment,使用show()、hide()方法控制,比如微信、QQ的底部tab等情景,如果你什么都不做的话,在“内存重启”后回到前台,app的这几个Fragment界面会重叠。
原因是FragmentManager帮我们管理Fragment,当发生“内存重启”,他会从栈底向栈顶的顺序一次性恢复Fragment;但是因为没有保存Fragment的mHidden属性,默认为false,即show状态,所以所有Fragment都是以show的形式恢复,我们看到了界面重叠。
还有一种场景,add和replace都有可能造成重叠: 在onCreate中加载Fragment,并且没有判断saveInstanceState==null,导致重复加载了同一个Fragment导致重叠。(PS:replace情况下,如果没有加入回退栈,则不判断也不会造成重叠,但建议还是统一判断下)
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
// 在页面重启时,Fragment会被保存恢复,而此时再加载Fragment会重复加载,导致重叠 ;
if(saveInstanceState == null){
// 正常情况下去 加载根Fragment
}
}
解决方案:
下面有三个解决方案:
第一个:通过findFragmentByTag
即在add()或者replace()时绑定一个tag,一般我们是用fragment的类名作为tag,然后在发生“内存重启”时,通过findFragmentByTag找到对应的Fragment,并hide()需要隐藏的fragment。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity);
TargetFragment targetFragment;
HideFragment hideFragment;
if (savedInstanceState != null) { // “内存重启”时调用
targetFragment = getSupportFragmentManager().findFragmentByTag(TargetFragment.class.getName);
hideFragment = getSupportFragmentManager().findFragmentByTag(HideFragment.class.getName);
// 解决重叠问题
getFragmentManager().beginTransaction()
.show(targetFragment)
.hide(hideFragment)
.commit();
}else{ // 正常时
targetFragment = TargetFragment.newInstance();
hideFragment = HideFragment.newInstance();
getFragmentManager().beginTransaction()
.add(R.id.container, targetFragment, targetFragment.getClass().getName())
.add(R.id,container,hideFragment,hideFragment.getClass().getName())
.hide(hideFragment)
.commit();
}
}
如果你想恢复到用户离开时的那个Fragment的界面,你还需要在onSaveInstanceState(Bundle outState)里保存离开时的那个可见的tag或下标,在onCreate“内存重启”代码块中,取出tag/下标,进行恢复。
第二个:通过getSupportFragmentManager().getFragments()恢复
通过getFragments()可以获取到当前FragmentManager管理的栈内所有Fragment。
Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity);
TargetFragment targetFragment;
HideFragment hideFragment;
if (savedInstanceState != null) { // “内存重启”时调用
List<Fragment> fragmentList = getSupportFragmentManager().getFragments();
for (Fragment fragment : fragmentList) {
if(fragment instanceof TartgetFragment){
targetFragment = (TargetFragment)fragment;
}else if(fragment instanceof HideFragment){
hideFragment = (HideFragment)fragment;
}
}
// 解决重叠问题
getFragmentManager().beginTransaction()
.show(targetFragment)
.hide(hideFragment)
.commit();
}else{ // 正常时
targetFragment = TargetFragment.newInstance();
hideFragment = HideFragment.newInstance();
// 这里add时,tag可传可不传
getFragmentManager().beginTransaction()
.add(R.id.container)
.add(R.id,container,hideFragment)
.hide(hideFragment)
.commit();
}
}
第三个:如果当前使用版本小于24.0.0,则需要下面代码:
定义一个基类管理
public class BaseFragment extends Fragment {
private static final String STATE_SAVE_IS_HIDDEN = "STATE_SAVE_IS_HIDDEN";
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
...
if (savedInstanceState != null) {
boolean isSupportHidden = savedInstanceState.getBoolean(STATE_SAVE_IS_HIDDEN);
FragmentTransaction ft = getFragmentManager().beginTransaction();
if (isSupportHidden) {
ft.hide(this);
} else {
ft.show(this);
}
ft.commit();
}
@Override
public void onSaveInstanceState(Bundle outState) {
...
outState.putBoolean(STATE_SAVE_IS_HIDDEN, isHidden());
}
}
在Activity中同时需要判断saveInstanceState是否为空
public class MainActivity ... {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
...
// 这里一定要在save为null时才加载Fragment,Fragment中onCreateView等生命周里加载根子Fragment同理
// 因为在页面重启时,Fragment会被保存恢复,而此时再加载Fragment会重复加载,导致重叠
if(saveInstanceState == null){
// 正常情况下去 加载根Fragment
}
}
}
ps:24.0.0以后的版本,该Bug已被修复。
第七个坑
多个Fragment同时出栈带来的Bug。
在Fragment库中如下4个方法是有BUG的:
- popBackStack(String tag,int flags)
- popBackStack(int id,int flags)
- popBackStackImmediate(String tag,int flags)
- popBackStackImmediate(int id,int flags)
上面4个方法作用是,出栈到tag/id的fragment,即一次多个Fragment被出栈。
FragmentManager栈中管理fragment下标位置的数组ArrayList mAvailIndeices的BUG
下面的方法FragmentManagerImpl类方法,产生BUG的罪魁祸首是管理Fragment栈下标的mAvailIndeices属性:
void makeActive(Fragment f) {
if (f.mIndex >= 0) {
return;
}
if (mAvailIndices == null || mAvailIndices.size() <= 0) {
if (mActive == null) {
mActive = new ArrayList<Fragment>();
}
f.setIndex(mActive.size(), mParent);
mActive.add(f);
} else {
f.setIndex(mAvailIndices.remove(mAvailIndices.size()-1), mParent);
mActive.set(f.mIndex, f);
}
if (DEBUG) Log.v(TAG, "Allocated fragment index " + f);
}
上面代码最终导致了栈内顺序不正确的问题,如下图:
对mAvailIndices进行一次Collections.reverseOrder()降序排序,保证栈内Fragment的index的正确。
public class FragmentTransactionBugFixHack {
public static void reorderIndices(FragmentManager fragmentManager) {
if (!(fragmentManager instanceof FragmentManagerImpl))
return;
FragmentManagerImpl fragmentManagerImpl = (FragmentManagerImpl) fragmentManager;
if (fragmentManagerImpl.mAvailIndices != null && fragmentManagerImpl.mAvailIndices.size() > 1) {
Collections.sort(fragmentManagerImpl.mAvailIndices, Collections.reverseOrder());
}
}
}
使用方法就是通过popBackStackImmediate(tag/id)多个Fragment后,调用
hanler.post(new Runnable(){
@Override
public void run() {
FragmentTransactionBugFixHack.reorderIndices(fragmentManager));
}
});
popBackStack的坑
popBackStack和popBackStackImmediate的区别在于前者是加入到主线队列的末尾,等其它任务完成后才开始出栈,后者是立刻出栈。
如果你popBackStack多个Fragment后,紧接着beginTransaction() add新的一个Fragment,接着发生了“内存重启”后,你再执popBackStack(),app就会Crash,解决方案是postDelay出栈动画时间再执行其它事务,但是根据我的观察不是很稳定。我的建议是:如果你想出栈多个Fragment,你应尽量使用popBackStackImmediate(tag/id),而不是popBackStack(tag/id),如果你想在出栈后,立刻beginTransaction()开始一项事务,你应该把事务的代码post/postDelay到主线程的消息队列里。
(第二篇到此结束)