Fragment Study

介绍

  • fragment 必须始终嵌入在Actvity中,其生命周期直接受宿主Activity生命周期影响, 例如,当 Activity 暂停时,其中的所有片段也会暂停;当 Activity 被销毁时,所有片段也会被销毁。 不过,当 Activity 正在运行(处于已恢复生命周期状态)时,您可以独立操纵每个片段,如添加或移除它们。
  • 不过,片段并非必须成为 Activity 布局的一部分;您还可以将没有自己 UI 的片段用作 Activity 的不可见工作线程

onCreateView的两种方式

` public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    enableLoadHelper(true);
    if (savedInstanceState != null) {
        fofItem = (AtlasAssetInfo.FOFItem) savedInstanceState.getSerializable(AtlasAssetDetailActivity.ATLAS_ORDER);
    }
    return onCreateView(R.layout.fragment_atlas_asset_detail, inflater, container);
}`

官方写法:

` public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    enableLoadHelper(true);
    if (savedInstanceState != null) {
        fofItem = (AtlasAssetInfo.FOFItem) savedInstanceState.getSerializable(AtlasAssetDetailActivity.ATLAS_ORDER);
    }
    return onCreateView(R.layout.fragment_atlas_asset_detail, inflater, container);
}`

参考:
https://developer.android.com/guide/components/fragments.html?hl=zh-cn#Lifecycle

Fragemnet的坑

介绍坑之前先介绍一个概念:内存重启
安卓app有一种特殊情况,就是 app运行在后台的时候,系统资源紧张的时候导致把app的资源全部回收(杀死app的进程),这时把app再从后台返回到前台时,app会重启。这种情况下文简称为:“内存重启”。(屏幕旋转等配置变化也会造成当前Activity重启,本质与“内存重启”类似)

先给出一张fragment详细的生命周期图

图片来自http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0605/2996.html

下面查阅资料结合自己实际中遇到困难总结了
有些场景都是开启了不保留活动

geActvity()空指针

原因:

能你遇到过getActivity()返回null,或者平时运行完好的代码,在“内存重启”之后,调用getActivity()的地方却返回null,报了空指针异常。
大多数情况下的原因:你在调用了getActivity()时,当前的Fragment已经onDetach()了宿主Activity。
比如:你在pop了Fragment之后,该Fragment的异步任务仍然在执行,并且在执行完成后调用了getActivity()方法,这样就会空指针。

解决方法:

在Fragment基类里设置一个Activity activity的全局变量,在onAttach(Activity activity)里赋值,使用mActivity代替getActivity(),保证Fragment即使在onDetach后,仍持有Activity的引用(有引起内存泄露的风险,但是相比空指针闪退,这种做法“安全”些)

`@Override
public void onAttach(Context context) {
super.onAttach(context);
this.mActivity = (Activity)context;
}`

或者如果该Context需要在Activity被销毁后还存在,则使用getActivity().getApplicationContext()

臭名昭著的Can not perform this action after onSaveInstanceState

原因:

在你离开当前Activity等情况下,系统会调用onSaveInstanceState()调用时机是onPause之后onStop之前帮你保存当前Activity&Fragment的一些状态、数据等,而在离开后(onSaveInstanceState()已经被执行),你又去执行Fragment的相关事务方法后,就会抛出该异常!

解决方式:

解决方法2个:

1、(不推荐)该事务使用commitAllowingStateLoss()方法提交,但是有可能导致该次提交无效!

2、(推荐)在重新回到该Activity的时候(比如onStart里),再执行该事务!

Note:上次的首页广告MainActivity中需要重新修改

================================================================================

The exception was thrown because you attempted to commit a FragmentTransaction after the activity’s state had been saved, resulting in a phenomenon known as Activity state loss

When the framework calls onSaveInstanceState(), it passes the method a Bundle object for the Activity to use to save its state, and the Activity records in it the state of its dialogs, fragments, and views. When the method returns, the system parcels the Bundle object across a Binder interface to the System Server process, where it is safely stored away. When the system later decides to recreate the Activity, it sends this same Bundle object back to the application, for it to use to restore the Activity’s old state.

So why then is the exception thrown? Well, the problem stems from the fact that these Bundle objects represent a snapshot of an Activity at the moment onSaveInstanceState() was called, and nothing more. That means when you call FragmentTransaction#commit() after onSaveInstanceState() is called, the transaction won’t be remembered because it was never recorded as part of the Activity’s state in the first place. From the user’s point of view, the transaction will appear to be lost, resulting in accidental UI state loss. In order to protect the user experience, Android avoids state loss at all costs, and simply throws an IllegalStateException whenever it occurs.

参考
http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html(已翻译)

Fragment重叠问题

源码分析:

Step1:Fragment恢复保存机制

我们知道Activity中有个onSaveInstanceState()方法,该方法在app进入后台、屏幕旋转前、跳转下一个Activity等情况下会被调用,此时系统帮我们保存一个Bundle类型的数据,我们可以根据自己的需求,手动保存一些例如播放进度等数据,而后如果发生了页面重启,我们可以在onRestoreInstanceState()或onCreate()里get该数据,从而恢复播放进度等状态。(关于onSaveInstance参见最后)

而产生Fragment重叠就是页面保存机制有关,大致原因是在页面重启前,帮我们保存了Fragment的状态,但是在重启恢复时,视图的可见状态并没有帮我们保存,而fragment默认状态是show状态,因此产生了页面重叠

`public class FragmentActivity extends ... {
final FragmentController mFragments = FragmentController.createController(new HostCallbacks());

protected void onCreate(@Nullable Bundle savedInstanceState) {
    ...省略
    if (savedInstanceState != null) {
        Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
        mFragments.restoreAllState(p, nc != null ? nc.fragments : null);
    }
}

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    Parcelable p = mFragments.saveAllState();
    ...省略
}
}`

可以看出fragmentActvity确实做了fragment的状态的保存,从上面可以看出最终实现还是在mFragments进行restoreAllState、saveAllState()的处理,而mFragments是FragmentController,它是一个Controller,内部通过FragmentHostCallback间接控制FragmentManagerImpl,因此具体实现还是在FragmentManagerImpl中

`final class FragmentManagerImpl extends FragmentManager {
Parcelable saveAllState() {
    ...省略 详细保存过程
    FragmentManagerState fms = new FragmentManagerState();
    fms.mActive = active;
    fms.mAdded = added;
    fms.mBackStack = backStack;
    return fms;
}

void restoreAllState(Parcelable state, List<Fragment> nonConfig) {
    // 恢复核心代码
    FragmentManagerState fms = (FragmentManagerState)state;
    FragmentState fs = fms.mActive[i];
    if (fs != null) {
        Fragment f = fs.instantiate(mHost, mParent);
    }
}
}`

从源码可以看出通过onSaveAllState可以看出保存状态其实是通过FragmentManager#FragmentManagerState()来保存了fragment的状态、回退栈、下标等,而在restoreAllState中通过Fragment#FragmentState的instantiate方法恢复了fragment的实例,继续看一下这两个类

`final class FragmentManagerState implements Parcelable {
FragmentState[] mActive;           // Fragment状态
int[] mAdded;                      // 所处Fragment栈下标
BackStackState[] mBackStack;       // 回退栈状态
...
}`

我们只看Fragment#FragmentState,它也实现了Parcelable,保存了Fragment的类名、下标、id、Tag、ContainerId以及Arguments等数据:

`final class FragmentState implements Parcelable {
final String mClassName;
final int mIndex;
final boolean mFromLayout;
final int mFragmentId;
final int mContainerId;
final String mTag;
final boolean mRetainInstance;
final boolean mDetached;
final Bundle mArguments;
...

//  在FragmentManagerImpl的restoreAllState()里被调用
public Fragment instantiate(FragmentHostCallback host, Fragment parent) {
    ...省略
    mInstance = Fragment.instantiate(context, mClassName, mArguments);
}
}`

至此我们就清楚了,FragmentActvity通过FragmentState来保存Fragment实例

Step2 发生重叠的根本原因?????

在Fragment#FragmentState中并没有保存Hidden字段,因此在以add方式加载Fragment的场景下,系统在恢复Fragment时,mHidden=false,即show状态,这样在页面重启后,Activity内的Fragment都是以show状态显示的,而如果你不进行处理,那么就会发生Fragment重叠现象!

Step3 为什么重复replace|add Fragment 或者 使用show , hide控制Fragment会导致重叠?

  • 重复replace|add Fragment

一般情况下,我们会在Activity的onCreate()里或者Fragment的onCreateView()里加载根Fragment,如果在这里没有进行页面重启的判断的话,就可能导致重复加载Fragment引起重叠,正确的写法应该是先判断 saveInstanceState是否为空

`@Override
 protected void onCreate(@Nullable Bundle savedInstanceState) {
  ...
  // 判空, Fragment同理
  if(saveInstanceState == null){
        // 这里replace或add 根Fragment
  }
}`
  • 使用show , hide控制Fragment

我们使用show(),hide()时,都是使用add的方式加载Fragment的,add配合hide使Fragment的视图改变为GONE状态;而replace是销毁Fragment 的视图。
当页面重启时add的fragment会走全部生命周期,创建生命周期,而repleace的非栈顶的fragment不会走生命周期,只有back时候才会走生命周期,创建视图在使用replace加载Fragment时,页面重启后,Fragment视图都还没创建,所以mHidden没有意义,不会发生重叠现象;
而在使用add加载时,视图是存在的并且叠加在一起,页面重启后 mHidden=false,所有的Fragment都会是show状态显示出来(即VISIBLE),从而造成了Fragment重叠!

http://www.jianshu.com/p/78ec81b42f92

step4 彻底解决

1、加载根fragment时候需要进行判断

`public class MainActivity ... {

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    ...
    // 这里一定要在save为null时才加载Fragment,Fragment中onCreateView等生命周里加载根子Fragment同理
    // 因为在页面重启时,Fragment会被保存恢复,而此时再加载Fragment会重复加载,导致重叠
    if(saveInstanceState == null){
          // 这里加载根Fragment
    }
}
}`

2、重写BaseFragment

`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());
}
}`

参考 http://www.jianshu.com/p/c12a98a36b2b

一般满足下面2个条件才可能会发生重叠:

1、发生了页面重启(旋转屏幕、内存不足等情况被强杀重启)。

2、重复replace|add Fragment 或者 使用show , hide控制Fragment;

大致原因就是系统在页面重启前,帮我们保存了Fragment的状态,但是在重启后恢复时,视图的可见状态没帮我们保存,而Fragment默认的是show状态,所以产生了Fragment重叠现象。

解决方式:

http://www.jianshu.com/p/c12a98a36b2b

要是使用fragmentpageAdapter

核心是FragmentState没帮我们保存Hidden状态,那就我们自己来保存,在页面重启后,我们自己来决定Fragment是否显示!
解决思路转变了,由Activity/父Fragment来管理子Fragment的Hidden状态转变为 由Fragment自己来管理自己的Hidden状态!

`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());
}
}`

其实是因为加载根Fragment时没有经过判断的原因,当在类似onCreate等初始化生命周期里加载根Fragment(即第一个Fragment)时,需要下面的判断,避免重复加载相同的Fragment:

`public class MainActivity ... {

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    ...
    // 这里一定要在save为null时才加载Fragment,Fragment中onCreateView等生命周里加载根子Fragment同理
    // 因为在页面重启时,Fragment会被保存恢复,而此时再加载Fragment会重复加载,导致重叠
    if(saveInstanceState == null){
          // 这里加载根Fragment
    }
}
}`

解决方式二:

` @Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    if (savedInstanceState != null) {
        List<Fragment> fragments = getSupportFragmentManager().getFragments();

        if (fragments != null && fragments.size() > 0) {
            boolean showFlag = false;

            FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
            for (int i = fragments.size() - 1; i >= 0; i--) {
                Fragment fragment = fragments.get(i);
                if (fragment != null) {
                    if (!showFlag) {
                        ft.show(fragments.get(i));
                        showFlag = true;
                    } else {
                        ft.hide(fragments.get(i));
                    }
                }
            }
            ft.commit();
        }
    }
}`

恶心的Activity重建以及恢复其Fragment

一个思路就是阻止系统恢复Fragment,我们可以自己来加载,因为重建也会走到Activity的onCreate,所以我们有理由重走一遍初始化流程。怎么阻止呢,就是在FragmentActivity保存所有Fragment状态前把Fragment从FragmentManager中移除掉。
@Override public void onSaveInstance(Bundle out) { FragmentTransaction ft = getSupportFragmentManager().benginTransaction(); ft.remove(frag); ft.commitAllowStateLoss(); super.onSaveInstance(out); }
http://toughcoder.net/blog/2015/04/30/android-fragment-the-bad-parts/

###FragmentPagerAdapter与FragmentStatePagerAdapter区别

http://www.cnblogs.com/lianghui66/p/3607091.html(已做总结)

##补充一下InstanceState详解:

http://www.cnblogs.com/hanyonglu/archive/2012/03/28/2420515.html

##让多个Fragment 切换时不重新实例化

通常是用repleace方法完成,该方法实际是将remove和add方法合在一起

`public void switchContent(Fragment fragment) {
if(mContent != fragment) {
    mContent = fragment;
    mFragmentMan.beginTransaction()
        .setCustomAnimations(android.R.anim.fade_in, R.anim.slide_out)
        .replace(R.id.content_frame, fragment) // 替换Fragment,实现切换
        .commit();
}
}`

但是这样做有一个问题,每次切换的时候Fragment都会实例化,重新加载一遍数据,这样非常消耗性能和用户的数据流量,如何让多个Fragment彼此切换时不重新实例化?
其实replace方法只是在上一个Fragment不再需要时采用的简便方法,正确的切换方式是add(),切换时hide,add另外一个Fragment,再次切换时只需要hide当前,show另外一个,这样就可以做到多个Fragment切换时不重新实例化。

`public void switchContent(Fragment from, Fragment to) {
if (mContent != to) {
    mContent = to;
    FragmentTransaction transaction = mFragmentMan.beginTransaction().setCustomAnimations(
            android.R.anim.fade_in, R.anim.slide_out);
    if (!to.isAdded()) {    // 先判断是否被add过
        transaction.hide(from).add(R.id.content_frame, to).commit(); // 隐藏当前的fragment,add下一个到Activity中
    } else {
        transaction.hide(from).show(to).commit(); // 隐藏当前的fragment,显示下一个
    }
}
}`

参考:
https://yrom.net/blog/2013/03/10/fragment-switch-not-restart/

##关于Fragment的懒加载

情景:一个Actvity里面可能会以viewPager(或者其它容器)与多个Fragment来组合使用,而如果每个Fragment都需要去加载数据、或者从本地加载、网络加载,那么在这个activity刚创建的时候就需要初始化大量资源,那么如何做到当切换到这个fragment的时候才去初始化?

方法:
利用setUserVisibleHint

`public abstract class LazyFragment extends Fragment {
protected boolean isVisible;
/**
 * 在这里实现Fragment数据的缓加载.
 * @param isVisibleToUser
 */
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
    super.setUserVisibleHint(isVisibleToUser);
    if(getUserVisibleHint()) {
        isVisible = true;
        onVisible();
    } else {
        isVisible = false;
        onInvisible();
    }
}
protected void onVisible(){
    lazyLoad();
}
protected abstract void lazyLoad();
protected void onInvisible(){}
}`

在这里主要增加了3个方法

  • onVisible 即fragment被设置为不可见时调用
  • lazyLoad抽象方法,在onVisible中调用

使用如下:
public class OpenResultFragment extends LazyFragment{ // 标志位,标志已经初始化完成。 private boolean isPrepared; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Log.d(LOG_TAG, "onCreateView"); View view = inflater.inflate(R.layout.fragment_open_result, container, false); //XXX初始化view的各控件 isPrepared = true; lazyLoad(); return view; } @Override protected void lazyLoad() { if(!isPrepared || !isVisible) { return; } //填充各控件的数据 } }
参考:
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2014/1021/1813.html

Useage

AtlasAssetDetailActivity#addFragment

`public class AtlasAssetDetailActivity extends BaseActivity {

public static final String ATLAS_ORDER = "atlas_order";

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_atlas_asset_detail);
    addFragment(AtlasAssetDetailFragment.newInstance(getIntent().getExtras()));
}

public void addFragment(Fragment fragment) {
    FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
    Fragment oldFragment = getSupportFragmentManager().findFragmentById(R.id.container);
    if (oldFragment != null) {
        transaction.hide(oldFragment);
        transaction.addToBackStack(null);
        transaction.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out);
    }
    transaction.add(R.id.container, fragment);
    transaction.commit();
}


public void popFragment() {
    getSupportFragmentManager().popBackStack();

}

}`

AtlasAssetDetailFragment#中通过selectTab切换hodler达到这种目的

0%