Android墓碑機制

Android墓碑機制

本文連接地址:http://www.itdecent.cn/p/f5be35aaed32

一、墓碑定義

墓碑機制是手機操作系統(tǒng)中的一個程序運行規(guī)則。說簡單點,就是手機上一個任務(wù)被迫中斷時(如有電話打入),系統(tǒng)記錄下當(dāng)前應(yīng)用程序的狀態(tài)后,(像把事件記錄在墓碑上一樣),然后中止程序。當(dāng)需要恢復(fù)時,根據(jù)“墓碑”上的內(nèi)容,將程序恢復(fù)到中斷之前的狀態(tài)。這樣的一種機制就是“墓碑機制”

二、墓碑的保存與恢復(fù)

而這種方式在Android的表示形式為:在內(nèi)存不夠的情況下應(yīng)用進入了后臺,系統(tǒng)會有可能殺死這個Activity,用戶切換回該應(yīng)用時就會恢復(fù)當(dāng)前Activity的內(nèi)容。因此出現(xiàn)這種狀況我們該怎么處理?

針對這種狀況,系統(tǒng)自帶的View或Fragment都已經(jīng)幫我們實現(xiàn)了狀態(tài)的自動保存與恢復(fù),但是對于自己開發(fā)的自定義View,就需要去保存狀態(tài)和恢復(fù)狀態(tài),這里系統(tǒng)提供了兩個API方便我們?nèi)崿F(xiàn)保存和恢復(fù),分別是onSaveInstanceStateonRestoreInstanceState這兩個方法。

三、如何觸發(fā)墓碑機制

簡單說就是onSaveInstanceStateonRestoreInstanceState函數(shù)的調(diào)用時間

  • 當(dāng)用戶按下HOME鍵時

  • 長按HOME鍵,選擇運行其他的程序時

  • 按下電源按鍵(關(guān)閉屏幕顯示)時

  • 從activity A中啟動一個新的activity時

  • 屏幕方向切換時,例如從豎屏切換到橫屏?xí)r

  • 語言的切換

先說第五、六點,在屏幕切換之前,系統(tǒng)會銷毀activity A,在屏幕切換之后系統(tǒng)又會自動地創(chuàng)建activity A,所以onSaveInstanceState()一定會被執(zhí)行,且也一定會執(zhí)行onRestoreInstanceState()

針對第五、六點打印的數(shù)據(jù)(activity A所發(fā)生的生命周期):

MainActivity: onPause

MainActivity: onSaveInstanceState

MainActivity: onStop

MainActivity: onDestroy

MainActivity: onCreate

MainActivity: onStart

MainActivity: onRestoreInstanceState

MainActivity: onResume

回到前4點,每次觸發(fā)都會調(diào)用onSaveInstanceState,但是再次喚醒卻不一定調(diào)用onRestoreInstanceState,這是為什么呢?onSaveInstanceStateonRestoreInstanceState難道不是配對使用的?

首先在Android中,onSaveInstanceState是為了預(yù)防Activity被后臺殺死的情況做的預(yù)處理,如果Activity沒有被后臺殺死,那么自然也就不需要進行現(xiàn)場的恢復(fù),也就不會調(diào)用onRestoreInstanceState,而大多數(shù)情況下,Activity不會那么快被殺死。

那么我們要如何測試這4種情況?

四、如何調(diào)試

前4種要在喚醒時候調(diào)用onRestoreInstanceState,那前提是只有Activity或者App被異常殺死,走恢復(fù)流程時候才會被調(diào)用。

應(yīng)用是如何知道我是被異常殺死的,由于底層涉獵不深,只能大概的描述下:應(yīng)用被異常殺死后在重新打開,系統(tǒng)底層會判斷該應(yīng)用是否異常退出,接著把當(dāng)時現(xiàn)場的數(shù)據(jù)傳遞給它,應(yīng)用拿到數(shù)據(jù)后傳給Activity,調(diào)用起onRestoreInstanceState,這是Framework里ActivityThread中啟動Activity的源碼:


private Activity performLaunchActivity(){

      ...

      mInstrumentation.callActivityOnCreate(activity, r.state);

          r.activity = activity;

          r.stopped = true;

          if (!r.activity.mFinished) {

              activity.performStart();

              r.stopped = false;

          }

          if (!r.activity.mFinished) {

              if (r.state != null) {

                  mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state);

              }

          }

          if (!r.activity.mFinished) {

              activity.mCalled = false;

              mInstrumentation.callActivityOnPostCreate(activity, r.state);

          }

}

可以看出,只有r.state != null的時候,才通過mInstrumentation.callActivityOnRestoreInstanceState回調(diào)OnRestoreInstanceState,而r.state就是ActivityManagerService通過Binder傳給ActivityThread數(shù)據(jù),主要用來做場景恢復(fù)。

那我們要怎么測試這種情況呢?

  • 開發(fā)者模式下勾選不保留活動選擇

該方式是為了方便測試,在開發(fā)者模式下勾選不保留活動選擇,這樣應(yīng)用的Activity進入后臺就不會保留,從而執(zhí)行onSaveInstanceState,再次恢復(fù)到前臺執(zhí)行onRestoreInstanceState。

image
  • 內(nèi)存不足下觸發(fā)OOM

先修改模擬起的內(nèi)存大小,然后在打開新的Activity里面加載大數(shù)據(jù),不斷打開新界面,這時候內(nèi)存會不斷增多,直到超出系統(tǒng)可分配的內(nèi)存,導(dǎo)致OOM并提示錯誤,確認后系統(tǒng)會殺掉應(yīng)用釋放內(nèi)存,這時候會重新恢復(fù)界面。

打印日志如下:

MainActivity: onCreate

MainActivity: onStart

MainActivity: onRestoreInstanceState

MainActivity: onResume
  • 直接殺掉應(yīng)用

按Home把當(dāng)前應(yīng)用放到后臺,然后從Android Studio進入Devive Monitor,選擇當(dāng)前應(yīng)用,接著選stop按鈕。

如圖:

image

這時恢復(fù)應(yīng)用時就會觸發(fā)onRestoreInstanceState。

五、關(guān)于onSaveInstanceState的探討

目前統(tǒng)計線上的bug,偶爾會看到這樣的一個bug:


java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState

at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1842)

at android.support.v4.app.FragmentManagerImpl.popBackStackImmediate(FragmentManager.java:775)

at android.support.v4.app.FragmentActivity.onBackPressed(FragmentActivity.java:178)

at android.app.Activity.onKeyUp(Activity.java:2282)

at android.view.KeyEvent.dispatch(KeyEvent.java:3232)

嘗試了網(wǎng)上各種方案,但總偶爾會出現(xiàn),要解決這個bug,我們不妨先提出這幾個問題:

  • 錯誤是在哪里出現(xiàn)的

  • 錯誤來源

  • 為什么會出現(xiàn)這個錯誤

  • 如何解決

1、錯誤是在哪里出現(xiàn)的

首先定位問題,觀察源碼可以發(fā)現(xiàn),它是在FragmentManagercheckStateLoss方法里面拋出錯誤。


private void checkStateLoss() {

    if (mStateSaved) {

        throw new IllegalStateException(

                "Can not perform this action after onSaveInstanceState");

    }

    if (mNoTransactionsBecause != null) {

        throw new IllegalStateException(

                "Can not perform this action inside of " + mNoTransactionsBecause);

    }

}

2、錯誤來源

我們根據(jù)該方法追溯上去。


@Override

public boolean popBackStackImmediate() {

    checkStateLoss();

    executePendingTransactions();

    return popBackStackState(mActivity.mHandler, null, -1, 0);

}

很明顯的看出,popBackStackImmediate這個出棧的方法調(diào)用之前會去檢查狀態(tài)是否改變,然后再去執(zhí)行Fragment操作。

繼續(xù)追蹤,看看到底是誰調(diào)用了。

FragmentActivityonBackPressed:


public void onBackPressed() {

    if (!mFragments.popBackStackImmediate()) {

        supportFinishAfterTransition();

    }

}

來源找到了,接著分析為什么出現(xiàn)錯誤。

3、為什么會出現(xiàn)這個錯誤

觀察上述代碼,產(chǎn)生該錯誤的原因是mStateSaved變量為true,而這個變量是從哪里設(shè)置的呢?

我們從Activity調(diào)用onSaveInstanceState方法開始,該方法先保存view的狀態(tài)


protected void onSaveInstanceState(Bundle outState) {

    outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());

    // view樹的狀態(tài)保存完之后,處理fragment相關(guān)的

    Parcelable p = mFragments.saveAllState();

    if (p != null) {

        outState.putParcelable(FRAGMENTS_TAG, p);

    }

    getApplication().dispatchActivitySaveInstanceState(this, outState);

}

接著調(diào)用mFragments.saveAllState();該方法里面對mStateSaved進行的設(shè)置true操作。


Parcelable saveAllState() {

        // Make sure all pending operations have now been executed to get

        // our state update-to-date.

        execPendingActions();

        mStateSaved = true;

        if (mActive == null || mActive.size() <= 0) {

            return null;

        }

    ...

}

而這個方法里面一系列操作都是保存fragment的狀態(tài)。

除了在onSaveInstanceState中設(shè)置以外,在onStop中也把mStateSaved置為true。


public void dispatchStop() {

    // See saveAllState() for the explanation of this.  We do this for

    // all platform versions, to keep our behavior more consistent between

    // them.

    mStateSaved = true;

    moveToState(Fragment.STOPPED, false);

}

那么什么時候才把mStateSaved設(shè)置為false呢。

回到ActivityonCreate方法里,這里可以發(fā)現(xiàn)它調(diào)用了FragmentdispatchCreate方法,dispatchCreatemStateSaved設(shè)置為false。


protected void onCreate(@Nullable Bundle savedInstanceState) {

        ...

        mFragments.dispatchCreate();

        getApplication().dispatchActivityCreated(this, savedInstanceState);

        if (mVoiceInteractor != null) {

            mVoiceInteractor.attachActivity(this);

        }

        mCalled = true;

    }

同理既然onCreate有設(shè)置,那么resume也有做設(shè)置


final void performResume() {

    performRestart();

  ...

    mFragments.dispatchResume();

    mFragments.execPendingActions();

    onPostResume();

    ...

}

以下幾個方法是FragmentManager源碼抽取的,被上述方法調(diào)用。


public void dispatchCreate() {

    mStateSaved = false;

    moveToState(Fragment.CREATED, false);

}

public void dispatchStart() {

    mStateSaved = false;

    moveToState(Fragment.STARTED, false);

}

public void dispatchResume() {

    mStateSaved = false;

    moveToState(Fragment.RESUMED, false);

}

至此,我們可以知道如果onBackPressed發(fā)生在onSavedInstanceState之后,那么就會出現(xiàn)上面的crash。

4、如何解決

  • 重載onBackPressed在里面做finish操作,這樣可以避免使用到Fragment api的出棧操作,因為在super.onBackPressed方法里面調(diào)用了FragmentManager#popBackStackImmediate()

  • 在基類里面管理屬于自己的mStateSaved,用它來控制是否要做onBackPressed操作。


public class FragmentStateLossActivity extends Activity {

    private boolean mStateSaved;

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_fragment_state_loss);

        mStateSaved = false;

    }

    @Override

    protected void onSaveInstanceState(Bundle outState) {

        // 不調(diào)用super對我們意義不大,還是會崩潰,而且會丟失現(xiàn)場

        super.onSaveInstanceState(outState);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {

            mStateSaved = true;

        }

    }

    @Override

    protected void onResume() {

        super.onResume();

        mStateSaved = false;

    }

    @Override

    protected void onPause() {

        super.onPause();

    }

    @Override

    protected void onStop() {

        super.onStop();

        mStateSaved = true;

    }

    @Override

    protected void onStart() {

        super.onStart();

        mStateSaved = false;

    }

    @Override

    protected void onDestroy() {

        super.onDestroy();

    }

    @Override

    public boolean onKeyDown(int keyCode, KeyEvent event) {

        if (!mStateSaved) {

            return super.onKeyDown(keyCode, event);

        } else {

            // State already saved, so ignore the event

            return true;

        }

    }

    @Override

    public void onBackPressed() {

        if (!mStateSaved) {

            super.onBackPressed();

        }

    }

}

最后從上述問題我們可以知道:

1.為什么要在一些生命周期之前完成Fragmentcommit操作

  • onCreate里面完成

  • onPostResume里面完成(onPostResume是在onResume后調(diào)用的,確保Activity加載完畢,mStateSaved狀態(tài)已經(jīng)改變)

  • onPause之前完成(onPause能確保在onSaveInstanceState之前執(zhí)行)

2.小心控制異步任務(wù),盡可能避免在一些生命周期函數(shù)中使用異步方法來調(diào)用commit,如AsyncTask 等。

3.使用commitAllowingStateLoss,它的意思是在狀態(tài)丟失是不會拋出異常,但在一些必須確保狀態(tài)被保存的場合下,盡量不使用commitAllowingStateLoss方法。它只能預(yù)防在create Fragment時候出現(xiàn)的問題,但是不能解決destroy Fragment時候出現(xiàn)的問題。

六、總結(jié)

1.了解了安卓的狀態(tài)保存與恢復(fù)大致流程

2.如何觸發(fā)安卓的狀態(tài)恢復(fù)

3.解決因為安卓的狀態(tài)保存導(dǎo)致出現(xiàn)的異常

參考資料

http://www.itdecent.cn/p/6e3e0176f74d

http://blog.csdn.net/a553181867/article/details/54600695

http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html

http://toughcoder.net/blog/2016/11/28/fear-android-fragment-state-loss-no-more/

https://stackoverflow.com/questions/7469082/getting-exception-illegalstateexception-can-not-perform-this-action-after-onsa

測試項目

https://github.com/whosea/TestSaveInstance

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容