對象池Pools優化

目錄介紹

  • 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萬字,陸續搬到網上],轉載請註明出處,謝謝!
  • 鏈接地址:https://github.com/yangchong2...
  • 如果覺得好,可以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的時候,試着把它們緩存在數組中實現複用。
    • 對於能夠複用的對象,同理可以使用對象池將它們緩存起來。
  • 大多數對象的複用,最終實施的方案都是利用對象池技術,要麼是在編寫代碼的時候顯式的在程序裏面去創建對象池,然後處理好複用的實現邏輯,要麼就是利用系統框架既有的某些複用特性達到減少對象的重複創建,從而減少內存的分配與回收。
  • 圖片縮放案例:https://github.com/yangchong2...

06.對象池的容量

  • 通常情況下,我們需要控制對象池的大小

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

其他介紹

01.關於博客彙總鏈接

02.關於我的博客

03.參考博客

對象池優化綜合案例:https://github.com/yangchong2...

對象池優化縮放圖片案例:https://github.com/yangchong2...

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