1. Activity 的生命週期
1.1 分類
在講解生命週期的方法之前,先放上這張官方的圖:
這張圖片講述了 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():
- 橫豎屏切換
- 按下電源鍵
- 按下選單鍵
- 切換到別的 Activity
- ….
onRestoreInstanceState():
- 橫豎屏切換
- 切換語言
- ….
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 顯式啟動
顯示啟動分類:
- 直接在 Intent 構造方法啟動:
Intent intent = new Intent(this, SecondActivity.class);
複製程式碼
- setComponent:
ComponentName componentName = new ComponentName(this, SecondActivity.class);
Intent intent = new Intent();
intent.setComponent(componentName);
複製程式碼
- 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 有三個標籤分別是:
- action
- category
- data
這三個標籤都有對應的匹配規則,下面會說到。這裡來說下使用 IntentFilter 要注意的地方
- 一個 Activity 中可以有多個 intent-filter
- 一個 intent-filter 同時可以有多個 action,category,data
- 一個 Intent 只要能匹配任何一組 intent-filter 即可啟動對應 Activity
- 新建的 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 的邏輯了,但是這個模式還有幾個需要注意的地方。
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" />
複製程式碼
然後根據以下步驟操作:
- 現在先執行應用 B,然後退出
- 再執行應用 A,點選按鈕啟動
- 按 home 鍵,回到主介面
- 啟動應用 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() 方法,但是並不是所有情況都這樣,總結如下圖:
以下使用程式碼來驗證一下這四種情況:
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