正常在Activity中使用Fragment的生命周期,第一次啟動過程是onAtach()-onCreate()-onCreateView()-onViewCreated()-onActivityCreated()-onStart()-onResume();隨著Activity被退棧銷毀,Fragm ...
正常在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的相關切換方式有些相同,可參考。