Fragment系列(二)

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到主线程的消息队列里。

(第二篇到此结束)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值