設計模式-單例模式(Singleton)在Android中的應用場景和實際使用遇到的問題

介紹

上篇博客中詳細說明了各種單例的寫法和問題。這篇主要介紹單例在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殺死進程。
Device Monitor
程序崩潰的這主要原因就是:

系統會恢復之前離開的狀態,直接進入某個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開發中單例的應用場景。單例模式其實在源碼和很多開源框架中都有應用,寫好單例分析單例的和靜態類方法和適用場景能夠寫出好的代碼。
  • 最後總結我在使用單例時遇到的坑和提出解決方案。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章