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

設計模式-單例模式(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開發中單例的應用場景。單例模式其實在原始碼和很多開源框架中都有應用,寫好單例分析單例的和靜態類方法和適用場景能夠寫出好的程式碼。
  • 最後總結我在使用單例時遇到的坑和提出解決方案。

相關推薦

no