活動是 Android 應用程式的基本建置組塊,而且可以存在於許多不同的狀態。 活動生命週期從具現化開始,並以解構結束,並包含介於 兩者之間的許多狀態。 當活動變更狀態時,會呼叫適當的生命週期事件方法,通知即將發生的狀態變更活動,並允許它執行程式代碼以適應該變更。 本文會檢查活動的生命週期,並說明活動在每個狀態變更期間所承擔的責任,以成為行為良好、可靠應用程式的一部分。
活動生命週期概觀
活動是 Android 特有的特殊程式設計概念。 在傳統應用程式開發中,通常會執行靜態主要方法來啟動應用程式。 不過,使用Android時,情況會有所不同;Android 應用程式可以透過應用程式內的任何已註冊活動啟動。 實際上,大部分的應用程式只會有指定為應用程式進入點的特定活動。 不過,如果應用程式當機,或由OS終止,則OS可以嘗試在上一個開啟的活動或上一個活動堆疊內的任何其他位置重新啟動應用程式。 此外,作業系統可能會在活動不在作用中時暫停它們,並在記憶體不足時將其回收。 必須仔細考慮,讓應用程式在重新啟動活動時正確地還原其狀態,特別是如果該活動相依於先前活動的數據時。
活動生命週期會實作為整個活動生命週期中OS呼叫的方法集合。 這些方法可讓開發人員實作滿足其應用程式狀態和資源管理需求所需的功能。
應用程式開發人員必須分析每個活動的需求,以判斷活動生命週期所公開的方法必須實作。 若無法這麼做,可能會導致應用程式不穩定、損毀、資源膨脹,甚至可能導致基礎OS不穩定。
本章會詳細檢查活動生命週期,包括:
- 活動狀態
- 生命週期方法
- 保留應用程式的狀態
本節也包含逐步指南,提供在活動生命周期期間如何有效率地儲存狀態的實用範例。 在本章結束時,您應該了解活動生命週期,以及如何在Android應用程式中加以支援。
活動生命週期
Android 活動生命週期包含活動類別中公開的方法集合,提供開發人員資源管理架構。 此架構可讓開發人員符合應用程式內每個活動的唯一狀態管理需求,並正確地處理資源管理。
活動狀態
Android OS 會根據其狀態來仲裁活動。 這有助於Android識別不再使用的活動,讓OS回收記憶體和資源。 下圖說明活動在其存留期內可經歷的狀態:
這些狀態可以分成 4 個主要群組,如下所示:
使用中或執行 – 若活動位於前景,則視為使用中或正在執行;也稱為活動堆疊的頂端。 這被視為 Android 中優先順序最高的活動,因此只有在極端情況下,OS 才會終止,例如,如果活動嘗試使用比裝置上可用的記憶體還多,這可能會導致 UI 沒有回應。
已暫停 – 當裝置進入睡眠狀態,或活動仍然可見,但被新出現的非全螢幕大小或透明的活動部分遮住時,該活動會被視為已暫停。 暫停的活動仍然在運行,也就是說,它們會維護所有狀態和成員資訊,並仍然附加在視窗管理器中。 這被視為 Android 中的第二高優先順序活動,因此,只有在終止此活動時,OS 才會終止此活動以滿足保持作用中/執行中活動穩定且回應所需的資源需求。
已停止/背景 – 被其他活動完全遮蔽的活動被視為已停止或處於背景狀態。 已停止的活動仍會盡量保留其狀態和成員資訊,但已停止的活動會被視為三個狀態的最低優先順序,因此,OS 會先終止處於此狀態的活動,以符合較高優先順序活動的資源需求。
重新啟動 – 處於暫停到停止生命週期範圍內的活動可能會被 Android 從記憶體中移除。 如果使用者導覽回到該活動,必須重新啟動活動,將其還原至先前儲存的狀態,然後顯示給使用者。
對配置變更的回應活動 Re-Creation
為了讓事情變得更複雜,Android 又增加了一個名為設定變更的麻煩。 配置變更是快速的活動解除和重建循環,當活動的配置發生變化時,例如當裝置旋轉時(活動需要重新建置於橫向或縱向模式)、顯示鍵盤時(活動有機會自行重新調整大小),或當裝置放置在擴充座時等情況。
設定變更仍然會導致在停止和重新啟動活動期間發生的相同活動狀態變更。 不過,為了確保應用程式在設定變更期間有回應且執行良好,請務必儘快處理它們。 因此,Android 有一個特定的 API,可用來在設定變更期間保存狀態。
我們將在稍後的<
活動生命週期方法
Android SDK 和 Xamarin.Android 架構會提供強大的模型來管理應用程式內的活動狀態。 當活動的狀態變更時,作業系統會通知活動,並呼叫該活動的特定方法。 下圖說明與活動生命週期相關的這些方法:
身為開發人員,您可以覆寫活動內的這些方法來處理狀態變更。 不過,請務必注意,所有生命週期方法都會在UI線程上呼叫,並且會封鎖OS執行下一段UI工作,例如隱藏目前活動、顯示新活動等。因此,這些方法中的程式代碼應該盡可能簡短,讓應用程式執行良好。 任何長時間執行的工作都應該在背景線程上執行。
讓我們檢查這些生命週期方法及其使用方式:
OnCreate
OnCreate 是建立活動時要呼叫的第一個方法。
OnCreate 一律會覆寫以執行活動可能需要的任何啟動初始化,例如:
- 建立檢視
- 初始化變數
- 將靜態數據系結至清單
OnCreate 會採用 Bundle 參數,這是一個用來在活動之間儲存和傳遞狀態資訊和物件的字典。如果該字典不是空值,這表示活動正在重啟,並且應該從上一個實例恢復其狀態。 下列程式代碼說明如何從套件組合擷取值:
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
string intentString;
bool intentBool;
if (bundle != null)
{
intentString = bundle.GetString("myString");
intentBool = bundle.GetBoolean("myBool");
}
// Set our view from the "main" layout resource
SetContentView(Resource.Layout.Main);
}
OnCreate完成後,Android 會呼叫 OnStart。
OnStart
在OnCreate 完成之後,系統一律會呼叫 OnStart。 活動如果需要在變得可見之前執行特定任務,例如刷新活動內的檢視目前值,可能會覆寫此方法。 Android 會在此方法之後立即呼叫 OnResume 。
OnResume
當活動準備好開始與用戶互動時,系統會呼叫 OnResume 。 活動元件應該覆寫此方法以執行下列工作:
- 提高幀速率(遊戲開發的常見工作)
- 啟動動畫
- 監聽 GPS 更新
- 顯示任何相關的警示或對話框
- 連接外部事件處理程式
例如,下列代碼段示範如何初始化相機:
protected override void OnResume()
{
base.OnResume(); // Always call the superclass first.
if (_camera==null)
{
// Do camera initializations here
}
}
OnResume 很重要,因為在 OnPause 中完成的任何作業都應該在 OnResume 中取消,因為這是將活動重新啟動後保證在 OnPause 之後執行的唯一生命週期方法。
OnPause
當系統即將使活動進入背景或活動部分被遮擋時,就會呼叫 OnPause。 如果活動需要覆寫此方法,應該這樣做:
將未儲存的變更提交至永久性資料
銷毀或清除其他耗用資源的物件
降低幀速率和暫停動畫
取消註冊外部事件處理程式或通知處理程式(亦即系結至服務的事件處理程式)。 這必須完成才能防止 Activity 記憶體洩漏。
同樣地,如果活動已顯示任何對話框或警示,則必須使用
.Dismiss()方法加以清除。
例如,下列代碼段會釋放相機,因為活動無法在暫停時使用它:
protected override void OnPause()
{
base.OnPause(); // Always call the superclass first
// Release the camera as other activities might need it
if (_camera != null)
{
_camera.Release();
_camera = null;
}
}
可能會在OnPause之後調用的兩種生命周期方法有:
- 如果要將 Activity 傳回前景,則會呼叫
OnResume。 -
OnStop會在活動被放在背景中時被呼叫。
OnStop
當使用者不再看到活動時,就會呼叫 OnStop。 發生下列其中一項時,就會發生這種情況:
- 正在啟動新活動,並覆蓋此活動。
- 將現有的活動提升至顯著位置。
- 活動正在被破壞。
OnStop 在記憶體不足的情況下,可能不會被呼叫,例如當 Android 資源匱乏而無法正確將活動置於背景時。 因此,最好不要依賴 OnStop 在準備銷毀 Activity 時被呼叫。 下一個在此生命周期之後可能會被呼叫的方法是 OnDestroy,如果 Activity 即將結束,或 OnRestart,如果 Activity 即將返回與用戶互動。
OnDestroy
OnDestroy 是在活動實例終結並完全從記憶體中移除之前,在活動實例上呼叫的最終方法。 在極端情況下,Android 可能會終止裝載活動的應用程式程式,這會導致 OnDestroy 不會叫用。 大部分的活動都不會實作此方法,因為大部分的清除和關閉工作已在OnPause和OnStop方法中完成。 通常會覆寫 OnDestroy 方法以清除可能會導致資源洩漏的長時間執行工作。 在OnCreate啟動的背景線程可能是其中一個範例。
在 Activity 被銷毀後將不會呼叫任何生命週期函式。
OnRestart
在活動停止之後,會先呼叫 OnRestart,再重新啟動。 這是一個很好的範例,就是當使用者在應用程式中的活動上按下首頁按鈕時。 當發生這種情況 OnPause ,然後 OnStop 呼叫方法時,活動會移至背景,但不會終結。 如果使用者接著使用任務管理員或類似的應用程式來還原應用程式,Android 會呼叫 OnRestart 活動的方法。
在 OnRestart 裡應該實現什麼樣的邏輯,並沒有通用的指導方針。 這是因為不論活動是被建立還是重新啟動,OnStart 總是會被呼叫,因此活動所需的任何資源都應該在 OnStart 中初始化,而不是在 OnRestart 中。
接下來在 OnRestart 之後呼叫的生命週期方法將是 OnStart。
返回對比首頁
許多 Android 裝置都有兩個不同的按鈕:[上一頁] 按鈕和 [首頁] 按鈕。 您可以在 Android 4.0.3 的下列螢幕快照中看到此範例:
這兩個按鈕之間有細微的差異,即使它們看起來達成的效果相同,都是將應用程式放到背景中。 當使用者按兩下 [上一頁] 按鈕時,他們會告訴Android他們已完成活動。 Android 將會終結活動。 相反地,當使用者按兩下 [首頁] 按鈕時,活動只會放在背景中 – Android 不會終止活動。
在整個生命周期中管理狀態
當活動停止或終結時,系統會提供一個機會來儲存活動的狀態,以供日後解除凍結。 這個儲存狀態稱為實例狀態。 Android 提供三個選項,可在活動生命週期期間儲存實例狀態:
將基本值儲存在名為Bundle的
Dictionary中,Android 將用於儲存狀態。建立將保存複雜值的自定義類別,例如點陣圖。 Android 會使用此自定義類別來儲存狀態。
規避設定變更生命週期,並承擔維護活動狀態的完整責任。
本指南涵蓋前兩個選項。
套件組合狀態
儲存實例狀態的主要選項是使用稱為 Bundle 的索引鍵/值字典物件。
回想一下,當建立活動時, OnCreate 方法會傳遞套件組合做為參數,這個配套可用來還原實例狀態。 不建議針對較複雜的數據使用套件組合,這些數據不會快速或輕鬆地串行化為索引鍵/值組(例如點陣圖):相反地,它應該用於簡單的值,例如字串。
活動提供方法來協助儲存和擷取套件組合中的實例狀態:
OnSaveInstanceState – 當活動被終結時,Android 會叫用此專案。 如果活動需要保存任何鍵值狀態項目,就可以實作此方法。
OnRestoreInstanceState – 在方法完成之後
OnCreate呼叫此選項,並提供另一個機會讓 Activity 在初始化完成之後還原其狀態。
下圖說明如何使用這些方法:
OnSaveInstanceState
當 Activity 正在停止時,將會呼叫 OnSaveInstanceState。 它會收到一個用於活動儲存狀態的包參數。 當裝置遇到設定變更時,活動可以使用傳入的 Bundle 物件,藉由覆寫 OnSaveInstanceState 來保留活動狀態。 例如,請考慮下列程式碼:
int c;
protected override void OnCreate (Bundle bundle)
{
base.OnCreate (bundle);
this.SetContentView (Resource.Layout.SimpleStateView);
var output = this.FindViewById<TextView> (Resource.Id.outputText);
if (bundle != null) {
c = bundle.GetInt ("counter", -1);
} else {
c = -1;
}
output.Text = c.ToString ();
var incrementCounter = this.FindViewById<Button> (Resource.Id.incrementCounter);
incrementCounter.Click += (s,e) => {
output.Text = (++c).ToString();
};
}
當按下名為 incrementCounter 的按鈕時,程式代碼會將整數 c 遞增,並將結果顯示在名為 output 的 TextView 中。 當組態變更發生時,例如裝置旋轉,上述程式碼會遺失c的值,因為bundle會變成null,如下圖所示:
若要保留此範例中的 值 c ,Activity 可以覆寫 OnSaveInstanceState,將值儲存在套件組合中,如下所示:
protected override void OnSaveInstanceState (Bundle outState)
{
outState.PutInt ("counter", c);
base.OnSaveInstanceState (outState);
}
現在,當裝置旋轉至新的方向時,整數會儲存在束中,然後透過這行程式碼擷取:
c = bundle.GetInt ("counter", -1);
備註
務必始終呼叫 OnSaveInstanceState 的基礎實作,以便儲存檢視階層的狀態。
檢視狀態
覆寫 OnSaveInstanceState 是一種適當的機制,可在活動中跨方向變更儲存暫時性數據,例如上述範例中的計數器。 不過,預設實作會負責在每個檢視的 UI 中保存暫時性數據,只要每個檢視都有指派的 ID。 例如,假設應用程式具有 EditText XML 中定義的元素,如下所示:
<EditText android:id="@+id/myText"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
EditText由於控制項已id指派,因此當使用者輸入某些資料並旋轉裝置時,仍會顯示相同的資料,如下所示:
OnRestoreInstanceState(恢復實例狀態)
OnRestoreInstanceState 會在 OnStart 之後被呼叫。 它為活動提供機會,以還原以前在上一次 OnSaveInstanceState 期間儲存到 Bundle 的任何狀態。 不過,這是提供給 OnCreate的相同套件組合。
下列程式碼示範如何在OnRestoreInstanceState中還原狀態:
protected override void OnRestoreInstanceState(Bundle savedState)
{
base.OnRestoreInstanceState(savedState);
var myString = savedState.GetString("myString");
var myBool = savedState.GetBoolean("myBool");
}
此方法的存在旨在提供恢復狀態時的一些彈性。 有時候,等待所有初始化完成後再還原實例狀態會更合適。 此外,現有活動的子類別可能只想要從實例狀態還原特定值。 在許多情況下,不需要覆寫 OnRestoreInstanceState,因為大部分的應用活動都可以使用提供給 OnCreate 的套件組合來還原狀態。
如需使用 Bundle儲存狀態的範例,請參閱 逐步解說 - 儲存活動狀態。
套件組合限制
雖然 OnSaveInstanceState 可讓您輕鬆地儲存暫時性數據,但有一些限制:
在並非所有情況下都會被呼叫。 例如,按 Home 或 Back 結束活動將不會產生
OnSaveInstanceState呼叫。傳入的
OnSaveInstanceState套件組合並不是為大型對象(如圖片)而設計。 如果是大型物件,最好從 OnRetainNonConfigurationInstance 儲存物件,如下所述。使用套件組合儲存的數據會串行化,這可能會導致延遲。
佈局狀態適用於不會使用太多記憶體的簡單資料,而 非配置實例資料 適用於更複雜的資料,或者從 Web 服務呼叫或複雜的資料庫查詢中擷取成本高的資料。 非組態實例數據會視需要儲存在物件中。 下一節將 OnRetainNonConfigurationInstance 介紹為透過組態變更保留更複雜的數據類型的方式。
持續保存複雜資料
除了在套件組合中保存數據之外,Android 也支持藉由覆寫 OnRetainNonConfigurationInstance 來儲存數據,並傳回包含要保存之數據的 實例 Java.Lang.Object 。 使用 OnRetainNonConfigurationInstance 來儲存狀態有兩個主要優點:
從
OnRetainNonConfigurationInstance傳回的物件在較大型且更複雜的數據類型上表現良好,因為記憶體保留此物件。方法
OnRetainNonConfigurationInstance會視需要呼叫,而且只有在需要時才呼叫。 這比使用手動快取更經濟。
使用 OnRetainNonConfigurationInstance 適用於擷取數據多次耗費資源的案例,例如在Web服務呼叫中。 例如,請考慮下列搜尋 Twitter 的程式代碼:
public class NonConfigInstanceActivity : ListActivity
{
protected override void OnCreate (Bundle bundle)
{
base.OnCreate (bundle);
SearchTwitter ("xamarin");
}
public void SearchTwitter (string text)
{
string searchUrl = String.Format("http://search.twitter.com/search.json?" + "q={0}&rpp=10&include_entities=false&" + "result_type=mixed", text);
var httpReq = (HttpWebRequest)HttpWebRequest.Create (new Uri (searchUrl));
httpReq.BeginGetResponse (new AsyncCallback (ResponseCallback), httpReq);
}
void ResponseCallback (IAsyncResult ar)
{
var httpReq = (HttpWebRequest)ar.AsyncState;
using (var httpRes = (HttpWebResponse)httpReq.EndGetResponse (ar)) {
ParseResults (httpRes);
}
}
void ParseResults (HttpWebResponse httpRes)
{
var s = httpRes.GetResponseStream ();
var j = (JsonObject)JsonObject.Load (s);
var results = (from result in (JsonArray)j ["results"] let jResult = result as JsonObject select jResult ["text"].ToString ()).ToArray ();
RunOnUiThread (() => {
PopulateTweetList (results);
});
}
void PopulateTweetList (string[] results)
{
ListAdapter = new ArrayAdapter<string> (this, Resource.Layout.ItemView, results);
}
}
此程式代碼會從格式化為 JSON 的 Web 擷取結果、剖析結果,然後在清單中呈現結果,如下列螢幕快照所示:
當組態變更發生時,例如,當裝置旋轉時,程式代碼會重複此程式。 若要重複使用原本擷取的結果,而不會造成不必要的重複網路呼叫,我們可以使用 OnRetainNonconfigurationInstance 來儲存結果,如下所示:
public class NonConfigInstanceActivity : ListActivity
{
TweetListWrapper _savedInstance;
protected override void OnCreate (Bundle bundle)
{
base.OnCreate (bundle);
var tweetsWrapper = LastNonConfigurationInstance as TweetListWrapper;
if (tweetsWrapper != null) {
PopulateTweetList (tweetsWrapper.Tweets);
} else {
SearchTwitter ("xamarin");
}
public override Java.Lang.Object OnRetainNonConfigurationInstance ()
{
base.OnRetainNonConfigurationInstance ();
return _savedInstance;
}
...
void PopulateTweetList (string[] results)
{
ListAdapter = new ArrayAdapter<string> (this, Resource.Layout.ItemView, results);
_savedInstance = new TweetListWrapper{Tweets=results};
}
}
現在當裝置旋轉時,會從 LastNonConfiguartionInstance 屬性擷取原始結果。 在此範例中,結果內含 string[] 推文。 由於 OnRetainNonConfigurationInstance 需要傳 Java.Lang.Object 回 ,因此 string[] 會包裝在子類別 Java.Lang.Object的類別中,如下所示:
class TweetListWrapper : Java.Lang.Object
{
public string[] Tweets { get; set; }
}
例如,嘗試使用 TextView 作為從 OnRetainNonConfigurationInstance 傳回的物件會導致 Activity 記憶體洩漏,如下列程式碼示例:
TextView _textView;
protected override void OnCreate (Bundle bundle)
{
base.OnCreate (bundle);
var tv = LastNonConfigurationInstance as TextViewWrapper;
if(tv != null) {
_textView = tv;
var parent = _textView.Parent as FrameLayout;
parent.RemoveView(_textView);
} else {
_textView = new TextView (this);
_textView.Text = "This will leak.";
}
SetContentView (_textView);
}
public override Java.Lang.Object OnRetainNonConfigurationInstance ()
{
base.OnRetainNonConfigurationInstance ();
return _textView;
}
在本節中,我們已瞭解如何使用 Bundle來保留簡單的狀態數據,並使用 來保存更複雜的數據類型 OnRetainNonConfigurationInstance。
總結
Android 活動生命週期為應用程式內的活動狀態管理提供強大的架構,但瞭解和實作可能很棘手。 本章介紹活動在其存留期間可能經歷的不同狀態,以及與這些狀態相關聯的生命週期方法。 接下來,會提供指引,說明每個方法中應該執行何種邏輯。