物件池Pools優化

楊充發表於2019-04-10

目錄介紹

  • 01.什麼是物件池
  • 02.glide哪裡用到物件池
  • 03.多條件key快取bitmap
    • 3.1 多條件key建立
    • 3.2 key值的複用
  • 04.glide物件池總結
  • 05.學以致用物件池
    • 5.1 使用場景
    • 5.2 實現步驟
    • 5.3 物件池使用
    • 5.4 專案實踐分享
  • 06.物件池的容量

好訊息

  • 部落格筆記大彙總【16年3月到至今】,包括Java基礎及深入知識點,Android技術部落格,Python學習筆記等等,還包括平時開發中遇到的bug彙總,當然也在工作之餘收集了大量的面試題,長期更新維護並且修正,持續完善……開源的檔案是markdown格式的!同時也開源了生活部落格,從12年起,積累共計N篇[近100萬字,陸續搬到網上],轉載請註明出處,謝謝!
  • 連結地址:github.com/yangchong21…
  • 如果覺得好,可以star一下,謝謝!當然也歡迎提出建議,萬事起於忽微,量變引起質變!

01.什麼時物件池

  • 物件池作用
    • 在某些時候,我們需要頻繁使用一些臨時物件,如果每次使用的時候都申請新的資源,很有可能會引發頻繁的 gc 而影響應用的流暢性。這個時候如果物件有明確的生命週期,那麼就可以通過定義一個物件池來高效的完成複用物件。
  • 物件池使用場景
    • glide中對載入圖片時頻繁建立物件使用到了物件池。

02.glide使用物件池

  • glide頻繁請求圖片
    • 比如Glide中,每個圖片請求任務,都需要用到類。若每次都需要重新new這些類,並不是很合適。而且在大量圖片請求時,頻繁建立和銷燬這些類,可能會導致記憶體抖動,影響效能。
    • Glide使用物件池的機制,對這種頻繁需要建立和銷燬的物件儲存在一個物件池中。每次用到該物件時,就取物件池空閒的物件,並對它進行初始化操作,從而提高框架的效能。

03.多條件key快取bitmap

3.1 多條件key建立

  • 首先看一個簡單的快取bitmap程式碼,程式碼如下所示
    • 就簡單的通過 HashMap 快取了Bitmap資源,只有在快取不存在時才會執行載入這個耗時操作。但是上面的快取條件十分簡單,是通過圖片的名字決定的,這很大程度上滿足不了實際的需求。可能會出現意想不到的問題……
    private final Map<String, Bitmap> cache = new HashMap<>()
    private void setImage(ImageView iv, String name){
        Bitmap b = cache.get(name);
        if(b == null){
            b = loadBitmap(name);
            cache.put(name, b);
        }
        iv.setImageBitmap(b);
    }
    複製程式碼
  • 多條件 Key
    • 所以我們就需要定義一個Key物件來包含各種快取的條件,例如我們除了圖片名字作為條件,還有圖片的寬度,高度也決定了是否是同一個資源,那麼程式碼將變成如下:
    • 注意多條件key需要重寫equals和hashCode方法。equals注意是比較兩個物件是否相同,而hashCode主要作用是當資料量很大的時候,使用equals一一比較比較會大大降低效率。hashcode實際上是返回物件的儲存地址,如果這個位置上沒有元素,就把元素直接儲存在上面,如果這個位置上已經存在元素,這個時候才去呼叫equal方法與新元素進行比較就可以提高效率呢!
    private final Map<Key, Bitmap> cache = new HashMap<>();
    private void setImage(ImageView iv, String name, int width, int height){
        Key key = new Key(name, width, height);
        Bitmap b = cache.get(key);
        if(b == null){
            b = loadBitmap(name, width, height);
            cache.put(key, b);
        }
        iv.setImageBitmap(b);
    }
    
    public class Key {
    
        private final String name;
        private final int width;
        private final int heifht;
    
        public Key(String name, int width, int heifht) {
            this.name = name;
            this.width = width;
            this.heifht = heifht;
        }
    
        public String getName() {
            return name;
        }
    
        public int getWidth() {
            return width;
        }
    
        public int getHeifht() {
            return heifht;
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            Key key = (Key) o;
            if (width != key.width) {
                return false;
            }
            if (heifht != key.heifht) {
                return false;
            }
            return name != null ? name.equals(key.name) : key.name == null;
        }
    
        @Override
        public int hashCode() {
            int result = name != null ? name.hashCode() : 0;
            final int prime = 31;
            result = prime * result + width;
            result = prime * result + heifht;
            return result;
        }
    }
    複製程式碼

3.2 key值的複用

  • key值的複用是如何操作的
    • 雖然可以支援多條件的快取鍵值了,但是每次查詢快取前都需要建立一個新的 Key 物件,雖然這個 Key 物件很輕量,但是終歸覺得不優雅。gilde原始碼中會提供一個 BitmapPool 來獲取 Bitmap 以避免 Bitmap 的頻繁申請。而 BitmapPool 中 get 方法的簽名是這樣的:
    • image
    • Bitmap 需要同時滿足三個條件(高度、寬度、顏色編碼)都相同時才能算是同一個 Bitmap,那麼內部是如何進行查詢的呢?需要知道的是,BitmapPool 只是一個介面,內部的預設實現是 LruBitmapPool
  • 看LruBitmapPool中get方法
    • 注意重點看這行程式碼:final Bitmap result = strategy.get(width, height, config != null ? config : DEFAULT_CONFIG);
    • strategy 是 LruPoolStrategy 介面型別,檢視其中一個繼承該介面類的 get 方法的實現
      @Override
      @NonNull
      public Bitmap get(int width, int height, Bitmap.Config config) {
        Bitmap result = getDirtyOrNull(width, height, config);
        if (result != null) {
          // Bitmaps in the pool contain random data that in some cases must be cleared for an image
          // to be rendered correctly. we shouldn't force all consumers to independently erase the
          // contents individually, so we do so here. See issue #131.
          result.eraseColor(Color.TRANSPARENT);
        } else {
          result = createBitmap(width, height, config);
        }
    
        return result;
      }
      
      
        @Nullable
      private synchronized Bitmap getDirtyOrNull(
          int width, int height, @Nullable Bitmap.Config config) {
        assertNotHardwareConfig(config);
        // 對於非公共配置型別,配置為NULL,這可能導致轉換以此處請求的配置方式天真地傳入NULL。
        final Bitmap result = strategy.get(width, height, config != null ? config : DEFAULT_CONFIG);
        if (result == null) {
          if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "Missing bitmap=" + strategy.logBitmap(width, height, config));
          }
          misses++;
        } else {
          hits++;
          currentSize -= strategy.getSize(result);
          tracker.remove(result);
          normalize(result);
        }
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
          Log.v(TAG, "Get bitmap=" + strategy.logBitmap(width, height, config));
        }
        dump();
    
        return result;
      }
    複製程式碼
  • 然後看一下SizeConfigStrategy類中的get方法
    • 看一下下面註釋的兩行重點程式碼。同樣也需要一個專門的型別用來描述鍵,但是鍵result居然是也是從一個物件池keyPool中獲取的。
    • 可以看到 Key 是一個可變物件,每次先獲取一個Key物件(可能是池中的,也可能是新建立的),然後把變數初始化。但是大家知道,HashMap 中的 Key 不應該是可變物件,因為如果 Key的 hashCode 發生變化將會導致查詢失效,那麼這裡是如何做到 Key 是可變物件的同時保證能正確的作為 HashMap 中的鍵使用呢?
      @Override
      @Nullable
      public Bitmap get(int width, int height, Bitmap.Config config) {
        int size = Util.getBitmapByteSize(width, height, config);
        Key bestKey = findBestKey(size, config);
        //第一處程式碼
        Bitmap result = groupedMap.get(bestKey);
        if (result != null) {
          decrementBitmapOfSize(bestKey.size, result);
          result.reconfigure(width, height,
              result.getConfig() != null ? result.getConfig() : Bitmap.Config.ARGB_8888);
        }
        return result;
      }
    
      private Key findBestKey(int size, Bitmap.Config config) {
        //第二處程式碼        
        Key result = keyPool.get(size, config);
        for (Bitmap.Config possibleConfig : getInConfigs(config)) {
          NavigableMap<Integer, Integer> sizesForPossibleConfig = getSizesForConfig(possibleConfig);
          Integer possibleSize = sizesForPossibleConfig.ceilingKey(size);
          if (possibleSize != null && possibleSize <= size * MAX_SIZE_MULTIPLE) {
            if (possibleSize != size
                || (possibleConfig == null ? config != null : !possibleConfig.equals(config))) {
              keyPool.offer(result);
              result = keyPool.get(possibleSize, possibleConfig);
            }
            break;
          }
        }
        return result;
      }
      
      @VisibleForTesting
      static class KeyPool extends BaseKeyPool<Key> {
        Key get(int width, int height, Bitmap.Config config) {
          Key result = get();
          result.init(width, height, config);
          return result;
        }
    
        @Override
        protected Key create() {
          return new Key(this);
        }
      }
    複製程式碼
  • 然後看一下groupedMap的程式碼
    • 在查詢時,如果沒有發現命中的值,那麼就會建立新的值,並將其連同 Key 儲存在 HashMap 中,不會對 Key 進行復用。而如果發現了命中的值,也就是說 HashMap 中已經有一個和當前 Key 相同的 Key 物件了,那麼 Key 就可以通過 offer 方法回收到了 KeyPool 中,以待下一次查詢時複用。
      @Nullable
      public V get(K key) {
        LinkedEntry<K, V> entry = keyToEntry.get(key);
        if (entry == null) {
          entry = new LinkedEntry<>(key);
          keyToEntry.put(key, entry);
        } else {
          key.offer();
        }
    
        makeHead(entry);
    
        return entry.removeLast();
      }
    複製程式碼

04.glide物件池總結

  • 優化點
    • 對開銷較大的 Bitmap 進行了複用,就連為了複用Bitmap時重複申請的Key物件都進行了複用,儘可能的減少了物件的建立開銷,保證了應用的流暢性。
  • 為何要多條件key
    • 針對bitmap,載入圖片特別頻繁且多,不建議只是簡單通過一個name圖片名稱作為鍵,因為可能圖片名稱是一樣的,比如有時候介面返回同樣名稱的圖片有大圖,正常圖,縮圖等,那樣可能會儲存重複或者碰撞。但是通過name,還有圖片寬高欄位,就可以大大減小這種問題呢。
  • HashMap中鍵儲存問題
    • 為了正確使用HashMap,選擇恰當的Key是非常重要的。Key在HashMap裡是不可重複的。也就是說這個key物件的hashcode是不能改變的。那麼多條件key是如何保證唯一了,如果要以可變物件作為key的話,那就必須要重寫hashcode和equals方法來達到這個目的,除此之外,別無他法。同時這個時候可以利用keyPool對key物件進行快取。
    • 那麼有人會問,要是key值變化了,怎麼辦?如果HashMap的Key的雜湊值在儲存鍵值對後發生改變,Map可能再也查詢不到這個Entry了。如果Key物件是可變的,那麼Key的雜湊值就可能改變。在HashMap中可變物件作為Key會造成資料丟失。這也就是為何key一般要用string或者int值的緣由呢。

05.學以致用物件池

5.1 使用場景

  • 在寫圖片縮放控制元件的時候,當雙手指滑動時,會頻繁操作讓圖片縮放和移動。這就會頻繁用到變化矩陣Matrix,還有RectF繪畫相關的工具類。為了防止記憶體抖動,所以可以使用物件池順利解決問題。
  • 記憶體抖動是由於在短時間內有大量的物件被建立或者被回收的現象,記憶體抖動出現原因主要是頻繁(很重要)在迴圈裡建立物件(導致大量物件在短時間內被建立,由於新物件是要佔用記憶體空間的而且是頻繁,如果一次或者兩次在迴圈裡建立物件對記憶體影響不大,不會造成嚴重記憶體抖動這樣可以接受也不可避免,頻繁的話就很記憶體抖動很嚴重),它伴隨著頻繁的GC。而我們知道GC太頻繁會大量佔用ui執行緒和cpu資源,會導致app整體卡頓。

5.2 實現步驟

  • 建立抽象ObjectsPool類,由於快取的物件可能是不同的型別,這裡使用泛型T。主要操作是從物件池請求物件的函式,還有釋放物件回物件池的函式。同時可以自己設定物件池的大小,可以使用佇列來實現儲存功能。
    • 程式碼如下:
    /**
     * <pre>
     *     @author yangchong
     *     blog  : https://github.com/yangchong211
     *     time  : 2017/05/30
     *     desc  : 物件池抽象類
     *     revise: 具體使用方法請看:https://github.com/yangchong211/YCGallery
     * </pre>
     */
    public abstract class ObjectsPool<T> {
    
    
        /*
         * 防止頻繁new物件產生記憶體抖動.
         * 由於物件池最大長度限制,如果吞度量超過物件池容量,仍然會發生抖動.
         * 此時需要增大物件池容量,但是會佔用更多記憶體.
         * <T> 物件池容納的物件型別
         */
    
        /**
         * 物件池的最大容量
         */
        private int mSize;
    
        /**
         * 物件池佇列
         */
        private Queue<T> mQueue;
    
        /**
         * 建立一個物件池
         *
         * @param size 物件池最大容量
         */
        public ObjectsPool(int size) {
            mSize = size;
            mQueue = new LinkedList<>();
        }
    
        /**
         * 獲取一個空閒的物件
         *
         * 如果物件池為空,則物件池自己會new一個返回.
         * 如果物件池內有物件,則取一個已存在的返回.
         * take出來的物件用完要記得呼叫given歸還.
         * 如果不歸還,讓然會發生記憶體抖動,但不會引起洩漏.
         *
         * @return 可用的物件
         *
         * @see #given(Object)
         */
        public T take() {
            //如果池內為空就建立一個
            if (mQueue.size() == 0) {
                return newInstance();
            } else {
                //物件池裡有就從頂端拿出來一個返回
                return resetInstance(mQueue.poll());
            }
        }
    
        /**
         * 歸還物件池內申請的物件
         * 如果歸還的物件數量超過物件池容量,那麼歸還的物件就會被丟棄
         *
         * @param obj 歸還的物件
         *
         * @see #take()
         */
        public void given(T obj) {
            //如果物件池還有空位子就歸還物件
            if (obj != null && mQueue.size() < mSize) {
                mQueue.offer(obj);
            }
        }
    
        /**
         * 例項化物件
         *
         * @return 建立的物件
         */
        abstract protected T newInstance();
    
        /**
         * 重置物件
         *
         * 把物件資料清空到就像剛建立的一樣.
         *
         * @param obj 需要被重置的物件
         * @return 被重置之後的物件
         */
        abstract protected T resetInstance(T obj);
    
    }
    複製程式碼
  • 然後,可以定義一個矩陣物件池,需要實現上面的抽象方法。如下所示
    public class MatrixPool extends ObjectsPool<Matrix>{
    
        /**
         * 矩陣物件池
         */
        public MatrixPool(int size) {
            super(size);
        }
    
        @Override
        protected Matrix newInstance() {
            return new Matrix();
        }
    
        @Override
        protected Matrix resetInstance(Matrix obj) {
            obj.reset();
            return obj;
        }
    }
    複製程式碼

5.3 物件池使用

  • 至於使用,一般是獲取矩陣物件,還有歸還矩陣物件。
    /**
     * 矩陣物件池
     */
    private static MatrixPool mMatrixPool = new MatrixPool(16);
    
    /**
     * 獲取矩陣物件
     */
    public static Matrix matrixTake() {
        return mMatrixPool.take();
    }
    
    /**
     * 獲取某個矩陣的copy
     */
    public static Matrix matrixTake(Matrix matrix) {
        Matrix result = mMatrixPool.take();
        if (matrix != null) {
            result.set(matrix);
        }
        return result;
    }
    
    /**
     * 歸還矩陣物件
     */
    public static void matrixGiven(Matrix matrix) {
        mMatrixPool.given(matrix);
    }
    複製程式碼
  • 注意事項
    • 如果物件池為空,則物件池自己會new一個返回。如果物件池內有物件,則取一個已存在的返回。take出來的物件用完要記得呼叫given歸還,如果不歸還,仍然會發生記憶體抖動,但不會引起洩漏。

5.4 專案實踐分享

  • 避免發生記憶體抖動的幾點建議:
    • 儘量避免在迴圈體內建立物件,應該把物件建立移到迴圈體外。
    • 注意自定義View的onDraw()方法會被頻繁呼叫,所以在這裡面不應該頻繁的建立物件。
    • 當需要大量使用Bitmap的時候,試著把它們快取在陣列中實現複用。
    • 對於能夠複用的物件,同理可以使用物件池將它們快取起來。
  • 大多數物件的複用,最終實施的方案都是利用物件池技術,要麼是在編寫程式碼的時候顯式的在程式裡面去建立物件池,然後處理好複用的實現邏輯,要麼就是利用系統框架既有的某些複用特性達到減少物件的重複建立,從而減少記憶體的分配與回收。
  • 圖片縮放案例:github.com/yangchong21…

06.物件池的容量

  • 通常情況下,我們需要控制物件池的大小
    • 如果物件池沒有限制,可能導致物件池持有過多的閒置物件,增加記憶體的佔用
    • 如果物件池閒置過小,沒有可用的物件時,會造成之前物件池無可用的物件時,再次請求出現的問題
    • 物件池的大小選取應該結合具體的使用場景,結合資料(觸發池中無可用物件的頻率)分析來確定。
  • 使用物件池也是要有一定代價的:短時間內生成了大量的物件佔滿了池子,那麼後續的物件是不能複用的。

其他介紹

01.關於部落格彙總連結

02.關於我的部落格

03.參考部落格

物件池優化綜合案例:github.com/yangchong21…

物件池優化縮放圖片案例:github.com/yangchong21…

相關文章