Android之Fragment分析

郭鹏

这篇文章是在阅读Android Doc中关于Fragment的部分时完成的,部分内容直接翻译自原文档,部分内容为实际经验总结。

概念与定义:

Fragment是作为一个比Activity更轻量的组件所存在的,可以把它当作activity的一个模块,同时它拥有自己的生命周期,更像是“sub activity”;Fragment的优势在于它的重用性与灵活性,你可以在一个Activity中使用多个Fragment组成整个UI,也可以在多个activity之间重用一个Fragment,因此在开发Fragment时也需要充分考虑到它的独立性,将其作为一个模块来开发,减少与外界的耦合(详见“Fragment之间的通讯”一节);Fragment与Activity是息息相关的,它必须嵌入在Activity中使用,不能独立的使用,它的生命周期也依赖于所嵌入的Activity的生命周期。

生命周期总结:

1、在Activity中添加Fragment的方法包括代码动态添加和在Layout中直接布局两种方法(详见“添加Fragment的方法”一节),两种方法添加的Fragment与其嵌入的Activity生命周期如下:

2、若只执行new Fragment而不将其加入Activity,从log可以看出,Fragment的生命周期不会执行:
3、动态添加Fragment1之后,执行replace,使用Fragment2替换,其中对比了使用或不使用addToBackStack的情况:
从表中可以看见,使用了addToBackStack的replace,对于之前的Fragment1不会将其销毁,只执行完了onDestoryView,而没有执行onDestory和onDetach。当当前fragment2点击back之后会进行fragment2的销毁,同时会恢复fragment1。再次点击back退出activity时会执行整个activity销毁流程。 同理,此时如果将replace换为removeFragment,添加addToBackStack之后,其销毁流程同replace相同,也只会执行到onDestoryView。

添加Fragment的方法总结:

1、在运行时添加。
即是在Activity的onCreate中实例化Fragment,然后使用fragmentTrasaction添加至一个FragmentContainer容器。 这里需要注意的是对Fragment的实例化,Fragment必须包含一个无参的public的构造函数,因为系统会经常在恢复fragment状态时重新实例化fragment,此时就会调用这个无参的构造函数,因此如果没这个空的构造函数,app可能会在某个case下发生运行时异常。因此,最好不要用构造函数传参的方法将数据赋给Fragment,而是调用Fragment默认的构造函数,然后使用Fragment.setArguments()的方法传递数据。

错误的方法:

private int mCount;  
//Do not do this
public Fragment1(int count) {  
    mCount = count;
}
如果非要在实例化时传递参数可以写一个newInstance的方法:

// like this
public static Fragment1 newInstance(int count){  
    Fragment1 fragment = new Fragment1();
    Bundle b = new Bundle();
    b.putInt("count", count);
    fragment.setArguments(b);
    return fragment;
}
以下代码是运行时添加Fragment的标准用法,来自Android Doc-Training-Building a Flexible UI with Fragments:
import android.os.Bundle;  
import android.support.v4.app.FragmentActivity;

public class MainActivity extends FragmentActivity {  
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.news_articles);`

        // Check that the activity is using the layout version with
        // the fragment_container FrameLayout
        if (findViewById(R.id.fragment_container) != null) {

            // However, if we're being restored from a previous state,
            // then we don't need to do anything and should return or else
            // we could end up with overlapping fragments.
            if (savedInstanceState != null) {
                return;
            }

            // Create an instance of ExampleFragment
            HeadlinesFragment firstFragment = new HeadlinesFragment();

            // In case this activity was started with special instructions from an Intent,
            // pass the Intent's extras to the fragment as arguments
            firstFragment.setArguments(getIntent().getExtras());

            // Add the fragment to the 'fragment_container' FrameLayout
            getSupportFragmentManager().beginTransaction()
                    .add(R.id.fragment_container, firstFragment).commit();
        }
    }

其中红字部分代码需要特别注意,注释是说,如果我们已经有了一个之前的savedInstanceState状态,就不需要再做addFragment的操作了,因为这样会造成一个重叠的Fragment;实测中发现,其原因是savedInstanceState中保存了fragmentManager的状态,当App在后台被杀掉时(可以使用AutoTaskKiller模拟),fragmentManager中的Fragment实际没被销毁,再次启动Activity执行onCreate时,如果没有上述代码就会又向Activity中添加一个重复的Fragment,这样就会造成Fragment的重叠;以下是没有红字部分代码的实际测试的数据,当App被杀再重新启动后,View树下显示fragment有两个,同时生命周期代码会打印两次: 如果再次杀掉App,再次重启,Fragment又会多添加一个,变成三个... 如果当这个Fragment功能很多,所带的View很庞大的时候,其实会造成很严重的内存泄露,因此在实际开发中应该十分注意这个问题,目前总结出避免该问题的方法有三种: ①和官方文档做法一样的判断savedInstanceState是不是null,不是null的时候不去添加fragment ②每处需要使用addFragment的代码都先将其find出来,然后判断是不是null,不是null再继续添加 ③都统一使用repalceFragment方法替代addFragment方法 以上三种方法只有第一种是官方的标准写法,后面两种方法在简单的测试工程中也能实验通过,避免上述问题。

2、直接布局在layout文件当中。
这种方法添加的Fragment,官方文档说不能被remove或者replace。实测中发现在layout布局中的fragment虽然可以调用remove和replace,被remove或者replace的Fragment的销毁生命周期也同样会执行,但实际Activity中的Fragment的View还在,没有被remove或者replace;特别是使用replace时,可能显示效果上和预期达到的效果是一样,但实际被替换的之前的fragment的view同样没被删除,如图,EditText是之前的fragment的View,它没有被替换掉,这样也相当于有了内存泄露:

因此,如果使用直接布局在layout文件当中的fragment时,我们应该去避免调用remove和replace方法来操作这个fragment。

Fragment之间的通信:

为了达到重用Fragment UI组件的目的,应该把每个Fragment设计为完全独立、模块化的类。当Fragment之间需要通信时,应该在相关的Activity中完成,两个Fragment之间绝对不要独立通信。 为了使Fragment和Activity之间进行通信,你可以在Fragment中定义一个接口,然后在Activity中实现它: 以下代码来自Android Doc-Training-Building a Flexible UI with Fragments:

public class HeadlinesFragment extends ListFragment {  
    OnHeadlineSelectedListener mCallback;

    // Container Activity must implement this interface
    public interface OnHeadlineSelectedListener {
        public void onArticleSelected(int position);
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);

        // This makes sure that the container activity has implemented
        // the callback interface. If not, it throws an exception
        try {
            mCallback = (OnHeadlineSelectedListener) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString()
                    + " must implement OnHeadlineSelectedListener");
        }
    }

    ...
}

以上代码,在HeadlinesFragment中定义了OnHeadlineSelectedListener接口,然后在onAttach中对实现该接口的activity进行注册(因为onAttach时会传来activity的实例),这样就不用Activity手动去调用setListener的方式注册,每个使用该Fragment的Activity只要实现了该接口就能自动被注册,同时也不必要remove这个listener,因为当activity被destory后,会自动进行对fragment进行销毁。 然后,注册了该接口的Activity就能收到该HeadlinesFragment的回调消息。

public static class MainActivity extends Activity  
        implements HeadlinesFragment.OnHeadlineSelectedListener{
    ...

    public void onArticleSelected(int position) {
        // The user selected the headline of an article from the HeadlinesFragment
        // Do something here to display that article
    }
}

最后就能在这里的回调中去执行对另外一个Fragment的操作了。