Android 对象池的使用

摘要

探讨了在短时间内频繁创建对象可能导致内存抖动和GC频繁的问题,提出对象池技术作为解决方案。对象池是一种预先创建并存储对象的技术,它允许重复利用对象而非每次都进行new操作,从而减少性能损耗。文中介绍了实现对象获取与释放的Pool<T>接口及其两个具体实现类:非线程安全的SimplePool和线程安全的SynchronizedPoolSimplePool采用数组存储对象,按需初始化,并提供懒加载机制;而SynchronizedPool则在此基础上增加了同步锁以确保多线程环境下的安全性。最后总结了对象池的优势和适用场景,强调对于包含大量资源初始化工作的对象以及多线程环境中,使用对象池可以节省资源初始化时间,但也指出了其可能带来的状态恢复开销和线程同步成本。

为什么会有对象池

在日常开发的时候,常常会遇到需要段时间内频繁创建对象的这种情形,会频繁出现 new 操作,这就会导致 内存抖动,从而导致 GC 频繁发生,进而产生性能上的问题,比如 UI卡顿等。这个时候使用 对象池技术就可以一定程度上解决这个问题。

所谓对象池,就是一个存放对象的池子,既然是池子,肯定会有自己的最大容量,即最多可以放多少个对象,还有就是有方法从池子中获取对象,同样也有方法往池子中放入对象。

放入对象 或取出对象的实现

对象的获取和对象的释放通过 Pool<T>接口来实现,具体接口如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface Pool<T> {

/**
* 从对象池中获取一个对象
*/
@Nullable
T acquire();

/**
* 释放一个对象(将对象重新放回对象池)
*
* @param instance 对象实例
* @return 对象是否成功放入到了对象池
*
* @throws IllegalStateException 如果对象已经在池子里就抛出异常
*/
boolean release(@NonNull T instance);
}

简单看一下其实现类,共有两个:SimplePoolSynchronizedPool,前者未采用锁(即多线程不安全的),后者对取对象和放入对象都进行了锁操作,是线程安全的。

SimplePool

对象池的简单实现,实现了Pool<T>接口,用一个数组承载对象,并采用了懒加载的方式(不是一开始就创建 若干个对象,而是在获取之前需要先向 池子 中添加对象)代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public static class SimplePool<T> implements Pool<T> {
private final Object[] mPool;

private int mPoolSize;

/**
* Creates a new instance.
*
* @param maxPoolSize The max pool size.
*
* @throws IllegalArgumentException If the max pool size is less than zero.
*/
public SimplePool(int maxPoolSize) {
if (maxPoolSize <= 0) {
throw new IllegalArgumentException("The max pool size must be > 0");
}
mPool = new Object[maxPoolSize];
}

@Override
@SuppressWarnings("unchecked")
public T acquire() {
if (mPoolSize > 0) {
final int lastPooledIndex = mPoolSize - 1;
T instance = (T) mPool[lastPooledIndex];
mPool[lastPooledIndex] = null;
mPoolSize--;
return instance;
}
return null;
}

@Override
public boolean release(@NonNull T instance) {
if (isInPool(instance)) {
throw new IllegalStateException("Already in the pool!");
}
if (mPoolSize < mPool.length) {
mPool[mPoolSize] = instance;
mPoolSize++;
return true;
}
return false;
}

private boolean isInPool(@NonNull T instance) {
for (int i = 0; i < mPoolSize; i++) {
if (mPool[i] == instance) {
return true;
}
}
return false;
}
}

说明:

  1. 根据SimplePool的实现,如果一开始就调用 acquire()方法,返回的肯定是空对象,这说明要使用 SimplePool 的话,需要先调用 release(@NonNull T instance)方法将对象先放入到对象池中;

  2. SimplePool中的对象并不是一次性都创建出来的,而是每次外部调用 release(@NonNull T instance)才加入到内存中的;

  3. 不在对象池中的对象才能调用release放入对象池中,否则会抛出IllegalStateException

SynchronizedPool

这个类继承自 SimplePool,和它的主要区别就是对 acquirerelease 操作加锁了,所以如果多线程中使用对象池,最好使用这个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Synchronized) pool of objects.
*
* @param <T> The pooled type.
*/
public static class SynchronizedPool<T> extends SimplePool<T> {
private final Object mLock = new Object();

public SynchronizedPool(int maxPoolSize) {
super(maxPoolSize);
}

@Override
public T acquire() {
synchronized (mLock) {
return super.acquire();
}
}

@Override
public boolean release(@NonNull T element) {
synchronized (mLock) {
return super.release(element);
}
}
}

总结

使用对象池有什么好处呢,其实我觉得这个玩意跟线程池是差不多的,只不过线程池 功能更明确,只产生线程对象(因为线程的创建是十分昂贵的)。同理,对于对象池,如果对象中存在较多的资源初始化工作,这个时候对象复用很有必要,毕竟节省了大量的资源初始化时间,但是如果对象有很多 运行时需要改变的状态,就不建议使用对象池了,因为如果对象有很多运行时改变的状态,那么在重新放入对象池或者重新从对象池取出使用时,就会存在状态的恢复开销(已经使用过的对象需要reset到初始态)。

另外,多线程场景中使用对象池也是有代价的,需要进行线程同步操作,这部分的操作可能比 使用对象池带来的收益还大,得不偿失。