文章目錄
- 1.活動是什麼
- 2.活動的基本用法
- 2.1 手動建立活動
- 2.2 建立和加載布局
- 2.3 在AndroidManifest檔案中注冊
- 2.4 在活動中使用Toast
- 2.5 在活動中使用Menu
- 2.6 銷毀一個活動
- 3.使用Intent在活動之間穿梭
- 3.1 使用顯式Intent
- 3.2 使用隐式Intent
- 3.3 更多隐式Intent的用法
- 3.4 向下一個活動傳遞資料
- 3.5 傳回資料給上一個活動
- 4.活動的生命周期
- 4.1 傳回棧
- 4.2 活動狀态
- 4.3 活動的生存期
- 4.4 體驗活動的生命周期
- 4.5 活動被回收了怎麼辦
- 5.活動的啟動模式
- 5.1 standard
- 5.2 singleTop
- 5.3 singleTask
- 5.4 singleInstance
- 6.活動的最佳實作
- 6.1 知曉目前是在哪一個活動
- 6.2 随時随地退出程式
- 6.3 啟動活動的最佳寫法
1.活動是什麼
活動(Activity)是最容易吸引使用者的地方,它是一種可以包含使用者界面的元件,主要用于和使用者進行互動。一個應用程式中可以包含零個或多個活動,但不包含任何活動的應用程式很少見,誰也不想讓自己的應用永遠無法被使用者看到吧?
2.活動的基本用法
2.1 手動建立活動
建立活動
建立ActivityTest項目成功後(在Add an Activity to Mobile界面中不再選擇Empty Activity,而是選擇Add No Activity),仍然會預設使用Android模式的項目結構,這裡我們手動改成Project模式。目前ActivityTest項目中雖然還是會自動生成很多檔案,但是app/src/main/java/com.example.activitytest目錄應該是空的了,如下圖所示:

現在右擊com.example.activitytest包->New->Activity->Empty Activity,會彈出一個建立活動的對話框,我們将活動命名為FirstActivity,并且不要勾選Generate Layout File和Launcher Activity這兩個選項,如圖所示:
其中:
- 勾選Generate Layout File:表示會自動為FirstActivity建立一個對應的布局檔案。
- 勾選Launcher Activity:表示會自動将FirstActivity設定為目前項目的主活動。
- 勾選BackwardsCompatibility:表示會為項目啟用向下相容的模式。
你需要知道,項目中的任何活動都應該重寫Activity的onCreate()方法,而目前我們的FirstActivity中已經重寫了這個方法,這是由Android Studio自動幫我們完成的,代碼如下所示:
可以看到,onCreate()方法非常簡單,就是調用了父類的onCreate()方法(super.onCreate():直接通路并調用父類中的方法)。
2.2 建立和加載布局
建立布局
在項目的目錄中,右擊app/src/main/res目錄->New->Directory,會彈出一個建立目錄的視窗,這裡先建立一個名為layout的目錄。然後對着layout目錄右鍵->New->Layout resource file,又會彈出一個建立布局資源檔案的視窗,我們将這個布局檔案命名為first_layout,根元素就預設選擇為LinearLayout,如下圖所示:
點選OK完成布局的建立,這時候可以看到如下圖所示的布局編輯器:
這是Android Studio為我們提供的可視化布局編輯器,你可以在螢幕的中央區域浏覽目前的布局。在視窗的右下方有兩個切換卡,左邊是Design,右邊是Text。其中:
- Design:是目前的可視化布局編輯器,在這裡你不僅可以浏覽目前的布局,還可以通過拖放的方式編輯布局。
- Text:是通過XML檔案的方式來編輯布局的。
現在點選一下Text切換卡,可以看到如下代碼:
由于我們剛才在建立布局檔案時選擇了LinearLayout作為根元素,是以現在布局檔案中已經有一個LinearLayout元素了。我們現在對這個布局稍做編輯,添加一個按鈕,如下所示:
這裡添加了一個Button元素,并在Button元素的内部增加了幾個屬性,其中:
- android:id:給目前的元素定義一個唯一辨別符,之後可以在代碼中對這個元素進行操作。如果你需要在XML中引用一個id,就使用@id/id_name這種文法;而如果你需要在XML中定義一個id,就使用@+id/id_name這種文法。
- android:layout_width:指定了目前元素的寬度,這裡使用match_parent表示讓目前元素和父元素一樣寬。
- android:layout_height:指定了目前元素的高度,這裡使用wrap_content表示讓目前元素的高度隻要能剛好包含裡面的内容就行。
-
android:text:指定了元素中顯示的文字内容。
現在按鈕已經添加了,可以通過右側工具欄的Preview來浏覽一下目前布局,如下圖所示:
-
可以看到,按鈕已經成功顯示出來了,這樣一個簡單的布局就編寫完成了。接下來要做的,就是在活動中加載這個布局。
重新回到FirstActivity,在onCreate()方法中加入如下代碼:
- 可以看到,這裡調用了setContentView()方法來給目前的活動加載一個布局,而在setContenView()方法中,我們一般都會傳入一個布局檔案的id。項目中添加的任何資源都會在R檔案中生成一個相應的資源id,是以我們剛才建立的first_layout.xml布局的id現在應該是已經添加到R檔案中了。隻需要調用R.layout.first_layout就可以得到first_layout.xml布局的id,然後将這個值傳入setContentView()方法即可。
2.3 在AndroidManifest檔案中注冊
所有的活動都要在AndroidManifest.xml中進行注冊才能生效,而實際上,FirstActivity已經在AndroidManifest.xml中進行過注冊了,打開app/src/main/AndroidManifest.xml,代碼如下所示:
可以看到,活動的注冊聲明要放在
<application>
标簽内,這裡是通過
<activity>
标簽來對活動進行注冊的。每當活動被建立時,Android Studio會自動在AndroidManifest.xml完成活動的注冊。
在
<activity>
标簽中我們使用了android:name來指明具體注冊哪一個活動,這裡填入的.FirstActivity其實就是com.example.activitytest.FirstActivity的縮寫而已。由于在最外層的
<manifest>
标簽中已經通過package屬性指定了程式的包名是com.example.activitytest,是以在注冊活動時這一部分就可以省略了,直接使用.FirstActivity就足夠了。
不過,僅僅是這樣注冊了活動,我們的程式仍然是不能運作的,因為還沒有為程式配置主活動。也就是說,當程式運作起來的時候,不知道要首先啟動哪個活動。配置主活動的方法,就是在
<activity>
标簽的内部加入
<intent-filter>
标簽,并在這個标簽裡添加
<action android:name="android.intent.action.MAIN"/>
和
<category android:name="android.intent.category.LAUNCHER"/>
這兩句聲明即可。
除此之外,我們還可以使用android:label指定活動中标題欄的内容,标題欄是顯示在活動最頂部的,待會兒運作的時候就能看到。需要注意的是,給主活動指定的label不僅會成為标題欄中的内容,還會成為啟動器(Launcher)中應用程式顯示的名稱。
修改AndroidManifest.xml檔案,代碼如下所示:
這樣的話,FirstActivity就成為我們這個程式的主活動了,即點選桌面應用程式圖示時首先打開的就是這個活動。另外需要注意的是,如果你的應用程式沒有聲明任何一個活動作為主活動,這個程式仍然是可以正常安裝的,隻是你無法在啟動器中看到或打開這個程式。這種程式一般都是作為第三方服務供其他應用在内部進行調用的,例如支付寶快捷支付服務。
現在,運作一下程式,結果如下圖所示:
在界面的最頂部是一個标題欄,界面顯示着我們剛才在注冊活動時指定的内容。标題欄的下面就是在布局檔案first_layout.xml中編寫的界面,還可以看到我們剛剛定義的按鈕。
2.4 在活動中使用Toast
Toast是Android系統提供的一種非常好的提醒方式,在程式中可以使用它将一些短小的資訊通知給使用者,這些資訊會在一段時間後自動消失,并且不會占用任何螢幕空間,我們現在就試一下在活動中使用Toast。
首先需要定義一個彈出Toast的觸發點,正好界面上有個按鈕,那我們就讓點選這個按鈕的時候彈出一個Toast吧。在onCreate()方法中添加如下代碼:
在活動中,可以通過findViewById()方法擷取到在布局檔案中定義的元素,這裡我們傳入R.id.button_1,來得到按鈕的執行個體,這個值是剛才在first_layout.xml中通過android:id屬性指定的。findViewById()方法傳回的是一個View對象,我們需要向下轉型将它轉成Button對象。得到按鈕的執行個體之後,我們通過調用setOnClickListener()方法為按鈕注冊一個監聽器,點選按鈕時就會執行監聽器中的onClick()方法。是以,彈出Toast的功能當然是要在onClick()方法中編寫了。
Toast的用法非常簡單,通過靜态方法makeText()建立出一個Toast對象,然後調用show()将Toast顯示出來就可以了。這裡需要注意的是,makeText()方法需要傳入3個參數。第一個參數是Context,也就是Toast要求的上下文,由于活動本身就是一個Context對象,是以這裡直接傳入FirstActivity.this即可。第二個參數是Toast顯示的文本内容,第三個參數是Toast顯示的時長,有兩個内置常量可以選擇Toast.LENGTH_SHORT和Toast.LENGTH_LONG。
現在重新運作一下程式,并點選一下按鈕,效果如圖所示:
2.5 在活動中使用Menu
Android給我們提供了一種方式,可以讓菜單都能得到展示的同時,還能不占用任何螢幕空間。
首先在res目錄下建立一個menu檔案夾,右擊res目錄->New->Directory,輸入檔案夾名menu,點選OK。接着在這個檔案夾下再建立一個名叫main的菜單檔案,右擊menu檔案夾->New->Menu resource file ,如圖所示:
檔案名輸入main,點選OK完成建立。然後在main.xml中添加如下代碼:
這裡我們建立了兩個菜單項,其中标簽就是用來建立具體的某一個菜單項,然後通過android:id給這個菜單項指定一個唯一的辨別符,通過android:title給這個菜單項指定一個名稱。
接着重新回到FirstActivity中來重寫onCreateOptionsMenu()方法,重寫方法可以使用Ctrl+O快捷鍵,如圖所示:
然後在onCreateOptionMenu()方法中編寫如下代碼:
通過getMenuInflater()方法能夠得到MenuInflater對象,再調用它的inflate()方法就可以給目前活動建立菜單了。inflate()方法接受兩個參數,第一個參數用于指定我們通過哪一個資源檔案夾建立菜單,這裡當然傳入R.menu.main。第二個參數用于指定我們的菜單項将添加到哪一個Menu對象當中,這裡直接使用onCreateOptionMenu()方法中傳入的menu參數。然後給這個方法傳回true,表示允許建立的菜單顯示出來,如果傳回了false,建立的菜單将無法顯示。
僅僅讓菜單顯示出來是不夠的,我們定義菜單不僅是為了看的,關鍵是要菜單真正可用才行,是以還要再定義菜單響應事件。在FirstActivity中重寫onOptionsItemSelected()方法:
在onOptionsItemSelected()方法中,通過調用item.getItemId()來判斷我們點選的是哪一個菜單項,然後給每個菜單項加入自己的邏輯處理,這裡就活學活用,彈出一個Toast。
重新運作程式,可以看到在标題欄的右側多了一個三點的符号,這個就是菜單按鈕,效果如圖所示:
2.6 銷毀一個活動
想要銷毀一個活動,除了按下Back鍵之外,Activity類提供了一個finish()方法,我們在活動中調用一下這個方法就可以銷毀目前活動了。
修改監聽器的方法,如圖所示:
重新運作程式,點選一下按鈕,目前的活動就會被成功摧毀,這跟按下Back鍵是一個效果
3.使用Intent在活動之間穿梭
3.1 使用顯式Intent
建立一個新的項目,選擇Empty Activity,但是在建立布局時不要勾選Launcher Activity選項。
建立一個名為SecondActivity的活動,并且讓其自動生成布局檔案activity_second.xml,随後修改其布局檔案,代碼如圖所示:
在布局中添加一個按鈕,按鈕上顯示Button 2。
之後,不用修改SecondActivity.java中的代碼,保持預設即可。
接下來,需要在AndroidManifest.xml中注冊活動,不過AndroidStudio已經幫我們自動完成了,代碼如圖所示:
由于SencondActivity不是主活動,是以不需要配置标簽裡的内容,注冊活動的代碼也變得簡單了很多。為了從第一個活動跳轉到第二個活動,這裡需要引入一個新的概念:Intent
Intent是Android程式中各元件之間進行互動的一種重要方式,它不僅可以指明目前元件想要執行的動作,還可以在不同元件之間傳遞資料。Intent一般可被用于啟動活動、啟動服務以及發送廣播等場景。
Intent分為兩種:顯示Intent和隐式Intent,這裡就先來看一下顯式Intent如何使用。
Intent有多個構造函數的重載,其中一個是
Intent(Context packageContetxt,Class<?> cls)
這個構造函數接受兩個參數,第一個參數Context要求提供一個啟動活動的上下文,第二個參數Class則是指定想要啟動的目标活動,通過這個構造函數就可以建構出Intent的“意圖”
那麼我們應該怎麼使用這個Intent呢?Activity類中提供了一個StartActivity()方法,這個方法是專門用于啟動活動的,它接收一個Intent參數,這裡将建構好的Intent傳入StartActivity()方法就可以啟動目标活動了。
修改MainActivity中按鈕的點選事件,代碼如圖所示:
這裡建構出了一個Intent,傳入MainActivity.this作為上下文,傳入SecondActivity.class作為目标活動,這樣的話,就可以在MainActivity這個活動的基礎上打開SecondActivity這個活動,然後通過startActivity()方法來執行這個Intent。
運作程式,點選按鈕,就可以從第一個活動跳轉到第二個活動了。如果想要傳回上一個活動,隻需按下Back鍵銷毀目前活動,進而就能回到上一個活動了。
3.2 使用隐式Intent
相比于顯式Intent,隐式Intent則含蓄了許多,它并不明确指出我們想要去啟動哪一個活動,而是指定了一系列更為抽象的action和category等資訊,然後交由系統去分析這個Intent并幫我們找出合适的活動去啟動。
通過在
<activity>
标簽下配置
<intent-filter>
的内容,可以指定目前活動能夠響應action和category,修改AndroidManifest.xml中的代碼,如圖所示:
在
<action>
标簽中我們指明了目前活動可以響應com.mxt.firstandroidapplication.ACTION_START這個action,而
<category>
标簽則包含了一些附加資訊,更精确地指明了目前的活動能夠響應的Intent中還可能帶有的category。隻有
<action>
和
<category>
中的内容同時能夠比對上Intent中指定的action和category時,這個活動才能夠響應該Intent。
修改MainActivity中按鈕的點選事件,代碼如圖所示:
可以看到,這裡使用了Intent的另一個函數,直接将action的字元串傳了進去,表明我們想要啟動能夠響應com.mxt.firstandroidapplication.ACTION_START這個action的活動。之前有提到過隻有
<action>
和
<category>
中的内容同時能夠比對上Intent中指定的action和category時才能響應,而這裡因為android.intent.category.DEFAULT是一種預設的category,在調用startActivity()方法的時候會自動将這個category添加到Intent中。
重新運作程式,可以發現跟之前調用顯式Intent一樣,發生了活動的跳轉。不同的是,這裡使用的隐式Intent的方式來啟動的。
每個Intent中隻能指定一個action,但卻能指定多個category,面前我們的Intent中隻有一個預設的category,現在就來再增加一個,修改按鈕的點選事件,如圖所示:
可以調用Intent中的addCategory()方法來添加一個category,這裡我們指定了一個自定義的category,值為com.mxt.firstandroidapplication.MY_CATEGORY
重新運作程式,點選一下按鈕,就會發現程式崩潰了,如圖所示:
觀察日志台,檢視錯誤日志,可以看出發生崩潰的原因是沒有任何一個活動能夠相應我們的Intent。因為剛剛在Intent中新增了一個category,而SecondActivity的
<intent-filter>
标簽中并沒有聲明可以相應這個category,是以就出現了沒有任何活動可以響應該Intent的情況,在
<intent-filter>
中再添加一個category的聲明,代碼如圖所示:
重新運作程式,一切正常。
3.3 更多隐式Intent的用法
通過上一節的學習,我們掌握了隐式Intent來啟動活動的方法,但實際上隐式Intent還有更多的内容需要你去了解,本節就來介紹一下。
使用隐式Intent,我們不僅可以啟動自己程式内的活動,還可以啟動其他程式的活動,這使得Android多個應用程式之間的功能共享成為了可能。
例如,在應用程式中展示一個網頁,不需要去實作一個浏覽器,隻需要調用系統的浏覽器打開這個網頁即可,修改按鈕點選事件的代碼,如圖所示:
這裡指定了Intent的action是Intent.ACTION_VIEW,這是一個Andorid系統内置的動作,其常量值為android.intent.action.VIEW,然後通過Uri.parse()方法,将一個網址字元串解析成一個Uri對象,再調用Intent的setData()方法将這個Uri對象傳遞進去。
運作程式,效果如下所示:
與此對應,我們還可以在
<intent-filter>
标簽中再配置一個
<data>
标簽,用于更精确地指定目前活動能夠響應什麼類型的資料。
<data>
标簽中主要可以配置以下内容:
- android:scheme:用于指定資料的協定部分,如上例中的http部分
- android:host:用于指定資料的主機名部分,如上例中的www.baidu.com部分
- android:port:用于指定資料的端口部分,一般緊随在主機名之後
- android:path:用于指定主機名和端口之後的部分,如一段網址中跟在域名之後的内容
- android:mimetype:用于指定可以處理的資料類型,允許使用通配符的方式進行指定
隻有<data>标簽中指定的内容和Intent中攜帶的Data完全一緻時,目前活動才能夠響應該Intent。不過一般在<data>标簽中都不會指定過多的内容,如上面浏覽器示例中,其實隻需要指定android:scheme為http,就可以響應所有的http協定的Intent 了。
為了讓你能夠更加直覺地了解,我們來自己建立一個活動,讓它也能響應打開網頁的Intent。
建立一個名為ThirdActivity的活動,在其預設布局中添加一個按鈕,如圖所示:
ThirdActivity.java代碼不用改變,隻需要在AndroidManifest.xml中修改注冊資訊即可,代碼如下:
我們在ThirdActivity的中配置了目前活動能夠響應的action是Intent. ACTION_VIEW的常量值,而category則毫無疑問指定了預設的category值,另外在 标簽中我們通過android:scheme指定了資料的協定必須是http協定,這樣ThirdActivity應該就和浏覽器一樣,能夠響應一個打開網頁的Intent 了。運作程式,點選按鈕,效果如圖:
可以看到,系統自動彈出了一個清單,顯示了目前能夠響應這個Intent的所有程式。選擇 Browser還會像之前一樣打開浏覽器,并顯示百度的首頁,而如果選擇了 ActivityTest,則會啟動 ThirdActivity。 JUST ONCE表示隻是這次使用選擇的程式打開,ALWAYS則表示以後一直都使用 這次選擇的程式打開。需要注意的是,雖然我們聲明了 ThirdActivity是可以響應打開網頁的Intent 的,但實際上這個活動并沒有加載并顯示網頁的功能,是以在真正的項目中盡量不要岀現這種有 可能誤導使用者的行為,不然會讓使用者對我們的應用産生負面的印象。
除了 http協定外,我們還可以指定很多其他協定,比如geo表示顯示地理位置、tel表示撥打 電話。下面的代碼展示了如何在我們的程式中調用系統撥号界面:
首先指定了 Intent的action是Intent .ACTION_DIAL,這又是一個Android系統的内置動 作。然後在data部分指定了協定是tel,号碼是10086。重新運作一下程式,在MainActivity的界 面點選一下按鈕,運作程式,結果如圖所示:
3.4 向下一個活動傳遞資料
經過前面幾節的學習,我們已經對Intent有了一定的了解。不過到目前為止,我們都隻是簡單地使用Intent來啟動一個活動,其實Intent還可以在啟動活動的時候傳遞資料,下面我們來一起看一下。
在啟動活動時傳遞資料的思路很簡單,Intent中提供了一系列putExtra()方法的重載,可以把我們想要傳遞的資料暫存在Intent中,啟動了另一個活動後,隻需要把這些資料再從Intent 中取岀就可以了。比如說MainActivity中有一個字元串,現在想把這個字元串傳遞到Second-Activity 中,你就可以這樣編寫:
這裡我們還是使用顯式Intent的方式來啟動SecondActivity,并通過putExtra()方法傳遞了 一個字元串。注意這裡putExtra()方法接收兩個參數,第一個參數是鍵,用于後面從Intent中取值,第二個參數才是真正要傳遞的資料。
然後我們在SecondActivity中将傳遞的資料取出,并列印出來,代碼如下所示:
首先可以通過getlntent()方法擷取到用于啟動SecondActivity的Intent,然後調用 getStringExtra()方法,傳入相應的鍵值,就可以得到傳遞的資料了。這裡由于我們傳遞的是字元串,是以使用getStringExtra()方法來擷取傳遞的資料。如果傳遞的是整型資料,則 使用getIntExtra()方法;如果傳遞的是布爾型資料,則使用getBooleanExtra()方法,以此類推。
重新運作程式,在MainActivity的界面點選一下按鈕會跳轉到SecondActivity,檢視日志列印資訊,如圖所示:
可以看到,我們在SecondActivity中成功得到了從MainActivity傳遞過來的資料。
3.5 傳回資料給上一個活動
既然可以傳遞資料給下一個活動,那麼能不能夠傳回資料給上一個活動呢?答案是肯定的。 不過不同的是,傳回上一個活動隻需要按一下Back鍵就可以了,并沒有一個用于啟動活動Intent來傳遞資料。通過查閱文檔你會發現,Activity中還有一個startActivityForResult ()方法也 是用于啟動活動的,但這個方法期望在活動銷毀的時候能夠傳回一個結果給上一個活動。毫無疑 問,這就是我們所需要的。
startActivityForResult ()方法接收兩個參數,第一個參數還是Intent,第二個參數是請求碼,用于在之後的回調中判斷資料的來源。我們還是來實戰一下,修改M愛你Activity中按鈕的 點選事件,代碼如圖所示:
這裡我們使用了 startActivityForResult()方法來啟動SecondActivity,請求碼隻要是一 個唯一值就可以了,這裡傳入了 l。接下來我們在SecondActivity中給按鈕注冊點選事件,并在 點選事件中添加傳回資料的邏輯,代碼如圖所示:
可以看到,我們還是建構了一個Intent,隻不過這個Intent僅僅是用于傳遞資料而已,它沒有指定任何的“意圖”。緊接着把要傳遞的資料存放在Intent中,然後調用了setResult()方法。這個方法非常重要,是專門用于向上一個活動傳回資料的。setResult()方法接收兩個參數, 第一個參數用于向上一個活動傳回處理結果,一般隻使用RESULT_0K或RESULT_CANCELED這兩個值,第二個參數則把帶有資料的Intent傳遞回去,然後調用了 finish(()方法來銷毀目前活動。
由于我們是使用 startActivityForResult()方法來啟動 SecondActivity 的,在 SecondActivity 被銷毀之後會回調上一個活動的onActivityResult()方法,是以我們需要在MainActivity中重寫這個方法來得到傳回的資料,如下所示:
onActivityResult()方法帶有三個參數,第一個參數requestcode,即我們在啟動活動時傳入的請求碼。第二個參數resultCode,即我們在傳回資料時傳入的處理結果。第三個參數 data,即攜帶着傳回資料的Intent。由于在一個活動中有可能調用startActivityForResult()方法去啟動很多不同的活動,每一個活動傳回的資料都會回調到onActivityResult()這個方法中,是以我們首先要做的就是通過檢查requestcode的值來判斷資料來源。确定資料是從 SecondActivity傳回的之後,我們再通過resultCode的值來判斷處理結果是否成功。最後從data 中取值并列印出來,這樣就完成了向上一個活動傳回資料的工作。
重新運作程式,在MainActivity的界面點選按鈕會打開SecondActivity,然後在SecondActivity 界面點選Button 2按鈕會回到MainActivity,這時檢視logcat的列印資訊,結果如圖所示:
可以看到,SecondActivity已經成功傳回資料給FirstActivity 了。
這時候你可能會問,如果使用者在SecondActivity中并不是通過點選按鈕,而是通過按下Back 鍵回到MainActivity,這樣資料不就沒法傳回了嗎?沒錯,不過這種情況還是很好處理的,我們 可以通過在SecondActivity中重寫onBackPressed ()方法來解決這個問題,代碼如圖所示:
這樣的話,當使用者按下Back鍵,就會去執行onBackPressedO方法中的代碼,我們在這裡添加傳回資料的邏輯就行了。
4.活動的生命周期
4.1 傳回棧
掌握活動的生命周期對任何Android開發者來說都非常重要,當你深入了解活動的生命周期之後,就可以寫出更加連貫流暢的程式,并在如何合理管理應用資源方面發揮得遊刃有餘。你的 應用程式将會擁有更好的使用者體驗。
經過前面幾節的學習,我相信你已經發現了這一點,Android中的活動是可以層疊的。我們每啟動一個新的活動,就會覆寫在原活動之上,然後點選Back鍵會銷毀最上面的活動,下面的一個活動就會重新顯示出來。
其實Android是使用任務(Task)來管理活動的,一個任務就是一組存放在棧裡的活動的集合,這個棧也被稱作傳回棧(Back Stack )。棧是一種後進先出的資料結構,在預設情況下,每當我們啟動了一個新的活動,它會在傳回棧中入棧,并處于棧頂的位置。而每當我們按下Back鍵或調用finish()方法去銷毀一個活動時,處于棧頂的活動會出棧,這時前一個入棧的活動就會重新處于棧頂的位置。系統總是會顯示處于棧頂的活動給使用者。
示意圖如下所示:
4.2 活動狀态
每個活動在其生命周期中最多可能會有4種狀态。
-
運作狀态
當一個活動位于傳回棧的棧頂時,這時活動就處于運作狀态。系統最不願意回收的就是處于 運作狀态的活動,因為這會帶來非常差的使用者體驗。
-
暫停狀态
當一個活動不再處于棧頂位置,但仍然可見時,這時活動就進入了暫停狀态。你可能會覺得既然活動已經不在棧頂了,還怎麼會可見呢?這是因為并不是每一個活動都會占滿整個螢幕的, 比如對話框形式的活動隻會占用螢幕中間的部分區域,你很快就會在後面看到這種活動。處于暫停狀态的活動仍然是完全存活着的,系統也不願意去回收這種活動(因為它還是可見的,回收可見的東西都會在使用者體驗方面有不好的影響),隻有在記憶體極低的情況下,系統才會去考慮回收這種活動。
-
停止狀态
當一個活動不再處于棧頂位置,并且完全不可見的時候,就進入了停止狀态。系統仍然會為這種活動儲存相應的狀态和成員變量,但是這并不是完全可靠的,當其他地方需要記憶體時,處于停止狀态的活動有可能會被系統回收。
-
銷毀狀态
當一個活動從傳回棧中移除後就變成了銷毀狀态。系統會最傾向于回收處于這種狀态的活動,進而保證手機的記憶體充足。
4.3 活動的生存期
Activity類中定義了 7個回調方法,覆寫了活動生命周期的每一個環節,下面就來一一介紹 這7個方法。
- onCreate()。這個方法你已經看到過很多次了,每個活動中我們都重寫了這個方法,它會在活動第一次被建立的時候調用。你應該在這個方法中完成活動的初始化操作,比如說加載布局、綁定事件等。
- onStart()。這個方法在活動由不可見變為可見的時候調用。
- onResume()。這個方法在活動準備好和使用者進行互動的時候調用。此時的活動一定位于傳回棧的棧頂,并且處于運作狀态。
- onPause()。這個方法在系統準備去啟動或者恢複另一個活動的時候調用。我們通常會在這個方法中将一些消耗CPU的資源釋放掉,以及儲存一些關鍵資料,但這個方法的執行速度一定要快,不然會影響到新的棧頂活動的使用。
- onStop()。這個方法在活動完全不可見的時候調用。它和onPause()方法的主要差別在 于,如果啟動的新活動是一個對話框式的活動,那麼onPause()方法會得到執行,而 onStop()方法并不會執行。
- onDestroy()。這個方法在活動被銷毀之前調用,之後活動的狀态将變為銷毀狀态。
- onRestart()。這個方法在活動由停止狀态變為運作狀态之前調用,也就是活動被重新啟動了。
以上7個方法中除了 onRestart()方法,其他都是兩兩相對的,進而又可以将活動分為3種生存期:
- 完整生存期。活動在onCreate()方法和onDestroy()方法之間所經曆的,就是完整生存期。一般情況下,一個活動會在onCreate()方法中完成各種初始化操作,而在 onDestroy()方法中完成釋放記憶體的操作。
- 可見生存期。活動在onStart()方法和onStop()方法之間所經曆的,就是可見生存期。 在可見生存期内,活動對于使用者總是可見的,即便有可能無法和使用者進行互動。我們可以通過這兩個方法,合理地管理那些對使用者可見的資源。比如在onStart()方法中對資源進行加載,而在onStop()方法中對資源進行釋放,進而保證處于停止狀态的活動不會占用過多記憶體。
- 前台生存期。活動在onResume()方法和onPause()方法之間所經曆的就是前台生存期。 在前台生存期内,活動總是處于運作狀态的,此時的活動是可以和使用者進行互動的,我 們平時看到和接觸最多的也就是這個狀态下的活動。
為了幫助你能夠更好地了解,Android官方提供了一張活動生命周期的示意圖,如圖所示:
4.4 體驗活動的生命周期
這段示例代碼過長,建議參考《第一行代碼 Android》的原文說明,這裡隻作簡單說明。
假設A是主活動,B是普通活動,C是對話框式活動(即隻會浮現出一個對話框,不會跳轉到新頁面)。A活動裡有兩個按鈕,分别可以進入B和C活動。假設A活動裡重寫了上一節提到的7個與活動的生命周期有關的回調方法,并且均加入了日志輸出語句。
- 啟動程式,自動跳轉到A活動,會調用onCreate、onStart、onResume方法(A活處于運作狀态)
- 從A活動中點選第一個按鈕,進入B活動,會調用onPause、onStop方法(A活動處于停止狀态)
- 在B活動中按下Back鍵,退回A活動,會調用onRestart、onStart、onResume方法(A活動處于運作狀态)
- 從A活動中點選第二個按鈕,進入C活動,會調用onPause方法(A活動處于暫停狀态)
- 在C活動中按下Back鍵,退回A活動,會調用onResume方法(A活動處于運作狀态)
- 在A活動中按下Back鍵,退出應用,會調用onPause、onStop、onDestroy方法(A活動處于銷毀狀态)
4.5 活動被回收了怎麼辦
前面我們已經說過,當一個活動進入到了停止狀态,是有可能被系統回收的。那麼想象以下場景:應用中有一個活動A,使用者在活動A的基礎上啟動了活動B,活動A就進入了停止狀态, 這個時候由于系統記憶體不足,将活動A回收掉了,然後使用者按下Back鍵傳回活動A,會出現什麼情況呢?其實還是會正常顯示活動A的,隻不過這時并不會執行,onRestart()方法,而是會 執行活動A的onCreate()方法,因為活動A在這種情況下會被重新建立一次。
這樣看上去好像一切正常,可是别忽略了一個重要問題,活動A中是可能存在臨時資料和狀态的。打個比方,MainActivity中有一個文本輸入框,現在你輸入了一段文字,然後啟動 NormalActivity,這時MainActivity由于系統記憶體不足被回收掉,過了一會你又點選了 Back鍵回到MainActivity,你會發現剛剛輸入的文字全部都沒了,因為MainActivity被重新建立了。
如果我們的應用出現了這種情況,是會嚴重影響使用者體驗的,是以必須要想想辦法解決這個問題。查閱文檔可以看出,Activity中還提供了一個onSaveInstanceState()回調方法,這個方法可以保證在活動被回收之前一定會被調用,是以我們可以通過這個方法來解決活動被回收時臨 時資料得不到儲存的問題。
onSaveInstanceState()方法會攜帶一個Bundle類型的參數,Bundle提供了一系列的方法用于儲存資料,比如可以使用putString()方法儲存字元串,使用putlnt()方法儲存整型數 據,以此類推。每個儲存方法需要傳入兩個參數,第一個參數是鍵,用于後面從Bundle中取值,第二個參數是真正要儲存的内容。
在MainActivity中添加如下代碼就可以将臨時資料進行儲存:
資料是已經儲存下來了,那麼我們應該在哪裡進行恢複呢?細心的你也許早就發現,我們一直使用的onCreate()方法其實也有一個Bundle類型的參數。這個參數在一般情況下都是null, 但是如果在活動被系統回收之前有通過onSaveInstanceState()方法來儲存資料的話,這個參數就會帶有之前所儲存的全部資料,我們隻需要再通過相應的取值方法将資料取出即可。
修改MainActivity的onCreate ()方法,如下所示:
取出值之後再做相應的恢複操作就可以了,比如說将文本内容重新指派到文本輸入框上,這裡我們隻是簡單地列印一下。
不知道你有沒有察覺,使用Bundle來儲存和取出資料是不是有些似曾相識呢?沒錯!我們在使用Intent傳遞資料時也是用的類似的方法。這裡跟你提醒一點,Intent還可以結合Bundle 一起用于傳遞資料,首先可以把需要傳遞的資料都儲存在Bundle對象中,然後再将Bundle對象存放在Intent裡。到了目标活動之後先從Intent中取出Bundle,再從Bundle中一一取出資料。
5.活動的啟動模式
在實際項目中,我們應該根據特定的需求為每個活動指定恰當的啟動模式。啟動模式一共有4種,分别是standard,singleTop,singleTask和 singlelnstance,可以在 AndroidManifest.xml 中通過給
<activity>
标簽指定 android: launchMode 屬性來選擇啟動模式。下面我們來逐個進行學習。
5.1 standard
standard是活動預設的啟動模式,在不進行顯式指定的情況下,所有活動都會自動使用這種啟動模式。是以,到目前為止我們寫過的所有活動都是使用的standard模式。經過上一節的學習, 你已經知道了Android是使用傳回棧來管理活動的,在standard模式(即預設情況)下,每當啟動一個新的活動,它就會在傳回棧中入棧,并處于棧頂的位置。對于使用standard模式的活動, 系統不會在乎這個活動是否已經在傳回棧中存在,每次啟動都會建立該活動的一個新的執行個體。
5.2 singleTop
可能在有些情況下,你會覺得standard模式不太合理。活動明明已經在棧頂了,為什麼再次啟動的時候還要建立一個新的活動執行個體呢?别着急,這隻是系統預設的一種啟動模式而已,你完 全可以根據自己的需要進行修改,比如說使用singleTop模式。當活動的啟動模式指定為 singleTop,在啟動活動時如果發現傳回棧的棧頂已經是該活動,則認為可以直接使用它,不會再建立新的活動執行個體。
5.3 singleTask
用singleTop模式可以很好地解決重複建立棧頂活動的問題,但是正如你在上一節所看到 的,如果該活動并沒有處于棧頂的位置,還是可能會建立多個活動執行個體的。那麼有沒有什麼辦法可以讓某個活動在整個應用程式的上下文中隻存在一個執行個體呢?這就要借助singleTask模式來實 現了。當活動的啟動模式指定為singleTask,每次啟動該活動時系統首先會在傳回棧中檢查是否存在該活動的執行個體,如果發現已經存在則直接使用該執行個體,并把在這個活動之上的所有活動統統 出棧,如果沒有發現就會建立一個新的活動執行個體。
5.4 singleInstance
singlelnstance模式應該算是4種啟動模式中最特殊也最複雜的一個了,你也需要多花點功夫來了解這個模式。不同于以上3種啟動模式,指定為singlelnstance模式的活動會啟用一個新的傳回棧來管理這個活動(其實如果singleTask模式指定了不同的taskAffinity,也會啟動一個新的返 回棧)。那麼這樣做有什麼意義呢?想象以下場景,假設我們的程式中有一個活動是允許其他程 序調用的,如果我們想實作其他程式和我們的程式可以共享這個活動的執行個體,應該如何實作呢? 使用前面3種啟動模式肯定是做不到的,因為每個應用程式都會有自己的傳回棧,同一個活動在 不同的傳回棧中入棧時必然是建立了新的執行個體。而使用singlelnstance模式就可以解決這個問題, 在這種模式下會有一個單獨的傳回棧來管理這個活動,不管是哪個應用程式來通路這個活動,都 共用的同一個傳回棧,也就解決了共享活動執行個體的問題。
6.活動的最佳實作
6.1 知曉目前是在哪一個活動
這個技巧将教會你如何根據程式目前的界面就能判斷出這是哪一個活動。可能你會覺得挺納 悶的,我自己寫的代碼怎麼會不知道這是哪一個活動呢?很不幸的是,在你真正進入到企業之後, 更有可能的是接手一份别人寫的代碼,因為你剛進公司就正好有一個新項目啟動的機率并不高。 閱讀别人的代碼時有一個很頭疼的問題,就是當你需要在某個界面上修改一些非常簡單的東西 時,卻半天找不到這個界面對應的活動是哪一個。學會了本節的技巧之後,這對你來說就再也不是難題了。
建立一個普通的類BaseActivity,并在其onCreate()方法中加入這行代碼:
Log.d("BaseActivity", getClass().getSimpleName());
需要讓BaseActivity成為該項目中所有活動的父類。修改所有活動的繼承結構,讓它們不再繼承自 AppCompatActivity, 而是繼承自BaseActivity。而由于BaseActivity又是繼承自AppCompatActivity的,是以項目中所有活動的現有功能并不受影響,它們仍然完全繼承了 Activity中的所有特性。
現在,每當我們進入到一個活動的界面,該活動的類名就會被列印出來(在logcat中),這樣我們就可以時時刻刻知曉目前界面對應的是哪一個活動了。
6.2 随時随地退出程式
如果目前你手機的界面還停留在一些深層活動中,你會發現目前想退出程式是非常不友善的, 可能需要連按多次Back鍵才行。按Home鍵隻是把程式挂起,并沒有退出程式。其實這個問題就足以引起你的思考,如果我們的程式需要一個登出或者退出的功能該怎麼辦呢?必須要有一個随時随地都能退出程式的方案才行。
其實解決思路也很簡單,隻需要用一個專門的集合類對所有的活動進行管理就可以了,下面就來實作一下。
在活動管理器中,我們通過一個List來暫存活動,然後提供了一個addActivity()方法用 于向List中添加一個活動,提供了一個removeActivity()方法用于從List中移除活動,最後提供了一個finishAll()方法用于将List中存儲的活動全部銷毀掉。
接下來修改BaseActivity中的代碼,如下所示:
在 BaseActivity 的 onCreate()方法中調用了 ActivityCollector 的 addActivity()方法,表明将目前正在建立的活動添加到活動管理器裡。然後在BaseActivity中重寫onDestroy()方法,并調用了 ActivityCollector的removeActivity()方法,表明将一個馬上要銷毀的活 動從活動管理器裡移除。
從此以後,不管你想在什麼地方退出程式,隻需要調用ActivityCollector. finishAll ()方法就可以了。
當然你還可以在銷毀所有活動的代碼後面再加上殺掉目前程序的代碼,以保證程式完全退出,殺掉程序的代碼如下所示:
android.os.Process.killProcess(android.os.Process.myPid());