对象池Pools优化

40次阅读

共计 9413 个字符,预计需要花费 24 分钟才能阅读完成。

目录介绍

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 方法的签名是这样的:

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. 关于博客汇总链接

1. 技术博客汇总

2. 开源项目汇总

3. 生活博客汇总

4. 喜马拉雅音频汇总

5. 其他汇总

02. 关于我的博客

我的个人站点:www.yczbj.org,www.ycbjie.cn
github:https://github.com/yangchong211

知乎:https://www.zhihu.com/people/…

简书:http://www.jianshu.com/u/b7b2…

csdn:http://my.csdn.net/m0_37700275

喜马拉雅听书:http://www.ximalaya.com/zhubo…

开源中国:https://my.oschina.net/zbj161…

泡在网上的日子:http://www.jcodecraeer.com/me…

邮箱:yangchong211@163.com
阿里云博客:https://yq.aliyun.com/users/a… 239.headeruserinfo.3.dT4bcV
segmentfault 头条:https://segmentfault.com/u/xi…

掘金:https://juejin.im/user/593943…

03. 参考博客
https://www.jianshu.com/p/b74…
对象池优化综合案例:https://github.com/yangchong2…

对象池优化缩放图片案例:https://github.com/yangchong2…

正文完
 0