是時(shí)候使用SaveState了

Android系統(tǒng)在5.0時(shí),對進(jìn)程內(nèi)的內(nèi)存管理做了一個(gè)優(yōu)化,但并沒有明確的文檔說明這個(gè)優(yōu)化。

這個(gè)優(yōu)化為解決Android應(yīng)用的內(nèi)存問題,提供了一個(gè)新的思路。但如果開發(fā)者習(xí)慣于單Task的應(yīng)用開發(fā),或者從來不考慮SaveState,那開發(fā)者可能根本無法體會這個(gè)新機(jī)制的好處。

本文首先從SaveState講起,對于了解SaveState的同學(xué),可以直接跳過

什么是SaveState

要了解什么是SaveState必須要先知道Activity的兩個(gè)關(guān)鍵方法

  • onSaveInstanceState
  • onRestoreInstanceState

onSaveInstanceState時(shí)系統(tǒng)做了些什么

Activity被回收之前,系統(tǒng)會調(diào)用onSaveInstanceState(Bundle outState)來保存View的狀態(tài),并到傳入的outState對象中。

  1. 保存Window
  2. 保存Fragment
  3. 調(diào)用外部注冊的回調(diào)方法
protected void onSaveInstanceState(Bundle outState) {
    outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
    Parcelable p = mFragments.saveAllState();
    if (p != null) {
        outState.putParcelable(FRAGMENTS_TAG, p);
    }
    getApplication().dispatchActivitySaveInstanceState(this, outState);
}

onRestoreInstanceState時(shí)系統(tǒng)做了些什么

Activity被重新創(chuàng)建時(shí),會通過onCreate(Bundle savedInstanceState)onRestoreInstanceState(Bundle savedInstanceState)傳入保存的狀態(tài)信息并恢復(fù)View的狀態(tài)。

  1. onCreate重建Fragment
  2. onRestoreInstanceState恢復(fù)Window狀態(tài)
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        if (DEBUG_LIFECYCLE) Slog.v(TAG, "onCreate " + this + ": " + savedInstanceState);
        if (mLastNonConfigurationInstances != null) {
            mFragments.restoreLoaderNonConfig(mLastNonConfigurationInstances.loaders);
        }
        if (mActivityInfo.parentActivityName != null) {
            if (mActionBar == null) {
                mEnableDefaultActionBarUp = true;
            } else {
                mActionBar.setDefaultDisplayHomeAsUpEnabled(true);
            }
        }
        if (savedInstanceState != null) {
            Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
            mFragments.restoreAllState(p, mLastNonConfigurationInstances != null
                    ? mLastNonConfigurationInstances.fragments : null);
        }
        mFragments.dispatchCreate();
        getApplication().dispatchActivityCreated(this, savedInstanceState);
        if (mVoiceInteractor != null) {
            mVoiceInteractor.attachActivity(this);
        }
        mCalled = true;
    }
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    if (mWindow != null) {
        Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG);
        if (windowState != null) {
            mWindow.restoreHierarchyState(windowState);
        }
    }
}

Window在save和restore時(shí)對View的處理

  1. Save時(shí),遍歷View的樹狀結(jié)構(gòu)調(diào)用 Parcelable onSaveInstanceState()
  2. 以View的id為key在Window的SparseArray<Parcelable>中保存這些 Parcelable
  3. Restore時(shí),Window從savedInstanceState獲取View的savedStates
  4. 遍歷View的樹狀結(jié)構(gòu)調(diào)用 onRestoreInstanceState(Parcelable state)
  5. View根據(jù)id獲取自己的state并恢復(fù)

小結(jié)

  1. Save和Restore的機(jī)制主要是用于保存和恢復(fù)View的
  2. 沒有id的View是不會被保存狀態(tài)的
  3. 如果id重復(fù),則View的狀態(tài)會被覆蓋
  4. 被保存的Fragment會在onCreate中被自動(dòng)創(chuàng)建和添加到FragmentActivity中
  5. 被保存的View不會被自動(dòng)創(chuàng)建,只是通過id獲取savedInstance用于更新View

關(guān)于SaveState的詳細(xì)介紹可以參考文章Android中SaveState原理分析

為什么開始使用SaveState

為什么很多人不重視SaveState

我們先了解下會用到Restore機(jī)制的地方

  1. FragmentStatePagerAdapter用于在ViewPager中使用可回收和重建的Fragment
  2. 應(yīng)用Crash時(shí),當(dāng)前頁面被銷毀,前一個(gè)頁面被Restore
  • 在4.0之前,系統(tǒng)不會自動(dòng)重啟應(yīng)用
  • 在4.0之后,系統(tǒng)會自動(dòng)重啟,并通過Restore機(jī)制恢復(fù)Crash的頁面。

FragmentStatePagerAdapter中考慮SaveState是必須的,所以大家都會被迫處理SaveState的問題。

大多數(shù)開發(fā)者不會考慮Crash重建的問題,所以SaveState很少被開發(fā)者重視。而認(rèn)真考慮過Crash重建的開發(fā)者一定不會對SaveState陌生。

5.0的新機(jī)制

在5.0中,SaveState有了新的作用,稍加利用,它會幫你解決OutOfMemory。而根據(jù)Google的統(tǒng)計(jì),到今年下半年,Android5.0及以上的系統(tǒng)占比將超過50%。

要觸發(fā)這個(gè)新機(jī)制,你的應(yīng)用必須是多Task結(jié)構(gòu)的。關(guān)于Task,那又是一個(gè)很大的話題,下面我只用一個(gè)簡單的例子看看這個(gè)新機(jī)制。

演示代碼可以通過git倉庫下載

這里看看關(guān)鍵的ActivivtyOne.java

  • ActivityOne是standard
  • ActivityTwo是singleInstance,所以他會在單獨(dú)的新的Task中
  • AcitivyOne可以啟動(dòng)ActivityTwo
  • ActivityOne可以不斷消耗內(nèi)存
public class ActivityOne extends BaseActivity {

    boolean forceOom = false;
    List<Bitmap> memory = new ArrayList<>();

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

    public void launchTwo(View view) {
        ActivityTwo.launch(this);
    }

    /**
     * 第一次點(diǎn)擊使內(nèi)存接近進(jìn)程能獲取的內(nèi)存上限,再次點(diǎn)擊觸發(fā)OOM
     * @param view
     */
    public void consumeMem(View view) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (!forceOom && isLowMemory()) {
                        forceOom = true;
                        break;
                    }
                    memory.add(Bitmap.createBitmap(1000, 1000, Bitmap.Config.ARGB_8888));
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {

                    }
                }
            }
        }).start();
    }


    /**
     * 判斷已使用的內(nèi)存是否接近了單進(jìn)程的內(nèi)存上限
     *
     * @return
     */
    public boolean isLowMemory() {
        ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
        long total = Runtime.getRuntime().totalMemory() / (1024l * 1024l);
        int max = activityManager.getMemoryClass();
        Log.w(getClass().getSimpleName(), total + "/" + max);

        if (total > activityManager.getMemoryClass() * 0.85) return true;
        return false;
    }

    public static void launch(Activity activity) {
        activity.startActivity(new Intent(activity, ActivityOne.class));
    }
}

操作步驟:

  1. ActivityOne啟動(dòng)ActivityTow
  2. ActivityTwo啟動(dòng)ActivityOne,從而切換到老的Task中,ActivityTwo不會被銷毀
  3. ActivityOne不斷消耗內(nèi)存,直到接近進(jìn)程使用內(nèi)存的上限(Android系統(tǒng)對每個(gè)進(jìn)程使用的最大內(nèi)存有一個(gè)限制)

這時(shí)通過logcat你會看到5.0的不同:

  • 5.0之前:不會有什么事情發(fā)生。再次點(diǎn)擊消耗內(nèi)存,會OOM,整個(gè)進(jìn)程被殺。
  • 5.0及之后:ActivityTwo#OnDestroy會被調(diào)用,這時(shí)再啟動(dòng)ActivityTwo,可以看到ActivityTwo#onRestoreInstanceState的調(diào)用。
W/ActivityOne: 83571877-onCreate: TaskId-7551
W/ActivityTwo: 128141518-onCreate: TaskId-7552
W/ActivityOne: 83571877-onSaveInstanceState
W/ActivityOne: 219591424-onCreate: TaskId-7551
W/ActivityTwo: 128141518-onSaveInstanceState
W/ActivityOne: 23/192
W/ActivityOne: 42/192
W/ActivityOne: 88/192
W/ActivityOne: 111/192
W/ActivityOne: 157/192

以下是5.0系統(tǒng)上才會出現(xiàn)的

W/ActivityTwo: 128141518-onDestroy
W/ActivityOne: 176/192
W/ActivityTwo: 80252517-onCreate: TaskId-7552
W/ActivityTwo: 80252517-onRestoreInstanceState
W/ActivityTwo: 80252517-onNewIntent
W/ActivityOne: 219591424-onSaveInstanceState

因此我們可以得出結(jié)論:

5.0之后,Android進(jìn)程在遇到內(nèi)存瓶頸時(shí),會通過主動(dòng)銷毀進(jìn)程中的Acitivty來釋放內(nèi)存。這些被銷毀的Activity都屬于后臺Task,當(dāng)被銷毀的Activity需要重新出現(xiàn)時(shí),會觸發(fā)Restore機(jī)制

當(dāng)然,這個(gè)結(jié)論又會引起很多疑問。

  1. 為什么不銷毀當(dāng)前Task中的后臺Activity?
  2. 如果后臺Task中有多個(gè)Activity是一起銷毀嗎?如果后臺Task中的多個(gè)Activity是屬于不同的進(jìn)程呢?
  3. ......

關(guān)于這些問題,需要分析源碼才能找到答案,但我不希望這篇文章變成對Android源碼的一次分析。我將在下篇文章,繼續(xù)介紹怎么處理SaveState。

當(dāng)然,即使沒有SaveState在5.0上帶來的好處,正確處理頁面的SaveState也是保證Android應(yīng)用程序健壯性的一個(gè)重要部分。

怎么處理SaveState,請看下篇文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,067評論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,084評論 2 375

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,677評論 25 708
  • 今天是個(gè)有意義的日子。 通過今天下午出去推廣的事情,和晚上喝了一頓酒,聊了一會,我對人性又有了新的認(rèn)識。 中午和小...
    冷劍書生閱讀 354評論 0 1
  • 看這部電影之前,我的心情是忐忑的。因?yàn)樗诙拱晟系脑u分只有5.1。通常低于7分的電影我都是不看的,免得浪費(fèi)時(shí)間。最...
    貓貓伸懶腰閱讀 1,517評論 0 0
  • 聊聊TTT 隨著移動(dòng)互聯(lián)網(wǎng)深入的發(fā)展和對人們無時(shí)無刻的影響,培訓(xùn)行業(yè)的多樣性和形式也出現(xiàn)了多種。國家下發(fā)的紅頭文件...
    林奎閱讀 1,056評論 0 2
  • 這TM就很尷尬了 虛弱的現(xiàn)實(shí)用法 喜歡主播的請點(diǎn)一波關(guān)注 人與人之間的信任呢? 神回復(fù) 再不給我人頭我就哭了喲 沒...
    f伐木累閱讀 8,551評論 0 1