1. 程式人生 > 其它 >Android入門教程之Activity(生命週期,啟動...)

Android入門教程之Activity(生命週期,啟動...)

Activity 是一個應用元件,使用者可與其提供的螢幕進行互動,以執行撥打電話、拍攝照片、傳送電子郵件或檢視地圖等操作。 每個 Activity 都會獲得一個用於繪製其使用者介面的視窗。視窗通常會充滿螢幕,但也可小於螢幕並浮動在其他視窗之上。

Activity

1. Activity 的使用

我們新建的工程中帶有一個基礎 activity。

新建工程中,需要注意3個檔案。

  • MainActivity.java 在src/main/java裡,對應的包名目錄下。
  • activity_main.xml 在res/layout裡。
  • AndroidManifest.xml 在src/main裡。這裡叫做“清單檔案”。

這3個檔案分佈在不同的地方。簡單來說,java檔案可以控制介面邏輯。
layout檔案(這裡指的是activity_main.xml)預設了UI如何擺放。
清單檔案告訴系統,我這個app有哪些元件,申請了什麼許可權。

2. layout - 介面佈局

新建的 layout 中,as 一般會預設給一個ConstraintLayout。比如 activity_main.xml

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <!-- 省略預設的TextView -->

</androidx.constraintlayout.widget.ConstraintLayout>

這裡為了用起來方便,我們把它換成LinearLayout
有的朋友會問,都2021年了,為什麼不直接用 ConstraintLayout?

現在不做什麼功能,先用LinearLayout,就是為了方便。 換成LinearLayout後,layout檔案長這樣。

換成LinearLayout後的activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <!-- 省略預設的TextView -->

</LinearLayout>

可以看到,標籤的開頭和結尾都換成了LinearLayout。其他地方暫時不修改。

as功能強大,非常便利。我們可以用滑鼠選中標籤開始的androidx...Layout,然後直接鍵盤輸入LinearLayout的前幾位字母。

as會自動彈出選擇框,在裡面雙擊選擇LinearLayout或者回車選擇,標籤就替換完成了。

3. Java - 控制介面

layout檔案設計的是介面的初始佈局。它決定了初始介面上放著什麼UI元件以及元件是怎麼組織安排的。

這裡我們說的是「初始介面」或者「初始佈局」。也就是說,我們可以控制介面上的UI元素。

先看預設的 MainActivity.java。在onCreate方法裡,R.layout.activity_main指的就是activity_main.xml

現在layout中有一個TextView,它可以用來顯示文字。我們想在MainActivity中控制它,該怎麼做呢?

現在改一下這個TextView。刪掉原來ConstraintLayout用到的那些屬性。

給它新增一個id。這個id在這個layout檔案中必須是獨一無二的。給它分配一個id叫做tv1,就像下面。

<TextView
    android:id="@+id/tv1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World!" />

現在TextView有了身份證,我們在activity中就可以找到它。用的是findViewById方法。

TextView tv1 = findViewById(R.id.tv1);

現在我們就拿到了介面上的這個TextView物件。可以對它進行操作了。
比如改變它顯示的文字。

TextView tv1 = findViewById(R.id.tv1); // 拿到textView的物件
tv1.setText("Today is a good day.");   // 改變文字
4. AndroidManifest.xml - 清單檔案

也可以簡稱為「manifest檔案」。清單檔案非常重要,它告訴系統我們的app有哪些activity,用到了什麼許可權等等資訊。

如果要新建activity,需要在清單中註冊。

AndroidManifest.xml

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

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

從這個預設的清單檔案中我們可以得知,activity 是屬於 application 的。application 就是我們的應用。

application 標籤中也指定了各種元素,例如應用的圖示,名字,主題等等。

MainActivity 是應用啟動的第一個 activity。可以觀察到它設定了 action 和category 屬性。

  • android.intent.action.MAIN 決定應用程式最先啟動的Activity。
  • android.intent.category.LAUNCHER 表示可以在手機“桌面”上看到應用圖示。

設定了這 2 個標籤,決定了這個 activity 是使用者點選應用圖示時第一個啟動的介面。

小結
activity是應用重要的元件之一。紛繁複雜的內容需要activity來承載。

之後我們會在activity中控制各種各樣的UI元件,處理使用者的操作,申請許可權等等。還要了解activity的生命週期,啟動方式和跳轉方法。


Activity 生命週期

生命週期圖示

1. 生命週期變化

執行一些常見的操作,打log看一下生命週期的變化。測試機型:RedMi。

啟動然後退出

onCreate
onStart
onResume
onWindowFocusChanged: hasFocus: true

onWindowFocusChanged: hasFocus: false
onPause
onStop
onDestroy

啟動後按home鍵

Act1: onCreate
Act1: onStart
Act1: onResume
Act1: onWindowFocusChanged: hasFocus: true

// 按home鍵
Act1: onWindowFocusChanged: hasFocus: false
Act1: onPause
Act1: onStop

// 再回來
Act1: onRestart
Act1: onStart
Act1: onResume
Act1: onWindowFocusChanged: hasFocus: true

// 按返回鍵退出act
Act1: onWindowFocusChanged: hasFocus: false
Act1: onPause
Act1: onStop
Act1: onDestroy

旋轉手機
activity 在切換橫豎屏的時候的生命週期。

[Life]: onCreate
[Life]: onStart
[Life]: onResume
[Life]: onWindowFocusChanged: hasFocus: true

// 橫屏
[Life]: onPause
[Life]: onStop
[Life]: onDestroy
[Life]: onCreate
[Life]: onStart
[Life]: onResume
[Life]: onWindowFocusChanged: hasFocus: true

// 豎屏
[Life]: onPause
[Life]: onStop
[Life]: onDestroy
[Life]: onCreate
[Life]: onStart
[Life]: onResume
[Life]: onWindowFocusChanged: hasFocus: true

// 返回
[Life]: onWindowFocusChanged: hasFocus: false
[Life]: onPause
[Life]: onStop
[Life]: onDestroy

來回切換的生命週期變化
以2個Activity啟動為例。

Act1: onCreate
Act1: onStart
Act1: onResume
Act1: onWindowFocusChanged: hasFocus: true
Act1: onPause
Act1: onWindowFocusChanged: hasFocus: false
Act2: onCreate
Act2: onStart
Act2: onResume
Act2: onWindowFocusChanged: hasFocus: true
Act1: onStop
Act2: onWindowFocusChanged: hasFocus: false
Act2: onPause
Act1: onRestart
Act1: onStart
Act1: onResume
Act1: onWindowFocusChanged: hasFocus: true
Act2: onStop
Act2: onDestroy
Act1: onWindowFocusChanged: hasFocus: false
Act1: onPause
Act1: onStop
Act1: onDestroy

彈出 AlertDialog
點選按鈕彈出一個AlertDialog。觀察發現呼叫 onWindowFocusChanged

onWindowFocusChanged: hasFocus: false
onWindowFocusChanged: hasFocus: true

這裡也可以用 DialogFragment 來做測試。

recreate
呼叫 recreate() 方法

[Life]: onCreate
[Life]: onStart
[Life]: onResume
[Life]: onWindowFocusChanged: hasFocus: true
[Life]: click [recreate]
[Life]: onPause
[Life]: onStop
[Life]: onDestroy
[Life]: onCreate
[Life]: onStart
[Life]: onResume

可以看到,呼叫recreate()方法後並沒有走onWindowFocusChanged回撥。

2. onCreate 和 onStart 的區別

activity的狀態區別

  • onCreate在系統首次建立 Activity 時觸發。Activity會在建立後進入已建立狀態。
  • 當 Activity 進入“已開始”狀態時,系統會呼叫此回撥。onStart() 呼叫使 Activity 對使用者可見,因為應用會為 Activity 進入前臺並支援互動做準備。

onStart() 方法會非常快速地完成,並且與“已建立”狀態一樣,Activity 不會一直處於“已開始”狀態。一旦此回撥結束,Activity 便會進入已恢復狀態,系統將呼叫 onResume() 方法。

3. onPause 和 onStop 的區別

onPause() 執行非常簡單,而且不一定要有足夠的時間來執行儲存操作。 因此,您不應使用 onPause() 來儲存應用或使用者資料、進行網路呼叫,或執行資料庫事務。因為在該方法完成之前,此類工作可能無法完成。

已進入已停止狀態,因此係統將呼叫 onStop() 回撥。舉例而言,如果新啟動的 Activity 覆蓋整個螢幕,就可能會發生這種情況。

onStop() 方法中,應用應釋放或調整應用對使用者不可見時的無用資源。例如,應用可以暫停動畫效果,或從細粒度位置更新切換到粗粒度位置更新。 使用 onStop() 而非 onPause() 可確保與介面相關的工作繼續進行,即使使用者在多視窗模式下檢視您的 Activity 也能如此。 您還應該使用 onStop() 執行 CPU 相對密集的關閉操作。


Activity 啟動,攜帶引數啟動

前面大致瞭解了Activity是一個應用元件,能為使用者提供一個介面。以及如何新增activity。 一個App中,通常有多個介面。假設每一個介面對應一個activity,不同介面之間怎麼跳轉呢?

1. Intent

通常activity之間的跳轉離不開Intent這個類。 Intent,直譯為“意圖”。我們把資訊包裹在intent物件中,然後執行。 比如啟動RelativeLayoutGuideAct這個activity。

startActivity(new Intent(getApplicationContext(), RelativeLayoutGuideAct.class));

這裡用到一個很常見的方法startActivity (Intent intent)startActivity屬於Context類,Activity是Context的子類。

java.lang.Object
android.content.Context
android.content.ContextWrapper
android.view.ContextThemeWrapper
android.app.Activity

現在我們知道了,啟動activity需要使用Intent,呼叫startActivity方法。

2. 帶引數的跳轉

在跳轉去下一個頁面時,我們可能會想攜帶一些資訊到下一個介面去。例如攜帶一些文字,數字等等。 或者是一個物件。 這些資訊我們可以交給Intent,傳遞到下一個activity去。下一個activity中拿到我們傳入的Intent。

攜帶基本型別和String

我們直接看intent的方法。

Intent intent = new Intent(getApplicationContext(), SendParamsDemo.class);
intent.putExtra(SendParamsDemo.K_INT, 100);
intent.putExtra(SendParamsDemo.K_BOOL, true);
intent.putExtra(SendParamsDemo.K_STR, "Input string");
startActivity(intent);

intent的putExtra方法,可以傳入引數。它接受1個String作為key,然後是具體引數。 例子中我們跳轉去了SendParamsDemo。

public class SendParamsDemo extends AbsActivity {

    public static final String K_INT = "k_int";
    public static final String K_BOOL = "k_bool";
    public static final String K_STR = "k_str";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        gotInput();
    }

    private void gotInput() {
        Intent intent = getIntent();
        if (intent != null) {
            int i = intent.getIntExtra(K_INT, -1);
            boolean b = intent.getBooleanExtra(K_BOOL, false);
            String str = intent.getStringExtra(K_STR);
            Log.d(TAG, "gotInput: i:" + i + ", b: " + b + ", str: " + str);
        } else {
            Log.d(TAG, "gotInput: input null.");
        }
    }
}
// log:
// com.rustfisher.tutorial2020 D/rustAppSendParamsDemo: gotInput: i:100, b: true, str: Input string

在這個activity中我們接收到了傳入的引數。

觀察intent的putExtra方法,我們發現它支援傳入很多種引數。

int,byte, char, float, double, long, boolean,string,CharSequence或是它們的陣列。 也可以傳入Parcelable,Serializable物件或是物件陣列。

傳入Serializable物件

除了基本型別和String,可以傳送物件嗎? 答案是肯定的。Intent可以攜帶Serializable物件。 Serializable本身是一個介面,自定義的物件實現這個介面後,就可以被Intent攜帶。 比如我們改造一下DataTest類,讓它實現Serializable介面。

public class DataTest implements Serializable { // 實現介面

然後將物件送給intent,再啟動activity。

Intent intent = new Intent(getApplicationContext(), RecyclerViewDemo2Act.class);
DataTest out = new DataTest("input time", 233, 666, 999);
Log.d(TAG, "startInputData: sending object: " + out);
intent.putExtra(RecyclerViewDemo2Act.K_INPUT_DATA, out);
startActivity(intent);

被啟動的activity接受傳入的intent並取出物件。

Intent intent = getIntent();
if (intent != null) {
    DataTest d = (DataTest) intent.getSerializableExtra(K_INPUT_DATA);
    // 取出了物件,拿去顯示
}

Serializable介面不含任何方法。實現了這個介面的類,系統會自動將其序列化。

我們打印出傳送和接收到的物件。

startInputData: sending object: com.rustfisher.tutorial2020.recycler.data.DataTest@fb43df5
getInputData: input data object: com.rustfisher.tutorial2020.recycler.data.DataTest@a588b5c

可以發現這2個物件並不是同一個引用。但它們的“內容”是一樣的。物件經歷了序列化和反序列化的過程。

值得注意的是,Intent 能攜帶的物件大小並不是無限制的。實際開發中,需要開發者自己預估傳輸的資料大小。

傳送 Parcelable 物件和傳送 Serializable 物件類似,用同樣的存入和取出操作。


Activity 相關面試題

1. 談一下返回棧

首先理解android是使用Task來管理活動,一個Task就是一組存放在棧裡的活動的集合,這個棧就叫做返回棧,每啟動一個新的活動,就會將其放入棧頂,當我們點選back回退或呼叫activity的finish函式處於棧頂的活動就會出棧,前一個入棧的活動就會到棧頂,系統總是顯示處於棧頂的活動。

2. 說下Activity的生命週期?
  • onCreate()方法:活動第一次建立的時候被呼叫,常做初始化的操作,比如載入佈局(setContentView),繫結事件(findViewById)。表示Activity正在建立。

  • onStart()方法:活動由不可見到可見的時候被呼叫,表示Activity正在啟動,此時Activity可見但不在前臺。

  • onResume()方法:活動準備好和使用者進行互動時呼叫。表示Acitivity獲得焦點,此時Activity可見且在前臺。

  • onPause()方法:系統準備去啟動或恢復另一個活動時呼叫。表示Activity正在停止,此時可做儲存資料,停止動畫等操作。

  • onStop()方法:在活動完全不可見的時候呼叫。表示Activity即將停止。

  • onDestory()方法:在活動被銷燬之前呼叫,表示Activity即將銷燬,常做回收工作、資源釋放。

  • onRestart()方法:在活動由停止狀態變為執行狀態之前呼叫。表示Activity即將重啟。

3. 說下活動的生存期

活動的生存期分為三個:

  • 完整生存期
  • 可見生存期
  • 前臺生存期

完整生存期:onCreate()方法與onDestory()都處於完整生存期,一般情況下,Activity會在onCreate()方法中完成各種初始化操作,而在onDestory()方法中完成釋放記憶體的操作。

可見生存期:onStart()方法與onStop()方法就是可見生存期,Activity對於使用者是可見的,但無法與使用者互動。onStart()方法中對資源進行載入,onStop()方法中對資源進行釋放。

前臺生存期:onResume方法與onPause方法就是前臺生存期,在前臺生存期內,活動處於執行狀態,此時可以與使用者互動。

4. 說下Activity處於onPasue()下可以執行那些操作?
  • 使用者返回該Activity,呼叫onResume()方法,重新running
  • 使用者打開了其他Activity,就會呼叫onStop()方法
  • 系統記憶體不足,擁有更高許可權的應用需要記憶體,該Activity就會被系統回收
  • 如果使用者返回到onStop()的Activity又顯示在前臺了,系統會呼叫
onRestart() -> onStart() -> onResume() 然後重新running

當Activity結束(呼叫finish()方法)就會呼叫onDestory()方法釋放所有佔用的資源。

生命週期的切換過程

  1. 啟動一個Activity onCreate->onStart->onResume
  2. 當一個Activity開啟另一個Activity都會回撥哪些方法,如果ActivityB是完全透明的呢,如果啟動的是一個對話方塊Activity呢? A:onPause->B:onCreate->B:onStart->B:onResume->A:onStop 如果ActivityB是完全透明的或對話方塊Activity則不會呼叫onStop。
  3. 啟動新Activity後,又返回到舊的Activity B:onPause->A:onRestart->A:onStart->A:onResume->B:onStop->B:onDestory
  4. 關閉螢幕/按Home鍵: onPause->onStop
  5. 當一個Activity按Home鍵切換到桌面後又回到該Activity回撥哪些方法。 onPause->onStop->onRestart->onStart->onResume
  6. 當一個Activity按back鍵回退時回撥哪些方法 onPause->onStop->onDestory

Activity的優先順序

  1. 可見且可以互動(前臺Acitivity):正在和使用者互動,優先順序最高。
  2. 可見但不可以互動(可見但非前臺Activity):比如當前Activity啟動了一個對話方塊Activity,當前Activity就是可見但不可以互動。
  3. 後臺Activity:已經被暫停的Activity,比如執行了onStop,優先順序最低。 當系統記憶體不足,會按照優先順序順序從低到高去殺死目標Activity所在的程序。
5. 優先順序低的Activity在記憶體不足被回收後怎樣做可以恢復到銷燬前狀態?

優先順序低的 Activity 在記憶體不足被回收後重新開啟(橫豎屏切換的過程中)會引發Activity重建。

在 Activity 由於異常情況被終止時,系統會呼叫 onSaveInstanceState 方法來儲存當前 Activity 的狀態,該方法調用於 onStop 之前,與 onPause 方法沒有時序關係。

當異常終止的 Activity 被重建時,會呼叫 onRestoreInstanceState 方法(該方法在 onStart 之後),並且把 Activity 銷燬時 onSaveInstanceState 儲存的 Bundle 物件引數同時傳遞給 onCreate 方法和onRestoreInstanceState 方法。該方法的呼叫是在 onStart 之前。

因此可通過 onRestoreInstanceState(Bundle savedInstanceState)onCreate((Bundle savedInstanceState) 來判斷 Activity 是否被重建,並取出資料進行恢復。但需要注意的是,在 onCreate 取出資料時一定要先判斷savedInstanceState 是否為空。

補充:其中 onCreate 和 onRestoreInstanceState 方法來恢復 Activity 的狀態的區別: onRestoreInstanceState 方法回撥則說明 bundle 物件非空,不需要加非空判斷,而 onCreate 需要非空判斷。

6. 談談 onSaveInstanceState()onRestoreIntanceState()

onSaveInstanceState()
這兩個方法並不是生命週期方法,它們並不一定會被觸發。當應用遇到意外情況(如:記憶體不足、使用者直接按Home鍵)由系統銷燬一個Activity時,onSaveInstanceState() 會被呼叫,該方法的呼叫在onStop之前,與onPause沒有時序關係。但是當用戶主動去銷燬一個Activity時,例如在應用中按返回鍵,onSaveInstanceState()就不會被呼叫。因為在這種情況下,使用者的行為決定了不需要儲存Activity的狀態。

onSaveInstanceState()時機:
(1)使用者按下Home鍵
(2)橫豎屏切換
(3)按下電源按鈕(關閉螢幕顯示)
(4)記憶體不足導致優先順序的Activity被殺死

onRestoreIntanceState()
當被系統異常銷燬的 Activity 被重建時,會呼叫 onRestoreIntanceState 或 onCreate 方法來恢復,而 onRestoreInstance 與 Oncreate 方法中傳入的 Bundle 物件是銷燬時 onSaveInstanceState 儲存的,onRestoreIntanceState 在 onStart之後。

7. onSaveInstanceState()與onPause()的區別?

onSaveInstanceState() 只適合用於儲存一些臨時性的狀態,而onPause()適合用於資料的持久化儲存。

8. 談談橫豎屏切換過程中呼叫的函式

要切記這裡活動已經被銷燬了。
onPause->onSaveInstanceState->onStop->onDestory()->onCreate->onStart->onRestoreIntanceState->onResume

9. 如何防止橫豎屏切換(配置改變)時Activity銷燬並切換

通過對AndroidManifest檔案的Activity中指定(configChanges)屬性:

android:configChanges = “orientation| screensize”

來避免橫豎屏切換時,Activity的銷燬和重建,而是回調了onCofigurationChanged()方法

@Override
 public void onConfigurationChanged(Configuration newConfig) {
 super.onConfigurationChanged(newConfig);
 }

這裡附上android configChanges 的所有屬性解釋

“mcc“ 移動國家號碼,由三位數字組成,每個國家都有自己獨立的MCC,可以識別手機使用者所屬國家。
“mnc“ 移動網號,在一個國家或者地區中,用於區分手機使用者的服務商。
“locale“ 所在地區發生變化。
“touchscreen“ 觸控式螢幕已經改變。(這不應該常發生。)
“keyboard“ 鍵盤模式發生變化,例如:使用者接入外部鍵盤輸入。
“keyboardHidden“ 使用者開啟手機硬體鍵盤
“navigation“ 導航型發生了變化。(這不應該常發生。)
“orientation“ 裝置旋轉,橫向顯示和豎向顯示模式切換。
“fontScale“ 全域性字型大小縮放發生改變

10. 說下Activity的四種啟動模式?

  1. standard模式(標準模式):普通啟動模式,每次啟動Activity時,就會建立一個例項。

  2. singletop模式(棧頂模式):當啟動Activity時,會判斷任務棧的棧頂是否為該Activity,如果是該Activity則不會建立例項,去回撥onNewIntent(intent)方法,否則會建立例項

  3. singletask模式(棧內模式):當啟動Activity時,只要該Activity在指定的棧中,就不會建立例項,去回撥onNewIntent(intent)**方法。如果不存在,會判斷是否指定的棧不存在,就建立一個棧並將Activity的例項壓入,如果指定的棧存在,就直接壓入該棧中。

  4. singleInstance模式(單例項模式):該模式下,建立Activity例項時,直接建立一個棧,棧中只有該Activity例項。之後無論哪個應用程式啟動該Activity,都只會呼叫棧中該例項。

11. 談談 singleTop 和 singleTask 的區別以及應用場景

singleTop 模式的含義是(參考上面問題),singleTask 模式的含義是(參考上面問題)

因此二者的差別為:

  • singleTop 模式:該模式下,任務棧中可能有多個相同 Activity 例項,因為它只是判斷當前啟動的 Activity 是否在棧頂。 該模式的 Activity 會預設進入啟動它所屬的任務棧,不涉及任務棧的轉換。常用於防止快速連續點選而建立多個 Activity 例項。

  • singleTask 模式:該模式向,任務棧中只會有一個Activity例項,因為它會判斷當前啟動的Activity是否在當前指定的棧中。該模式下Activity可以通過taskAffinity去指定需要的任務棧,可能涉及任務棧的轉換,常用於首頁或登入頁。因為不論我們在進入首頁後進入了多少個Activity,當我們返回首頁後,還是希望退出首頁直接可以退出應用。該模式下會把棧中位於要啟動的Activity上面的Activity都出棧。

12. onNewIntent()呼叫時機?

有兩個呼叫時機,分別是singleTop模式下與singleTask模式下啟動Activity。
singleTop模式:當啟動的Activity是在任務棧的棧頂時,會回撥onNewIntent方法。
singleTask模式:當啟動的Activity存在於任務棧中,會回撥onNewIntent方法。

13. 瞭解哪些Activity啟動模式的標記位?

  • FLAG_ACTIVITY_SINGLE_TOP:對應singleTop啟動模式
  • FLAG_ACTIVITY_NEW_TASK:對應singleTask模式

最後:更多Android零基礎入門教程視訊學習