Activity的生命週期和啓動模式--Activity的生命週期的全面分析

本節將Activity的生命週期分爲兩部分內容,一部分是典型情況下的生命週期,另一部分是異常情況下的生命週期。所謂典型情況下的生命週期,是指在有用戶參與的情況下,Activity所經過的生命週期的改變;而異常情況下的生命週期是指Activity被系統回收或由於當前設備的Configuration發送改變從而導致Activity被銷燬重建,異常情況下的生命週期的關注點和典型情況下略有不同。

1.典型情況下的生命週期分析

正常情況下,Activity會經歷如下生命週期。

  • onCreate:表示Activity正在被創建,這是生命週期的第一個方法。在這個方法中,我們可以做一些初始化工作,比如調用setContentView去加載界面佈局資源、初始化Activity所需數據等。
  • onRestart:表示Activity正在重新啓動。一般情況下,噹噹前Activity從不可見重新變爲可見狀態時,onRestart就會被調用。這種情形一般時用戶行爲所導致的,比如用戶按Home鍵切換到桌面或者用戶打開了一個新的Activity,這時當前的Activity就會暫停,也就是onPause和onStop被執行了,接着用戶又回到了這個Activity,就會出現這種情況。

  • onStart:表示Activity正在被啓動,即將開始,這時Activity已經可見了,但是還是沒有出現在前臺,還無法和用戶交互。這個時候其實可以理解爲Activity已經顯示出來了,但是我們還看不到。
  • onResume:表示Activity已經可見了,並且出現在前臺並開始活動。要注意這個和onStart的對比,onStart和onResume都表示Activity已經可見,但是onStart的時候Activity還在後臺,onResume的時候Activity才顯示到前臺。

  • onPause:表示Activity正在停止,正常情況下,緊接着onStop就會被調用。在特殊情況下,如果這個時候迅速地再回到當前Activity,那麼onResume會被調用。筆者地理解是,這種情況屬於極端情況,用戶操作很難重現這一場景。此時可以做一些存儲數據、停止動畫等工作,但是注意不能太耗時,因爲會影響到Activity地顯示,onPause必須先執行完,新地Activity的onResume纔會執行。
  • onStop:表示Activity即將停止,可以做一些稍微重量級的回收工作,同樣不能太耗時。

  • onDestroy:表示Activity即將被銷燬,這是Activity生命週期中的最後一個回調,在這裏,我們可以做一些回收工作和最終的資源釋放。

正常情況下,Activity的常用生命週期就只有上面7個,下圖更加詳細地描述了Activity各種生命週期的切換過程。

針對上圖,這裏再附加一下說明,分如下幾種情況。
  • 針對一個特定的Activity,第一次啓動,回調如下:onCreate-->onStart-->onResume.

  • 當用戶打開新的Activity或者切換到桌面的時候,回調如下:onPause-->onStop。這裏又一種特殊情況,如果新Activity採用了透明主題,那麼當前Activity不會回調onStop.
  • 當用戶再次回到原Activity時,回調如下:onRestart-->onStart-->onResume。

  • 當用戶按back鍵回退時,回調如下:onPause-->onStop-->onDestory。
  • 當Activity被系統回收後再次打開,生命週期方法回調過程和(1)一樣,注意只是生命週期方法一樣,不代表所有過程都一樣,這個問題在下一節會詳細說明。

  • 從整個生命週期來說,onCreate和onDestory時配對的,分別標識着Activity的創建和銷燬,並且只能有一次調用。從Activity是否可見來說onStart和onStop是相配對的,隨着用戶的操作或者設備屏幕的點亮和熄滅,這兩個方法可能被調用多次;從Activity是否在前臺來說,onResume和onPause是配對的,隨着用戶操作或者設備屏幕的點亮和熄滅,這兩個方法可能被調用多次。

有兩個問題,不知道大家是否清楚。

問題1:onStart和onResume、onPause和onStop從描述上來看差不多,對我們來說有什麼實質的不同呢?

問題2:假設當前Activity爲A,如果這是用戶打開一個新ActivityB,那麼B的onResume和A的onPause哪個先執行呢?

先說第一個問題,從實際使用過程來說,onStart和onResume、onPause和onStop看起來的確差不多,甚至我們可以只保留其中一對,比如只保留onStart和onStop。既然如此,那爲什麼Android系統還要提供看起來重複的接口呢?根據上面的分析,我們知道,這兩個配對的回調分別表示不同的意義,onStart和onStop是從Activity是否可見這個角度來回調的,而onResume和onPause是從Activity是否位於前臺這個角度來回調的,除了這種區別,在實際使用中沒有其他明顯區別。

第二個問題可以從Android源碼裏得到解釋。關於Activity的工作原理在續章節會進行介紹,這裏我們先大概瞭解即可。從Activity的啓動過程來看,我們來看一下系統源碼。Activity的啓動過程的源碼相當複雜,涉及Instrumentation、ActivityThread和ActivityManagerService(下面簡稱AMS)。這裏不詳細分析這一過程,簡單理解,啓動Activity的請求會由Instrumentation來處理,然後它通過Binder向AMS發請求,AMS內部維護着一個ActivityStack並負責棧內的Activity的狀態同步,AMS通過ActivityThread去同步Activity的狀態從而完成生命週期方法的調用。在ActivityStack中的resumeTopActivityInnerLocked方法中,有這麼一段代碼:

        // We need to start pausing the current activity so the top one
        // can be resumed...
        boolean pausing = mStackSupervisor.pauseBackStacks(userLeaving);
        if (mResumedActivity != null) {
            pausing = true;
            startPausingLocked(userLeaving, false);
            if (DEBUG_STATES) Slog.d(TAG, "resumeTopActivityLocked: Pausing " + mResumedActivity);
        }
從上述代碼可以看出,在新Activity啓動之前,棧頂的Activity需要先onPause後,新Activity才能啓動。最終,在ActivityStackSupervisor中的realStartActivityLocked方法會調用如下代碼。

app.thread.scheduleLaunchActivity(new Intent(r.intent), r.appToken,
                    System.identityHashCode(r), r.info,
                    new Configuration(mService.mConfiguration), r.compat,
                    app.repProcState, r.icicle, results, newIntents, !andResume,
                    mService.isNextTransitionForward(), profileFile, profileFd,
                    profileAutoStop);

我們知道,這個app.thread的類型是IApplicationThread,而IApplicationThread的具體實現是ActivityThread中的ApplicationThread。所以,這段代碼實際上調用到了ActivityThread,即ApplicationThread的scheduleLaunchActivity方法,而scheduleLaunchActivity方法最終會完成新的Activity的onCreate、onStart、onResume的調用過程。因此可以得出結論,是舊Activity先onPause,然後新Activity再啓動。

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        // If we are getting ready to gc after going to the background, well
        // we are back active so skip it.
        unscheduleGcIdler();

        if (r.profileFd != null) {
            mProfiler.setProfiler(r.profileFile, r.profileFd);
            mProfiler.startProfiling();
            mProfiler.autoStopProfiler = r.autoStopProfiler;
        }

        // Make sure we are running with the most recent config.
        handleConfigurationChanged(null, null);

        if (localLOGV) Slog.v(
            TAG, "Handling launch of " + r);
            
        //這裏新Activity被創建出來,其onCreate和onStart會被調用
        Activity a = performLaunchActivity(r, customIntent);

        if (a != null) {
            r.createdConfig = new Configuration(mConfiguration);
            Bundle oldState = r.state;
            //這裏新Activity的onResume會被調用
            handleResumeActivity(r.token, false, r.isForward,
                    !r.activity.mFinished && !r.startsNotResumed);

        //省略...
    }
從上面的分析可以看出,當新啓動一個Activity的時候,舊Activity的onPause會先執行,然後纔會啓動新的Activity。到底是不是這樣呢?我們寫一個例子驗證下,如下是2個Activity的代碼,在MainActivity中點擊按鈕舊可以跳轉到SecondActivity,同時爲了分析我們的問題,在生命週期方法中打印出了日誌,通過日誌我們就能看出他們的調用順序。
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";

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

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

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

    public void btnStartSecondActivity(View view) {
        Intent intent = new Intent(this, SecondActivity.class);
        startActivity(intent);
    }
}
public class SecondActivity extends AppCompatActivity {

    private static final String TAG = "SecondActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        Log.e(TAG, "onCreate");
    }

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

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

我們來看一下log,是不是和我們上面的分析一樣,如下所示。

通過Log可以發現,舊Activity的onPause先調用,然後新Activity才啓動 ,這也證實了我們上面的分析過程。也許有人會問,你只是分析了Android5.0的源碼,你怎麼知道所有版本的源碼都是相同的邏輯呢?關於這個問題,我們的確不大可能把所有版本的源碼都分析一邊,但是作爲Android運行過程中的基本機制,隨着版本的更新並不會有大的跳轉,因爲Android系統也需要兼容性,不能說在不同的版本上同一個運行機制有着截然不同的表現。關於這一點我們需要把握一個度,就是對於Android運行的基本機制在不同Android版本上具有延續性。從另一個角度來說,Android官方文檔對onPause的解釋有這麼一句:不能在onPause中做重量級的操作,因爲必須onPause完成後,新的Activity才能Resume,從這一點也能間接的證明我們的結論。通過分析這個問題,我們知道onPause和onStop都不能執行耗時的操作,尤其是onPause,這也意味着,我們應當儘量在onStop中操作,從而使得新Activity儘快顯示出來,並切換到前臺。

2.異常情況下的生命週期分析

上一節我們分析了典型情況下Activity的生命週期,本節我們接着分析Activity在異常情況下的生命週期。我們知道,Activity除了受用戶操作所導致的正常的生命週期方法調度,還有一些異常情況,比如當系統資源相關的系統配置發生改變以及系統內存不足時,Activity就可能被殺死。下面我們具體分析這兩種情況。

1.情況1:資源相關的系統配置發生改變導致Activity被殺死並重新創建
理解這個問題,我們首先要對系統的資源加載機制有一定了解,這裏不詳細分析系統的資源加載機制,只是簡單說明一下。拿最簡單的圖片來說,當我們把一張圖片放在drawable目錄後,就可以通過Resources去獲取這張圖片。同時爲了兼容不同的設備,我們可能還需要在其他一些目錄放置不同的圖片,比如drawable-mdpi、drawable-hdpi、drawable-land燈。這樣,當應用程序啓動時,系統就會根據當前設備的情況去加載合適的Resources資源,比如橫屏手機和豎屏手機會拿到不同的圖片(設定了landspace或者portait狀態下的圖片)。比如說當前Activity處於豎屏狀態,如果屏幕突然旋轉,由於系統配置發生了改變,在默認情況下,Activity就會被銷燬並且重新創建,當然我們也可以組織系統重新創建我們的Activity。
在默認情況下,如果我們的Activity不做特殊處理,那麼當系統配置發生改變後,Activity就會被銷燬並重新創建,其生命週期如圖所示。

當系統配置發生改變後,Activity會被銷燬,其onPause、onStop、onDestory均會被調用,同時由於Activity是在異常情況下終止的,系統會調用onSaveInstanceState來保存當前Activity的狀態,這個方法的調用時機是在onStop之前,他和onPause沒有既定的時序關係,它極可能在onPause之前被調用,也可能在onPause之後調用。需要強調的一點是,這個方法只會出現在Activity被異常種植的情況下,正常情況下系統不會回調這個方法。當Activity被重新創建後,系統會調用onRestoreInstanceState,並且把Activity銷燬時onSaveInstanceState方法所保存的Bundle對象作爲參數同時傳遞給onRestoreInstanceState和onCreate方法。因此,我們可以通過onRestoreInstanceState和onCreate方法來判斷Activity是否被重建了,如果被重建了,那麼我們就可以取出之前保存的數據並恢復,從時許上來說onRestoreInstanceState的調用時機在onStart之後。

同時,我們要知道,在onSaveInstanceState和onRestoreInstanceState方法中,系統自動爲我們做了一定的恢復工作。當Activity在異常情況下需要重新創建時,系統會默認爲我們保存當前Activity的視圖結構,並且在Activity重啓後爲我們回覆這些數據,比如文本框中用戶輸入的數據、ListView滾動的位置等,這些View相關的狀態系統都能夠默認爲我們回覆。具體針對某一特定的View系統能爲我們回覆哪些數據,我們可以查看View的源碼。和Activity一樣,每個View都有onSaveInstanceState和onRestoreInstanceState這兩個方法,看一下他們的具體實現,就能知道系統能夠自動爲每個View回覆哪些數據。

關於保存和恢復View層次結構,系統的工作流程時這樣的:首先Activity被意外終止時,Activity會調用onSaveInstanceState去保存數據,然後Activity會委託Window去保存數據,接着Window再委託它上面的頂級容器去保存數據。頂層容器是一個ViewGroup,一般來說它很可能是DecorView。最後頂層容器再去一一通知它的子元素來保存數據,這樣整個數據保存過程就完成了。可以發現,這是一種典型的委託思想,上層委託下層、父容器委託子容器去處理一件事情,這種思想再Android中又很多應用,比如View的繪製過程、時間分發等都是採用類似的思想。至於數據恢復過程也是類似,這裏就不再重複介紹了。接下來舉個例子,拿ProgressBar來說,我們分析一下它到底保存了哪些數據。(PS-我用的是Android-19的源碼)

    @Override
    public Parcelable onSaveInstanceState() {
        // Force our ancestor class to save its state
        Parcelable superState = super.onSaveInstanceState();
        SavedState ss = new SavedState(superState);
        
        ss.progress = mProgress;
        ss.secondaryProgress = mSecondaryProgress;
        
        return ss;
    }

從上述源碼可以很容易看出,ProgressBar保存了自己的progress和secondaryProgress,並且通過查看onRestoreInstanceState方法的源碼,可以發現它的確恢復了這些數據,具體源碼就不再貼出了,讀者可以去看看源碼。下面我們來看看實際的例子,對比一下Activity正常終止和異常終止的不同,同時驗證系統的數據恢復能力。爲了方便,我們選擇旋轉屏幕來異常終止Activity,如圖所示。

通過上圖,我們選擇屏幕以後,Activity被銷燬後重新創建,我們設置的Progress會被正確的還原,這說明系統的確能夠自動地做一些View層次結構方面地數據存儲和恢復。下面再用一個例子,來證明我們自己做數據存儲和恢復的情況,代碼如下:

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

        if (savedInstanceState != null) {
            //被異常重啓Activity
            String test = savedInstanceState.getString(EXTRA_TEST);
            Log.e(TAG, "[onCreate] restore extra_test:" + test);
        }
    }
    
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        Log.e(TAG, "onSaveInstanceState");
        outState.putString(EXTRA_TEST, "test");
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);

        String test = savedInstanceState.getString(EXTRA_TEST);
        Log.e(TAG, "[onRestoreInstanceState] restore extra_test:" + test);
    }

上面的代碼很簡單,首先我們再onSaveInstanceState中存儲一個字符串,然後當Activity被銷燬並重新創建後,我們再去獲取之前存儲的字符串。接受的位置可以選擇onCreate或onRestoreInstanceState,二者的區別是onRestoreInstanceState一旦被調用,其參數Bundle savedInstanceState一定是有值的,我們不用額外判斷是否爲空;但是onCreate不行,onCreate正常啓動的話,其參數Bundle savedInstanceState爲null,所以必須要額外判斷。這兩個方法我們選擇任意一個都可以進行數據恢復,但是官方文檔建議採用onRestoreInstanceState去恢復數據。下面我們看一下運行的日誌,如下圖所示。

如圖所示,Activity被銷燬了以後調用了onSaveInstanceState來保存數據,重新創建以後再onCreate和onRestoreInstanceState中都能夠正確地恢復我們之前存儲的字符串。這個例子很好地證明了上面我們分析地結論。針對onSaveInstanceState方法還有一點需要說明,那就是系統只會再Activity即將被銷燬並且有機會重新顯示地情況下才會去調用它。考慮這麼一種情況,當Activity正常銷燬的時候,系統不會調用onSaveInstanceState,因爲被銷燬的Activity不可能再次被顯示。這句話不好理解,但是我們可以對比一下屏幕旋轉所造成的Activity被銷燬的同時會立刻創建新的Activity實例,這個時候Activity有機會再次立刻展示,所以系統要進行數據存儲,這裏可以簡單地這麼理解,系統只再Activity異常終止地時候纔會調用onSaveInstanceState和onRestoreInstanceState來存儲和恢復數據,其他情況不會觸發這個過程。

2.情況2:資源內存不足導致低優先級地Activity被殺死
這種情況我們不好模擬,但是其數據存儲和恢復過程和情況1完全一致。這裏我們描述一下Activity的優先級情況。Activity按照優先級從高到低,可以分爲如下三種情況:

  • 前臺Activity——正在和用戶交互的Activity,優先級最高。
  • 可見前臺但非前臺Activity——比如Activity彈出了一個對話框,導致Activity可見,但是位於後臺無法和用戶直接交互。

  • 後臺Activity——已經被暫停的Activity,比如執行了onStop,優先級最低。
當系統內存不足時,系統就會按照上述優先級去殺死目標Activity所在的進行,並再後續通過onSaveInstanceState和onRestoreInstanceState來存儲和恢復數據。如果一個進程中沒有四大組件在執行,那麼這個進程將很快被系統殺死,因此,一些後臺工作不適合脫離四大組件而獨自運行在後臺中,這樣進行很容易被殺死。比較好的方法時將後臺工作放入Service中從而保證進行有一定的優先級,這樣就不會輕易地被系統殺死。

上面分析了系統地數據存儲和恢復機制,我們知道,當系統配置發生改變後,Activity會被重新創建,那麼有沒有辦法不重新創建呢?答案時有的,接下來我們就來分析這個問題。系統配置中有很多內容,如果當某項內容發生改變後,我們不想系統重新創建Activity,可以給Activity知道configChanges屬性。比如不想讓Activity在屏幕旋轉的時候重新創建,就可以給configChanges屬性添加orientation這個值,如下所示。

android:configChanges="orientation"
如果我們想指定多個值,可以用“|”連接起來,比如android:configChanges="orientation|keyboardHidden"。系統配置中所含的項目是非常多的,下面介紹每個項目的含義。如下表所示。
congifChanges的項目和含義
項目含義
mccSIM卡唯一標識符IMSI(國際移動用戶識別碼)中的國家代碼,由三位數字組成,中國爲460。此項標識mcc代碼發生了改變。
mncSIM卡唯一標識符IMSI(國際移動用戶識別碼)中的運營商代碼,由兩位數字組成,中國移動TD系統爲00,中國聯通爲01,中國電信爲03,此標識mnc發生改變
locale設備的本地位置發生了改變,一般指切換了語言系統
touchscreen觸摸屏發生了改變,這個很費解,正常情況下無法發生,可以忽略它    
keyboard鍵盤類型發生了改變,比如用戶使用了外插鍵盤
keyboardHidden鍵盤的可訪問性發生了改變,比如用戶調出了鍵盤
navigation系統導航方式發生了改變,比如採用了軌跡球導航,這個有點非接,很難發生,可以忽略它
screenLayout屏幕布局發生了改變,很可能是用戶激活了另外一個顯示設備
fontScale系統字體縮放比例發生了改變,比如用戶選擇了一個新字號
uiMode用戶界面模式發生了改變,比如是否開啓了夜間模式(API8新添加)
orientation屏幕方向發生了改變,這個是最常用了,比如旋轉了手機屏幕
screenSize當屏幕尺寸信息發生了改變,當旋轉設備屏幕時,屏幕尺寸會發生變化,這個選項比較特殊,它和編譯選項有關,當編譯選項中minSdkVersion和targetSdkVersion均低於13時,此選項不會導致Activity重啓,否則會導致Activity重啓(API13添加)
smallestScreenSize設備的物理屏幕尺寸發生改變,這個項目和屏幕方向沒有關係,僅僅表示在實際物理屏幕的尺寸改變的時候發生,比如用戶切換到了外部的顯示設備,這個選項和screenSize一樣,當編譯選項中的minSdkVersion和targetSdkVersion均低於13時,此選項不會導致Activity重啓,否則會導致Activity重啓(API13添加)
layoutDirection當佈局方向發生變化,這個屬性用的比較少,正常情況下無法修改佈局的layoutDirection

從上邊可以知道,如果我們沒有在Activity的configChanges屬性中指定該選項的話,當配置發生改變後就會導致Activity重新創建。表格中的項目很多,但是我們常用的只有locale、orientation和keyboardHidden這三個選項,其他很少使用。需要注意的是screenSize和smallestScreenSize,他們兩個比較特殊,他們的行爲和編譯選項有關,但和運行環境無關。下面我們再看一個demo,看看當我們指定了configChanges屬性後,Activity是否真的不會重新創建了。我們所要修改的代碼很簡單,只需要再AndroidMenifest.xml中加入Activity的聲明即可,代碼如下:

<uses-sdk android:minSdkVersion="8"
        android:targetSdkVersion="19" />
<activity android:name=".MainActivity"
            android:configChanges="orientation|screenSize">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        Log.e(TAG, "[onConfigurationChanged] newOrientation:" + newConfig.orientation);
    }

需要說明的是,由於編譯時筆者指定的minSdkVersion和targetSdkVersion有一個大於13,所以爲了放置屏幕旋轉時Activity重啓,除了orientation,我們還加上了screenSize,原因在上面的表格已經說明了。其他的代碼還是不變,運行後看看log,如下圖所示。

由上面的日誌可見,Activity的確沒有重新創建,並且也沒有調用onSaveInstanceState和onRestoreInstanceState來存儲和恢復數據,取而代之的時系統調用了Activity的onConfigurationChanged方法,這個時候我們就可以做一些自己的處理了。

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