關于Fragment的那些事、那些坑

引言

在Android3.0開始,Android引入了Fragment。當初最主要的目的是為了給大屏幕手持設備提供更加靈活和動態的UI設計。隨著應用功能越來越多,界面越來越復雜,我們會利用Fragment對Activity的界面進行模塊化編程。比如一個Activity界面內有多個請求,每個請求得到的結果展示不同的UI片段,那么我們就可以將Activity利用Fragment來完成UI模塊化。目前在我目前的項目中,Fragment的地位已經舉足輕重了。當然我寫篇文章,并不是簡單的介紹Fragment的用法,而是結合我在項目中使用Fragment所遇到的問題進行說明。

Fragment的生命周期

Fragment與Activity一樣也是具有生命周期的,它的生命周期與Activity息息相關。下面看下Fragment生命周期流程圖(摘自Android官方開發文檔)

fragment_lifecycle

大體上Fragment的生命周期與Activity類似,但是也有自己獨有的生命周期方法。每個生命周期的意義大家可以看看Android Fragment,這里就不做詳解了。

Fragment與Activity之間生命周期的關系

每個Fragment實例存在于單獨的一個Activity實例中,因此Fragment必須以某種方式與Activity的生命周期合作。Fragment不僅與Activity的生命周期合作,而且他們之間有“密切”聯系。


Fragment lifecycle, from added to active

上圖是Fragment創建到激活過程與Activity相關的生命周期圖,為了驗證該過程,我們寫一個demo程序來進行驗證。
打印Activity生命周期方法相關方法代碼:

public class MainActivity extends FragmentActivity {
    private static final String TAG = "KissonChan";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "Activity onCreate");
        setContentView(R.layout.activity_main);
        TestFragment fragment = (TestFragment)getFragmentManager().findFragmentById(R.id.test);
    }

    @Override
    public void onAttachFragment(Fragment fragment) {
        super.onAttachFragment(fragment);
        Log.d(TAG, "Activity onAttachFragment");
    }

    @Override
    protected void onStart() {
        super.onStart();
        Log.d(TAG, "Activity onStart");
    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.d(TAG, "Activity onResume");
    }

    @Override
    protected void onPause() {
        super.onPause();
        Log.d(TAG, "Activity onPause");
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.d(TAG, "Activity onStop");
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        Log.d(TAG, "Activity onSaveInstanceState");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "Activity onDestroy");
    }

    @Override
    protected void onRestart() {
        super.onRestart();
        Log.d(TAG, "Activity onRestart");
    }
}

打印Fragment生命周期方法相關方法代碼:

public class TestFragment extends Fragment {
    private static final String TAG = "KissonChan";
    private View mRootView;

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        Log.e(TAG, "Fragment onAttach");
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.e(TAG, "Fragment onCreate");
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState) {
        Log.e(TAG, "Fragment onCreateView");
        mRootView = inflater.inflate(R.layout.fragment_test, container, false);
        return mRootView;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        Log.e(TAG, "Fragment onActivityCreated");
    }

    @Override
    public void onStart() {
        super.onStart();
        Log.e(TAG, "Fragment onStart");
    }

    @Override
    public void onResume() {
        super.onResume();
        Log.e(TAG, "Fragment onResume");
    }

    @Override
    public void onPause() {
        super.onPause();
        Log.e(TAG, "Fragment onPause");
    }

    @Override
    public void onStop() {
        super.onStop();
        Log.e(TAG, "Fragment onStop");
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        Log.e(TAG, "Fragment onSaveInstanceState");
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        Log.e(TAG, "Fragment onDestroyView");
    }

    @Override
    public void onDetach() {
        super.onDetach();
        Log.e(TAG, "Fragment onDetach");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.e(TAG, "Fragment onDestroy");
    }
}

程序的執行結果是:

05-12 11:38:42.901 32590-32590/? D/KissonChan: Activity onCreate
05-12 11:38:42.911 32590-32590/? E/KissonChan: Fragment onAttach
05-12 11:38:42.911 32590-32590/? D/KissonChan: Activity onAttachFragment
05-12 11:38:42.911 32590-32590/? E/KissonChan: Fragment onCreate
05-12 11:38:42.911 32590-32590/? E/KissonChan: Fragment onCreateView
05-12 11:38:42.911 32590-32590/? E/KissonChan: Fragment onActivityCreated
05-12 11:38:42.911 32590-32590/? D/KissonChan: Activity onStart
05-12 11:38:42.911 32590-32590/? E/KissonChan: Fragment onStart
05-12 11:38:42.911 32590-32590/? D/KissonChan: Activity onResume
05-12 11:38:42.911 32590-32590/? E/KissonChan: Fragment onResume

在Fragment從激活態到銷毀也有類似的生命周期過程,不過與創建過程是相反。當Fragment的生命周期方法調用之后才調用對應的Activity生命周期方法,如下圖。


Fragment lifecycle, from active to destroy

注意:如果正常結束Activity,onSaveInstanceState方法將不被調用。如果用戶按下home鍵或者電源鍵等相關操作,將Activity置于后臺,那么onSaveInstanceState將被調用。

正常結束Activity程序運行結果是:

05-12 14:10:44.131 8266-8266/? E/KissonChan: Fragment onPause
05-12 14:10:44.131 8266-8266/? D/KissonChan: Activity onPause
05-12 14:10:44.381 8266-8266/? E/KissonChan: Fragment onStop
05-12 14:10:44.381 8266-8266/? D/KissonChan: Activity onStop
05-12 14:10:44.381 8266-8266/? E/KissonChan: Fragment onDestroyView
05-12 14:10:44.381 8266-8266/? E/KissonChan: Fragment onDestroy
05-12 14:10:44.381 8266-8266/? E/KissonChan: Fragment onDetach
05-12 14:10:44.381 8266-8266/? D/KissonChan: Activity onDestroy

按下home鍵程序運行結果是:

05-12 14:12:01.781 8266-8266/? E/KissonChan: Fragment onPause
05-12 14:12:01.781 8266-8266/? D/KissonChan: Activity onPause
05-12 14:12:02.041 8266-8266/? E/KissonChan: Fragment onSaveInstanceState
05-12 14:12:02.041 8266-8266/? D/KissonChan: Activity onSaveInstanceState
05-12 14:12:02.041 8266-8266/? E/KissonChan: Fragment onStop
05-12 14:12:02.041 8266-8266/? D/KissonChan: Activity onStop

如果對Fragment和Activity的生命周期有深入了解想法的小伙伴,可以自己閱讀相關源碼。我這里僅僅是拋磚引玉,做個簡單介紹,為后續介紹工作過程中碰到的問題做一個鋪墊。

Fragment的兩種聲明方式

Android提供了兩種聲明Fragment的方式,這兩種方式上使用會有不同。同時也有一些需要注意的地方。

在Activity布局中添加Fragment

這種方式數據比較常用,在Activity布局中聲明fragment。

    <fragment
        android:id="@+id/test"
        android:name="com.dighammer.kisson.testfragment.TestFragment"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

隨后,在Activity通過如下方法初始化Fragment。

TestFragment fragment = (TestFragment)getFragmentManager().findFragmentById(R.id.test);

在代碼中將Fragment添加到已有ViewGroup中

這種方式我自己用的比較多,方法如下:

FragmentTransaction transaction = getFragmentManager().beginTransaction();
TestFragment fragment = new TestFragment();
transaction.add(R.id.container,fragment);
//transaction.replace(R.id.container,fragment);
transaction.commit();

這里咱們需要注意FragmentTransaction中add和replace方法的區別。

  1. add方法是把Fragment添加到ViewGroup中,通過該方法可以添加多個Fragment。
  2. replace方法會把ViewGroup中的原有Fragment先remove掉然后再add新Fragment。

使用add方法可以避免再次創建Fragment實例,一般情況是建議使用add方法,并配合FragmentTransaction的show和hide方法來使用。

那些年,我踩過的Fragment的坑

第一個坑

 java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
 at android.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1331)
at android.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1349)
at android.app.BackStackRecord.commitInternal(BackStackRecord.java:735)
at android.app.BackStackRecord.commit(BackStackRecord.java:711)
at com.dighammer.kisson.testfragment.MainActivity$1.run(MainActivity.java:30)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5273)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:908)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:703)

該類型的Crash開始的時候在我們的后臺崩潰日志出現很多次。
這里我寫了一段代碼,運行后可以百分比復現。

public class MainActivity extends FragmentActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                FragmentTransaction transaction = getFragmentManager().beginTransaction();
                TestFragment fragment = new TestFragment();
                transaction.add(R.id.container, fragment);
                transaction.commit();
            }
        }, 5000);
    }

當程序界面顯示出來后,按home鍵,過接近5秒后,就會出現該奔潰日志。
原因是FragmentTransaction中的commit方法必須在onSaveInstanceState之前調用。
那么如何解決了?在FragmentTransaction中提供了commitAllowingStateLoss方法,通過調用該方法就不用關心Activity的狀態是否保存。

第二個坑

情景再現,先看代碼。

public class MainActivity extends FragmentActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        FragmentTransaction transaction = getFragmentManager().beginTransaction();
        TestFragment fragment = new TestFragment();
        transaction.add(R.id.container, fragment);
        transaction.commit();
        fragment.setText("kisson chan");

    }
}

運行后,會報如下錯誤。

java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.TextView.setText(java.lang.CharSequence)' on a null object reference
                                                       at com.dighammer.kisson.testfragment.TestFragment.setText(TestFragment.java:53)
                                                       at com.dighammer.kisson.testfragment.MainActivity.onCreate(MainActivity.java:18)
                                                       at android.app.Activity.performCreate(Activity.java:6041)
                                                       at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1109)
                                                       at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2283)
                                                       at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2392) 
                                                       at android.app.ActivityThread.access$800(ActivityThread.java:154) 
                                                       at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1308) 
                                                       at android.os.Handler.dispatchMessage(Handler.java:102) 
                                                       at android.os.Looper.loop(Looper.java:135) 
                                                       at android.app.ActivityThread.main(ActivityThread.java:5273) 
                                                       at java.lang.reflect.Method.invoke(Native Method) 
                                                       at java.lang.reflect.Method.invoke(Method.java:372) 
                                                       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:908) 
                                                       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:703) 

這里是空指針異常,TestFragment中的TextView還未被初始化。但是,如果我們通過Activity布局中聲明Fragment則不會報錯,代碼如下。

public class MainActivity extends FragmentActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TestFragment fragment = (TestFragment)getFragmentManager().findFragmentById(R.id.test);
        fragment.setText("kisson");
    }
}

原因其實很簡單,通過Activity布局中聲明Fragment,Fragment的初始化以及其布局中控件的初始化都發生在Activity的setContentView方法中。所以在setContentView方法調用之后,我們可以直接操作Fragment中的控件。
但是通過代碼將Fragment添加到ViewGroup的方式,并不能在Activity的onCreate方法中去操作Fragment中的控件,只能等到onCreate完成之后調用。
如果必須要在Activity的onCreate方法中操作Fragment中的控件,這里我提供一個參考辦法。

public class TestFragment extends Fragment{

    private View mRootView;

    private TextView mTextView;

    private String mTextStr;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        mRootView = inflater.inflate(R.layout.fragment_test,container,false);
        return mRootView;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        mTextView = (TextView)mRootView.findViewById(R.id.text);
        if (!TextUtils.isEmpty(mTextStr)){
            mTextView.setText(mTextStr);
        }
    }
    /**
     * 如果mTextView為空,即mTextView還未被初始化,那么將text賦值給mTextStr,等到onActivityCreated調用后再setText
     * 如果mTextView不為空,則直接調用TextView的setText方法進行賦值
     * */
    public void setText(String text){
        if (mTextView == null){
            mTextStr = text;
        }
        else {
            mTextView.setText(text);
        }
    }
}

第三個坑

情景再現,先看代碼。

public class TestFragment extends Fragment{

    private View mRootView;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        mRootView = inflater.inflate(R.layout.fragment_test,container,false);
        return mRootView;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                ((MainActivity) (getActivity())).print();
            }
        },5000);
    }
}

繼續上代碼

public class MainActivity extends FragmentActivity {

    private static final String TAG = "KissonChan";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        FragmentTransaction transaction = getFragmentManager().beginTransaction();
        TestFragment fragment = new TestFragment();
        transaction.add(R.id.container,fragment);
        transaction.commit();
    }

    public void print() {
        Log.d(TAG, "KissonChan");
    }
}

當程序Activity顯示出來后,接著按下返回鍵結束Activity,靜待五秒后,出現以下崩潰日志。

 Process: com.dighammer.kisson.testfragment, PID: 15870
java.lang.NullPointerException: Attempt to invoke virtual method 'void com.dighammer.kisson.testfragment.MainActivity.print()' on a null object reference
                                                       at com.dighammer.kisson.testfragment.TestFragment$1.run(TestFragment.java:41)
                                                       at android.os.Handler.handleCallback(Handler.java:739)
                                                       at android.os.Handler.dispatchMessage(Handler.java:95)
                                                       at android.os.Looper.loop(Looper.java:135)
                                                       at android.app.ActivityThread.main(ActivityThread.java:5273)
                                                       at java.lang.reflect.Method.invoke(Native Method)
                                                       at java.lang.reflect.Method.invoke(Method.java:372)
                                                       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:908)
                                                       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:703)

因為MainActivity已經被銷毀,五秒鐘后在TestFragment實例中會調用MainActivity的print方法,所以拋出空指針異常。
這里很奇怪,Activity已經被銷毀了,為什么Fragment還能繼續茍活。
這是因為Fragment并不會隨著Activity的回收而被系統回收,所有被創建的Fragment都會保存在一個Bundle里面。所以會導致以上崩潰。所以在一些延時操作或者線程操作中調用getActivity最好做下為空判斷!

結尾

Fragment作為模塊化UI編程類,極大方便了我們代碼設計。但是同時我們也要深刻理解其生命周期及其意義。有時間大家可以看看源碼,發現其中的奧秘!

文中圖片均摘自我個人CSDN,并無侵權!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容