Fragment全面解析

生命週期

圖1

這裏寫圖片描述

圖2

這裏寫圖片描述

Fragment必須是依存於Activity而存在的,因此Activity的生命週期會直接影響到Fragment的生命週期,並且Fragment的生命週期和Activity的生命週期很相似。

onAttach()

onAttach()將在Fragment與其Activity關聯之後調用。需要使用Activity作爲其他操作的上下文,將在此回調方法中實現。

onCreate(Bundle savedInstanceState)

當Fragment的onCreate()回調時,該Fragment還沒有獲得Activity的onCreate()已完成的通知,所以不能將依賴於Activity視圖層次結構存在性的代碼放入此回調方法中。在onCreate()回調方法中,我們應該儘量避免耗時操作。

但可以通過getArguments()獲得setArguments()設置的Bundle參數

  • 注意:只有在Fragment添加到Activity之前可以調用setArguments()來給Fragment設置參數。

onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)

創建該Fragment的視圖。

  • 注意:不要將視圖層次結構附加到傳入的ViewGroup父元素中,該關聯會自動完成。如果在此回調中將Fragment的視圖層次結構附加到父元素,很可能會出現異常。這句話什麼意思呢?就是不要把初始化的view視圖主動添加到container裏面,因爲系統會自動完成關聯,所以inflate函數的第三個參數必須填false,而且不能出現container.addView(v)的操作。

onActivityCreated()

onActivityCreated()會在Activity完成其onCreate()回調之後調用

在調用onActivityCreated()之前,Activity的視圖層次結構已經準備好了,這是在用戶看到界面之前你可對界面進行最後調整的地方

onStart(),onResume(),onPause(),onStop()

它們與Activity的回調方法進行綁定,也就是說與Activity中的生命週期相對應,這裏不做過多介紹。

onDestroyView()

與onCreateView()相對應,當該Fragment的視圖被移除時調用

onDestroy()

不再使用Fragment時調用

  • 注意:Fragment仍然附加在Activity,也可以被找到,但是不能執行其他操作。

onDetach()

與onAttach()相對應,Fragment與Activity解除綁定,釋放資源

創建實例

像普通的類一樣,Fragment 擁有自己的構造函數,於是我們可以像下面這樣在 Activity 中創建 Fragment 實例:

MainFragment mainFragment = new MainFragment();

如果需要在創建 Fragment 實例時傳遞參數進行初始化的話,可以創建一個帶參數的構造函數,並初始化 Fragment 成員變量等。這樣做,看似沒有問題,但在一些特殊狀況下還是有問題的。

我們知道,Activity 在一些特殊狀況下會發生 destroy 並重新 create 的情形,比如屏幕旋轉、內存喫緊時;對應的,依附於 Activity 存在的 Fragment 也會發生類似的狀況。而一旦重新 create 時,Fragment 便會調用默認的無參構造函數,導致無法執行有參構造函數進行初始化工作

     * Default constructor.  <strong>Every</strong> fragment must have an
     * empty constructor, so it can be instantiated when restoring its
     * activity's state.  It is strongly recommended that subclasses do not
     * have other constructors with parameters, since these constructors
     * will not be called when the fragment is re-instantiated; instead,
     * arguments can be supplied by the caller with {@link #setArguments}
     * and later retrieved by the Fragment with {@link #getArguments}.

好在Fragment提供了相應的 API setArguments()幫助我們解決這個問題。利用Bundle傳遞數據,參考代碼如下:

public static OneFragment newInstance(int args){
    OneFragment oneFragment = new OneFragment();
    Bundle bundle = new Bundle();
    bundle.putInt("someArgs", args);
    oneFragment.setArguments(bundle);
    return oneFragment;
}

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Bundle bundle = getArguments();
    int args = bundle.getInt("someArgs");
}

嵌入方式

Activity 嵌入 Fragment 分爲佈局靜態嵌入代碼動態嵌入兩種。前者在 Activity 的 Layout 佈局中使用 < fragment > 標籤嵌入指定 Fragment,如:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <fragment
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        class="com.yifeng.samples.OneFragment"/>
</LinearLayout>

後者在 Activity 的 Java 代碼中藉助管理器類 FragmentManager 和 事務類 FragmentTransaction 提供的 replace() 方法替換 Activity 的 Layout 中的相應容器佈局,如:

FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.fl_content, OneFragment.newInstance());
ft.commit();

這兩種嵌入方式對應的 Fragment 生命週期略有不同。相比佈局靜態嵌入方式,代碼動態嵌入方式更爲常用,畢竟後者能夠實現靈活控制多個 Fragment,動態改變 Activity 中的內容

獲取Fragment的管理器

getFragmentManager() ,當然我們一般不使用它。

我們使用的Fragment一般都是v4包下的,所以應該使用getSupportFragmentManager()

然而有時候,我們會在 Fragment 裏面繼續嵌套二級甚至三級 Fragment,即 Activity 嵌套多級 Fragment。此時在 Fragment 裏管理子 Fragment 時,也需要使用到 FragmentManager。但是一定要使用 getChildFragmentManager() 方法獲取 FragmentManager 對象!

從官方文檔註釋上也可以看出這兩個方法獲取到的 FragmentManager 對象的區別:

Activity:getFragmentManager() 

Return the FragmentManager for interacting with fragments associated with this activity.
Fragment:getChildFragmentManager()

Return a private FragmentManager for placing and managing Fragments inside of this Fragment.

操作Fragment

Fragment 的動態添加、刪除等操作都需要藉助於 FragmentTransaction 類來完成,比如上面提到的 replace() 操作等。

add()

添加 Fragment 到 Activity 界面中。

remove()

移除 Activity 中指定的 Fragment。

replace()

通過內部調用 remove() 和 add() 完成 Fragment 的修改。

hide() 和 show()

隱藏和顯示Activity中的Fragment。

  • 補充:hide()和show()的時候會回調onHiddenChanged(),而創建Fragment的時候不會回調onHiddenChanged()。

addToBackStack()

添加當前事務到回退棧中,即當按下返回鍵時,界面迴歸到當前事物狀態

commit()

提交事務,所有通過上述方法對 Fragment 的改動都必須通過調用 commit() 方法完成提交

所以操作流程是這樣的:

FragmentManager fm = getFragmentManager();
//開啓事物
FragmentTransaction ft = fm.beginTransaction();

//其中夾雜一系列操作,比如add(),remove()
……

//提交事物
ft.commit();
  • 注意:動態切換顯示 Activity 中的多個 Fragment 時,可以通過 replace() 實現,也可以 hide() 和 show() 方法實現。事實上,我們更傾向於使用後者,因爲 replace() 方法不會保留 Fragment 的狀態,也就是說諸如 EditText 內容輸入等用戶操作在 remove() 時會消失。當然,如果你不想保留用戶操作的話,可以選擇前者,視情況而定。

BackStack(回退棧)

在移除片段的事務執行期間通過調用 addToBackStack() 顯式請求保存實例時,系統纔會將片段放入由宿主 Activity 管理的返回棧。

當用戶按下返回鍵時,如果回退棧中保存有之前的事務,便會執行事務回退,而不是 finish 掉當前 Activity。

舉個例子,比如 App 中有一個新用戶註冊功能,包括設置用戶名、密碼、手機號等等流程,設計師在 UI 設計上將每個流程單獨設計成一個界面,引導用戶一步步操作。作爲開發人員,如果將每一個完善信息的流程單獨設置成一個 Activity 的話操作起來就比較繁瑣,並且也不易於應用裏的邏輯處理,而如果使用 Fragment 並結合回退棧的話,就非常合適了。

將每一個設置的流程寫成一個 Fragment,通過狀態控制顯示不同的 Fragment,並利用回退棧實現返回上一步操作的功能。比如從 FirstStepFragment 進入 SecondStepFragment 時,比如可以在 LoginActivity.java 中這樣操作:

FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.hide(firstStepFragment);
if (secondStepFragment==null){
    ft.add(R.id.fl_content, secondStepFragment);
}else {
    ft.show(secondStepFragment);
}
ft.addToBackStack(null);
ft.commit();
  • 注意:這裏使用了 hide() 方法,而不是 replace() 方法,因爲我們當然希望用戶返回上一步操作時,之前設置的內容不會消失。

通信方式

通常,Fragment 與 Activity 通信存在三種情形:

  • Activity 操作內嵌的 Fragment

  • Fragment 操作宿主 Activity

  • Fragment 操作同屬 Activity中的其他 Fragment

由於 Activity 持有所有內嵌的 Fragment 對象實例(創建實例時保存的 Fragment 對象,或者通過 FragmentManager 類提供的 findFragmentById()findFragmentByTag() 方法也能獲取到 Fragment 對象),所以可以直接操作 Fragment。

Fragment 通過 getActivity() 方法可以獲取到宿主 Activity 對象(強制轉換類型即可),進而可以操作宿主 Activity;那麼很自然的,獲取到宿主 Activity 對象的 Fragment 便可以操作其他 Fragment 對象。

雖然上述操作已經能夠解決 Activity 與 Fragment 的通信問題,但會造成代碼邏輯紊亂的結果,極度不符合這一編程思想:高內聚,低耦合。Fragment 做好自己的事情即可,所有涉及到 Fragment 之間的控制顯示等操作,都應交由宿主 Activity 來統一管理。

所以強烈推薦,使用對外開放接口的形式將 Fragment 的一些對外操作傳遞給宿主 Activity。具體實現方式如下:

public class OneFragment extends Fragment implements View.OnClickListener{
    public interface IOneFragmentClickListener{
        void onOneFragmentClick();
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View contentView = inflater.inflate(R.layout.fragment_one, null);
        contentView.findViewById(R.id.edt_one).setOnClickListener(this);
        return contentView;
    }

    @Override
    public void onClick(View v) {
        if (getActivity() instanceof IOneFragmentClickListener){
             ((IOneFragmentClickListener) getActivity()).onOneFragmentClick();
        }
    }
}

只要在宿主 Activity 實現 Fragment 定義的對外接口 IOneFragmentClickListener,便可以實現 Fragment 調用 Activity 的功能。

當然,你可以這樣做:

public class OneFragment extends Fragment implements View.OnClickListener{

    private IOneFragmentClickListener clickListener;

    public interface IOneFragmentClickListener{
        void onOneFragmentClick();

    public void setClickListener(IOneFragmentClickListener clickListener) {
        this.clickListener = clickListener;
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View contentView = inflater.inflate(R.layout.fragment_one, null);
        contentView.findViewById(R.id.edt_one).setOnClickListener(this);
        return contentView;
    }

    @Override
    public void onClick(View v) {
        clickListener.onOneFragmentClick();
    }
}

原理是一樣的,只是相比第一種方式,需要在宿主 Activity 中額外添加一步監聽設置:

oneFragment.setClickListener(this);

Fragment 重疊問題

原因

前面我們介紹 Fragment 初始化時提到 Activity 銷燬重建的問題,試想一下,當 Activity 重新執行 onCreate() 方法時,是不是會再次執行 Fragment 的創建和顯示等操作呢?而之前已經存在的 Fragment 實例也會銷燬再次創建,這不就與 Activity 中 onCreate() 方法裏面第二次創建的 Fragment 同時顯示從而發生 UI 重疊的問題了嗎?

根據經驗,通常我們會在 AndroidManifest 裏將 Activity 設置爲橫屏模式,所以不會由於屏幕旋轉導致這種問題的出現。一種比較多的出現方式是,應用長時間處於後臺,但由於設備內存喫緊,導致 Activity 被銷燬,而當用戶再次打開應用時便會發生 Fragment 重疊的問題。但是這種問題在開發階段由於應用的頻繁使用導致我們很難遇見,但確確實實存在着。所以開發過程中,一定要注意這類問題。

解決方案

知道問題的根源所在之後,對應的解決方案也就有啦。就是在 Activity 中創建 Fragment 實例時,添加一個判斷即可,處理方式有三種:

在 Activity 提供的 onAttachFragment() 方法中處理:

@Override
public void onAttachFragment(Fragment fragment) {
    super.onAttachFragment(fragment);
    if (fragment instanceof  OneFragment){
        oneFragment = (OneFragment) fragment;
    }
}

在創建 Fragment 前添加判斷,判斷是否已經存在:

ragment tempFragment = getSupportFragmentManager().findFragmentByTag("OneFragment");
if (tempFragment==null) {
    oneFragment = OneFragment.newInstance();
    ft.add(R.id.fl_content, oneFragment, "OneFragment");
}else {
    oneFragment = (OneFragment) tempFragment;
}

更爲簡單,直接利用Activity的onCreate()中的Bundle參數savedInstanceState判斷即可:

if (savedInstanceState==null) {
    oneFragment = OneFragment.newInstance();
    ft.add(R.id.fl_content, oneFragment, "OneFragment");
}else {
    oneFragment = (OneFragment) getSupportFragmentManager().findFragmentByTag("OneFragment");
}

getActivity()引用問題

使用中,經常會在 Fragment 中通過 getActivity() 獲取到宿主 Activity 對象,但稍有不慎便會引發下面這兩個問題:

Activity 的實例銷燬問題

比如,Fragment 中存在類似網絡請求之類的異步耗時任務,當該任務執行完畢回調 Fragment 的方法並用到宿主 Activity 對象時,很有可能宿主 Activity 對象已經銷燬,從而引發 NullPointException 等異常,甚至造成程序崩潰。

所以,異步回調時需要注意添加空值等判斷(譬如:fragment.isAdd(),getActivity()!=null 等),或者在 Fragment 創建實例時就通過 getActivity().getApplicationContext() 方法保存整個應用的上下文對象,再來使用。

內存泄漏問題

如果 Fragment 持有宿主 Activity 的引用,會導致宿主 Activity 無法回收,造成內存泄漏。所以,如果可以的話,儘量不要在 Fragment 中持有宿主 Activity 的引用。

onActivityResult()

Fragment 類提供有 startActivityForResult() 方法用於 Activity 間的頁面跳轉和數據回傳,其實內部也是調用 Activity 的對應方法。但是在頁面返回時需要注意 Fragment 沒有提供 setResult() 方法,可以通過宿主 Activity 實現

舉個例子,在 ActivityA 中的 FragmentA 裏面調用 startActivityForResult() 跳轉至 ActivityB 中,並在 ActivityB 中的 FragmentB 裏面返回到 ActivityA,返回代碼如下:

Intent intent = new Intent();
// putExtra
getActivity().setResult(Activity.RESULT_OK, intent);
getActivity().finish();

在回調時,先會回調 ActivityA 中的 onActivityResult() 方法,然後再分發回調 FragmentA 中的 onActivityResult() 方法,從 FragmentActivity 類的源碼中可以看出:

/**
* Dispatch incoming result to the correct fragment.
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    mFragments.noteStateNotSaved();
    int requestIndex = requestCode>>16;
    if (requestIndex != 0) {
        requestIndex--;
        String who = mPendingFragmentActivityResults.get(requestIndex);
        mPendingFragmentActivityResults.remove(requestIndex);
        if (who == null) {
            Log.w(TAG, "Activity result delivered for unknown Fragment.");
            return;
        }
        Fragment targetFragment = mFragments.findFragmentByWho(who);
        if (targetFragment == null) {
            Log.w(TAG, "Activity result no fragment exists for who: " + who);
        } else {
            targetFragment.onActivityResult(requestCode & 0xffff, resultCode, data);
        }
        return;
    }
    super.onActivityResult(requestCode, resultCode, data);
}

再拓展一下,如果 FragmentA 中又嵌入一層 FragmentAA ,然後從 FragmentAA 中跳轉至 ActivityB,那麼在 FragmentAA 中的 onActivityResult() 方法中能收到回調嗎?顯然不能。從上述源碼中可以看出 FragmentActivity 只進行到一級分發。所以,如果想實現多級分發,就得自己在各級 Fragment 中手動添加分發代碼,至下一級 Fragment 中

未必靠譜的出棧方法remove()

如果你想讓某一個Fragment出棧,使用remove()在加入回退棧時並不靠譜。

如果你在add()的同時將Fragment加入回退棧:addToBackStack(name)的情況下,它並不能真正將Fragment從棧內移除。

如果你在2秒後(確保Fragment事務已經完成)打印getSupportFragmentManager().getFragments(),會發現該Fragment依然存在,並且依然可以返回到被remove的Fragment,而且是空白頁面。

這類似於Activity銷燬了,但是沒有finish()。

如果你沒有將Fragment加入回退棧,remove()可以正常出棧。

ViewPager中Fragment的生命週期

關於ViewPager中Fragment的生命週期,請看這篇文章ViewPager中Fragment的生命週期

分析到這裏就結束了,總結的其實不夠詳盡,有待補充

參考:
1.Android Fragment 真正的完全解析(上)
2.Android Fragment 真正的完全解析(下)
3.Android Fragment 的使用,一些你不可不知的注意事項
4.Activity與Fragment生命週期探討
5.Fragment全解析系列(一):那些年踩過的坑
6.谷歌官方文檔–Fragment

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