Fragment你真的熟悉吗?看了才知道

来源:互联网 发布:sql表中添加字段 编辑:程序博客网 时间:2024/05/03 11:04

上一篇总结了Activity的那些事,有兴趣的可以前往传送门:Activity你真的熟悉吗?看了才知道,这一篇将给大家全面总结fragment的那些事和那些坑

Fragment是在Android 3.0 (API level 11)开始引入的,它能让你的app在现有基础上性能大幅度提高,并且占用内存降低,同样的界面Activity占用内存比Fragment要多,响应速度Fragment比Activty在中低端手机上快了很多,甚至能达到好几倍,“单Activity + 多Fragment架构”“多模块Activity + 多Fragment架构”应运而生!

本文将从Fragment的生命周期,静态和动态使用Fragment,Fragment回退栈,Fragment事务,以及Fragment遇到内存回收后导致的问题等做个总结。

目录:
一、Fragment的生命周期
二、Fragment的静动态使用方法
三、Fragment的通信方案
四、Fragment的常见错误及解决方法(重点)
1.getActivity()空指针
2.Fragment重叠异常

一、Fragment的生命周期

Fragment必须是依存于Activity而存在的,因此Activity的生命周期会直接影响到Fragment的生命周期,他们存在依赖关系。官网这张图很好的说明了俩者的关系:
  
可以看到Fragment比Activity多了几个额外的生命周期回调函数:
- onAttach(Activity)
当Activity与Fragment发生关联时调用
- onCreateView(LayoutInflater,ViewGroup,Bundle);
创建该Fragment的视图
- onActivityCreate(bundle);
当Activity的onCreate();方法返回时调用
- onDestoryView();
与onCreateView相对应,当改Fragment被移除时调用
- onDetach();
与onAttach()相对应,当Fragment与Activity的关联被取消时调用

注意:除了onCreateView,其他的所有方法如果你重写了,必须调用父类对于该方法的实现。

二、Fragment的使用

静态使用Fragment

这是使用Fragment最简单的一种方式,把Fragment当成普通的控件,直接写在Activity的布局文件中,用布局文件调用Fragment。
步骤:
1. 继承Fragment,重写onCreateView决定Fragment布局。
2. 在Activity中声明此Fragment,就当和普通的View一样。

例子:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent">       <fragment            android:name="com.example.TitleFragment"           android:id="@+id/title"           android:layout_height="45dp"           android:layout_width="match_parent"/>       <fragment            android:layout_below="@id/title"        android:name="com.example.ContentFragment"           android:id="@+id/content"           android:layout_height="fill_parent"               android:layout_width="fill_parent"/></RelativeLayout>

ContentFragment.java文件

public class ContentFragment extends Fragment {    @Override    public View onCreateView(LayoutInflater inflater, ViewGroup container,            Bundle savedInstanceState) {        return inflater.inflate(R.layout.content_fragment, container,false);    }}
动态使用fragment

动态使用fragment是最简单的方式,下面介绍如何动态的添加、更新、以及删除Fragment
为了动态使用Fragment,我们修改一下Actvity的布局文件,中间使用一个FrameLayout

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent" >    <FrameLayout        android:id="@+id/id_content"        android:layout_width="fill_parent"        android:layout_height="fill_parent"/></RelativeLayout>

首先是将上面的布局文件通过setContentView()加载进来.然后通过代码把Fragment添加到FrameLayout中

public class MainActivity extends FragmentActivity{    private ContentFragment exampleFragment;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        requestWindowFeature(Window.FEATURE_NO_TITLE);        setContentView(R.layout.activity_main);    }    @SuppressLint("NewApi")    private void setDefaultFragment() {        FragmentManager manager = getFragmentManager();        FragmentTransaction transaction = manager.beginTransaction();        exampleFragment= new ContentFragment();        transaction.replace(R.id.id_content, exampleFragment);        transaction.commit();    }}

可以看到我们使用FragmentManager对Fragment进行了动态的加载,这里使用的是replace方法,下面详细介绍FragmentManager的常用API。

注:如果使用Android3.0以下的版本,需要引入v4的包,然后Activity继承FragmentActivity,然后通过getSupportFragmentManager获得FragmentManager。不过还是建议版Menifest文件的uses-sdk的minSdkVersion和targetSdkVersion都改为11以上,这样就不必引入v4包了。

Fragment家族常用的API

Fragment常用的三个类:
android.app.Fragment 主要用于定义Fragment
android.app.FragmentManager 主要用于在Activity中操作Fragment
android.app.FragmentTransaction 保证一些列Fragment操作的原子性,熟悉事务这个词,一定能明白~
a、获取FragmentManage的方式:
getFragmentManager() // v4中,getSupportFragmentManager
b、主要的操作都是FragmentTransaction的方法
FragmentTransaction transaction = fm.benginTransatcion();//开启一个事务
- transaction.add() 
往Activity中添加一个Fragment
- transaction.remove()
从Activity中移除一个Fragment,如果被移除的Fragment没有添加到回退栈,这个Fragment实例将会被销毁。
- transaction.replace()
使用另一个Fragment替换当前的,实际上就是remove()然后add()的合体
- transaction.hide()
隐藏当前的Fragment,仅仅是设为不可见,并不会销毁,
- transaction.show()
显示之前隐藏的Fragment

注意:当使用add(),show(),hide()跳转新的Fragment时,旧的Fragment回调onHiddenChanged(),不会回调onStop()等生命周期方法,而新的Fragment在创建时是不会回调onHiddenChanged(),这点要切记。
- detach()
会将view从UI中移除,和remove()不同,此时fragment的状态依然由FragmentManager维护。
- attach()
重建view视图,附加到UI上并显示。
- transatcion.commit()
提交一个事务
注意:常用Fragment的哥们,可能会经常遇到这样Activity状态不一致:State loss这样的错误。主要是因为:commit方法一定要在Activity.onSaveInstance()之前调用。
上述,基本是操作Fragment的所有的方式了,在一个事务开启到提交可以进行多个的添加、移除、替换等操作。
值得注意的是:如果你喜欢使用Fragment,一定要清楚这些方法,哪个会销毁视图,哪个会销毁实例,哪个仅仅只是隐藏,这样才能更好的使用它们。

一些使用建议:

  1. 对Fragment传递数据,建议使用setArguments(Bundle args),而后在onCreate中使用getArguments()取出,在 “内存重启”前,系统会帮你保存数据,不会造成数据的丢失。和Activity的Intent原理一致。
  2. 使用newInstance(参数)创建Fragment对象,优点是调用者只需要关系传递的哪些数据,而无需关心传递数据的Key是什么。
  3. 如果你需要在Fragment中用到宿主Activity对象,建议在你的基类Fragment定义一个Activity的全局变量,在onAttach中初始化,这不是最好的解决办法,但这可以有效避免一些意外Crash。下面会说到此问题的解决方案。

三、Fragment的通信方案

Activity与Fragment之间是存在依赖关系的,因此它们之间必然会涉及到通信问题,解决通信问题必然会涉及到对象之间的引用。因为Fragment的出现有一个重要的使命就是:模块化,从而提高复用性。若达到此效果,Fragment必须做到高内聚,低耦合。
目前能想到的解决它们之间通信的方案有:handler,广播,EvnetBus,setArguments,接口等,下面聊聊这些方案

handler方案:
public class MainActivity extends FragmentActivity{       //声明一个Handler       public Handler mHandler = new Handler(){                 @Override           public void handleMessage(Message msg) {                 super.handleMessage(msg);                 ...相应的处理代码           }     }     ...相应的处理代码   }     public class MainFragment extends Fragment{           //保存Activity传递的handler           private Handler mHandler;           @Override           public void onAttach(Activity activity) {                 super.onAttach(activity);               //这个地方已经产生了耦合,若还有其他的activity,这个地方就得修改                 if(activity instance MainActivity){                       mHandler =  ((MainActivity)activity).mHandler;                 }           }           ...相应的处理代码     }

该方案存在的缺点:
- Fragment对具体的Activity存在耦合,不利于Fragment复用
- 不利于维护,若想删除相应的Activity,Fragment也得改动
- 没法获取Activity的返回数据

广播方案:
具体的代码就不写了,说下该方案的缺点:

  • 用广播解决此问题有点大材小用了
  • 广播性能肯定会差(不要和我说性能不是问题,对于手机来说性能是大问题)
  • 传播数据有限制(必须得实现序列化接口才可以)

EventBus方案:
具体的EventBus的使用可以自己搜索下:
- EventBus是用反射机制实现的,性能上会有问题
- EventBus难于维护代码
- 没法获取Activity的返回数据

SetArguments方案:

public class ExampleFragment extends Fragment{    private String type;    public static ExampleFragment newInstance(String type) {        ExampleFragment myFragment = new ExampleFragment();        Bundle args = new Bundle();        args.putString("type", type);        myFragment.setArguments(args);        return myFragment;        }    @Override    public View onCreateView(LayoutInflater inflater, ViewGroup container,            Bundle savedInstanceState) {        // TODO Auto-generated method stub        type=getArguments().getString("type");        ...相应的处理代码    }}

这种activity向fragment传值方法是最简单有效的,而且在fragment异常重建的时候bundle将参数保存了下来

接口方案:
这种方案是大家最易想到,也应该是使用最多的一种方案吧,具体上代码:

 //MainActivity实现MainFragment开放的接口   public class MainActivity extends FragmentActivity implements FragmentListener{         @override         public void toH5Page(){ }       ...其他处理代码省略   }     public class MainFragment extends Fragment{         public FragmentListener mListener;          //MainFragment开放的接口         public static interface FragmentListener{             //跳到h5页面           void toH5Page();         }         @Override         public void onAttach(Activity activity) {               super.onAttach(activity);               //对传递进来的Activity进行接口转换               if(activity instance FragmentListener){                   mListener = ((FragmentListener)activity);               }         }         ...其他处理代码省略   }

这种方案应该是既能达到复用,又能达到很好的可维护性,并且性能很好(推荐使用),另外,还有位大神实现了 用一个类来模拟Javascript中的一个Function来存放值得新方法,有兴趣的可以进传送门Activity与Fragment通信(99%)完美解决方案

前方高能,请绷紧神经,嘀嘀嘀,上车

四、Fragment的常见错误及解决方法(重点)

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

在系统要把app回收之前,系统会把Activity的状态保存下来,Activity的FragmentManager负责把Activity中的Fragment保存起来。在“内存重启”后,Activity的恢复是从栈顶逐步恢复,Fragment会在宿主Activity的onCreate方法调用后紧接着恢复(从onAttach生命周期开始)。

1.getActivity()空指针

可能你遇到过getActivity()返回null,或者平时运行完好的代码,在“内存重启”之后,调用getActivity()的地方却返回null,报了空指针异常。

大多数情况下的原因:你在调用了getActivity()时,当前的Fragment已经onDetach()
了宿主Activity。比如:你在pop了Fragment之后,该Fragment的异步任务仍然在执行,并且在执行完成后调用了getActivity()方法,这样就会空指针。

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

protected Activity mActivity;@Overridepublic void onAttach(Activity activity) {    super.onAttach(activity);    this.mActivity = activity;}/***  如果你用了support 23的库,上面的方法会提示过时,有强迫症的小伙伴,可以用下面的方法代替*/@Overridepublic void onAttach(Context context) {    super.onAttach(context);    this.mActivity = (Activity)context;
2.Fragment重叠异常

如果你add()了几个Fragment,使用show()、hide()方法控制,比如微信、QQ的底部tab等情景,如果你什么都不做的话,在“内存重启”后回到前台,app的这几个Fragment界面会重叠。

**原因:**FragmentManager帮我们管理Fragment,当发生“内存重启”,他会从栈底向栈顶的顺序一次性恢复Fragment;
但是因为没有保存Fragment的mHidden属性,默认为false,即show状态,所以所有Fragment都是以show的形式恢复,我们看到了界面重叠。
(如果是replace,恢复形式和Activity一致,只有当你pop之后上一个Fragment才开始重新恢复,所有使用replace不会造成重叠现象)
解决方案:
目前就我所知主要有以前三种解决方案

(1)是大家比较熟悉的 findFragmentByTag:

即在add()或者replace()时绑定一个tag,一般我们是用fragment的类名作为tag,然后在发生“内存重启”时,通过findFragmentByTag找到对应的Fragment,并hide()需要隐藏的fragment。

下面是个标准恢复写法:

@Overrideprotected 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/下标,进行恢复。

(2)使用getSupportFragmentManager().getFragments()恢复

通过getFragments()可以获取到当前FragmentManager管理的栈内所有Fragment。

标准写法如下:

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

从代码看起来,这种方式比较复杂,但是这种方式在一些场景下比第一种方式更加简便有效。

(3)自己保存Fragment的Hidden状态(号称:9行代码让你App内的Fragment对重叠说再见)

发生Fragment重叠的根本原因在于FragmentState没有保存Fragment的显示状态,即mHidden,导致页面重启后,该值为默认的false,即show状态,所以导致了Fragment的重叠。此方案由 由Fragment自己来管理自己的Hidden状态
基类Fragment代码:

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、同级Fragment等场景,全都可以正常工作,不会发生重叠!
缺点:其实也不算缺点,就是需要在加载根Fragment时判断savedInstanceState是否为null才初始化fragment,否则重复加载,导致重叠,判断如下:

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

最后,Fragment总结到此结束了,向大家推荐几篇优秀的fragment博文,本文很多观点也是从中提炼出来的,做个总结,自己从中学习的同时也分享给大家,感谢您那么有耐心地看完了这么长的一篇总结博文!
Fragment系列文章:
1、Fragment全解析系列(一):那些年踩过的坑
2、Fragment全解析系列(二):正确的使用姿势
3、Fragment之我的解决方案:Fragmentation

更多精彩文章请关注微信公众号”Android经验分享“:这里将长期为您分享Android高手经验、中外开源项目、源码解析、框架设计和Android好文推荐!
扫一扫加我哦

0 0
原创粉丝点击