Android墓碑機(jī)制

Android墓碑機(jī)制

本文連接地址:http://www.reibang.com/p/f5be35aaed32

一、墓碑定義

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

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

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

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

三谚咬、如何觸發(fā)墓碑機(jī)制

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

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

  • 長按HOME鍵,選擇運(yùn)行其他的程序時(shí)

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

  • 從activity A中啟動一個(gè)新的activity時(shí)

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

  • 語言的切換

先說第五择卦、六點(diǎn),在屏幕切換之前郎嫁,系統(tǒng)會銷毀activity A秉继,在屏幕切換之后系統(tǒng)又會自動地創(chuàng)建activity A,所以onSaveInstanceState()一定會被執(zhí)行泽铛,且也一定會執(zhí)行onRestoreInstanceState()尚辑。

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

MainActivity: onPause

MainActivity: onSaveInstanceState

MainActivity: onStop

MainActivity: onDestroy

MainActivity: onCreate

MainActivity: onStart

MainActivity: onRestoreInstanceState

MainActivity: onResume

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

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

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

四坠狡、如何調(diào)試

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

應(yīng)用是如何知道我是被異常殺死的,由于底層涉獵不深漩勤,只能大概的描述下:應(yīng)用被異常殺死后在重新打開,系統(tǒng)底層會判斷該應(yīng)用是否異常退出缩搅,接著把當(dāng)時(shí)現(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的時(shí)候置谦,才通過mInstrumentation.callActivityOnRestoreInstanceState回調(diào)OnRestoreInstanceState,而r.state就是ActivityManagerService通過Binder傳給ActivityThread數(shù)據(jù)亿傅,主要用來做場景恢復(fù)媒峡。

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

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

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

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

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

打印日志如下:

MainActivity: onCreate

MainActivity: onStart

MainActivity: onRestoreInstanceState

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

按Home把當(dāng)前應(yīng)用放到后臺河泳,然后從Android Studio進(jìn)入Devive Monitor,選擇當(dāng)前應(yīng)用奏司,接著選stop按鈕乔询。

如圖:

image

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

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

目前統(tǒng)計(jì)線上的bug竿刁,偶爾會看到這樣的一個(gè)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)搪缨,要解決這個(gè)bug食拜,我們不妨先提出這幾個(gè)問題:

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

  • 錯(cuò)誤來源

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

  • 如何解決

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

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


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痹届、錯(cuò)誤來源

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


@Override

public boolean popBackStackImmediate() {

    checkStateLoss();

    executePendingTransactions();

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

}

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

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

FragmentActivityonBackPressed:


public void onBackPressed() {

    if (!mFragments.popBackStackImmediate()) {

        supportFinishAfterTransition();

    }

}

來源找到了迫淹,接著分析為什么出現(xiàn)錯(cuò)誤秘通。

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

觀察上述代碼敛熬,產(chǎn)生該錯(cuò)誤的原因是mStateSaved變量為true肺稀,而這個(gè)變量是從哪里設(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進(jìn)行的設(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;

        }

    ...

}

而這個(gè)方法里面一系列操作都是保存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);

}

那么什么時(shí)候才把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();

    ...

}

以下幾個(gè)方法是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的出棧操作形导,因?yàn)樵?code>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時(shí)候出現(xiàn)的問題癣缅,但是不能解決destroy Fragment時(shí)候出現(xiàn)的問題厨剪。

六、總結(jié)

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

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

3.解決因?yàn)榘沧康臓顟B(tài)保存導(dǎo)致出現(xiàn)的異常

參考資料

http://www.reibang.com/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

測試項(xiàng)目

https://github.com/whosea/TestSaveInstance

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末友存,一起剝皮案震驚了整個(gè)濱河市祷膳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌屡立,老刑警劉巖直晨,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異膨俐,居然都是意外死亡勇皇,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門焚刺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來敛摘,“玉大人,你說我怎么就攤上這事乳愉⌒忠” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵蔓姚,是天一觀的道長捕虽。 經(jīng)常有香客問我,道長坡脐,這世上最難降的妖魔是什么泄私? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮备闲,結(jié)果婚禮上晌端,老公的妹妹穿的比我還像新娘。我一直安慰自己浅役,他們只是感情好斩松,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著觉既,像睡著了一般惧盹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瞪讼,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天钧椰,我揣著相機(jī)與錄音,去河邊找鬼符欠。 笑死嫡霞,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的希柿。 我是一名探鬼主播诊沪,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼养筒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了端姚?” 一聲冷哼從身側(cè)響起晕粪,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎渐裸,沒想到半個(gè)月后巫湘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡昏鹃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年尚氛,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片洞渤。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡阅嘶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出您宪,到底是詐尸還是另有隱情奈懒,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布宪巨,位于F島的核電站磷杏,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏捏卓。R本人自食惡果不足惜极祸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望怠晴。 院中可真熱鬧遥金,春花似錦捌锭、人聲如沸谴咸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至恢着,卻和暖如春菜拓,著一層夾襖步出監(jiān)牢的瞬間蹬跃,已是汗流浹背梯捕。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工厢呵, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人傀顾。 一個(gè)月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓襟铭,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子寒砖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344