活動開發週期
活動是 Android 應用程式的基本建置組塊,而且可以存在於許多不同的狀態。 活動生命週期從具現化開始,並以解構結束,並包含介於 兩者之間的許多狀態。 當活動變更狀態時,會呼叫適當的生命週期事件方法,通知即將發生的狀態變更活動,並允許它執行程式代碼以適應該變更。 本文會檢查活動的生命週期,並說明活動在每個狀態變更期間所承擔的責任,以成為行為良好、可靠應用程式的一部分。
活動生命週期概觀
活動是 Android 特有的異常程式設計概念。 在傳統應用程式開發中,通常會執行靜態主要方法來啟動應用程式。 不過,使用Android時,情況會有所不同;Android 應用程式可以透過應用程式內的任何已註冊活動啟動。 實際上,大部分的應用程式只會有指定為應用程式進入點的特定活動。 不過,如果應用程式當機,或由OS終止,則OS可以嘗試在上一個開啟的活動或上一個活動堆疊內的任何其他位置重新啟動應用程式。 此外,OS 可能會在活動不在作用中時暫停活動,並在記憶體不足時加以回收。 必須仔細考慮,讓應用程式在重新啟動活動時正確地還原其狀態,特別是如果該活動相依於先前活動的數據時。
活動生命週期會實作為整個活動生命週期中OS呼叫的方法集合。 這些方法可讓開發人員實作滿足其應用程式狀態和資源管理需求所需的功能。
應用程式開發人員必須分析每個活動的需求,以判斷活動生命週期所公開的方法必須實作。 若無法這麼做,可能會導致應用程式不穩定、損毀、資源膨脹,甚至可能導致基礎OS不穩定。
本章會詳細檢查活動生命週期,包括:
- 活動狀態
- 生命週期方法
- 保留應用程式的狀態
本節也包含逐步解說,提供如何在活動生命周期期間有效率地儲存狀態的實用範例。 在本章結束時,您應該了解活動生命週期,以及如何在Android應用程式中加以支援。
活動開發週期
Android 活動生命週期包含活動類別中公開的方法集合,提供開發人員資源管理架構。 此架構可讓開發人員符合應用程式內每個活動的唯一狀態管理需求,並正確地處理資源管理。
活動狀態
Android OS 會根據其狀態來仲裁活動。 這有助於Android識別不再使用的活動,讓OS回收記憶體和資源。 下圖說明活動在其存留期內可經歷的狀態:
這些狀態可以分成 4 個主要群組,如下所示:
使用中或執行 – 如果活動位於前景,則活動會被視為作用中或正在執行,也稱為活動堆棧的頂端。 這被視為 Android 中優先順序最高的活動,因此只有在極端情況下,OS 才會終止,例如,如果活動嘗試使用比裝置上可用的記憶體還多,這可能會導致 UI 沒有回應。
已暫停 – 當裝置進入睡眠狀態,或活動仍會顯示,但部分隱藏於新的非完整大小或透明活動時,活動會被視為暫停。 暫停的活動仍然作用中,也就是說,它們會維護所有狀態和成員資訊,並維持附加至視窗管理員。 這被視為 Android 中的第二高優先順序活動,因此,只有在終止此活動時,OS 才會終止此活動以滿足保持作用中/執行中活動穩定且回應所需的資源需求。
已停止/背景 – 其他活動完全遮蔽的活動會被視為已停止或背景中。 已停止的活動仍會盡量保留其狀態和成員資訊,但已停止的活動會被視為三個狀態的最低優先順序,因此,OS 會先終止處於此狀態的活動,以符合較高優先順序活動的資源需求。
重新啟動 – Android 可從暫停到停止生命週期的任何位置的活動都可以從記憶體中移除。 如果使用者巡覽回必須重新啟動的活動,請還原至先前儲存的狀態,然後顯示給使用者。
回應設定變更的活動重新建立
為了讓事情變得更複雜,Android 在稱為設定變更的混合中又擲回一個扳手。 組態變更是快速的活動解構/重新建立週期,當活動設定變更時,例如當裝置 旋轉 時(以及活動需要重新建置於橫向或直向模式)、顯示鍵盤時(以及活動有機會自行重設大小),或當裝置放置在擴充座時, 等等。
設定變更仍然會導致在停止和重新啟動活動期間發生的相同活動狀態變更。 不過,為了確保應用程式在設定變更期間有回應且執行良好,請務必儘快處理它們。 因此,Android 有一個特定的 API,可用來在設定變更期間保存狀態。 我們將在稍後的 <整個生命週期 管理狀態>一節中討論這一點。
活動生命週期方法
Android SDK 和 Xamarin.Android 架構會提供強大的模型來管理應用程式內的活動狀態。 當活動的狀態變更時,OS 會通知活動,該操作系統會呼叫該活動的特定方法。 下圖說明與活動生命週期相關的這些方法:
身為開發人員,您可以覆寫活動內的這些方法來處理狀態變更。 不過,請務必注意,所有生命週期方法都會在UI線程上呼叫,並且會封鎖OS執行下一段UI工作,例如隱藏目前活動、顯示新活動等。因此,這些方法中的程式代碼應該盡可能簡短,讓應用程式執行良好。 任何長時間執行的工作都應該在背景線程上執行。
讓我們檢查這些生命週期方法及其使用方式:
OnCreate
OnCreate 是建立活動時要呼叫的第一個方法。
OnCreate
一律會覆寫以執行活動可能需要的任何啟動初始化,例如:
- 建立檢視
- 初始化變數
- 將靜態數據系結至清單
OnCreate
會採用 Bundle 參數,這是用來在活動之間儲存和傳遞狀態資訊和物件如果套件組合不是 Null 的字典,這表示活動正在重新啟動,而且應該從上一個實例還原其狀態。 下列程式代碼說明如何從套件組合擷取值:
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 。 如果活動需要下列專案,應該覆寫此方法:
將未儲存的變更認可至永續性數據
終結或清除其他耗用資源的物件
遞增幀速率和暫停動畫
取消註冊外部事件處理程式或通知處理程式(亦即系結至服務的事件處理程式)。 這必須完成才能防止活動記憶體流失。
同樣地,如果活動已顯示任何對話框或警示,則必須使用
.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
呼叫:
OnResume
如果要將活動傳回前景,則會呼叫 。OnStop
如果在背景中放置活動,則會呼叫 。
OnStop
當使用者不再看到活動時,就會呼叫 OnStop 。 發生下列其中一項時,就會發生這種情況:
- 正在啟動新的活動,並涵蓋此活動。
- 將現有的活動帶到前景。
- 活動正在終結。
OnStop
在記憶體不足的情況下,可能不一定呼叫,例如當Android因資源不足而無法正確背景活動時。 因此,最好不要在準備活動以銷毀時呼叫 OnStop
。 下一個在此生命周期之後呼叫的方法會是 OnDestroy
活動即將消失,或 OnRestart
活動回來與用戶互動。
OnDestroy
OnDestroy 是在活動實例終結並完全從記憶體中移除之前,在活動實例上呼叫的最終方法。 在極端情況下,Android 可能會終止裝載活動的應用程式程式,這會導致 OnDestroy
不會叫用。 大部分的活動都不會實作此方法,因為和方法中OnPause
OnStop
已完成大部分的清除和關閉。 通常 OnDestroy
會覆寫 方法,以清除可能會流失資源的長時間執行工作。 其中一個範例可能是 在中 OnCreate
啟動的背景線程。
在活動終結之後,將不會呼叫任何生命週期方法。
OnRestart
在活動停止之後,會先呼叫 OnRestart ,再重新啟動。 這是一個很好的範例,就是當使用者在應用程式中的活動上按下首頁按鈕時。 當發生這種情況 OnPause
,然後 OnStop
呼叫方法時,活動會移至背景,但不會終結。 如果使用者接著使用任務管理員或類似的應用程式來還原應用程式,Android 會呼叫 OnRestart
活動的方法。
中應該實 OnRestart
作何種邏輯沒有一般指導方針。 這是因為 OnStart
一律叫用活動,不論活動是建立還是重新啟動,因此活動所需的任何資源都應該在 中 OnStart
初始化,而不是 OnRestart
。
之後呼叫 OnRestart
的下一個生命週期方法將是 OnStart
。
返回與首頁
許多 Android 裝置都有兩個不同的按鈕:[上一頁] 按鈕和 [首頁] 按鈕。 您可以在 Android 4.0.3 的下列螢幕快照中看到此範例:
這兩個按鈕之間有細微的差異,即使它們似乎有相同的效果,將應用程式放在背景中也一樣。 當使用者按兩下 [上一頁] 按鈕時,他們會告訴Android他們已完成活動。 Android 將會終結活動。 相反地,當使用者按兩下 [首頁] 按鈕時,活動只會放在背景中 – Android 不會終止活動。
在整個生命週期中管理狀態
當活動停止或終結時,系統會提供一個機會來儲存活動的狀態,以供日後解除凍結。 這個儲存狀態稱為實例狀態。 Android 提供三個選項,可在活動生命週期期間儲存實例狀態:
將基本值儲存在 Android 將用來儲存狀態的套件組合中
Dictionary
。建立將保存複雜值的自定義類別,例如點陣圖。 Android 會使用此自定義類別來儲存狀態。
規避設定變更生命週期,並承擔維護活動狀態的完整責任。
本指南涵蓋前兩個選項。
套件組合狀態
儲存實例狀態的主要選項是使用稱為 Bundle 的索引鍵/值字典物件。
回想一下,當建立活動時, OnCreate
方法會傳遞套件組合做為參數,這個配套可用來還原實例狀態。 不建議針對較複雜的數據使用套件組合,這些數據不會快速或輕鬆地串行化為索引鍵/值組(例如點陣圖):相反地,它應該用於簡單的值,例如字串。
活動提供方法來協助儲存和擷取套件組合中的實例狀態:
OnSaveInstanceState – 當活動被終結時,Android 會叫用此專案。 如果活動需要保存任何索引鍵/值狀態專案,就可以實作此方法。
OnRestoreInstanceState – 在方法完成之後
OnCreate
呼叫此選項,並提供另一個機會讓 Activity 在初始化完成之後還原其狀態。
下圖說明如何使用這些方法:
OnSaveInstanceState
OnSaveInstanceState 將會呼叫,因為活動正在停止。 它會收到活動可以儲存其狀態的套件組合參數。 當裝置遇到設定變更時,Activity 可以使用 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
是一種適當的機制,可在活動中跨方向變更儲存暫時性數據,例如上述範例中的計數器。 不過,的默認實 OnSaveInstanceState
作會負責在每個檢視的 UI 中儲存暫時性數據,只要每個檢視都已指派標識符。 例如,假設應用程式具有 EditText
XML 中定義的元素,如下所示:
<EditText android:id="@+id/myText"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
EditText
由於控件已id
指派,因此當使用者輸入某些數據並旋轉裝置時,仍會顯示數據,如下所示:
OnRestoreInstanceState
OnRestoreInstanceState 會在 之後 OnStart
呼叫。 它提供一個活動機會,以還原先前在上 OnSaveInstanceState
一個 期間儲存至套件組合的任何狀態。 不過,這是提供給 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 活動生命週期為應用程式內的活動狀態管理提供強大的架構,但瞭解和實作可能很棘手。 本章介紹活動在其存留期間可能經歷的不同狀態,以及與這些狀態相關聯的生命週期方法。 接下來,會提供指引,說明每個方法中應該執行何種邏輯。