在一個App裡面總有一些資料需要在多個地方用到。這些資料可能是一個 session token,一次費時計算的結果等。通常為了避免activity之間傳遞物件的開銷 ,這些資料一般都會儲存到持久化儲存裡面。
有人建議將這些資料儲存到 Application 物件裡面,這樣這些資料對所有應用內的activities可用。這種方法簡單,優雅而且……完全扯淡。
假設把你的資料都儲存到Application物件裡面去了,那麼你的應用最後會以一個NullPointerException 異常crash掉。
一個簡單的測試案例
程式碼
Application 物件:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// access modifiers omitted for brevity class MyApplication extends Application { String name; String getName() { return name; } void setName(String name) { this.name = name; } } |
第一個activity,我們往application物件裡面儲存了使用者姓名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// access modifiers omitted for brevity class WhatIsYourNameActivity extends Activity { void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.writing); // Just assume that in the real app we would really ask it! MyApplication app = (MyApplication) getApplication(); app.setName("Developer Phil"); startActivity(new Intent(this, GreetLoudlyActivity.class)); } } |
第二個activity,我們呼叫第一個activity設定並存在application裡面的使用者姓名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// access modifiers omitted for brevity class GreetLoudlyActivity extends Activity { TextView textview; void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.reading); textview = (TextView) findViewById(R.id.message); } void onResume() { super.onResume(); MyApplication app = (MyApplication) getApplication(); textview.setText("HELLO " + app.getName().toUpperCase()); } } |
測試場景
- 使用者啟動app。
- 在 WhatIsYourNameActivity裡面,要求使用者輸入姓名,並儲存到 MyApplication。
- 在 GreetLoudlyActivity裡面,你從MyApplication 物件中獲得使用者姓名,並且顯示。
- 使用者按home鍵離開這個app。
- 幾個小時後,Android系統為了回收記憶體kill掉了這個app。到目前為止,一切尚好。接下來就是crash的部分了…
- 使用者重新開啟這個App。
- Android系統建立一個新的 MyApplication 例項並恢復 GreetLoudlyActivity。
- GreetLoudlyActivity 從新的 MyApplication 例項中獲取使用者姓名,可得到的為空,最後導致NullPointerException。
為什麼會Crash?
在上面這個例子中,app會crash得原因是這個 Application 物件是全新的,所以這個name 變數裡面的值為 null,當呼叫String#toUpperCase() 方法時就導致了NullPointerException。
整個問題的核心在於:application 物件不會一直呆著記憶體裡面,它會被kill掉。與大家普遍的看法不同之處在於,實際上app不會重新開始啟動。Android系統會建立一個新的Application 物件,然後啟動上次使用者離開時的activity以造成這個app從來沒有被kill掉得假象。
你以為你的application可以儲存資料,卻沒想到你的使用者在沒有開啟activity A 之前就就直接開啟了 activity B ,於是你就收到了一個 crash 的 surprise。
有哪些替代方法呢?
這裡沒啥神奇的解決方法,你可以試試下面幾種方法:
- 直接將資料通過intent傳遞給 Activity 。
- 使用官方推薦的幾種方式將資料持久化到磁碟上。
- 在使用資料的時候總是要對變數的值進行非空檢查。
如果模擬App被Kill掉
更新: Daniel Lew指出,kill app更簡單的方式就是使用DDMS裡面“停止程式” 。你在除錯你的應用的時候可以使用這招。
為了測試這個,你必須使用一個Android模擬器或者一臺root過的Android手機。
- 使用home按鈕退出app。
- 在終端裡:
123456789101112131415# find the process idadb shell ps# then find the line with the package name of your app# Mac/Unix: save some time by using grep:adb shell ps | grep your.app.package# The result should look like:# USER PID PPID VSIZE RSS WCHAN PC NAME# u0_a198 21997 160 827940 22064 ffffffff 00000000 S your.app.package# Kill the app by PIDadb shell kill -9 21997# the app is now killed - 長按home按鈕回到之前的app。
你現在是出於一個新的application例項中了。
總結
不要在application物件裡面儲存資料,這容易出錯,導致你的app crash。
要麼將你後面要用的資料儲存到磁碟上面或者儲存到intent得extra裡面直接傳遞給activity 。
這些結論不但對application物件有用,對你app裡面的單例物件(singleton)或者公共靜態變數(public static)同樣適用。