正常在Activity中使用Fragment的生命周期,第一次启动过程是onAtach()-onCreate()-onCreateView()-onViewCreated()-onActivityCreated()-onStart()-onResume();随着Activity被退栈销毁,Fragment的声明周期依次为onPause()-onStop()-onDestroyView()-onDestroy()-onDetach();
如果在Fragment中使用EventBus等通过反射进行的操作,在Fragment执行完onCreate()之后就会直接调用反射相关方法,由于还没有走onCreateView()等方法创建视图,所以在反射相关方法中如果直接做UI层更新就会出现空指针异常等情况。这个Bug引导我开始关注FragmentManager的原理。
下面是Bug的使用场景:在一个Activity中创建AFragment和BFragment和一个相对应的两个Button,在点击AButton时显示AFragment,点击BButton是显示BFragment,同时在BFragment中执行相应操作后,使用EventBus发送一个event,之后在Activity和AFragment中分别接收这个event,并执行相应操作使得界面显示到AFragment并对AFragment中的数据进行更新。由于Activity中使用FragmentManager对两个Fragment进行切换,所以在AFragment接收event时有各种问题。原始未修复代码如下:
1 SecondActivity.class 2 @Override 3 protected void onCreate(Bundle savedInstanceState) { 4 super.onCreate(savedInstanceState); 5 EventBus.getDefault().register(this); 6 setContentView(R.layout.activity_second); 7 but1= (Button) findViewById(R.id.but1); 8 but2= (Button) findViewById(R.id.but2); 9 but1.setOnClickListener(this); 10 but2.setOnClickListener(this); 11 aFragment=AFragment.newInstance(null, null); 12 bFragment=BFragment.newInstance(null, null); 13 changeFragment(aFragment); 14 // addFragmentTransaction(aFragment); 15 // addFragmentTransaction(bFragment); 16 17 } 18 19 @Subscribe(threadMode = ThreadMode.MAIN) 20 public void handlerEvent(FragmentEvent event){ 21 Log.i(TAG, "SecondActivity.onMainThread: event="+event); 22 changeFragment(aFragment); 23 } 24 25 private void changeFragment(Fragment f){ 26 changeFragment1(f); 27 } 28 29 private void changeFragment1(Fragment f){ 30 Log.i(TAG, "SecondActivity.changeFragment1: f="+f.getClass().getName()); 31 FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); 32 transaction.replace(R.id.fl_container, f); 33 // transaction.addToBackStack(f.getClass().getName()); 34 transaction.commit(); 35 if(f.getClass().getName().equals(aFragment.getClass().getName())){ 36 bFragment.setUserVisibleHint(false); 37 aFragment.setUserVisibleHint(true); 38 }else { 39 bFragment.setUserVisibleHint(true); 40 aFragment.setUserVisibleHint(false); 41 } 42 }
1 AFragment.class 2 3 private TextView titleTV; 4 @Override 5 public void onCreate(Bundle savedInstanceState) { 6 Log.i(TAG, "AFragment.onCreate: "); 7 super.onCreate(savedInstanceState); 8 if (getArguments() != null) { 9 mParam1 = getArguments().getString(ARG_PARAM1); 10 mParam2 = getArguments().getString(ARG_PARAM2); 11 } 12 EventBus.getDefault().register(this); 13 } 14 15 @Override 16 public View onCreateView(LayoutInflater inflater, ViewGroup container, 17 Bundle savedInstanceState) { 18 Log.i(TAG, "AFragment.onCreateView: "); 19 // Inflate the layout for this fragment 20 return inflater.inflate(R.layout.fragment_a, container, false); 21 } 22 23 @Override 24 public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 25 Log.i(TAG, "AFragment.onViewCreated: "); 26 super.onViewCreated(view, savedInstanceState); 27 titleTV=(TextView)view.findViewById(R.id.tv_fa_title); 28 } 29 30 @Override 31 public void onDestroy() { 32 Log.i(TAG, "AFragment.onDestroy: "); 33 super.onDestroy(); 34 EventBus.getDefault().unregister(this); 35 } 36 @Subscribe(threadMode = ThreadMode.MAIN) 37 public void handlerEvent(FragmentEvent event){ 38 Log.i(TAG, "AFragment.handlerFragmentEvent: event="+event); 39 Log.i(TAG, "AFragment.handlerEvent: titleTv="+titleTV); 40 titleTV.setText(event.toString()); 41 }
1 BFragment.class 2 3 private TextView titleTV; 4 @Override 5 public View onCreateView(LayoutInflater inflater, ViewGroup container, 6 Bundle savedInstanceState) { 7 // Inflate the layout for this fragment 8 return inflater.inflate(R.layout.fragment_b, container, false); 9 } 10 11 @Override 12 public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 13 super.onViewCreated(view, savedInstanceState); 14 titleTV= (TextView) view.findViewById(R.id.tv_fb_title); 15 titleTV.setOnClickListener(new View.OnClickListener() { 16 @Override 17 public void onClick(View v) { 18 Log.i(TAG, "BFragment.onClick: postEvent"); 19 EventBus.getDefault().post(new FragmentEvent(23)); 20 } 21 }); 22 }
在Activity中使用FragmentManager进行切换布局,有两种方式,一是布局替换,即FragmentManager.replace()相关方法,第二种是布局显隐,即FragmentManager.hide()和FragmentManager.show()等相关方法。原始代码中使用replace()直接替换Fragment,这种方式导致每次替换的Fragment都是从onAttach()开始重新执行,所以之前在BFragment中发送event时,当前的AFragment还没有创建,也就不会执行对event的处理操作。这样就有两种解决方案。
方案一:使用EventBus的postSticky()方法延缓发送event,而在AFragment中接收event时注解为sticky=true,默认为false。所以更新代码如下
1 AFragment.class 2 @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) 3 public void handlerEvent(FragmentEvent event){ 4 Log.i(TAG, "AFragment.handlerFragmentEvent: event="+event); 5 Log.i(TAG, "AFragment.handlerEvent: titleTv="+titleTV); 6 titleTV.setText(event.toString()); 7 }
1 BFragment.class 2 @Override 3 public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 4 super.onViewCreated(view, savedInstanceState); 5 titleTV= (TextView) view.findViewById(R.id.tv_fb_title); 6 titleTV.setOnClickListener(new View.OnClickListener() { 7 @Override 8 public void onClick(View v) { 9 Log.i(TAG, "BFragment.onClick: postEvent"); 10 EventBus.getDefault().postSticky(new FragmentEvent(23)); 11 } 12 }); 13 }
使用方案一走日志发现在AFragment中的确已经正常接收到了event并对其进行了处理,但是处理event之后又从onCreateView()开始执行更新视图等一系列操作,导致event更新的视图被更新之后的视图所覆盖,处理这个问题的话,只要在handlerEvent中先不着急更新view,而是新创建一个全局event来存储当前的event,在onViewCreated()中判断全局event如果非空就更新数据。方案一最终修改代码如下:
1 AFragment.class 2 private FragmentEvent mEvent; 3 @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) 4 public void handlerEvent(FragmentEvent event){ 5 Log.i(TAG, "AFragment.handlerFragmentEvent: event="+event); 6 Log.i(TAG, "AFragment.handlerEvent: titleTv="+titleTV); 7 // titleTV.setText(event.toString()); 8 mEvent=event; 9 } 10 @Override 11 public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 12 Log.i(TAG, "AFragment.onViewCreated: "); 13 super.onViewCreated(view, savedInstanceState); 14 titleTV=(TextView)view.findViewById(R.id.tv_fa_title); 15 if(mEvent!=null){ 16 titleTV.setText(mEvent.toString()); 17 } 18 }
方案二:使用FragmentManager的显隐方法而不是替换方法切换Fragment,使用hide()和show(),必须要先调用add()把Fragment添加到FragmentManager中,而此时切换两个Fragment,并不会重新执行onCreate()--onResume()等一系列流程,只有在Activity中显示调用Fragment的setUserVisibleHint()方法表示当前Fragment的显隐,说明Fragment已经绑定到Activity中,其生命周期只有在Activity的onResume()之中保存。方案二修改后的代码如下:
1 SecondActivity.class 2 @Override 3 protected void onCreate(Bundle savedInstanceState) { 4 super.onCreate(savedInstanceState); 5 EventBus.getDefault().register(this); 6 setContentView(R.layout.activity_second); 7 but1= (Button) findViewById(R.id.but1); 8 but2= (Button) findViewById(R.id.but2); 9 but1.setOnClickListener(this); 10 but2.setOnClickListener(this); 11 aFragment=AFragment.newInstance(null, null); 12 bFragment=BFragment.newInstance(null, null); 13 changeFragment(aFragment); 14 addFragmentTransaction(aFragment); 15 addFragmentTransaction(bFragment); 16 17 } 18 private void addFragmentTransaction(Fragment fragment) { 19 FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); 20 transaction.add(R.id.fl_container, fragment); 21 transaction.commit(); 22 } 23 private void changeFragment(Fragment f){ 24 changeFragment2(f); 25 } 26 27 private void changeFragment2(Fragment f){ 28 Log.i(TAG, "SecondActivity.changeFragment2: f="+f.getClass().getName()); 29 FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); 30 if(f.getClass().getName().equals(aFragment.getClass().getName())){ 31 transaction.hide(bFragment); 32 transaction.show(aFragment); 33 bFragment.setUserVisibleHint(false); 34 aFragment.setUserVisibleHint(true); 35 }else { 36 transaction.hide(aFragment); 37 transaction.show(bFragment); 38 aFragment.setUserVisibleHint(false); 39 bFragment.setUserVisibleHint(true); 40 } 41 transaction.commit(); 42 }
以上两种解决方案各有优缺点,总体来说,如果想每次Fragment显示时都要重新更新View,使用方案一的方式更好,但是方案一如果多次切换,由于之前的Fragment已经与当前Activity解除绑定,所以没有了引用的地方,但是仍然存在内存中,如果系统释放内存不及时,就会有内存泄漏的隐患;同样对于方案二来说,每次切换不会重新更新界面,所以如果想在显隐时做些操作,只能显示调用Fragment的setUserVisibleHint()方法并重写该方法以做操作。说到方案二的这种形式,和ViewPager的相关切换方式有些相同,可参考。