Fragment的填坑之旅

前言

目前主流的應用中,多是採用單Actvity多Fragment的方式實現的。隨着應用功能越來越多,界面越來越複雜,我們會利用Fragment對Activity的界面進行模塊化編程。Fragment有着種種優點吸引着我們,如比Activity有着更好的性能,能夠輕量切換,開銷比Activity小等等。

作爲 view 界面的一部分,Fragment 的存在必須依附於 FragmentActivit使用,並且與 FragmentActivit 一樣,擁有自己的獨立的生命週期,同時處理用戶的交互動作。同一個 FragmentActivit 可以有一個或多個 Fragment 作爲界面內容,同樣Fragment也可以擁有多個子Fragment,並且可以動態添加、刪除 Fragment,讓UI的重複利用率和易修改性得以提升,同樣可以用來解決部分屏幕適配問題。另一方面,support v4 包中也提供了 Fragment,兼容 Android 3.0 之前的系統,使用兼容包需要注意兩點:
宿主Activity 必須繼承自 FragmentActivity;
使用getSupportFragmentManager() 方法獲取 FragmentManager 對象;

儘管Fragment帶來許多便利,讓我們脫離了重複的構建Activity中,但是我們還是無法忽視它給我們帶來的許多麻煩。下面我將列出在使用Fragment時,可能出現的問題,並且會分析其原因和提供解決方案。

當app長時間運行在系統後臺時,如果出現系統資源緊張時會導致將該app的資源全部回收,這時如果將app再從後臺返回前臺運行時,app會重新啓動。這種情況稱爲“內存重啓”。在系統要把app回收之前,會先調用方法onSaveInstanceState 將Activity的狀態保存下來,Activity的FragmentManager負責把Activity的Fragment信息保存起來。在“內存重啓”後,Activity的恢復是從棧底到棧頂逐步恢復,Fragment會在宿主Activity的onCreate方法調用後緊接着恢復(從onAttach生命週期開始)。

getActivity() 爲空的異常

在出現上述“內存重啓”之後,調用getActivity()時,有時會返回null,提示空指針異常。
原因:
出現這種情況,在調用getActivity()時,當前的Fragment已經onDetach了宿主Activity。但是Fragment還執行了異步任務,結束後調用該方法,這樣就會出現空指針。
再次打開該Activity時, 在onCreate方法裏取出bundle裏的fragment狀態, 但這時fragment對應的Activity早就不在了, 所以getActivity爲空。

解決方法:
在Fragment基類裏設置一個Activity mActivity的全局變量,在onAttach(Activity activity)裏賦值,使用mActivity代替getActivity(),保證Fragment即使在onDetach後,仍持有Activity的引用(有引起內存泄露的風險,但是相比空指針閃退,這種做法“安全”些),即:
@Override
public void onAttach(Context context) {
super.onAttach(context);
activity = (Activity) context;
}

Fragment重疊異常

當App的頁面中,add了多個Fragment時,在“內存重啓”之後,回到前臺時,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。
@Override
protected 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。

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity);

TargetFragment targetFragment;
HideFragment hideFragment;

if (savedInstanceState != null) {  // “內存重啓”時調用
    List<Fragment> fragmentList = getSupportFragmentManager().getFragments();

//此處遍歷時,不能獲取isHidden的狀態,因爲沒有保存
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的狀態
發生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
    }
}

}
4.手動修改不保存所有記錄
在Activity中重寫onSaveInstanceState方法:
@Override
protected void onSaveInstanceState(Bundle outState) {
//super.onSaveInstanceState(outState); //註釋掉該方法, 即不保存狀態
}
這樣做雖然比較隨意,但是相比頁面重疊,用戶體驗來說要好上不少。

異常:Can not perform this action after onSaveInstanceState
在你離開當前Activity等情況下,系統會調用onSaveInstanceState()幫你保存當前Activity的狀態、數據等,直到再回到該Activity之前(onResume()之前),你使用commit()提交了Fragment事務,就會拋出該異常!
解決辦法:
該事務使用commitAllowingStateLoss()方法提交,但是有可能導致該次提交無效!(在此次離開時恰巧Activity被強殺時)。

關於onHiddenChanged和setUserVisibleHint

onHiddenChanged的回調時機
當使用add()+show(),hide()跳轉新的Fragment時,舊的Fragment回調onHiddenChanged(),不會回調setUserVisibleHint和onStop()等生命週期方法,而新的Fragment在創建時是不會回調onHiddenChanged(),這點要切記。(第一次加載時,不會調用該方法);
使用FragmentPagerAdapter+ViewPager時,切換回上一個Fragment頁面時(已經初始化完畢),不會回調任何生命週期方法以及onHiddenChanged(),只有setUserVisibleHint(boolean isVisibleToUser)會被回調,所以如果你想進行一些懶加載,需要在這裏處理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章