介紹
在上篇博客中詳細說明了各種單例的寫法和問題。這篇主要介紹單例在Android開發中的各種應用場景以及和靜態類方法的對比考慮,舉實際例子說明。
單例的思考
寫了這麼多單例,都快忘記我們到底爲什麼需要單例,複習單例的本質
單例的本質:控制實例的數量
全局有且只有一個對象,並能夠全局訪問得到。
控制實例數量
有時候會思考如果我們需要控制實例的數量不是隻有一個,而是2、3、4或者任意多個呢?我們怎樣控制實例的數量,其實實現思路也簡單,就是通過Map緩存實例,控制緩存的數量,當有調用就返回某個實例,這其中就涉及到調度問題。考慮在實際Android開發中有這樣的情況嗎?還真有,如果看過我的上篇分析單例的博客提到郭神和洪洋大神都有LruCache實現圖片緩存,不就是控制實例數量的應用場景嗎。LruCache內部用LinkedHashMap持有對象。用LruCache緩存圖片到內存,圖片數量就是我們需要控制的實例數量,一般是根據內存的大小開空間存圖片,根據圖片地址url取內存中的圖片沒有訪問網絡獲取,內部採用最近最少使用調度算法控制圖片的存儲。
具體實現看比較複雜,詳情去看兩位大神的CDNS博客吧。
單例的應用場景
Android開發中單例模式應用
單例在Android開發中的實際使用場景,圖片加載框架就是一個很好的例子。我在剛接觸Android的時候使用的Android Universal Image Loader
就採用了單例,這是因爲它需要緩存圖片,對緩存的圖片集合做各種操作,需要關注單例中的對象狀態,而且明顯是需要訪問資源的。這就很契合單例的特性。同樣在熱門的EventBus中也採用了單例,因爲它內部緩存了各個組件發送過來的event對象,並負責分發出去,各個組件需要向同一個EventBus對象註冊自己,才能接收到event事件,肯定是需要全局唯一的對象,所以採用了單例。
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開發中單例的應用場景。單例模式其實在源碼和很多開源框架中都有應用,寫好單例分析單例的和靜態類方法和適用場景能夠寫出好的代碼。
- 最後總結我在使用單例時遇到的坑和提出解決方案。