設計模式-單例模式(Singleton)在Android中的應用場景和實際使用遇到的問題
介紹
在上篇部落格中詳細說明了各種單例的寫法和問題。這篇主要介紹單例在Android開發中的各種應用場景以及和靜態類方法的對比考慮,舉實際例子說明。
單例的思考
寫了這麼多單例,都快忘記我們到底為什麼需要單例,複習單例的本質
單例的本質:控制例項的數量
全域性有且只有一個物件,並能夠全域性訪問得到。
控制例項數量
有時候會思考如果我們需要控制例項的數量不是隻有一個,而是2、3、4或者任意多個呢?我們怎樣控制例項的數量,其實實現思路也簡單,就是通過Map快取例項,控制快取的數量,當有呼叫就返回某個例項,這其中就涉及到排程問題。考慮在實際Android開發中有這樣的情況嗎?還真有,如果看過我的
具體實現看比較複雜,詳情去看兩位大神的CDNS部落格吧。
單例的應用場景
Android開發中單例模式應用
單例在Android開發中的實際使用場景,圖片載入框架就是一個很好的例子。我在剛接觸Android的時候使用的Android Universal Image Loader
EventBus的單例採用的是雙重檢查加鎖單例
static volatile EventBus defaultInstance;
public static EventBus getDefault() {
if (defaultInstance == null) {
synchronized (EventBus.class) {
if (defaultInstance == null) {
defaultInstance = new EventBus();
}
}
}
return defaultInstance;
}
最後在Android原始碼中發現,一個非常重要的類LayoutInflater本身也採用的是單例模式。
單例的替代
回到開發的場景中,思考我們為什麼需要單例。如果是需要提供一個全域性的訪問點用getInstance()
做些操作。除了單例我們還有其他的選擇嗎?
回去翻看Android原始碼,有這樣一個類。java.lang.Math類它提供對數字的操作和方法計算,它的實現就是全部方法用static修飾符
包裝提供類級訪問。因為當我們呼叫Math類時只要它的某個類方法做資料操作並不關心物件狀態。
單例不需要維護任何狀態,僅僅提供全域性訪問的方法,這種情況考慮使用靜態類,靜態方法比單例更快,因為靜態的繫結是在編譯期就進行。
如果你需要將一些工具方法集中在一起時,你可以選擇使用靜態方法,但是別的東西,要求單例訪問資源並關注物件狀態時,應該使用單例模式。
Retrofit框架靜態類構造工具類
在我的一個專案中使用到Retrofit做網路訪問,這就需要一個具體的Retrofit物件操作網路。而且最好提供方法得到這個全域性唯一的Retrofit物件。一開始我也在糾結是單例還是靜態類。因為國內網站上對Retrofit的分析使用不是很多,而且網路上對這單例和靜態類的分析爭辯實在太多而且混亂。
最後直到看到這篇部落格,感覺還是老外靠譜,最後我的專案採用下面的程式碼例項化Retrofit物件。具體程式碼是這樣的。目前使用沒有問題,大家當做使用Retrofit時候的例項化參考吧。(程式碼依據最新的Retrofit-2.0版本)
public class ServiceGenerator {
public static final String API_BASE_URL = "http://your.api-base.url";
private static OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
private static Retrofit.Builder builder =
new Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create());
public static <S> S createService(Class<S> serviceClass) {
Retrofit retrofit = builder.client(httpClient.build()).build();
return retrofit.create(serviceClass);
}
}
只所以這麼寫,採用靜態類而不是單例,是因為把網路訪問看做工具類,只需要拿到Retrofit例項物件做網路操作,ServiceGenerator工具類內部不維護內部變數也不關心內部變數的狀態變化。
單例開發實際問題
踩坑是每個開發者必須經歷的過程,下面說明我在採用單例之後遇到的坑。相信每個初級Android開發者都遇到這樣的問題。兩個Activity元件之間傳遞資料,Intent和Bundle只能傳遞簡單的基本型別資料和String物件
(當然也可以傳遞物件這就需要Parcelable和Serializable介面)。
當需要傳遞的只是幾個值問題不大,但是如果需要傳遞的資料比較多就感覺程式碼不簡潔而且key值多容易接收出錯,傳遞物件需要物件繼承Parcelable介面寫大量的重複的模板程式碼。有沒有優雅一點解決辦法呢?
用單例物件傳遞物件的坑
Application傳遞物件的坑
相信有些人跟當時的我一樣看過這樣的部落格”優雅的用Application傳遞物件”。當時的我看見這樣部落格,真實感覺遇到救星一樣,感覺一下就解決了元件間傳遞物件的問題。
長者語:too young too simple sometimes naive
下面來說說如果你真的用Application傳遞物件會怎麼樣。原文部落格是這樣認為的Application由系統提供是全域性唯一的物件,並且任何元件都可以訪問到。哪就在自定義繼承Application的子類裡,儲存內部變數,由傳送的Activity取出內部變數並設值,startActivity之後在接收的Activity中也訪問Application物件取出內部變數得到需要傳遞的物件。就沒有複雜的Intent傳值了。
但是如果你真的這麼做:程式肯定會崩或者是取不到資料。
實際執行情況是這樣的:
1. 如果你在接收資料的Activity中,按下Home鍵返回桌面,長時間的沒有返回你的App。
2. 系統有可能會在系統記憶體不足的時候殺掉程序。
3. 當你再從最近程式執行列表進入你的App,系統會預設恢復剛剛離開的狀態,直接進入接收資料的Activity中。
4. 然後呼叫各個生命週期方法回撥,其中只要執行到從Application取資料行,程式就會彈出空指標NullPointerException異常導致崩潰。
5. 相信我一定是這樣的,如果沒有崩潰也只是因為你在內部變數中有預設初始化方法。這樣肯定也是取不到想要的資料。
因為整個流程需要很長時間,我們可以使用adb命令殺掉程序adb shell kill
,模擬長時間沒有回到應用而由系統殺死程序的操作。如果覺得麻煩還可以開啟Device Monitor-選中你的應用-使用紅色按鈕 Stop Process殺死程序。
程式崩潰的這主要原因就是:
系統會恢復之前離開的狀態,直接進入某個Activity元件而不是再依次開啟Activity,這樣你的傳送資料的Activity沒有執行也就不會向Application中傳值,自然也取不到值。
所以千萬不要相信”優雅的用Application傳遞物件”這寫部落格,這是個坑!實際情況複雜得多,真使用起來還有很多問題。
指出這個問題原文是dont-store-data-in-the-application-object中文翻譯的部落格在這,大家可以點選檢視會有詳細說明。
EventBus的坑
當時也是在寫一個專案,覺得Intent傳遞資料太麻煩,根據Appliaction可以傳遞資料的思路,其實自己也可以寫個單例用來儲存全域性資料,各個元件取出實現元件間傳遞資料。然後很網路上搜索,發現EventBus同樣實現了這樣的思路,EventBus本身就是採用了單例模式。上篇部落格的伏筆就在這。
EventBus: Android 事件釋出/訂閱框架,通過解耦釋出者和訂閱者簡化 Android 事件傳遞
由一個元件傳送事件,另一個元件向EventBus註冊然後響應的方法就會得到資料。這裡面也有坑啊。
當然我沒有說EventBus有問題,只是使用不當會導致Crash程式崩潰。
當時專案是就是按照標準的EventBus使用流程寫的程式碼,沒有問題。還是上文的情況,按下Home鍵長時間沒有返回應用,再次進入程式Crash。
原因還是一樣的:
系統恢復離開的現場,直接執行接收資料的Activity,而沒有執行到傳送資料的Activity元件,取不到資料,因為根本就沒有資料傳送。
順帶提一句,
用Kill App這個方法能夠檢查出App中很多意想不到的問題
解決辦法
用單例傳遞資料實質是用記憶體儲存資料,然後全域性方法。但是記憶體是很容易被虛擬機器回收的。我們要解決的就是怎麼樣儲存資料,持久化資料。
其實也沒有什麼好的解決方案。
- 還是直接將資料通過intent傳遞給 Activity 。
- 使用官方推薦的幾種方式將資料持久化到磁碟上,再取資料。
- 在使用資料的時候總是要對變數的值進行非空檢查,這樣還是取不到資料
- 使用EventBus傳遞資料時採用onSaveInstanceState(Bundle outState)方法儲存資料,使用onCreate(Bundle savedInstanceState)等待恢復取值。
包裝Activity跳轉方法
針對第一項,我提供一個簡單的包裝跳轉方法,簡化Inten傳遞資料的程式碼邏輯
public class MyActivity extends AppCompatActivity{
//Intent的key值
protected static final String TYPE_KEY = "TYPE_KEY";
protected static final String TYPE_TITLE = "TYPE_TITLE";
//接收的資料
public String mKey;
public String mTitle;
//包裝的跳轉方法
public static void launch(Activity activity, String key, String title) {
Intent intent = new Intent(activity, BoardDetailActivity.class);
intent.putExtra(TYPE_TITLE, title);
intent.putExtra(TYPE_KEY, key);
activity.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
//獲取資料
mKey = getIntent().getStringExtra(TYPE_KEY);
mTitle = getIntent().getStringExtra(TYPE_TITLE);}
}
使用程式碼,就一行
MyActivity.launch(this, key, title);
整個的邏輯是,在跳轉的元件中實現類方法,把傳遞值的key值以成員類變數的形式寫定在Activity中,需要傳遞的資料放入Intent中,簡化呼叫方的使用程式碼。
onSaveInstanceState儲存資料
onSaveInstanceState()方法的呼叫時機是:
只要某個Activity是做入棧並且非棧頂時(啟動跳轉其他Activity或者點選Home按鈕),此Activity是需要呼叫onSaveInstanceState的,
如果Activity是做出棧的動作(點選back或者執行finish),是不會呼叫onSaveInstanceState的。
這正是上文我們程式Crash的場景,產生問題的關鍵操作點。
所有我們需要做的就是在onSaveInstanceState回撥方法中儲存資料,等待資料恢復。
程式碼沒什麼好貼的就是outState.putParcelable(KEY, mData);
,然後在OnCreate中取savedInstanceState中的資料。
提示被put的資料需要實現Parcelable介面,如果不想寫大量的模板程式碼可以使用Android Parcelable Code Generator外掛快捷成成程式碼。
總結
- 總算寫完了,一個單例模式寫了兩篇部落格,上篇部落格主要是說明各種單例的寫法和分析。
- 本文主要介紹比較新的列舉單例
- 還有單例的應用場景和思考,以及在Android開發中單例的應用場景。單例模式其實在原始碼和很多開源框架中都有應用,寫好單例分析單例的和靜態類方法和適用場景能夠寫出好的程式碼。
- 最後總結我在使用單例時遇到的坑和提出解決方案。