對象池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萬字,陸續搬到網上],轉載請註明出處,謝謝!
  • 連接地址: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…

相關文章
相關標籤/搜索