講給新手的一些話
相信有一定Android開發經驗的朋友或多或少的都遇到過 Activity 中某些物件莫名其妙的出現了空指標的異常。這種異常通常發生在 Activity 切換到了後臺,然後又做了其他一些記憶體開銷比較大的事,比如玩遊戲,比如開啟了很多其他應用。因為在後臺,所以這個 Activity 的優先順序就降低了,在記憶體比較吃緊的時候極有可能被回收。還不熟悉這一套機制的朋友在這裡可能就會遇到一些問題了。
雖然記憶體被回收了,但是由於這是系統的決定,而非程式碼觸發的行為,當使用者再啟動這個應用的時候,並不會重新走啟動流程,而是直接回到離開時候的 Activity。又因為 Activity 已經被回收了,所以理所當然的會重新建立、重走從 onCreate 開始的生命週期。
針對這種情況,Android 有 onSaveInstanceState、onRestoreInstanceState 這一對生命週期用來儲存和取出來還原 Activity 至被回收之前的狀態,具體詳情可參考: http://blog.csdn.net/initphp/article/details/11973651
正文
onSaveInstanceState、onRestoreInstanceState 真的可以解決所有問題了嗎?
@Override
public void onCreate(Bundle savedInstanceState, PersistableBundle persistentState) {
super.onCreate(savedInstanceState, persistentState);
Data data = getIntent().getParcelableExtra("data");
if (data.type == 0) {
setContentView(R.layout.activity_to);
//TODO
} else {
setContentView(R.layout.activity_from);
//TODO
}
}
複製程式碼
以上程式碼的意圖很清晰,是根據上一個介面傳遞過來的引數 data 中的 type 來決定載入什麼佈局。這段程式碼合不合理暫且不考慮,想一下這種情景下記憶體被回收了會發生什麼就很有意思了。
1.getIntent()無法再獲取到上一介面傳遞過來的 bundle 資料,此時 data 物件必然為空; 2.onRestoreInstanceState 的執行時機是在 onCreate 之後的,也就是說,如果我用在 onCreate 的生命週期中用 getIntent() 方法獲取上一個介面傳遞過來的引數,然後立即使用的話,其實 onRestoreInstanceState 是還沒來得及呼叫的; 3.空指標呼之欲出 T_T
So ?
我曾經是這樣做的
@Override
public void onCreate(Bundle savedInstanceState, PersistableBundle persistentState) {
super.onCreate(savedInstanceState, persistentState);
if (savedInstanceState == null) {
data = getIntent().getParcelableExtra("data");
} else {
data = savedInstanceState.getParcelable("data");
}
if (data.type == 0) {
setContentView(R.layout.activity_to);
//TODO
} else {
setContentView(R.layout.activity_from);
//TODO
}
}
複製程式碼
savedInstanceState 是儲存的 Activity 的狀態,如果為 null,則代表是正常進入,否則即為我們考慮的記憶體被回收後重啟的情況。
這段程式碼就是根據不同的情況,去從 getIntent() 或 savedInstanceState 中再去獲取 data 物件。當然,前提條件是我有這樣子做:
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putParcelable("data", data);
super.onSaveInstanceState(outState);
}
複製程式碼
即在被回收前將 data 物件儲存至 bundle 中。
整體看上去還算簡單對吧? 我慶幸於我從此脫離了NullPointer的苦海,但這只是苦難的開始 T_T
真 · 正文
實際開發過程當中,bundle 中不一定只有 data 這麼一個引數,有幾個引數的情況也很常見,於是上述程式碼就會擴張開來。
程式碼行數約等於 引數數量*2(取) + 引數數量 *1(存)
也就是說,每增加一個傳遞的引數,我就要多寫三行程式碼,毫無營養價值的三行程式碼。這是第一層意義上的重複勞動。
然後呢,每一個要接收 Bundle 引數的 Activity,我都要寫這麼幾段程式碼,邏輯基本上完全相同,這是第二層意義上的重複勞動。
有一句話說得好:不在沉默中爆發,就在沉默中滅亡。終於有一天我忍無可忍,開始想辦法怎麼才能更優(偷)雅(懶)的去解決這個問題。
要避免每個 Activity 都寫同樣的邏輯程式碼,第一個想到的就是在基類中去寫
private Bundle savedBundle;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initBundle(savedInstanceState);
initData(savedBundle);
}
複製程式碼
可以看到 onCreate 方法中除了執行父類的方法之外,就只執行了 initBundle 和 initData 這兩個方法。
/**
* 傳入 onCreate 中的 bundle,根據是否為空進行相應處理
* 如果為 null,代表 activity 還沒有儲存過 bundle
* @param savedInstanceState
*/
private void initBundle(Bundle savedInstanceState) {
if (savedInstanceState == null) {
savedBundle = getIntent().getExtras();
} else {
savedBundle = savedInstanceState.getBundle("savedBundle");
}
//如果沒有任何引數,則初始化 savedBundle,避免呼叫時 null pointer
if (savedBundle == null) {
savedBundle = new Bundle();
}
}
/**
* 儲存 bundle
* @param outState
*/
protected void onSaveInstanceState(Bundle outState) {
outState.putBundle("savedBundle", this.savedBundle);
super.onSaveInstanceState(outState);
}
複製程式碼
是不是和之前的處理方式很像?唯一的區別就是在 getIntent() 的時候,我一股腦的把所有資料都取了出來,並賦值給 savedBundle,儲存的時候也只儲存 savedBundle 這麼一個物件。
#####我並沒有生產新的資料,我只是資料的搬運工
對了,還有一個 initData 方法是幹嘛的?
/**
* 提供給子類取值的方法
* @param savedBundle
*/
protected void initData(Bundle savedBundle) {
}
複製程式碼
原來什麼都沒做。。
####How to use ? 在繼承自基類的 Activity 中,覆寫 initData 方法即可
@Override
protected void initData(Bundle savedBundle) {
super.initData(savedBundle);
int data = savedBundle.getInt("data");
Log.i("bundle", " data = " + data);
}
複製程式碼
要什麼引數直接從 initData 方法提供的 savedBundle 中去取,安全無汙染,省力更省心。