1. 程式人生 > >【Fragment】 Android Fragment生命週期詳解(圖文)

【Fragment】 Android Fragment生命週期詳解(圖文)

Fragment API(映象地址):

Fragment/Activity生命週期圖


Fragment狀態變化常用方法

1. onHiddenChanged(boolean hidden)

Activity內部通過show和hide方法切換Fragment時,引發的狀態變遷。

2. setUserVisibleHint(boolean isVisibleToUser)

ViewPager和Fragment一起使用時,ViewPager就是利用這個方法控制Fragment的顯示與隱藏的,所以通過這個方法可以實現ViewPager中Fragment的懶載入模式。

3. isVisible()

如果該Fragment物件對使用者可見,那麼就返回true。這就意味著:1.已經被新增到Activity中;2.它的View物件已經被繫結到視窗中;3.沒有被隱藏。

isVisible()方法需要注意和getUserVisibleHint()方法的區別。

isVisible() 方法可用於fragment的 onResume() 生命週期方法中,用於判斷當app從其他介面進入(初次建立或者返回)fragment所attach的activity時,當前Fragment是否可見。配合使用 onHiddenChanged方法,常用於Fragment介面的重新整理。

特別說明:存在這樣一種情況,當從其他 Activity 通過setResult方法返回到 包含多個Fragment的Activity,並且在目標Activity的onActivityResult方法中指定顯示處於隱藏狀態的一個Fragment時,如果僅僅依據isVisible方法是無法區分所要顯示的Fragment的onHiddenChanged和onResume方法,因為onActivityResult方法要優先於onResume方法,所以此時需要用到 isResumed() 方法來限制介面重新整理的呼叫:

    @Override
    public void onResume() {
        super.onResume();
        if (isVisible()){
            //介面重新整理操作,如網路請求
        }
    }

    @Override
    public void onHiddenChanged(boolean hidden) {
        super.onHiddenChanged(hidden);
        if (isVisible() && isResumed()){
            //介面重新整理操作,如網路請求
        }
    }


4. setArguments(Bundle args)

Fragment使用常見問題

1. Fragment切換導致UI重疊問題

問題演示:

   

問題模擬重現

1.首先開啟,預設選中的是第一個tab,如上面的一張圖片正常那樣。
2.切換到tab2,並把tab1 hide掉;
3.再切回到tab1,並不會觸發tab1對應fragment的任何生命週期;
4.然後home鍵進入後臺,我在activity的onPause()中手動對IndexFragment賦空,模擬長時間後臺,系統銷燬了該引用:IndexFragment=null;
5.再次啟動,其實tab1 的fragment例項在記憶體中還在,只是他的引用被銷燬了。
6.再切到tab2,這裡其實是先把tab1的hide,在show tab2,但是tab1 的fragment引用為空,所以無法hide,就出現了tab2疊在tab1上的花屏情況。
7.再切到tab1,tab1就會重複建立物件。

問題分析

fragment的切換有兩種方式:

1. replace方式

transaction.replace(R.id.content, IndexFragment);

使用replace方式不會出現上述問題,但是會重複建立物件,每次replace會把生命週期全部執行一遍,如果在這些生命週期函式 里拉取資料的話,就會不斷重複的載入重新整理資料。

2. add - hide - show方式

transaction.add(R.id.content, IndexFragment); transaction.hide(otherfragment); transaction.show(thisfragment);

public class TestFragmentTab extends FragmentActivity {
	
	private static final String TAG = "TestFragmentTab";
	
	private int tabIds[] = new int[]{
			R.id.home_tab_main,
			R.id.home_tab_search,
			R.id.home_tab_category,
	};
	
	private FragmentTab1 mTab1;
	private FragmentTab2 mTab2;
	private FragmentTab3 mTab3;

	@Override
	public void onAttachFragment(Fragment fragment) {
		// TODO Auto-generated method stub
		super.onAttachFragment(fragment);
		Log.d(TAG,"onAttachFragment");
	}

	@Override
	protected void onDestroy() {
		// TODO Auto-generated method stub
		super.onDestroy();
		Log.d(TAG,"onDestroy");
	}

	@Override
	protected void onPause() {
		// TODO Auto-generated method stub
		super.onPause();
		Log.d(TAG,"onPause");
	}

	@Override
	protected void onResume() {
		// TODO Auto-generated method stub
		super.onResume();
		Log.d(TAG,"onResume");
	}

	@Override
	protected void onStart() {
		// TODO Auto-generated method stub
		super.onStart();
		Log.d(TAG,"onStart");
	}

	@Override
	protected void onStop() {
		// TODO Auto-generated method stub
		super.onStop();
		Log.d(TAG,"onStop");
	}

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		// TODO Auto-generated method stub
		super.onCreate(savedInstanceState);
		Log.d(TAG,"onCreate");
		setContentView(R.layout.layout_test_fragment_tab);
		
		RadioGroup tabButtonGroup = (RadioGroup) findViewById(R.id.bottom_bar);
		tabButtonGroup.setOnCheckedChangeListener(new OnCheckedChangeListener() {
			
			@Override
			public void onCheckedChanged(RadioGroup group, int checkedId) {
				// TODO Auto-generated method stub
				for (int i = 0; i < tabIds.length; i++) {
					if (tabIds[i] == checkedId) {
						setSelection(i);
						break;
					}
				}
			}
		});
		
		setSelection(0);
	}
	
	private void setSelection(int position){
		
		FragmentManager fm = getSupportFragmentManager();
		FragmentTransaction ft = fm.beginTransaction();
		hideAllFragments(ft);
		
		switch (position) {
		case 0:
			if (mTab1 == null) {
				mTab1 = new FragmentTab1();
				ft.add(R.id.content, mTab1);
			}else {
				ft.show(mTab1);
			}
			break;
			
		case 1:
			if (mTab2 == null) {
				mTab2 = new FragmentTab2();
				ft.add(R.id.content, mTab2);
			}else {
				ft.show(mTab2);
			}
			break;
			
		case 2:
			if (mTab3 == null) {
				mTab3 = new FragmentTab3();
				ft.add(R.id.content, mTab3);
			}else {
				ft.show(mTab3);
			}
			break;

		default:
			break;
		}
		
		ft.commit();
	}
	
	private void hideAllFragments(FragmentTransaction ft){
		if (mTab1 != null) {
			ft.hide(mTab1);
		}
		
		if (mTab2 != null) {
			ft.hide(mTab2);
		}
		
		if (mTab3 != null) {
			ft.hide(mTab3);
		}
	}

}

使用第二種方式做Fragment的切換時,經常會出現上圖所示Fragment重疊問題。直接back鍵退出應用再進入時,則沒有出現該問題。當應用被強行關閉後(通過手機管家軟體手動強關,或系統為節省記憶體自動關閉應用),再次進入應用時,每次都會出現這種花屏現象。

通過分析發現,正常back鍵退出應用時,Activity及所屬的Fragment物件均會被銷燬,因此再次進入時會在切換到Tab時建立對應的Fragment物件。
但是當強行關閉應用後,Activity雖然被回收,但Fragment物件仍然保持,再次進入應用時,系統會分別呼叫Fragment的onAttach方法將其附加到Activity上,後面會分別呼叫兩個fragment的onCreateView方法,因此這兩個Fragment對應的View層次結構都會加到Activity的View層次中。
雖然切換Fragment時會把所有fragment先隱藏再顯示選中的物件,但由於此時Activity中Fragment物件的成員變數還未初始化,因此會再次例項化fragment物件,之後add、show及hide的都是在第二次建立的物件上操作的,而之前被保持的fragment物件的檢視層次已經反映到Activity檢視中並且不會被hide,因此發生了上述重疊現象。

解決辦法有三種:

第一種:

在Activity的onAttachFragment方法中,有一個fragment引數,它就是onAttach方法對應的Fragment物件,
通過判斷這個fragment物件,如果屬於我們的FragmentTabX類並且該類還未被例項化過,則將Activity的成員變數mFragmentTabX指向該fragment物件,這樣就可以在原來的fragment物件上操作add/show/hide,因此不會有重疊現象

@Override
	public void onAttachFragment(Fragment fragment) {
		// TODO Auto-generated method stub
		super.onAttachFragment(fragment);
		Log.d(TAG,"onAttachFragment");
		
		if (mTab1 == null && fragment instanceof FragmentTab1) {
			mTab1 = (FragmentTab1)fragment;
		}else if (mTab2 == null && fragment instanceof FragmentTab2) {
			mTab2 = (FragmentTab2)fragment;
		}else if (mTab3 == null && fragment instanceof FragmentTab3) {
			mTab3 = (FragmentTab3)fragment;
		}
	}

第二種:

呼叫三個引數的add函式,新增Tag引數用於標記:

transaction.add(R.id.content, IndexFragment,”Tab1″);

然後,在fragment初始化前,新增引用判斷,找打對應的引用,如果依舊為空,再呼叫fragment建構函式進行初始化:

IndexFragment=FragmentManager.findFragmentByTag(“Tab1″);

第三種:

在進入onCreate函式時,先去判斷savedInstanceState是否為null,如果不為null,則表示裡面有儲存這四個fragment。則不再重新去add這四個fragment,而是通過Tag從前儲存的資料中直接去讀取。

        FragmentManager fManager;
	@Override
	public void onCreate(Bundle savedInstanceState) {
		// TODO Auto-generated method stub
		fManager = getFragmentManager();
		if (savedInstanceState != null) {
			allFrg = (AllOfficialAccountFragment) fManager.findFragmentByTag("allFrg");
			movieFrg = (MovieOfficialAccountFragment) fManager.findFragmentByTag("movieFrg");
			newsFrg = (NewsOfficialAccountFragment) fManager.findFragmentByTag("newsFrg");
			otherFrg = (OtherOfficialAccountFragment) fManager.findFragmentByTag("otherFrg");			
		}
		super.onCreate(savedInstanceState);		
	}


2.Fragment not attached to Activity異常

出現該問題,主要是因為在Fragment還沒有被關聯到Activity的時候或者被Attach的Activity已經destroy了的時候,呼叫了需要上下文Context的函式,常見的如fragment的getResource()方法。解決辦法主要有:

一:將呼叫的程式碼移動到onStart()方法中;

三:使用被Attach的Activity的相應方法,如getActivity().getResource...等;

二:在呼叫的程式碼前新增Fragment的isAdd()方法進行判斷,推薦使用這種方式!

3. Fragment中onActivityResult()注意問題

通常,我們會在FragmentActivity中巢狀一層Fragment使用,甚至在Fragment中再次層層巢狀Fragment,此時,會出現第二層及更深層次的子Fragment物件無法接收到onActivityResult()事件。

檢視FragmentActivity原始碼:

	 @Override 
	 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 
		 mFragments.noteStateNotSaved(); 
	     int index = requestCode>>16; 
	     if (index != 0) { 
	              index--; 
	              if (mFragments.mActive == null || index < 0 || index >= mFragments.mActive.size()) { 
	                   Log.w(TAG, "Activity result fragment index out of range: 0x" 
	                           + Integer.toHexString(requestCode)); 
	                   return; 
	               } 
	               Fragment frag = mFragments.mActive.get(index); 
	               if (frag == null) { 
	                   Log.w(TAG, "Activity result no fragment exists for index: 0x" 
	                           + Integer.toHexString(requestCode)); 
	               } else { 
	                   frag.onActivityResult(requestCode&0xffff, resultCode, data); 
	               } 
	               return; 
		} 
	             
	    super.onActivityResult(requestCode, resultCode, data); 
	}

可以看出,FragmentActivity沒有處理巢狀Fragment的情況,也就是說,只是回撥到第一級的Fragment中,然後沒有繼續分發下去。

所以,我們需要在第一級的Fragment的onActivityResult()中控制分發onActivityResult事件。

4. FragmentTransaction的commit問題

FragmentTransaction有commit()和commitAllowingStateLoss()兩個提交方法,通過原始碼看他們的區別:

commit():

Schedules a commit of this transaction. The commit does not happen immediately; it will be scheduled as work on the main thread to be done the next time that thread is ready. 
A transaction can only be committed with this method prior to its containing activity saving its state. If the commit is attempted after that point, an exception will be thrown. This is because the state after the commit can be lost if the activity needs to be restored from its state. See commitAllowingStateLoss() for situations where it may be okay to lose the commit.

Returns:
Returns the identifier of this transaction's back stack entry, if addToBackStack(String) had been called. Otherwise, returns a negative number.

commitAllowingStateLoss():
Like commit but allows the commit to be executed after an activity's state is saved. This is dangerous because the commit can be lost if the activity needs to later be restored from its state, so this should only be used for cases where it is okay for the UI state to change unexpectedly on the user.

顧名思義,可以看出,使用commit()方法,如果在Activity儲存狀態後發生了commit行為,將丟擲異常:
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
	at android.support.v4.app.FragmentManagerImpl.checkStateLoss(Unknown Source)
	at android.support.v4.app.FragmentManagerImpl.enqueueAction(Unknown Source)
	at android.support.v4.app.BackStackRecord.commitInternal(Unknown Source)
	at android.support.v4.app.BackStackRecord.commit(Unknown Source)

而commitAllowingStateLoss()方法允許這種呼叫,但是當Activity回覆狀態時,之前fragment的提交狀態將丟失。所以,如果你允許這種丟失commit狀態的話,可以使用commitAllowingStateLoss()。

參考地址: