- Android Studio開發實戰:從零基礎到App上線(第2版)
- 歐陽燊
- 3817字
- 2019-12-06 12:07:22
3.5 活動Activity基礎
本節介紹Android四大組件之一Activity的基本概念和常見用法。首先說明Activity的生命周期,接著說明Intent的組成部分與工作原理,然后闡述如何使用Intent完成活動頁面之間的消息傳遞,包括如何傳遞請求參數、如何返回應答參數等。
3.5.1 Activity的生命周期
看到這里,相信讀者對Activity已經不陌生了。首先,一個Activity代表一個頁面。其次,Activity的onCreate方法是頁面的入口函數。更細心的讀者也許已經知道調用startActivity方法可以跳轉到下一個頁面。之所以到這時才介紹Activity,是因為Activity的邏輯復雜、概念繁多,必須在有一定基礎后講解才合適,不然一開始就講解高深的專業術語,讀者恐怕很難理解。
首先介紹Activity的生命周期,如同花開花落一般,Activity也有從含苞待放到盛開再到凋零的生命過程。下面是Activity與生命周期有關的方法說明。
- onCreate:創建頁面。把頁面上的各個元素加載到內存中。
- onStart:開始頁面。把頁面顯示在屏幕上。
- onResume:恢復頁面。讓頁面在屏幕上活動起來,例如開啟動畫、開始任務等。
- onPause:暫停頁面。讓頁面在屏幕上的動作停下來。
- onStop:停止頁面。把頁面從屏幕上撤下來。
- onDestroy:銷毀頁面。把頁面從內存中清除掉。
- onRestart:重啟頁面。重新加載內存中的頁面數據。
下面針對幾個常見的業務場景探究一下Activity的生命周期,主要有3個場景:頁面之間的跳轉、豎屏與橫屏的切換、按HOME鍵與返回App。用于場景測試的代碼如下,主要在每個生命周期函數中增加打印屏幕日志和后臺日志。



圖3-20 活動頁面跳轉時的界面日志截圖
1. 頁面之間的跳轉
首先進入測試頁面ActJumpActivity,接著從該頁面跳轉到ActNextActivity,然后從ActNextActivity返回ActJumpActivity。界面上的日志截圖如圖3-20所示。其中,區域1表示進入頁面ActJumpActivity時的生命周期過程,區域2表示跳轉到ActNextActivity時的生命周期過程,區域3表示返回ActJumpActivity時的生命周期過程。
從日志截圖可以看到,下一個頁面的創建伴隨上一個頁面的停止,不過顯示的日志信息不夠完整。下面跟蹤一下logcat里的日志,看看這中間到底發生了什么。
首先打開頁面ActJumpActivity,調用方法的順序為:本頁面onCreate→onStart→onResume。日志如下:
11:30:18.352:D/ActJumpActivity(2315):onCreate 11:30:18.352:D/ActJumpActivity(2315):onStart 11:30:18.352:D/ActJumpActivity(2315):onResume
從ActJumpActivity跳轉到ActNextActivity,調用方法的順序為:上一個頁面onPause→下一個頁面onCreate→onStart→onResume→上一個頁面onStop。日志如下:
11:30:32.668:D/ActJumpActivity(2315):onPause 11:30:32.688:D/ActNextActivity(2315):onCreate 11:30:32.688:D/ActNextActivity(2315):onStart 11:30:32.688:D/ActNextActivity(2315):onResume 11:30:33.116:D/ActJumpActivity(2315):onStop
從ActNextActivity回到ActJumpActivity(按返回鍵或在代碼中調用finish方法),調用的方法順序為:下一個頁面onPause→上一個頁面onRestart→onStart→onResume→下一個頁面onStop→onDestroy。日志如下:
11:30:40.740:D/ActNextActivity(2315):onPause 11:30:40.752:D/ActJumpActivity(2315):onRestart 11:30:40.752:D/ActJumpActivity(2315):onStart 11:30:40.752:D/ActJumpActivity(2315):onResume 11:30:41.160:D/ActNextActivity(2315):onStop 11:30:41.164:D/ActNextActivity(2315):onDestroy

圖3-21 活動頁面在橫豎屏切換時的界面日志截
至此,基本上可以弄清楚頁面跳轉時的生命周期了。總體上是跳轉前的頁面先調用onPause方法,然后跳轉后的頁面依次調用onCreate/onRestart→onStart→onResume,最后跳轉前的頁面調用onStop方法(若返回上級頁面,則下級頁面還需調用onDestroy方法)。
2. 豎屏與橫屏的切換
首先進入測試頁面ActRotateActivity,此時默認為豎屏顯示;接著倒轉手機切換到橫屏,觀察日志;然后倒轉手機切換回豎屏,觀察日志。3個屏幕的顯示日志時間沒有重復,這里的日志截圖是3次截圖拼接而成的,如圖3-21所示。
從日志截圖可以看出,豎屏與橫屏似乎在每次切換時頁面都要重新創建。為進一步驗證實驗結果,再一次查看logcat里的日志信息如下:
21:02:10.179 D/ActRotateActivity: onCreate 21:02:10.179 D/ActRotateActivity: onStart 21:02:10.179 D/ActRotateActivity: onResume 21:02:13.227 D/ActRotateActivity: onPause 21:02:13.227 D/ActRotateActivity: onStop 21:02:13.227 D/ActRotateActivity: onDestroy 21:02:13.247 D/ActRotateActivity: onCreate 21:02:13.247 D/ActRotateActivity: onStart 21:02:13.247 D/ActRotateActivity: onResume 21:02:16.239 D/ActRotateActivity: onPause 21:02:16.239 D/ActRotateActivity: onStop 21:02:16.239 D/ActRotateActivity: onDestroy 21:02:16.279 D/ActRotateActivity: onCreate 21:02:16.279 D/ActRotateActivity: onStart 21:02:16.279 D/ActRotateActivity: onResume
分析日志的時間與內容,無論是豎屏切換到橫屏,還是橫屏切換到豎屏,都是原屏幕的頁面從onPause到onStop再到onDestroy一路銷毀,然后新屏幕的頁面從onCreate到onStart再到onResume一路創建而來。
3. 按HOME鍵與返回App
首先進入測試頁面ActHomeActivity;接著按HOME鍵,屏幕回到桌面;然后按任務鍵或長按HOME鍵(不同手機的操作不一樣),屏幕調出進程視圖;最后點擊測試App,屏幕返回測試頁面。一路下來的屏幕日志截圖如圖3-22所示。

圖3-22 按HOME鍵的界面日志截圖
從日志截圖可以看到,此時測試頁面的生命周期是典型的從活動狀態變為暫停狀態(回到桌面時)再到活動狀態(返回App頁面時)。觀察logcat的后臺日志,發現后臺日志與屏幕日志保持一致。
3.5.2 使用Intent傳遞消息
Intent的中文名是意圖,意思是我想讓你干什么,簡單地說,就是傳遞消息。Intent是各個組件之間信息溝通的橋梁,既能在Activity之間溝通,又能在Activity與Service之間溝通,也能在Activity與Broadcast之間溝通。總而言之,Intent用于處理Android各組件之間的通信,完成的工作主要有3部分:
(1)Intent需標明本次通信請求從哪里來、到哪里去、要怎么走。
(2)發起方攜帶本次通信需要的數據內容,接收方對收到的Intent數據進行解包。
(3)如果發起方要求判斷接收方的處理結果,Intent就要負責讓接收方傳回應答的數據內容。
為了做好以上工作,就要給Intent配上必須的裝備,Intent的組成部分見表3-5。
表3-5 Intent組成元素的列表說明

表達Intent的來往路徑有兩種方式,一種是顯式Intent,另一種是隱式Intent。
1. 顯式Intent,直接指定來源類與目標類名,屬于精確匹配。
在聲明一個Intent對象時,需要指定兩個參數,第一個參數表示跳轉的來源頁面,第二個參數表示接下來要跳轉到的頁面類。具體的聲明方式有如下3種:
(1)在構造函數中指定,示例代碼如下:
Intent intent = new Intent(this, ActResponseActivity.class); // 創建一個目標確定的意圖
(2)調用setClass方法指定,示例代碼如下:
Intent intent = new Intent(); // 創建一個新意圖 intent.setClass(this, ActResponseActivity.class); // 設置意圖要跳轉的活動類
(3)調用setComponent方法指定,示例代碼如下:
Intent intent = new Intent(); // 創建一個新意圖 ComponentName component = new ComponentName(this, ActResponseActivity.class); intent.setComponent(component); // 設置意圖攜帶的組件信息
2. 隱式Intent,沒有明確指定要跳轉的類名,只給出一個動作讓系統匹配擁有相同字串定義的目標,屬于模糊匹配。
因為我們常常不希望直接暴露源碼的類名,只給出一個事先定義好的名稱,這樣大家約定俗成、按圖索驥就好,所以隱式Intent起到了過濾作用。這個定義好的動作名稱是一個字符串,可以是自己定義的動作,也可以是已有的系統動作。系統動作的取值說明見表3-6。
表3-6 系統動作的取值說明

這個動作名稱通過setAction方法指定,也可以通過構造函數Intent(String action)直接生成Intent對象。當然,由于動作是模糊匹配,因此有時需要更詳細的路徑,比如知道某人住在天通苑小區,并不能直接找到他家,還得說明他住在天通苑的哪一期、哪號樓、哪一層、哪一個單元。Uri和Category便是這樣的路徑與門類信息,Uri數據可通過構造函數Intent(String action,Uri uri)在生成對象時一起指定,也可通過setData方法指定(setData這個名字有歧義,實際就是setUri);Category可通過addCategory方法指定,之所以用add而不用set方法,是因為一個Intent可同時設置多個Category,一起進行過濾。
下面是一個調用系統撥號程序的例子,其中就用到了Uri:
Intent intent = new Intent(); // 創建一個新意圖 intent.setAction(Intent.ACTION_CALL); // 設置意圖動作為直接撥號 Uri uri = Uri.parse("tel:" + phone); // 聲明一個撥號的Uri intent.setData(uri); // 設置意圖前往的路徑 startActivity(intent); // 啟動意圖通往的活動頁面
隱式Intent還用到了過濾器的概念,即把不符合匹配條件的過濾掉,剩下符合條件的按照優先順序調用。創建一個Android工程,AndroidManifest.xml里的intent-filter就是XML中的過濾器。比如下面這個最常見的主頁面MainAcitivity,activity節點下面便設置了action和category的過濾條件。其中,android.intent.action.MAIN表示App的入口動作,android.intent.category.LAUNCHER表示在App啟動時調用。

3.5.3 向下一個Activity傳遞參數
前面講過,Intent的setData方法只指定到達目標的路徑,并非本次通信所攜帶的參數信息,真正的參數信息存放在Extras中。Intent重載了很多種putExtra方法傳遞各種類型的參數,包括String、int、double等基本數據類型,甚至Parcelable、Serializable等序列化結構。不過只是調用putExtra方法顯然不好管理,像送快遞一樣大小包裹隨便扔,不但找起來不方便,丟了也難以知道。所以Android引入了Bundle概念,可以把Bundle理解為超市的寄包柜或快遞收件柜,大小包裹由Bundle統一存取,方便又安全。
Bundle內部用于存放數據的實質結構是Map映射,可添加元素、刪除元素,還可判斷元素是否存在。開發者把Bundle全部打包好只需調用一次putExtras方法,把Bundle全部取出來也只需調用一次getExtras方法。
下面是前一個頁面向后一個頁面發送請求數據的代碼:
Intent intent = new Intent(MainActivity.this, FirstActivity.class); // 創建一個目標確定的意圖 Bundle bundle = new Bundle(); // 創建一個新包裹 bundle.putString("name", "張三"); // 往包裹存入一個字符串 bundle.putInt("age", 30); // 往包裹存入一個整型數 bundle.putDouble("height", 170.0f); // 往包裹存入一個雙精度數 intent.putExtras(bundle); // 把快遞包裹塞給意圖 startActivity(intent); // 啟動意圖所向往的活動頁面
下面是后一個頁面接收前一個頁面請求數據的代碼:
Intent intent = getIntent(); // 獲取前一個頁面傳來的意圖 Bundle bundle = intent.getExtras(); // 卸下意圖里的快遞包裹 String name = bundle.getString("name", ""); // 從包裹中取出字符串 int age = bundle.getInt("age", 0); // 從包裹中取出整型數 double height = bundle.getDouble("height", 0.0f); // 從包裹中取出雙精度數
3.5.4 向上一個Activity返回參數
如同一般的通信一樣,Intent有時只把請求數據發送到下一個頁面就行,有時還要處理下一個頁面的應答數據(通常發生在下一個頁面返回到上一個頁面時)。如果只把請求數據發送到下一個頁面,前一個頁面調用startActivity方法就可以;如果還要處理一下個頁面的應答數據,此時就得分多步處理,詳細步驟如下:
步驟01 前一個頁面打包好請求數據,調用方法startActivityForResult(Intent intent, int requestCode),表示需要處理結果數據,第二個參數表示請求編號,用于標識每次請求的唯一性。
步驟02 后一個頁面接收請求數據,進行相應處理。
步驟03 后一個頁面在返回前一個頁面時,打包應答數據并調用setResult方法返回信息。setResult的第一個參數表示應答代碼(成功還是失敗),代碼示例如下:
Intent intent = new Intent(); // 創建一個新意圖 Bundle bundle = new Bundle(); // 創建一個新包裹 bundle.putString("job", "碼農"); // 往包裹存入一個字符串 intent.putExtras(bundle); // 把快遞包裹塞給意圖 setResult(Activity.RESULT_OK, intent); // 攜帶意圖返回前一個頁面 finish(); // 關閉當前頁面
步驟04 前一個頁面重寫方法onActivityResult,該方法的輸入參數包含請求編號和應答代碼,請求編號用于判斷對應哪次請求,應答代碼用于判斷后一個頁面是否處理成功。然后對應答數據進行解包處理,代碼示例如下:

下面是完整的請求頁面代碼與應答頁面代碼,結合效果界面加深對Activity處理參數傳遞的理解。請求頁面的代碼示例如下:

應答頁面的代碼示例如下:

具體的效果圖分別如圖3-23、圖3-24、圖3-25所示。其中,圖3-23是當前頁面要向下一個頁面發送請求時的界面,圖3-24是下一個頁面準備返回上一個頁面時的界面,圖3-25是上一個頁面收到下一個頁面應答時的界面。

圖3-23 準備向下一個頁面發送請求

圖3-24 下一個頁面準備返回消息

圖3-25 上一個頁面收到返回消息