对象池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…

相关文章
相关标签/搜索