Fragment總結

Fragment是Android開發中經常使用的組件之一,也是最容易出問題的組件,爲了更好地使用它,對此進行一個簡單的總結。java

說明: 因爲v4包中的Fragment具備更好的兼容性,且可隨時更新到指定版本,因此本文的討論僅限v4包中的Fragment。git

使用Fragment

使用Fragment的方式有2種:github

  1. 在佈局文件中使用標籤,將其中的name屬性值設置爲須要加載的Fragment的全路徑;編程

當系統在建立Activity時,會實例化佈局中指定的Fragment,並調用它的onCreateView方法,以返回的View來替換元素。網絡

  1. 動態建立Fragment對象,經過FragmentTransaction來加載;

在建立Fragment的時候須要注意,Android不推薦使用自定義構造方法的方式來建立Fragment,可以使用官方推薦的方式來傳參:框架

不提倡的方式:異步

public ChatFragment(int id, String name) {
	this.id = id;
	this.name = name;
}
複製代碼

推薦的方式:ide

public static Fragment newInstance(int id, String name) {
	Fragment fragment = new ChatFragment();
	Bundle bundle = new Bundle();
	bundle.putInt("id", id);
	bundle.putString("name", name);
	fragment.setArguments(bundle);
	return fragment;
}
複製代碼

固然,也可以使用Fragment提供的靜態初始化方法來構造Fragment:模塊化

/**
 * @param context 加載Fragment的Activity實例
 * @param fname 須要加載的Fragment的全路徑名 [其本質是經過反射調用的]
 * @param args 須要傳遞的參數
 * @return
 */
public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) {

}

// 示例
Bundle bundle = new Bundle();
bundle.putInt("id", id);
bundle.putString("name", name);
Fragment.instantiate(this, "com.sxu.fragment.ChatFragment", bundle);
複製代碼

而後使用FragmentTransaction將Fragment提交給FragmentManager:佈局

FragmentManager fm = getSupportFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
transaction.add(R.id.container_layout, ContentFragment.newInstance(0);
transaction.commit();
複製代碼

Fragment生命週期

相比Activity, Fragment的生命週期要複雜不少,使用官方的一張圖來展現:

image

下面對Fragment生週期中幾個核心的階段說明一下:

onAttach/onDetach

onAttach是Activity與Fragment關聯時被調用此,可在此方法中保存Activity實例解決getActivity爲空的問題。與之對應的是onDetach, 表示與Activity解除綁定;

onCreate

與Activity同樣,Fragment也可使用onSaveInstanceState在退到後臺時保存頁面數據,但它沒有提供onRestoreInstanceState方法,因此可在onCreate中進行恢復操做;

onCreateView/onDestroyView

加載Fragment View的地方,相似於Activity中的setContentView, 它返會Fragment加載的View。從圖上能夠看出, Fragment從回退棧中返回時,會今後方法開始調用,因此可將Fragment加載的View以成員變量的形式保存,當其爲空時進行再加載操做, 從而避免View的屢次加載。

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
	if (mContentView == null) {
		mContentView = inflater.inflate(R.layout.fragment_content_layout, container, false);
	}
	
	return mContentView;
}
複製代碼

FragmentView加載完成後onViewCreated會被調用,調用時機先於onActivityCreated,其中的參數View就表示Fragment加載的佈局,因此可在此方法中獲取佈局中的各個View。

與之對應的是onDestroyView, 它會銷燬Fragment中的View.

onActivityCreated

Activity的onCreate執行完成後被調用,Fragment中的邏輯一般在此方法中執行。

Fragment生命週期與Activity生命週期的關係

Fragment雖然有完整的生命週期,但仍然須要以Activity爲宿主來存在,因此它的生命週期與Activity生命週期有着直接的關係,如圖所示:

image

從圖上能夠看出,Fragment的生命週期和Activity基本保持一致。

與Activity不一樣的是,Fragment生命週期並老是在頁面可見性發生變化時變化。在如下場景中,Fragment的可見性發生變化時,不會調用生命週期的任何方法。

Fragment show/hide:

Fragment在顯示或隱藏時會回調onHiddenChanged, 參數hidden爲true表示Fragment被隱藏, 不然表示被顯示;

ViewPager中已加載的Fragment切換時:

ViewPager中已加載的Fragment在切換時會調用setUserVisibleHint, 參數isVisibleToUser爲true表示Fragment被切換到當前頁.因爲ViewPager中的Fragment在首次加載時,也會調用setUserVisibleHint,致使出現監聽重複的問題,因此在setUserVisibleHint須要添加條件判斷:

@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
	super.setUserVisibleHint(isVisibleToUser);
	if (isResumed() && isVisibleToUser) {
		// 頁面被顯示
	}
}
複製代碼

因此,監聽Fragment頁面的顯示,除了監聽Fragment生命週期中的onPause/onResume方法,還須要監聽onHiddenChanged方法和setUserVisibleHint。

在統計Fragment頁面的顯示時長時,須要綜合考慮這幾個方法,具體見Android無埋點方案實踐項目——Tracker中對Fragment生命週期的監聽過程FragmentLifecycleListener

Fragment懶加載

懶加載(或者叫延遲加載),也就是延遲數據的請求過程。經常使用於ViewPager+Fragment模式中,不一樣的Fragment可能使用不一樣的接口,在頁面顯示的時候,可能會同時請求offscreenPageLimit 個接口,致使頁面出現卡頓。爲了解決這種問題,可延遲未顯示的Fragment的數據請求過程,即在Fragment顯示時,再進行網絡請求。Fragment中的setUserVisibleHint方法在ViewPager中的Fragment顯示時被調用,因此咱們可在其中實現數據的請求。

@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
	super.setUserVisibleHint(isVisibleToUser);
	if (isVisibleToUser && !dataRequested) {
		requestData();
	}
}
複製代碼

getContext()與getActivity()的區別

關於這二者的區別,可從源碼來看:

@Nullable
public Context getContext() {
    return mHost == null ? null : mHost.getContext();
}

 @Nullable
final public FragmentActivity getActivity() {
    return mHost == null ? null : (FragmentActivity) mHost.getActivity();
}
複製代碼

從它們的實現來看,都是直接返回mHost對象中的成員,mHost的類型爲FragmentHostCallback,它的構造方法以下:

public FragmentHostCallback(Context context, Handler handler, int windowAnimations) {
    this(context instanceof Activity ? (Activity) context : null, context, handler,
            windowAnimations);
}

FragmentHostCallback(FragmentActivity activity) {
    this(activity, activity /*context*/, activity.mHandler, 0 /*windowAnimations*/);
}

FragmentHostCallback(Activity activity, Context context, Handler handler,
        int windowAnimations) {
    mActivity = activity;
    mContext = context;
    mHandler = handler;
    mWindowAnimations = windowAnimations;
}
複製代碼

經過對源碼進行搜索,發現只是FragmentActivity中直接調用了FragmentHostCallback的構造方法:

class HostCallbacks extends FragmentHostCallback<FragmentActivity> {
    public HostCallbacks() {
        super(FragmentActivity.this /*fragmentActivity*/);
    }
    
    ...
}
複製代碼

就目前源碼的實現來講,這二者沒有任何區別,引用的都是它所在的Activity的實例,可是它提供的公開的構造方法的實現卻說明: getContext()爲空的可能性能更大。 因此,在Fragment中獲取Context實例時最好使用getActivity()。

getActivity返回null

在使用Fragment的過程當中,getActivity()爲null的異常應該是最多見的。其根本緣由:Fragment與以前關聯的Activity失去了聯繫!

使用Fragment時,咱們的Activity繼承的都是FragmentActivity, FragmentActivity在被異常關閉時會保存已加載的Fragment,具體以下:

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    markFragmentsCreated();
    // 保存Fragment
    Parcelable p = mFragments.saveAllState();
    if (p != null) {
        outState.putParcelable(FRAGMENTS_TAG, p);
    }
    ...
}
複製代碼

而後在其onCreate中對保存的Fragment進行了恢復:

protected void onCreate(@Nullable Bundle savedInstanceState) {
    mFragments.attachHost(null /*parent*/);
    super.onCreate(savedInstanceState);
    ...
    if (savedInstanceState != null) {
        Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
        mFragments.restoreAllState(p, nc != null ? nc.fragments : null);
    }
    ...
}
複製代碼

雖然對Fragment進行了恢復,並與其關聯了新的Activity實例,但Fragment以前關聯的Activity實例已被銷燬,若是這些Fragment中有一些延時任務,並使用了getActivity(), 就會出現空指針異常。

解決方案
  1. 在Fragment的onDetach中移除延時任務:

這是從根本上杜絕getActivity爲空的方案。平常開發中,使用的延時任務最多的狀況莫過於網絡請求和Handler。 對於網絡請求,封裝的時候最好考慮Activity/Fragment的生命週期, 在onDestroy和onDetach取消網絡請求,上傳/下載等大數據量的網絡請求,可引用ApplicationContext,放在後臺服務進行執行。對於Handler, 只須要在onDetach中清除任務便可:

@Override
public void onDetach() {
	super.onDetach();
	handler.removeCallbacksAndMessages(null);
}
複製代碼
  1. 在onAttach中保存Activity的引用;

經過保存的Activity實例替代getActivity。Activity雖然重建了,但以前的實例由於Fragment的持有而不會被內存清理,會形成短暫性的內存泄漏。

@Override
public void onAttach(Context context) {
	super.onAttach(context);
	this.mContext = context;
}
複製代碼

具體使用哪一種方法,看本身的需求,若是項目框架良好,團隊又有良好的編程規範,天然是推薦第一種。不然仍是使用第二種方案,雖然會形成短暫性的內存泄漏,倒也不會有什麼大的影響。

Fragment頁面重疊問題

FragmentActivity默認狀況下在異常銷燬時會保存Fragment,並在onCreate中進行恢復,而在重建時又會建立新的Fragment,就會出現頁面重疊的問題。同時致使內存中出現n(n+1)/2個Fragment實例,這會大大增長內存消耗。這裏可採用如下兩種方案進行優化:

  1. 只在首次或沒有Fragment實例存在的時候才建立新的Fragment:

    protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState);

    FragmentManager fm = getSupportFragmentManager();
     // 從新關聯保存的Fragment
     if (savedInstanceState == null || fm.getFragments().size() == 0) {
     	FragmentTransaction transaction = fm.beginTransaction();
     	transaction.add(R.id.container_layout, ContentFragment.newInstance(fm.getFragments().size()));
     	transaction.commit();
     }
     ...
    複製代碼

    }

  2. Activity被異常關閉時,不要保存Fragment:

    @Override public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) { FragmentManager fm = getSupportFragmentManager(); List fragmentList = fm.getFragments(); if (fragmentList.size() == 0) { super.onSaveInstanceState(outState, outPersistentState); } }

getSupportFragmentManager與getChildFragmentManager的區別

getSupportFragmentManager是V4包中管理Activity中的Fragment的管理器,而getChildFragmentManager是管理Fragment中的Fragment的管理器,也就是Fragment嵌套時應該使用getChildFragmentManager而不是getSupportFragmentManager。

Fragment Commit介紹

動態建立Fragment時,須要事務的配合,事務添加完成後須要提交,FragmentTransaction中提供了多個提交方法:

commit();       // 異步提交
commitNow();    // 同步提交
commitAllowingStateLoss();      // 異步提交,容許狀態丟失
commitNowAllowingStateLoss();   // 同步提交,容許狀態丟失
複製代碼

commit()和commitNow()不容許在onSaveInstanceState後調用,不然會拋出java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState異常,由於onSaveInstanceState就是用來保存Fragment的狀態,onSaveInstanceState後面再次提交事務,與這些事務關聯的Fragment的狀態就會丟失,因此拋出了異常。在這種狀況下,若是肯定狀態丟失不會產生影響,可以使用commitAllowingStateLoss()或commitNowAllowingStateLoss()。

Fragment的回退棧

與Activity相似,Fragment也有回退棧的概念,使用addBackToStack方法便可將Fragment添加到回退棧中(注意:須要在commit以前調用),加入到回退棧中的Fragment在執行Actiivty的onBackPressed()方法時,會逐漸出棧。

執行replace操做時,若是添加到回退棧中,則被替換的Fragment的onDestroy()和onDetach()不會被調用,按返回鍵可返回到以前的Fragment,並從onCreateView()開始調用。若是不添加到回退棧中,則會調用被替換Fragment的onDestroy()和onDetach()方法。

==建議:== Activity中的第一個Fragment不要添加到回退棧中,不然返回時須要多執行一次onBackPressed()(具體UI表現:第一個Fragment出棧後出現空白頁面);

Fragment與View的關係

與其將Fragment與Activity比較,倒不如將其與View進行比較,畢竟它們都是不可單獨存在的元素,都須要Activity做爲宿主。Fragment與View很像,能夠動態建立,也能夠在佈局文件中定義,因此可視爲是加入了生命週期的View. 只不過它的管理須要藉助於FragmentManager和FragmentTransaction.

Fragment UI問題

在使用Fragment過程當中,常常會遇到一些UI問題:

Fragment背景透明

Fragment不像Activity,沒有主題的概念,若是其中加載的佈局沒有設置背景,默認就是透明的。若是Activity只加載了一個Fragment,看起來背景就是主題的背景,當添加多個Fragment的時候,就會發現頁面出現重疊,由於背景是透明的,此時須要爲Fragment中的View設置背景。

解決辦法

基於減小頁面重繪的原則,可以使用如下方案:

  • 對於Activity中只有一個Fragment的狀況,不要爲Fragment中的View設置背景,直接設置Activity主題的背景;
  • 對於Activity須要多個Fragment的狀況,在添加新的Fragment的時候可將底層的Fragment先隱藏;

事件穿透

Fragment中的View默認狀況下是不可點擊的,因此不會攔截事件。一般須要將Fragment中的根佈局View的clickable屬性設置爲true,以屏蔽事件穿透。

頁面內容丟失

有時候將當前頁面切換到後臺,而後恢復到前臺時會發現頁面內容丟失。出現這個問題,緣由主要出如今onSaveInstanceState方法,有時候可能不須要保存Fragment的狀態,因此在super.onSaveInstanceState以前清空了FragmentManager中的Fragment,當頁面被切換到前臺時,就會出現頁面爲空的問題。至於如何正確保存/恢復Fragment的狀態,前面的頁面重疊部分已提供瞭解決方案。

ViewPager+Fragment的正確實現

ViewPager+Fragment是一個經典組合,基本上每一個APP中都會使用它。但在使用過程當中有一些細節須要關注。下面以一個實例來講明一下:

fragmentList.add(new FirstFragment());
fragmentList.add(new SecondFragment());
fragmentList.add(new ThirdFragment());
viewPager.setAdapter(new MyFragmentPagerAdapter(getSupportFragmentManager()));

public static class MyFragmentPagerAdapter extends FragmentPagerAdapter {

	public MyFragmentPagerAdapter(FragmentManager fm) {
		super(fm);
	}

	@Override
	public Fragment getItem(int position) {
		return fragmentList.get(position);
	}

	@Override
	public int getCount() {
		return fragmentList.size();
	}
}
複製代碼

這種寫法應該很熟悉,一般狀況下不會出現什麼問題,但當Activity重建時,就會發現,也不會有大的問題,只是多建立了幾個Fragment而已,下面看一下FragmentPagerAdapter的實現:

public Object instantiateItem(ViewGroup container, int position) {
    final long itemId = getItemId(position);
    String name = makeFragmentName(container.getId(), itemId);
    Fragment fragment = mFragmentManager.findFragmentByTag(name);
    if (fragment != null) {
        mCurTransaction.attach(fragment);
    } else {
        fragment = getItem(position);
        mCurTransaction.add(container.getId(), fragment,
                makeFragmentName(container.getId(), itemId));
    }
    
    return fragment;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    ...
    mCurTransaction.detach((Fragment)object);
}
複製代碼

能夠看到Adapter在初始化Item的時候,會先查看該position是否已存在Fragment,若是存在就從新attach, 沒有的時候纔會調用getItem建立新的Fragment。而destroyItem的實現說明已加載的Fragment,若是不在mOffscreenPageLimit範圍內,也只是detach掉了,其Fragment實例仍然是存在的,也就是說,每一個position上的Fragment僅會建立一次(每一個position上的getItem只被調用一次),即使是Activity被重建。也就是說,FragmentPagerAdapter內置了Fragment從新關聯的功能。

再回到上面的問題,若是在onCreate中建立Fragment, 那麼每次Activity重建時,都會建立新的Fragment,然而這些新建立的Fragment並無什麼用,由於FragmentPagerAdapter關聯的仍是以前存在的Fragment。因此推薦將Fragment的構造寫在getItem中。

@Override
public Fragment getItem(int position) {
	return ContentFragment.newInstance(position) ;
}
複製代碼

Fragment轉場動畫

Fragment設置轉場動畫使用setCustomAnimations方法,它有2個重載方法,詳細介紹以下:

/**
 * @param entry 新fragment進入的動畫,只對add和replace操做有效
 * @param exit 當前fragment退出的動畫,只對replace和remove操做有效
 * @return
 */
public FragmentTransaction setCustomAnimations(@AnimatorRes @AnimRes int entry, @AnimatorRes @AnimRes int exit);

/**
 * @param enter 新頁面進入的動畫 
 * @param exit 當前頁面退出的動畫
 * @param popEnter 當前頁面進入的動畫
 * @param popExit 新頁面退出的動畫
 * @return
 */
public FragmentTransaction setCustomAnimations(@AnimatorRes @AnimRes int enter, @AnimatorRes @AnimRes int exit,
       @AnimatorRes @AnimRes int popEnter, @AnimatorRes @AnimRes int popExit)
複製代碼

注意:

  1. setCustomAnimations必須在事務操做(add, replace或remove)以前調用纔有效;
  2. 2個參數的setCustomAnimations中的enter參數表示新fragment進入的動畫,而exit參數表示當前fragment退出的動畫,並非新添加的fragment的退出動畫,因此使用這個方法時,新添加的Fragment退出時沒有動畫效果;
  3. 4個參數的setCustomAnimations前兩個參數表示頁面進入時2個頁面的動畫效果,然後兩個參數表示從回退棧中返回時2個頁面的動畫效果;

除了setCustomAnimations方法,Android在5.0中還擴展了轉場動畫,可以使用如下方法實現:

/**
 * 新Fragment進入時的動畫
 * @param transition
 */
public void setEnterTransition(@Nullable Object transition);

/**
 * 新Fragment退出時的動畫
 * @param transition
 */
public void setReturnTransition(@Nullable Object transition);

/**
 * 新Fragment進入時當前Fragment的退出動畫, 需在當前Fragment對象中設置
 * @param transition
 */
public void setExitTransition(@Nullable Object transition);

/**
 * 新Fragment退出時原Fragment的進入動畫,需在原Fragment對象中設置
 * @param transition
 */
public void setReenterTransition(@Nullable Object transition);
複製代碼

其中的參數,Android提供了幾種實現:

  • Explode: 擴散動畫
  • Fade: 漸變更畫
  • Slide: 平移動畫

示例:

FragmentManager fm = mContext.getSupportFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
Fragment fragment = ContentFragment.newInstance(fm.getFragments().size());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
	fragment.setEnterTransition(new Slide(Gravity.RIGHT));
	fragment.setReturnTransition(new Slide(Gravity.RIGHT));
}
transaction.add(R.id.container_layout, fragment);
transaction.addToBackStack(null);
transaction.commit();
複製代碼

Fragment與Activity如何取捨

這是一個頗有爭議性的話題,有人認爲一個APP只須要一個Activity便可,也有人認爲Fragment只須要使用在需複用的頁面便可。關於這個問題,可從如下幾方面來探討:

  • 複用性;
  • 開發效率;
  • 業務耦合度;

基於這三點,咱們可在單個業務模塊中使用單Activity+多Fragment的模式來實現,如登陸模塊,只須要一個LoginActivity, 其餘的功能如註冊,找回密碼等都使用Fragment實現。一方面,Fragment比Activity更輕量,另外一方面,使模塊化在組件層面有了明顯的區分。

總結

這只是Fragment的一個簡單總結,可能還有不少細節未說起,歡迎補充。

相關文章
相關標籤/搜索