对象池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/yangchong211/YCBlogs
- 如果觉得好,可以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/yangchong211/YCZoomImage
06.对象池的容量
- 通常情况下,我们需要控制对象池的大小
- 如果对象池没有限制,可能导致对象池持有过多的闲置对象,增加内存的占用
- 如果对象池闲置过小,没有可用的对象时,会造成之前对象池无可用的对象时,再次请求出现的问题
- 对象池的大小选取应该结合具体的使用场景,结合数据(触发池中无可用对象的频率)分析来确定。
- 使用对象池也是要有一定代价的:短时间内生成了大量的对象占满了池子,那么后续的对象是不能复用的。
其他介绍
01.关于博客汇总链接
02.关于我的博客
- 我的个人站点:www.yczbj.org,www.ycbjie.cn
- github:https://github.com/yangchong211
- 知乎:https://www.zhihu.com/people/yczbj/activities
- 简书:http://www.jianshu.com/u/b7b2c6ed9284
- csdn:http://my.csdn.net/m0_37700275
- 喜马拉雅听书:http://www.ximalaya.com/zhubo/71989305/
- 开源中国:https://my.oschina.net/zbj1618/blog
- 泡在网上的日子:http://www.jcodecraeer.com/member/content_list.php?channelid=1
- 邮箱:yangchong211@163.com
- 阿里云博客:https://yq.aliyun.com/users/article?spm=5176.100- 239.headeruserinfo.3.dT4bcV
- segmentfault头条:https://segmentfault.com/u/xiangjianyu/articles
- 掘金:https://juejin.im/user/5939433efe88c2006afa0c6e
03.参考博客
对象池优化综合案例:https://github.com/yangchong211/LifeHelper
对象池优化缩放图片案例:https://github.com/yangchong211/YCZoomImage
对象池Pools优化的更多相关文章
- 对象池2(方法功能)Pools
对象池Pools(主要调用方法功能) namespace kernal { public class Pools : MonoBehaviour { [HideInInspector] public ...
- Unity 游戏框架搭建 (十九) 简易对象池
在Unity中我们经常会用到对象池,使用对象池无非就是解决两个问题: 一是减少new时候寻址造成的消耗,该消耗的原因是内存碎片. 二是减少Object.Instantiate时内部进行序列化和反序列化 ...
- netty源码分析 - Recycler 对象池的设计
目录 一.为什么需要对象池 二.使用姿势 2.1 同线程创建回收对象 2.2 异线程创建回收对象 三.数据结构 3.1 物理数据结构图 3.2 逻辑数据结构图(重要) 四.源码分析 4.2.同线程获取 ...
- Unity实现”对象池管理器“
前言:警告!这可能是坨屎,空闲时间写成,仅作娱乐 在Unity中生成或销毁一个物体会占用较大的资源,如果是制作FPS射击游戏,子弹生成更是雪上加霜.所以我自己写了一个PoolManager,不能和网上 ...
- simple_pool对象池——优化<二>
本文章由cartzhang编写.转载请注明出处. 所有权利保留. 文章链接:http://blog.csdn.net/cartzhang/article/details/55051570 作者:car ...
- Unity性能优化-对象池
1.对象池Object Pool的原理: 有些GameObject是在游戏中需要频繁生成并销毁的(比如射击游戏中的子弹),以前的常规做法是:Instantiate不断生成预设件Prefab,然后采用碰 ...
- Unity 对象池的使用
在游戏开发过程中,我们经常会遇到游戏发布后,测试时玩着玩着明显的感觉到有卡顿现象.出现这种现象的有两个原因:一是游戏优化的不够好或者游戏逻辑本身设计的就有问题,二是手机硬件不行.好吧,对于作为程序员的 ...
- String字符串针对常量池的优化
String对象是java语言中重要的数据类型,但是不是基本数据类型.相对于c语言的char java做了一些封装和延伸. 针对常量池的优化:当两个String拥有相同的值时,它们只引用常量池中的同一 ...
- Egret中的对象池ObjectPool
为了可以让对象复用,防止大量重复创建对象,导致资源浪费,使用对象池来管理. 对象池具体含义作用,自行百度. 一 对象池A 二 对象池B 三 字符串key和对象key的效率 一 对象池A /** * 对 ...
- paip.提升性能----数据库连接池以及线程池以及对象池
paip.提升性能----数据库连接池以及线程池以及对象池 目录:数据库连接池c3po,线程池ExecutorService:Jakartacommons-pool对象池 作者Attilax 艾龙, ...
随机推荐
- Linux-wget命令使用及参数详解
wget简介 Linux系统中的wget是一个下载文件的工具,它用在命令行下.对于Linux用户是必不可少的工具,我们经常要下载一些软件或从远程服务器恢复备份到本地服务器.wget支持HTTP,HTT ...
- K8S部署之VMWare网络拓扑踩坑
目录 背景 VMWare 虚拟网络 安装 Ubuntu Server 20.04 时遇到的网络问题 解决方法和解释 总结 背景 知乎上最近发现一篇好文 图解K8S(01):基于Ubuntu 20.04 ...
- Asp .Net Core 系列:Asp .Net Core 集成 Panda.DynamicWebApi
目录 简介 Asp .Net Core 集成 Panda.DynamicWebApi 配置 原理 什么是POCO Controller? POCO控制器原理 ControllerFeatureProv ...
- ABC 340
忘记打了,VP 了一把,前五题都是板子. F 题意:坐标系上给定一个整点 \((x,y)\),求另一个整点 \((a,b)\),满足 \((0,0),(x,y),(a,b)\) 组成的三角形面积为 \ ...
- 掌握C语言文件操作:从入门到精通的完整指南!
欢迎大家来到贝蒂大讲堂 养成好习惯,先赞后看哦~ 所属专栏:C语言学习 贝蒂的主页:Betty's blog 1. 什么是文件 文件其实是指一组相关数据的有序集合.这个数据集有一个名称,叫做文件名.文 ...
- AT_abc270_g [ABC270G] Sequence in mod P 题解
题目传送门 前置知识 大步小步算法 解法 递推式为 \(x_{n}=(ax_{n-1}+b) \bmod p\),发现可以统一消去 \(\bmod p\) ,只在最后参与计算.以下过程省去模运算. 当 ...
- NC20240 [SCOI2005]互不侵犯KING
题目链接 题目 题目描述 在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案. 国王能攻击到它上下左右,以及左上 左下右上右下八个方向上附近的各一个格子,共8个格子. 输入描述 只有一行 ...
- Shiro 框架的MD5加密算法实现原理
直接上代码:该代码可以直接用于项目中做MD5加密,加盐加密,多层散列加密 import java.io.UnsupportedEncodingException; import java.securi ...
- VueRouter导航守卫
VueRouter导航守卫 vue-router提供的导航守卫主要用来通过跳转或取消的方式守卫导航,简单来说导航守卫就是路由跳转过程中的一些钩子函数,路由跳转是一个大的过程,这个大的过程分为跳转前中后 ...
- SpringCloud Config配置中心实战
介绍 本文以理论结合实践编写,篇幅较长,各位看官保持耐心:),部分内容引用自网络. 什么是配置中心? 当微服务过多的时候,每个微服务的配置很难集中管理.SpringCloud Config通过git代 ...