享元模式在Android中的簡單應用

享元模式是常見設計模式中的一種,其目的是爲了複用已經創建的對象,而避免在程序中短時間內產生大量重複的對象,而這些對象又在短時間內失去引用,從而又變成可回收狀態,等待虛擬機回收,以至於消耗不必要的資源。

享元模式並沒有什麼固定的範本式的實現代碼,其核心思想就是緩存對象。我們在這裏先舉一個享元模式在Android Framework層中應用的例子。

Android消息機制想必大家都不陌生,無論理解的深還是淺,在我們初學Android的時候,至少都使用過Handler在子線程中更新UI;Handler,Looper,MessageQueues,Message這四者是密不可分的一整套系統,而其中,這個Message就是使用了享元模式的一個典型例子。我們在創建一個Message對象的時候,最好是應該使用如下代碼:

Message message = Message.obtain();
當然你也可以使用Message的構造方法:

Message message = new Message();
這樣寫系統也並不會報錯,但是這兩種寫法是有區別的。這裏由於篇幅原因,我只簡述一下,使用構造方法,也就是new關鍵字,其實也就是等同於在告訴虛擬機,我要申請一塊內存區域,來存放我即將新構造的一個對象,如果程序中不停的new出新對象,那對象就會堆積如山,最終塞滿整個內存,導致內存溢出(這裏提示一下,一直被強引用持有的對象,是不會被虛擬機回收的)或者頻繁觸發虛擬機的gc機制。

但是obtain()方法則並不是這樣,它在內部使用了一個鏈表來緩存Message對象,也就是說,一開始,如果用戶想使用Message對象,它會進行創建,當這個Message對象被使用完成之後,它並不會失去引用,成爲在內存中等待回收的“垃圾”,而是被清除數據,然後重新加入到鏈表頭部,這樣,當用戶多次通過obtain()方法獲取Message對象以後,用戶得到的Message對象則可能是之前創建而後又被緩存到鏈表中的。

在Message中,享元模式的具體實現是鏈表,而其它使用場景中,緩存對象的容器實際上是可以隨便切換的,例如你想用HashMap之類的也是可以的。



說到這裏,我們簡單介紹了一下享元模式的思想,說白了就是重複使用之前創建的對象唄,不錯,接下來我將介紹幾個實例,都非常簡單,甚至不需要我們使用容器類。

在前幾年,Android網絡請求庫中比較流行Volley,當然,Volley還有一個附帶功能就是加載圖片,通常情況下,你需要一個ImageLoader對象,在Volley的圖片加載中,除非你使用最基本的ImageRequest,否則,無論是使用ImageLoader方式還是NetworkImageView方式,都需要一個ImageLoader對象,如果是使用ImageLoader方式,則是調用ImageLoader的get方法,如果使用NetworkImageView方式,則是把ImageLoader作爲參數,從setUrl方法中傳入,但實際上,傳入後的ImageLoader還是調用了get方法,簡單介紹完了之後,我們來分析一個事情,ImagwLoader在這裏實際上是一個加載器的角色,也就是說,當你使用它的時候,實際上是它ImageLoader做了“圖片加載這件事”,在這裏,ImageLoader是一個工作者的角色,因此我們可以簡單設想,我們每次加載圖片的時候是否可以都使用同一個ImageLoader對象,讓這個工作者做多份工作,而不是每次一有圖片加載任務的時候就創建一個ImageLoader,然後當這個任務結束它就“失業”?我懷着這樣的猜想,把ImageLoader放在了Application中,讓整個app全局內只存在這一個ImageLoader對象,每次需要加載圖片的時候,通過我編寫的get方法,去Application中去取,而不是每次都創建一個新的ImagrLoader,事實證明這是可行的。因此,這唯一的一個ImageLoader,就被多次複用,從而變成了一個被“共享”的對象。


Volley畢竟是前幾年的主角,最近兩年剛剛開始學習Android開發的小夥伴也許對它並不熟悉,對上面我舉的例子也沒啥概念。那我現在舉一個如今非常流行的一個網絡庫——Retrofit的例子。

Retrofit的使用我也不多說了,總之就是首先需要創建一個Retrofit對象,然後給每一種請求都創建一個寫有相關參數的接口,然後通過Retrofit對象來創建這些接口的實例,最終通過這些接口的實例來發出網絡請求。Retrofit在這一列事情中的角色是接口實例的創建者,所以它也是一個“工作者”,因此,把它放到Applicaction中就無可厚非了。那接口實例呢?

我們按照之前的流程,先給一個Retrofit髮網絡請求的例子。

首先我們定一個接口:

public interface IGetDiaryService {
        @GET("diary.php")
        Call<Entries> get(@Query("diary_ID") String diaryId);
    }
然後寫一個用來發起網絡請求的方法:

private void getDiary(boolean ifSet) {
        IGetDiaryService service = MyApplication.getRetrofit().create(GetDiaryService.IGetDiaryService.class);
        Call<Entries> call = service.get(entries.getId());
        call.enqueue(new Callback<Entries>() {
            @Override
            public void onResponse(@NonNull Call<Entries> call, @NonNull Response<Entries> response) {
                initView();
                tvTitle.setText(entries.getTitle());
                String content = response.body().getContent();
                tvContent.setText(content);
                tvMonth.setText(DateConversion.MonthConversion(entries.getMonth()));
                tvDate.setText(entries.getDate());
                tvWeek.setText(DateConversion.weekConversion(entries.getWeek()));
                String location = response.body().getLocation();
                tvLocation.setText(location.equals("無") ? "沒有位置信息" : location);
                int imageCount = response.body().getImageCount();
                Glide.with(fragment).load(SpinnerImage.getWeatherList()
                        .get(entries.getWeather() - 1)).into(imWeather);
                Glide.with(fragment).load(SpinnerImage.getMoodList()
                        .get(entries.getMood() - 1)).into(imMood);
                if (ifSet) {
                    entries.setContent(content);
                    entries.setLocation(location);
                    entries.setImageCount(imageCount);
                    MainActivity.setTodayEntries(entries);
                }
                setGridView(imageCount);
            }

            @Override
            public void onFailure(@NonNull Call<Entries> call, @NonNull Throwable throwable) {
                Log.d("日記加載", throwable.getMessage());
                Snackbar.make(recyclerView, "不好意思,加載發生了錯誤,請稍後再試", Snackbar.LENGTH_LONG).show();
            }
        });
    }

這是我寫的一個小demo中的一段代碼,onResponse中的具體邏輯可以忽略。我們可以看到,在這個方法的第一行,就是使用Retrofit創建一個IGetDiaryService接口的實例,這個方法的作用在我的小app中是爲了從服務器加載日記,假如app的用戶多次使用從網絡加載日記這個功能,也就是說他可能會多次調用getDiary這個方法,那麼這個時候,每調用一次,就會創建出一個IGetDiaryService實例,而之前創建的實例由於上一次執行getDiary方法完畢,強引用實效,從而進入到了可回收狀態。如果用戶短時間內多次調用此方法,就會在內存中遺留下一大堆IGetDiaryService的待回收垃圾。這肯定是相對來說不好的。

那我們現在該怎麼辦,使用一個全局變量來緩存IGetDiaryService嗎?這時候我不得不考慮另一個問題,在具體的業務中,app用戶如果使用加載日記這個功能,則有可能在短時間內多次使用,但是一旦用戶不想再看日記了,轉而去做其它操作了,那可能這次使用本app就不會再使用加載日記這個功能了,這時候,如果還一直使用全局變量這種強引用來緩存對象則實際上造成了某種意義上的內存泄漏(一個再也不會被使用的對象,一直佔用內存空間),這時候我們該怎麼辦呢?答案當然是使用軟引用或者弱引用,這裏,我們需要把IGetDiaryService接口包裝一下:

public class GetDiaryService {

    private static SoftReference<IGetDiaryService> serviceSoftReference;

    public static IGetDiaryService getService() {
        if (serviceSoftReference == null || serviceSoftReference.get() == null) {
            IGetDiaryService service = MyApplication.getRetrofit().create(IGetDiaryService.class);
            serviceSoftReference = new SoftReference<>(service);
        }
        return serviceSoftReference.get();
    }

    public interface IGetDiaryService {
        @GET("diary.php")
        Call<Entries> get(@Query("diary_ID") String diaryId);
    }

}
看吧,我們使用一個軟引用保存了IGetDiaryService對象,這樣就較好的避免了上面所說的幾個問題,每個IGetDiaryService對象既可以被複用,又能避免在沒用的時候佔據內存。


我通過以上兩個比較詳細的例子說明了在Android中哪些情況下,應該複用對象,我相信在大量實戰中,我們會共同找到更多可以被複用的對象,這樣,我們把它們都做成共享的,那麼就可以大幅提升性能,特別是那些佔據內存空間大,複用次數又多的對象,意義明顯。


還有最後兩件事情,第一件,我們來總結一下,具體什麼樣的對象是可能可以複用的。首先,就是我上面說的那種,充當“工作者”的對象,它們往往不會通過調用某些方法,來接收大量改變其自身字段的參數,它們的作用一般來說是任務的主要承擔者,它們是“做”任務的,而不是被別人“做”,這類對象通常可以複用。與這類對象相反就有另一種對象,例如,主要是Data類的對象,Data類就是那些在你的app中扮演業務角色的類,例如用戶,文章,評論,等等,它們通常都有大量的字段,幷包含了每個字段的get/set方法,這樣的對象通常都非常有“個性”,比如在你的app中有兩個文章類的對象,但它們顯然是代表兩篇不同的文章,它們有不同的標題,內容,作者等字段,這兩個對象是絕對不容混淆的。像這樣的類的對象,通常是很難找到複用場景的(但這不是絕對的,要看具體場景,例如最上面講的Message就是這種情況的反例,每個Message對象是在用完以後被清除所有字段從而實現複用的)。還有一類對象,它們常常充當“配置項”的角色,例如,在使用RecyclerView時,我們要傳入佈局管理器,還要傳入這個RecyclerView的動畫,而佈局管理器,動畫,等等這些對象就是“配置項”,它們被作爲參數傳入以後,通常只會被別的對象只做“讀”操作,而不會被進行“寫”操作,說白了就是不會改變它們內部的行爲,這樣的對象,在很多情況下也是可以複用的。


最後一件事,在哪些情況下,我這裏舉出的複用對象的方法是不適用的。

首先,如果你的app結構非常複雜,而你要做對象複用,會大量增加新的類和方法,從而導致你的代碼可讀性和結構性大幅降低的情況下,特別是這個對象如果本身佔用內存又不大時,就不要使用;無論在什麼時候,把代碼搞亂都是一個值得慎重考慮的事情,我記得《Effective Java》中說過,最好的優化就是保持代碼的整潔,然後不要優化,當代碼結構清晰,容易閱讀時,性能會隨之而來,當你的程序中有大量刻意優化的代碼時,也許這些被優化過的地方的局部性能會有所提升,但是由於破壞了整體架構,整個程序在執行起來的效率也許會明顯下降,這種以全部局部的做法是相當不值得推崇的。

第二種情況,注意高併發時的資源競爭問題。試想一下,如果被你複用的對象會在高併發場景中被使用,那多個線程就會同時競爭同一個資源從而造成無法想象的錯誤後果。你也許會說,那上鎖不就行了嗎?確實,上鎖能避免造成無法想象的錯誤後果,但是同時,併發也就失效了,因爲同一時刻,這個資源只能被同一個線程使用,其它多個線程都只能等待這個線程使用完畢,當第一個線程使用完畢以後,第二個線程又佔據資源,而剩下的線程又處於等待狀態,這樣一輪又一輪的等待下去,會耽誤大量時間,而併發在這裏由於資源有限,也變得毫無意義。如果你仍然堅持在這種情況下複用對象,我覺得有一個好方法就是使用ThreadLocal,在每個線程內都儲存一個獨立的該對象給每個線程使用,這樣,相對來說是一種兩方權衡的結果,因爲既不會創建出遠多於線程數量的對象,又規避了資源競爭問題,合理的分配了資源。



享元模式也就介紹這麼多了,它只是一種複用對象的思想,至於到底如何複用,就要看你的具體業務邏輯的情況而定啦。








發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章