Activity 必知必會

ZedeChan發表於2019-03-04

1. Activity 的生命週期

1.1 分類

在講解生命週期的方法之前,先放上這張官方的圖:

Activity 必知必會

這張圖片講述了 Activity 的回撥的方法,下表分類講解這些方法的作用。

生命週期方法 作用
onCreate 表示 Activity 正在被建立
onRestart 表示 Activity 正在重新啟動
onStart 表示 Activity 正在被啟動
onResume 表示 Activity 已經可見
onPause 表示 Activity 正在停止
onStop 表示 Activity 即將停止
onDestroy 表示 Activity 即將被銷燬

1.2 各種情況下生命週期的回撥

以下總結一下各種情況下,生命週期中的回撥情況(表中的 A,B 代表的是兩個 Activity):

情況 回撥
第一次啟動 onCreate() -> onStart() -> onResume()
從 A 跳轉到不透明的 B A_onPause() -> B_onCreate() -> B_onStart() -> B_onResume() -> A_onStop()
從 A 跳轉到透明的 B A_onPause() -> B_onCreate() -> B_onStart() -> B_onResume()
從不透明的 B 再次回到 A B_onPause() -> A_onRestart() -> A_onStart() -> A_onResume() -> B_onStop()
從透明的 B 再次回到 A B_onPause() -> A_onResume() -> B_onStop() -> B_onDestroy()
使用者按 home 鍵 onPause() -> onStop()
按 home 鍵回後回到應用 onRestart() -> onStart() -> onResume()
使用者按 back 鍵回退 onPause() -> onStop() -> onDestroy()

1.3 onSaveInstanceState() 與 onRestoreInstanceState()

這兩個方法只有在應用遇到意外情況下才會觸發。可以用於儲存一些臨時性的資料。

1.3.1 觸發場景

onSaveInstanceState():

  1. 橫豎屏切換
  2. 按下電源鍵
  3. 按下選單鍵
  4. 切換到別的 Activity
  5. ....

onRestoreInstanceState():

  1. 橫豎屏切換
  2. 切換語言
  3. ....

2. Activity 之間的跳轉

2.1 相關API

2.1.1 startActivity()

怎麼用:

Intent intent = new Intent(MainActivity.this, SecondActivity.class);
startActivity(intent);
複製程式碼

使用這個方法就可以跳轉到 SecondActivity

2.1.2 startActivityforResult()

怎麼用:

MainActivity.java:

Intent intent = new Intent(MainActivity.this, SecondActivity.class);
startActivityForResult(intent, 1);
複製程式碼

這裡第二個引數是一個 requestCode,這個引數會在 onActivityResult 回撥回來。

SecondActivity.java:

setResult(2);
finish();
複製程式碼

當 SecondActivity finish 後會回撥 MainActivity 中的 onActivityResult 方法:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    Log.e("chan", "==================onActivityResult main= " + requestCode + " " + resultCode);
}
複製程式碼

列印結果:

E/chan: ==================onActivityResult main= 1 2
複製程式碼

當然你也可以不呼叫 setResult() 方法,這時回撥過來的 resultCode 就是 0。

2.2 顯式啟動

顯示啟動分類:

  1. 直接在 Intent 構造方法啟動:
Intent intent = new Intent(this, SecondActivity.class);
複製程式碼
  1. setComponent:
ComponentName componentName = new ComponentName(this, SecondActivity.class);
Intent intent = new Intent();
intent.setComponent(componentName);
複製程式碼
  1. setClass / setClassName:
Intent intent = new Intent();
intent.setClass(this, SecondActivity.class);
intent.setClassName(this, "com.example.administrator.myapplication.SecondActivity");
複製程式碼

2.3 隱式啟動

隱式啟動就是要在該 Activity 中設定 IntentFilter 屬性,只要啟用的 Intent 匹配 IntentFilter 的條件就可以啟動相應的 Activity。

要理解隱式啟動就必須要理解 IntentFilter 是如何使用的

2.3.1 IntentFilter 的使用

IntentFilter 有三個標籤分別是:

  1. action
  2. category
  3. data

這三個標籤都有對應的匹配規則,下面會說到。這裡來說下使用 IntentFilter 要注意的地方

  1. 一個 Activity 中可以有多個 intent-filter
  2. 一個 intent-filter 同時可以有多個 action,category,data
  3. 一個 Intent 只要能匹配任何一組 intent-filter 即可啟動對應 Activity
  4. 新建的 Activity 必須加上以下這句:
<category android:name="android.intent.category.DEFAULT"/>
複製程式碼

否則就會出現如下錯誤:

android.content.ActivityNotFoundException: No Activity found to handle Intent { act=com.chan1 }
複製程式碼

2.3.2 action 的匹配規則

action 的匹配規則就是隻要滿足其中一個 action 就可以啟動成功。

在 Manifest 定義一個 SecondActivity:

<activity android:name=".SecondActivity">
    <intent-filter>
        <action android:name="com.chan" />
        <action android:name="com.chan2" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
    <intent-filter>
    <action android:name="com.chan3" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>
複製程式碼

MainActivity:

Intent intent = new Intent();
intent.setAction("com.chan2");
startActivity(intent);
複製程式碼

這樣就可以啟動 SecondActivity,要注意的是 action 是區分大小寫的。

2.3.3 category 匹配規則

category 在程式碼設定如下:

intent.addCategory("com.zede");
複製程式碼

這句可以新增也可以不新增,因為程式碼預設會為我們匹配 “android.intent. category.DEFAULT”。

2.3.4 data 匹配規則

data 主要是由 URI 和 mimeType 組成的。URI 的結構如下:

<scheme> :// <host> : <port> [<path>|<pathPrefix>|<pathPattern>]
複製程式碼

這些值在 Manifest 檔案中可以定義,語法如下:

<data android:scheme="string"
      android:host="string"
      android:port="string"
      android:path="string"
      android:pathPattern="string"
      android:pathPrefix="string"
      android:mimeType="string" />
複製程式碼

以下用一個例子來說明:

Manifest:

<activity android:name=".SecondActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <data
            android:host="zede"
            android:port="1010"
            android:scheme="chan" />
    </intent-filter>
</activity>
複製程式碼

MainActivity:

Intent intent = new Intent();
intent.setData(Uri.parse("chan://zede:1010"));
startActivity(intent);
複製程式碼

通過這個方法就可以跳轉到 SecondActivity。

我們也可以建立一個 html 檔案,來實現跳轉 SecondActivity。

test.html:

<!DOCTYPE html>
<html>
<head>
	<title></title>
</head>
<body>
<a href="chan://zede:1010">跳轉至SecondActivity</a>
</body>
</html>
複製程式碼

使用手機瀏覽器開啟這個 html 檔案,點選這個超連結也可以跳轉到 SecondActivity。

通過這個連結也可以傳輸資料到 SecondActivity,程式碼如下:

<!DOCTYPE html>
<html>
<head>
	<title></title>
</head>
<body>
<a href="chan://zede:1010/mypath?user=admin&psd=123456">跳轉至SecondActivity</a>
</body>
</html>
複製程式碼

在 SecondActivity 接收資料:

Intent intent = getIntent();
Uri uri = intent.getData();

Log.e("chan", "==================getScheme= " + intent.getScheme());
Log.e("chan", "==================getHost= " + uri.getHost());
Log.e("chan", "==================getPort= " + uri.getPort());
Log.e("chan", "==================getPath= " + uri.getPath());

Log.e("chan", "==================getQuery= " + uri.getQuery());

Set < String > names = uri.getQueryParameterNames();

Iterator < String > iterator = names.iterator();
while (iterator.hasNext()) {
    String key = iterator.next();
    uri.getQueryParameter(key);
    Log.e("chan", "==================getQueryParameter= " + uri.getQueryParameter(key));
}
複製程式碼

列印結果:

07-19 10:47:54.969 19201-19201/com.example.administrator.myapplication E/chan: ==================getScheme= chan
07-19 10:47:54.970 19201-19201/com.example.administrator.myapplication E/chan: ==================getHost= zede
    ==================getPort= 1010
    ==================getPath= /mypath
    ==================getQuery= user=admin&psd=123456
    ==================getQueryParameter= admin
    ==================getQueryParameter= 123456
複製程式碼

另外還需要注意另一個屬性:android:mimeType,這個屬性就是說要傳遞什麼型別的資料,通常有 text/plain 或 image/jpeg。

可以通過以下程式碼來啟動 Activity:

intent.setType("text/plain");
複製程式碼

不過需要注意的是,如果同時設定了 URI 和 mimeType 的話就必須使用如下程式碼才可以跳轉:

intent.setDataAndType(Uri.parse("chan://zede:1010"), "text/plain");
複製程式碼

因為如果使用 setData() 或者 setType() 的話,分別會將相應 type 和 data 置為 null。

3. Activity的啟動模式

3.1 分類:

啟動模式 作用
standard 每次啟動都會重新建立一個 Activity
singleTop 如果該棧頂上有所要啟動的 Activity,那麼就不會重新建立該 Activity,並會回撥 onNewIntent()
singleTask 如果棧內已經有所要啟動的 Activity 就不會被建立,同時也會呼叫 onNewIntent()
singleInstance 建立該 Activity 系統會建立一個新的任務棧

這裡重點說下 singleTask。

3.2 singleTask 分析

singleTask 叫做棧內複用模式,這個啟動模式的啟動邏輯如下圖:

singleTask 邏輯

相信看了上面這個圖,大家也清楚 singleTask 的邏輯了,但是這個模式還有幾個需要注意的地方。

3.2.1 taskAffinity

前面提到 A 想要的任務棧,那什麼是 A 想要的任務棧呢?這就提到一個屬性 taskAffinity,以下詳細介紹這個屬性。

3.2.1.1 作用

標識一個 Activity 所需要的任務棧的名字。如果不設定這個屬性值,預設值是應用的包名。

3.2.1.2 taskAffinity 與 singleTask 配對使用

如果啟動了設定了這兩個屬性的 Activity,這個 Activity 就會在 taskAffinity 設定的任務棧中,下面用程式碼來驗證下:

建立 SecondActvitiy,在 Mnifest 檔案設定 SecondActvitiy,程式碼如下:

<activity android:name=".SecondActivity"
    android:taskAffinity="com.chan"
    android:launchMode="singleTask" />
複製程式碼

現在使用 MainActivity 啟動 SecondActvitiy,這裡的程式碼就不展示了,我們直接看看結果,在終端輸入以下命令:

adb shell dumpsys activity activities | sed -En -e '/Running activities/,/Run #0/p'
複製程式碼

這個命令可以檢視正在執行的 Activity,結果如下:

Running activities (most recent first):
    TaskRecord{762a040 #63 A=com.chan U=0 StackId=1 sz=1}
    Run #1: ActivityRecord{3881f68 u0 com.example.activitydemo/.SecondActivity t63}
    TaskRecord{351eb79 #62 A=com.example.activitydemo U=0 StackId=1 sz=1}
複製程式碼

從列印結果可以看出, MainActivity 和 SecondActivity 執行在不同的任務棧中。

3.2.1.3 taskAffinity 和 allowTaskReparenting 配對使用

allowTaskReparenting 這個屬性直接解釋的話,可能很多人都會聽得懵逼,下面直接使用例子來解釋:

現在建立兩個應用,一個應用的包名為:com.example.activitydemo,以下成為應用 A。另一個應用的包名為:com.example.hellodemo,以下稱為應用 B。

在應用 A 的 MainActivtiy 中新增如下程式碼:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    findViewById(R.id.start1).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
        Intent intent=new Intent();
        intent.setClassName("com.example.hellodemo", "com.example.hellodemo.HelloActivity");
        startActivity(intent);
        }
    });

}
複製程式碼

應用 B 中建立 HelloActivity,並在 Manifest 檔案中設定 HelloActivity 的 allowTaskReparenting 為 true,程式碼如下:

<activity android:name=".HelloActivity"
    android:exported="true"
    android:allowTaskReparenting="true" />
複製程式碼

然後根據以下步驟操作:

  1. 現在先執行應用 B,然後退出
  2. 再執行應用 A,點選按鈕啟動
  3. 按 home 鍵,回到主介面
  4. 啟動應用 B

完成第二步的時候,在終端看下任務棧的情況:

Running activities (most recent first):
    TaskRecord{5d54c1c #85 A=com.example.activitydemo U=0 StackId=1 sz=2}
    Run #1: ActivityRecord{ff0b8e u0 com.example.hellodemo/.HelloActivity t85}
    Run #0: ActivityRecord{95ee35c u0 com.example.activitydemo/.MainActivity t85}
複製程式碼

可以看出 HelloActivity 執行在應用 A 的任務棧中。

完成第四步後,再看下任務棧:

Running activities (most recent first):
    TaskRecord{74c894d #86 A=com.example.hellodemo U=0 StackId=1 sz=2}
        Run #1: ActivityRecord{ff0b8e u0 com.example.hellodemo/.HelloActivity t86}
    TaskRecord{5d54c1c #85 A=com.example.activitydemo U=0 StackId=1 sz=1}
        Run #0: ActivityRecord{95ee35c u0 com.example.activitydemo/.MainActivity t85}
複製程式碼

從結果可以看到,HelloActivity 從應用 A 的任務棧移動到應用 B 的任務棧。

現在再修改下 HelloActivity 的 taskAffinity 屬性,程式碼如下:

<activity android:name=".HelloActivity"
    android:exported="true"
    android:allowTaskReparenting="true"
    android:taskAffinity="com.chan"/>
複製程式碼

重新根據以上步驟操作,操作完畢後看下任務棧資訊:

Running activities (most recent first):
    TaskRecord{50264fe #90 A=com.example.hellodemo U=0 StackId=1 sz=1}
    Run #2: ActivityRecord{bc77713 u0 com.example.hellodemo/.MainActivity t90}
    TaskRecord{41abf9e #89 A=com.example.activitydemo U=0 StackId=1 sz=2}
    Run #1: ActivityRecord{2d0b7bb u0 com.example.hellodemo/.HelloActivity t89}
    Run #0: ActivityRecord{8b57551 u0 com.example.activitydemo/.MainActivity t89}
複製程式碼

可以看出 HelloActivity 並沒有移動到應用 B 的主任務棧中,因為這並不是 HelloActivity 想要的任務棧。

繼續修改 HelloActivity 配置屬性,增加 singleTask 屬性:

<activity android:name=".HelloActivity"
    android:exported="true"
    android:allowTaskReparenting="true"
    android:taskAffinity="com.chan"
    android:launchMode="singleTask"/>
複製程式碼

繼續操作,任務棧結果如下:

Running activities (most recent first):
    TaskRecord{775e709 #95 A=com.example.hellodemo U=0 StackId=1 sz=1}
        Run #2: ActivityRecord{757bb47 u0 com.example.hellodemo/.MainActivity t95}
    TaskRecord{aa75b2 #94 A=com.chan U=0 StackId=1 sz=1}
        Run #1: ActivityRecord{76e2133 u0 com.example.hellodemo/.HelloActivity t94}
    TaskRecord{21c8903 #93 A=com.example.activitydemo U=0 StackId=1 sz=1}
        Run #0: ActivityRecord{be84df4 u0 com.example.activitydemo/.MainActivity t93}
複製程式碼

可以看出與沒有增加 singleTask 屬性的結果是一樣的,其實 allowTaskReparenting 這個屬性的最主要作用就是將這個 Activity 轉移到它所屬的任務棧中,例如一個簡訊應用收到一條帶網路連結的簡訊,點選連結會跳轉到瀏覽器中,這時候如果 allowTaskReparenting 設定為 true 的話,開啟瀏覽器應用就會直接顯示剛才開啟的網頁頁面,而開啟簡訊應用後這個瀏覽器介面就會消失。

3.3 指定啟動模式的方式

指定啟動模式的方式有兩種,一種是在 AndroidMenifest 檔案設定 launchMode 屬性,另一種就是在 Intent 當中設定標誌位。第二種方式的優先順序會比第一種的要高,如果兩種都設定了會以第二種方式為準。我們來驗證一下:

在 MainActivity 設定如下程式碼:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.start1).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent();
                intent.setClass(MainActivity.this, SecondActivity.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                startActivity(intent);
            }
        });

    }
複製程式碼

SecondActivity 在 AndroidMenifest 設定如下:

        <activity android:name=".SecondActivity"
            android:taskAffinity="com.chan" />
複製程式碼

這裡我並沒有設定 SecondActivity 為 sigleTask,來驗證下啟動 SecondActivity 是否會開啟一個新的任務棧。

執行後,任務棧的結果為:

    Running activities (most recent first):
      TaskRecord{148d7c5 #143 A=com.chan U=0 StackId=1 sz=1}
        Run #2: ActivityRecord{de59b2d u0 com.example.activitydemo/.SecondActivity t143}
      TaskRecord{520151a #142 A=com.example.activitydemo U=0 StackId=1 sz=1}
        Run #1: ActivityRecord{d80bfc1 u0 com.example.activitydemo/.MainActivity t142}
複製程式碼

從結果可以看出,是開啟了一個新的任務棧的,也證明了第二種方式的優先順序比較高

3.4 onNewIntent 回撥時機

啟動 singleTask 的 Activity 的時候會回撥 onNewIntent() 方法,但是並不是所有情況都這樣,總結如下圖:

onNewIntent 回撥時機

以下使用程式碼來驗證一下這四種情況:

3.4.1 不存在 A 所需的任務棧

程式碼如下:

MainActivity.java:

public class MainActivity extends AppCompatActivity {


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.start1).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(MainActivity.this, SecondActivity.class));
            }
        });

    }


    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        Log.e("chan", "MainActivity=======================onNewIntent");
    }


}
複製程式碼

SecondActivity.java:

public class SecondActivity extends AppCompatActivity {


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        findViewById(R.id.start2).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(SecondActivity.this, ThirdActivity.class));
            }
        });

    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        Log.e("chan", "SecondActivity=======================onNewIntent");
    }
}
複製程式碼

清單檔案中將 SecondActivity 設定為 singleTask,taskAffinity 屬性設定一個非該程式包名的值,程式碼如下:

        <activity android:name=".SecondActivity"
            android:taskAffinity="com.onnewintent"
            android:launchMode="singleTask" />
複製程式碼

以上程式碼的結果並沒有列印任何東西,證明這樣並不會回撥 onNewIntent()。

3.4.2 存在 A 所需的任務棧

這種情況還要分兩種子情況,一種就是 A 不在棧中,另一種 A 不在棧中。

3.4.2.1 A 不在棧中

還是用回上面的例子的程式碼,新增一個 ThridActivity,ThridActivity 在清單檔案描述如下:

        <activity android:name=".ThirdActivity"
            android:taskAffinity="com.onnewintent"
            android:launchMode="singleTask" />

複製程式碼

點選 SecondActivity 跳轉按鈕後同樣也不會有任何列印,證明並不會回撥 onNewIntent()。

3.4.2.2 A 在棧中

這種情況也會分兩種子情況,一種就是 A 在棧頂,另一種就是 A 不在棧頂。

3.4.2.2.1 A 在棧頂

同樣也是使用上面的例子,修改 ThirdActivity,程式碼如下:

public class ThirdActivity extends AppCompatActivity {



    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);
        findViewById(R.id.start3).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(ThirdActivity.this, ThirdActivity.class));
            }
        });

    }


    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        Log.e("chan", "ThirdActivity=======================onNewIntent");
    }


}
複製程式碼

到 ThirdActivity 時,ThirdActivity 已經是在棧頂了,這時候點選按鈕再次啟動 ThirdActivity,列印結果如下:

chan    : ThirdActivity=======================onNewIntent
複製程式碼

可以發現這種情況會回撥 ThirdActivity 的 onNewIntent。

3.4.2.2.2 A 不在在棧頂

同樣改動 ThirdActivity,這次啟動的是 SecondActivity。具體程式碼就不寫了,列印結果如下:

chan    : SecondActivity=======================onNewIntent
複製程式碼

可以發現這種情況會回撥 SecondActivity 的 onNewIntent。

3.5 Activity 中的 Flags

3.5.1 FLAG_ACTIVITY_NEW_TASK

與啟動模式 singleTask 的作用一樣,必須設定 taskAffinity

3.5.2 FLAG_ACTIVITY_SINGLE_TOP

與啟動模式 singleTop 的作用一樣

3.5.3 FLAG_ACTIVITY_CLEAR_TOP

從名字就可以看出這個標誌的作用就是如果這個 Activity 頂部有別的 Activity 的話,那麼就會它頂部的 Activity 全部出棧,如果這個 Activity 的啟動模式為 standard 的話,就會將之前的 Activity 出棧,並且建立一個新的 Activity,如果不是的話就會呼叫 onNewIntent 方法。

3.5.4 FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS

不能在檢視歷史 Activity 中檢視到此 Activity

相關文章