Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> java/android 設計模式學習筆記(5)---對象池模式

java/android 設計模式學習筆記(5)---對象池模式

編輯:關於Android編程

這次要介紹一下對象池模式(Object Pool Pattern),這個模式為常見 23 種設計模式之外的設計模式,介紹的初衷主要是在平時的 android 開發中經常會看到,比如 ThreadPool 和 MessagePool 等。

在 java 中,所有對象的內存由虛擬機管理,所以在某些情況下,需要頻繁創建一些生命周期很短使用完之後就可以立即銷毀,但是數量很大的對象集合,那麼此時 GC 的次數必然會增加,這時候為了減小系統 GC 的壓力,對象池模式就很適用了。對象池模式也是創建型模式之一,它不是根據使用動態的分配和銷毀內存,而是維護一個已經初始化好若干對象的對象池以供使用。客戶端使用的時候從對象池中去申請一個對象,當該對象使用完之後,客戶端會返回給對象池,而不是立即銷毀它,這步操作可以手動或者自動完成。

從 Java 語言的特性來分析一下,在 Java 中,對象的生命周期大致包括三個階段:對象的創建,對象的使用,對象的清除。因此,對象的生命周期長度可用如下的表達式表示:T = T1 + T2 +T3。其中T1表示對象的創建時間,T2 表示對象的使用時間,而 T3 則表示其清除時間。由此,我們可以看出,只有 T2 是真正有效的時間,而 T1、T3 則是對象本身的開銷。下面再看看 T1、T3 在對象的整個生命周期中所占的比例。Java對象是通過構造函數來創建的,在這一過程中,該構造函數鏈中的所有構造函數也都會被自動調用。另外,默認情況下,調用類的構造函數時,Java 會把變量初始化成確定的值:所有的對象被設置成 null,整數變量(byte、short、int、long)設置成 0, float 和 double 變量設置成 0.0,邏輯值設置成false。所以用new關鍵字來新建一個對象的時間開銷是很大的,如下表所示:

運算操作 示例 標准化時間 本地賦值 i = n 1.0 實例賦值 this.i = n 1.2 方法調用 Funct() 5.9 新建對象 New Object() 980 新建數組 New int[10] 3100

從表中可以看出,新建一個對象需要980個單位的時間,是本地賦值時間的980倍,是方法調用時間的166倍,而若新建一個數組所花費的時間就更多了。

再看清除對象的過程,我們知道,Java 語言的一個優勢,就是 Java 程序員勿需再像 C/C++ 程序員那樣,顯式地釋放對象,而由稱為垃圾收集器(Garbage Collector)的自動內存管理系統,定時或在內存凸現出不足時,自動回收垃圾對象所占的內存。凡事有利總也有弊,這雖然為 Java 程序設計者提供了極大的方便,但同時它也帶來了較大的性能開銷。這種開銷包括兩方面,首先是對象管理開銷,GC為了能夠正確釋放對象,它必須監控每一個對象的運行狀態,包括對象的申請、引用、被引用、賦值等。其次,在 GC 開始回收“垃圾”對象時,系統會暫停應用程序的執行,而獨自占用 CPU。

因此,如果要改善應用程序的性能,一方面應盡量減少創建新對象的次數;同時,還應盡量減少 T1、T3 的時間,而這些均可以通過對象池技術來實現。所以對象池主要是用來提升性能,在某些情況下,對象池對性能有極大的幫助。但是還有一點需要注意,對象池會增加對象生命周期的復雜度,這是因為從對象池獲取的對象和返還給對象池的對象都沒有真正的創建或者銷毀。

特點

從上面的介紹可以總結:對象池模式適用於“需要使用到大量的同一類對象,這些對象的初始化會消耗大量的系統資源,而且它們只需要使用很短的時間,這種操作會對系統的性能有一定影響”的情況,總結一下就是在下面兩種分配模式下可以選擇使用對象池:

對象以固定的速度不斷地分配,垃圾收集時間逐步增加,內存使用率隨之增大;對象分配存在爆發期,而每次爆發都會導致系統遲滯,並伴有明顯的GC中斷。在絕大多數情況下,這些對象要麼是數據容器,要麼是數據封裝器,其作用是在應用程序和內部消息總線、通信層或某些API之間充當一個信封。這很常見。例如,數據庫驅動會針對每個請求和響應創建 Request 和 Response 對象,消息系統會使用 Message 和 Event 封裝器,等等。對象池可以幫助保存和重用這些構造好的對象實例。
  對象池模式會預先創建並初始化好一個對象集合用於重用,當某處需要一個該類型的新對象時,它直接向對象池申請,如果此時對象池中有已經預先初始化好的對象,它就直接返回,如果沒有,對象池就會創建一個並且返回;當使用完該對象之後,它會將該對象返還給對象池,對象池會將其重用以避免該對象笨重的初始化操作。有一點需要注意的是,一旦一個對象被對象池重用返回給需要使用的地方之後,該對象已經存在引用都將會變成非法不可使用。需要注意的是,在一些系統資源很緊缺的系統上,對象池模式將會限制對象池最大的大小,當已經達到最大的數量之後,再次獲取可能會拋出異常或者直接被阻塞直到有一個對象被對象池回收。

 

UML類圖

這裡寫圖片描述vcfJqzwvcD4NClJldXNhYmxlo7q21M/zwOCjrLjDttTP89TayrW8ysq508PW0NDo0qrGtbextcS0tL2ous3P+rvZo6yyosfSs/XKvLuvstnX97vhuty63M/7usTKsbzkus3Q1MTco7tDbGllbnSjusq508MgUmV1c2FibGUgwOC21M/ztcS9x8mro7tSZXVzYWJsZVBvb2yjurncwO0gUmV1c2FibGUgttTP88DgtcTL+dPQttTP86OsuakgQ2xpZW50IL3HyavKudPDoaOhoaGhzaizo8fpv/bPwqOsztLDx8+jzfvL+dPQtcQgUmV1c2FibGUgttTP87a8u+GxuyBSZXN1YWJsZVBvb2wgudzA7aOsy/nS1L/J0tTKudPDPGEgaHJlZj0="http://blog.csdn.net/self_study/article/details/50835410">單例模式構建 ReusablePool,當然其他的工廠方法模式也是可以的 。Client 調用 getInstance 方法獲取到 ReusablePool 的對象,接著調用 acquireReusable 方法去獲取 Reusable 對象,使用完之後調用 releaseReusable 方法傳入該對象重新交給 ReusablePool 管理。

 

示例與源碼

  我們先以 android 源碼中的 MessagePool 為例子來分析一下 ObjectPool 在實際開發中的使用效果,之後寫一個小的 demo 。

MessagePool源碼分析

在 android 中使用 new Message() , Message.obtain() 和 Handler.obtainMessage() 都能夠獲得一個 Message 對象,但是為什麼後兩個的效率會高於前者呢?先來看看源碼的 Message.obtain() 函數:

/**
 * Return a new Message instance from the global pool. Allows us to
 * avoid allocating new objects in many cases.
 */
public static Message obtain() {
    synchronized (sPoolSync) {
        if (sPool != null) {
            Message m = sPool;
            sPool = m.next;
            m.next = null;
            m.flags = 0; // clear in-use flag
            sPoolSize--;
            return m;
        }
    }
    return new Message();
}

返回的是 sPool 這個對象,繼續看看 Handler.obtainMessage() 函數:

/**
 * Returns a new {@link android.os.Message Message} from the global message pool. More efficient than
 * creating and allocating new instances. The retrieved message has its handler set to this instance (Message.target == this).
 *  If you don't want that facility, just call Message.obtain() instead.
 */
public final Message obtainMessage()
{
    return Message.obtain(this);
}

一樣是調用到了 Message.obtain() 函數,那麼我們就從這個函數開始分析, Message 類是如何構建 MessagePool 這個角色的呢?看看這幾處的代碼:

//變量的構建
private static final Object sPoolSync = new Object();
private static Message sPool;
private static int sPoolSize = 0;

private static final int MAX_POOL_SIZE = 50;

private static boolean gCheckRecycle = true;

Message 對象的回收

/**
 * Return a Message instance to the global pool.
 * 

* You MUST NOT touch the Message after calling this function because it has * effectively been freed. It is an error to recycle a message that is currently * enqueued or that is in the process of being delivered to a Handler. *

*/ public void recycle() { if (isInUse()) { if (gCheckRecycle) { throw new IllegalStateException("This message cannot be recycled because it " + "is still in use."); } return; } recycleUnchecked(); } /** * Recycles a Message that may be in-use. * Used internally by the MessageQueue and Looper when disposing of queued Messages. */ void recycleUnchecked() { // Mark the message as in use while it remains in the recycled object pool. // Clear out all other details. flags = FLAG_IN_USE; what = 0; arg1 = 0; arg2 = 0; obj = null; replyTo = null; sendingUid = -1; when = 0; target = null; callback = null; data = null; synchronized (sPoolSync) { if (sPoolSize < MAX_POOL_SIZE) { next = sPool; sPool = this; sPoolSize++; } } }

從這幾處代碼可以很清楚的看到:

sPool 這個靜態對象實際上是相當於構建了一個長度為 MAX_POOL_SIZE 的鏈表, recycleUnchecked 方法以內置鎖的方式(線程安全),判斷當前對象池的大小是否小於50,若小於50,則會被加到鏈表的頭部,並把 next 對象指向上一次鏈表的頭部;若大於等於50,則直接丟棄掉,那麼這些被丟棄的Message將交由GC處理。recycleUnchecked 方法將待回收的Message對象字段先置空,以便節省 MessagePool 的內存和方便下一次的直接使用;源碼這麼一看, android 中 MessagePool 其實實現方式還是很簡單的。

 

示例

我們參照上面的 uml 類圖寫一個 demo,首先要定義 Reusable 這個角色,為了直觀,我們將該類定義了很多成員變量,用來模擬 expensive initialization:
Reusable.class

public class Reusable {

    public String a;
    public String b;
    public String c;
    public String d;
    public String e;
    public String f;
    public String g;
    public ArrayList h;
    public ArrayList i;
    public ArrayList j;
    public ArrayList k;
    public ArrayList l;


    public Reusable(){
        h = new ArrayList<>();
        i = new ArrayList<>();
        j = new ArrayList<>();
        k = new ArrayList<>();
        l = new ArrayList<>();
    }
}

然後用一個 IReusablePool.class接口來定義對象池的基本行為:

public interface IReusablePool {
    Reusable requireReusable();
    void releaseReusable(Reusable reusable);
    void setMaxPoolSize(int size);
}

最後實現該接口:

public class ReusablePool implements IReusablePool {
    private static final String TAG = "ReusablePool";

    private static volatile ReusablePool instance = null;

    private static List available = new ArrayList<>();
    private static List inUse = new ArrayList<>();

    private static final byte[] lock = new byte[]{};
    private static int maxSize = 5;
    private int currentSize = 0;

    private ReusablePool() {
        available = new ArrayList<>();
        inUse = new ArrayList<>();
    }

    public static ReusablePool getInstance() {
        if (instance == null) {
            synchronized (ReusablePool.class) {
                if (instance == null) {
                    instance = new ReusablePool();
                }
            }
        }
        return instance;
    }

    @Override
    public Reusable requireReusable() {
        synchronized (lock) {
            if (currentSize >= maxSize) {
                throw new RuntimeException("pool has gotten its maximum size");
            }

            if (available.size() > 0) {
                Reusable reusable = available.get(0);
                available.remove(0);
                currentSize++;
                inUse.add(reusable);
                return reusable;
            } else {
                Reusable reusable = new Reusable();
                inUse.add(reusable);
                currentSize++;
                return reusable;
            }
        }
    }

    @Override
    public void releaseReusable(Reusable reusable) {
        if (reusable != null) {
            reusable.a = null;
            reusable.b = null;
            reusable.c = null;
            reusable.d = null;
            reusable.e = null;
            reusable.f = null;
            reusable.g = null;
            reusable.h.clear();
            reusable.i.clear();
            reusable.j.clear();
            reusable.k.clear();
            reusable.l.clear();
        }

        synchronized (lock) {
            inUse.remove(reusable);
            available.add(reusable);
            currentSize--;
        }
    }

    @Override
    public void setMaxPoolSize(int size) {
        synchronized (lock) {
            maxSize = size;
        }
    }
}

最後測試程序:

@Override
public void onClick(View v) {
    switch (v.getId()) {
        case R.id.btn_get:
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Reusable reusable = ReusablePool.getInstance().requireReusable();
                        Log.e("CORRECT", "get a Reusable object " + reusable);
                        Thread.sleep(5000);
                        ReusablePool.getInstance().releaseReusable(reusable);
                    }catch (Exception e){
                        Log.e("ERROR", e.getMessage());
                    }
                }
            }).start();
            break;
    }
}

需要說明的是:

releaseReusable 方法不要忘記將 Reusable 對象中的成員變量重置,要不然下次使用會有問題;模擬了 5s 的延時,在 demo 中如果達到了 maxPoolSize ,繼續操作是拋出 RunTimeException ,這個在實際使用該過程中可以按照情況更改(可以拋出異常,新建一個對象返回或者直接在多線程模式下阻塞,直到有新對象,參考鏈接:
https://en.wikipedia.org/wiki/Object_pool_pattern);在程序中使用的是單例模式獲取的 ReusablePool 對象,這個在實際使用過程中也可以換成工廠方法模式等的其他設計模式。

 

總結

對象池模式從上面來分析可以說是用處很大,但是這個模式一定要注意使用的場景,也就是最上面提到的兩點:“對象以固定的速度不斷地分配,垃圾收集時間逐步增加,內存使用率隨之增大”和“對象分配存在爆發期,而每次爆發都會導致系統遲滯,並伴有明顯的GC中斷”,其他的一般情況下最好不要使用對象池模式,我們後面還會提到它的缺點和很多人對其的批判。

優點

  優點上面就已經闡述的很清楚了,對於那些”the rate of instantiation and destruction of a class is high” 和 “the cost of initializing a class instance is high”的場景優化是及其明顯的,例如 database connections,socket connections,threads 和類似於 fonts 和 Bitmap 之類的對象。

陷阱

上面已經提過了,使用對象池模式時,每次進行回收操作前都需要將該對象的相關成員變量重置,如果不重置將會導致下一次重復使用該對象時候出現預料不到的錯誤,同時的,如果回收對象的成員變量很大,不重置還可能會出現內存 OOM 和信息洩露等問題。另外的,對象池模式絕大多數情況下都是在多線程中訪問的,所以做好同步工作也是極其重要的。原文:https://en.wikipedia.org/wiki/Object_pool_pattern
  除了以上兩點之外,還需要注意以下問題:

引用洩露:對象在系統中某個地方注冊了,但沒有返回到池中。過早回收:消費者已經決定將對象返還給對象池,但仍然持有它的引用,並試圖執行寫或讀操作,這時會出現這種情況。隱式回收:當使用引用計數時可能會出現這種情況。大小錯誤:這種情況在使用字節緩沖區和數組時非常常見:對象應該有不同的大小,而且是以定制的方式構造,但返回對象池後卻作為通用對象重用。重復下單:這是引用洩露的一個變種,存在多路復用時特別容易發生:一個對象被分配到多個地方,但其中一個地方釋放了該對象。就地修改:對象不可變是最好的,但如果不具備那樣做的條件,就可能在讀取對象內容時遇到內容被修改的問題。縮小對象池:當池中有大量的未使用對象時,要縮小對象池。對於如何根據使用情況放大和縮小對象池的大小,一個廣為人知但鮮有人用的技巧:對象池 這篇文章中講述的很清楚,感興趣的可以看看。

 

討論與批判

一些開發者不建議在某些語言例如 Java 中使用對象池模式,特別是對象只使用內存並且不會持有其他資源的語言。這些反對者持有的觀點是 new 的操作只需要 10 條指令,而使用對象池模式則需要成百上千條指令,明顯增加了復雜度,而且 GC 操作只會去掃描“活著”的對象引用所指向的內存,而不是它們的成員變量所使用的那塊內存,這就意味著,任何沒有引用的“死亡”對象都能夠被 GC 以很小的代價跳過,相反如果使用對象池模式,持有大量“活著“的對象反而會增加 GC 的時間。原文:https://en.wikipedia.org/wiki/Object_pool_pattern

源碼下載

  https://github.com/zhaozepeng/Design-Patterns/tree/master/ObjectPoolPattern

 
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved