文章目录
内存性能优化简介:
-
Android系统通过在Android Lollipop替换Dalvik为ART,且在Android N添加JIT的方式提升编译安装性能
-
对象内存管理,避免内存抖动、内存泄漏
-
按需要提供变量的基本数据类型
-
避免自动装箱拆箱的转换
-
在某些场景使用Android提供的SparseArray集合组和ArrayMap代替HashMap能达到内存高效
-
尽量使用for each循环
-
使用Annotation @IntDef实现枚举
-
常量使用static final声明能节约内存
-
使用StringBuilder或StringBuffer拼接字符串
-
生命周期内不变的变量声明为本地变量对象,不在方法内创建节省内存
-
对象数量固定不变的列表,使用数组比集合更内存高效
-
尽量少创建临时对象,因为会频繁触发垃圾回收;避免实例化非必要对象,因为会对内存和计算性能带来影响
-
对于要大量创建耗费资源的对象时,使用对象池模式或享元模式
1 Android编译器
从Android Lollipop(API Level 21)开始,ART(Android runtime)变成了唯一的运行时,取代了Dalvik。
我们要优化内存分配,以便能有一个快速的垃圾回收过程,哪怕停止其他所有的操作。当内存的使用到达上限时,垃圾回收启动它的任务,暂停其他每个方法、任务、线程和进程的执行,直到垃圾回收结束后,这些对象才会恢复执行。
自动垃圾回收不是万能的,糟糕的内存管理会导致糟糕的UI性能,进而带来糟糕的用户体验。如果想要让应用程序拥有最佳的性能,还是需要对代码和内存的使用情况进行分析,这点是无法避免的。
ART运行时使用的是提前编译(ahead-of-time complication,AOT),它在应用程序首次安装时执行编译。AOT可以做到以下事情:
-
归功于预编译(pre-complication),设备的电量消耗下降
-
执行应用程序的速度比Dalvik快
-
改善内存管理和垃圾回收
然而,这些好处也需要付出一些代价。安装软件所花费的时间更多了,因为系统需要在安装时编译应用程序。这就导致程序的安装速度相对其他类型的编译器较慢。
Google在Android N中,为ART的AOT编译器添加了一个即时编译器(just-in-time complier,JIT)。JIT作用于应用程序执行期间,并且只在需要时才起作用,它采用的编译方式不同于AOT编译器。JIT编译器使用了代码分析技术,它不是用来替代AOT编译器,而是作为AOT的一个补充,它的引入能为系统的性能带来改善。它能根据设备的使用情况,缓存并重用应用程序的方法。该特性能为每种系统节省编译时间,提升性能。
总结:在内存性能提升上,Android系统通过在Android Lollipop替换Dalvik为ART,且在Android N添加JIT的方式提升编译安装性能
2 内存泄漏
内存泄漏指的是,一个不再被使用的对象被另外一个还存活着的对象引用着。在这种情况下,垃圾回收器会跳过它,因为这种引用关系足以让该对象继续驻留在内存中。
- Activity内存泄漏
避免Activity泄漏的一般准则是,当我们要调用某个方法时,如果不是Activity中的特定方法,就不要使用Activity作为Context。我们可以通过方法得到Application的Context。
- 静态字段
静态字段非常危险,它们会关联Activity以及其他一些对象,或者被它们所关联,大部分的内存问题都是由此引起的。静态对象的生命周期和应用程序的生命周期一样长,这意味着直到应用程序结束,它们才会被回收。
// 典型的静态字段内存泄漏
public class MainActivity extends Activity {
// private static View view;
// private static Drawable drawable;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// view = findViewById(R.id.textView);
view = findViewById(R.id.textView);
view.setBackground(drawable);
}
}
- 非静态内部类
由于内部类需要在它的整个生命周期都能访问外部类,因此,当Activity被销毁而AsyncTask仍然在工作时,内存泄漏就发生了。这不仅会发生在Activity.finish()方法被调用时,甚至由于配置发生变化或内存紧张,导致Activity被重新创建时,泄漏问题也可能发生,比如屏幕旋转等。
// 非静态内部类内存泄漏解决方案
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new MyAsyncTask(this).execute();
}
private static class MyAsyncTask extends AsyncTask {
private WeakReference<MainActivity> activity;
public MyAsyncTask(MainActivity activity) {
this.activity = new WeakReference<>(activity);
}
.....
}
}
通常,作为一个好习惯,在处理线程时,对Activity采用弱引用的方式进行持有,哪怕该线程并非一个内部类。
- 单例
public class Singleton {
private static Singleton singleton;
// private Callback callback;
private WeakReference<Callback> callback;
public static Singleton getInstance() {
if (singleton == null)
singleton = new Singleton();
return singleton;
}
public Callback getCallback() {
return callback;
}
// 使用这种方式,在Activity销毁时要setCallback(null)
// public void setCallback(Callback callback) {
// this.callback = callback;
// }
public void setCallback(Callback callback) {
this.callback = new WeakReference<>(callback);
}
public interface Callback {
void callback();
}
}
- 匿名内部类
匿名内部类默认持有外部类的引用,所以会导致内存泄漏。同样可以使用弱引用的方式解决。
- Handler
Handler在Activity使用时,配合弱引用使用,且在Activity销毁时要清空消息队列handler.removeMessageAndCallbacks(null);
- Service
系统通过最近最少使用算法(Least Recently Used,LRU)来缓存活跃着的进程,这意味着它可以强制关闭早期使用过的进程,并保留最新进程。每次我们让一个不再被使用的Service继续保持活跃状态,相当于是在利用Service创建一个内存泄漏,并且阻碍了系统清理堆栈空间以分配给新进程。所以,当Service在后台执行完工作后,一定要记得将它关闭释放。在使用Service时,应尽量使用IntentService,因为IntentService能在后台工作执行完成后自动结束。
3 内存抖动
在短时间内,大量的新对象被实例化,运行时无法承载这样的内存分配。在这种情况下,垃圾回收事件被大量调用,影响到应用程序内存及UI的整体性能。
4 引用
Java提供了4种级别的引用强度:
-
Normal:主要的引用类型。它对应的是简单的对象创建,当这个对象不再被引用时会被回收
-
Soft:当一个垃圾回收事件被触发时,这种强度的引用不足以让它继续驻留在内存中。因此,在执行期间,它可能随时会变为null
SoftReference<SampleObject> sampleObjectSoftRef = new SoftReference<SampleObject>(new SampleObject());
SampleObject sampleObject = sampleObjectSoftRef.get();
- Weak:和Soft引用类似,但是强度更弱
WeakReference<SampleObject> sampleObjectWeakRef = new WeakReference<SampleObject>(new SampleObject());
- Phantom:这是最弱的引用,所引用的对象是专门被用来回收的。这种引用很少使用,通过PhantomReference.get()方法返回的总是null。这是专门给引用队列使用的
具体的引用区别参考:Java四种引用类型
5 取消部分后台服务
为了改善内存管理,减少后台进程带来的不良影响,下列广播将不再发送:
-
ConnectivityManager.CONNECTIVITY_ACTION:从Android N开始,只有通过代码注册BroadcastReceiver,并且应用程序处于前台时,才能接收到网络变化的广播
-
Camera.ACTION_NEW_PICTURE:当设备拍下一张新照片并添加到存储媒介时会触发该action。现在该action将不再可用,无论是接收或发送都不可用
-
Camera.ACTION_NEW_VIDEO:当设备录下一段视频并添加到存储媒介时会触发该action。该action也不再可用
开发应用程序时,应该避免使用隐式广播,以减少他们对后台操作带来的影响,因为这会导致非必要的内存浪费和电量消耗。
6 数据类型
Java所提供的基本类型,系统根据不同的数据类型为其分配相应数量的内存:
数据类型 | 字节大小 |
---|---|
byte | 8bit |
short | 16bit |
int | 32bit |
long | 64bit |
float | 32bit |
double | 64bit |
boolean | 通常是8bit,具体的bit位取决于虚拟机 |
char | 16bit |
如果不是确实需要,不要使用一个比需求更大的基本类型,每次CPU在处理时,会浪费不必要的内存和计算量。在计算一个表达式时,系统需要做一个隐式转换,将表达式中的基本类型转为其中最大的那个基本类型。
7 自动装箱
自动装箱类型:
-
java.lang.Byte
-
java.lang.Short
-
java.lang.Integer
-
java.lang.Long
-
java.lang.Float
-
java.lang.Double
-
java.lang.Boolean
-
java.lang.Character
使用自动装箱并非改善应用程序性能的正确方式,它会带来许多额外消耗。
Integer sum = 0;
for (int i = 0; i < 500; i++) {
sum += i;
}
一个Integer对象需要16byte的内存空间,而基本类型只需16bit,自动装箱会耗费更多的内存。
8 Sparse数组集
在集合中,我们不能使用基本类型作为泛型参数来实现下列这些接口,这样就不得不使用包装类:
List<Integer> list;
Map<Integer, Object> map;
Set<Integer> set;
每次使用集合中的一个Integer对象时自动装箱至少会发生一次,也会造成内存浪费。
Android提供了一系列非常有用的对象,用于替代Map对象,避免发生自动装箱,防止无意义的大量内存分配,就是Sparse数组。
下列是各种Sparse数组以及他们能替换的相应Map类型:
-
SparseBooleanArray:HashMap<Integer, Boolean>
-
SparseLongArray:HashMap<Integer, Long>
-
SparseIntArray:HashMap<Integer, Integer>
-
SparseArray:HashMap<Integer, E>
-
LongSparseArray:HashMap<Long, E>
SparseArray对象使用两个不同的数组,分别用于存储哈希和对象。第一个数组存储key哈希过后的列表,而第二个数组存储的是按照key哈希过后的列表进行排列的键值对:
当需要添加一个值时,与HashMap类似,我们需要向SparseArray.put()方法指定一个整型的key及其对应的值。如果有多个key的哈希被放进同一个位置,则会造成冲突。
当需要获取一个值时,只要调用SparseArray.get()指定相关的key。在内部会使用该key对哈希数组进行二分搜索(先计算出该key的哈希值,再进行二分搜索),找到哈希的索引,再通过该索引,进一步找到其在另外一个数组中所对应的值。
当我们在二分搜索中得到索引,通过该索引在另外一个数组中找到key,如果这个key和原始的key不匹配,我们就认为发生了冲突。搜索会以该key为中心,在上下两边方向上继续进行,直到找出相同的key及其所对应的值。如果数组中包含大量对象,那么查找的时间也会显示增加。
这些数组所使用的内存区域是连续的,因此,每次从SparseArray中移除一个键值对时,数据会被压实(Compaction)或调整(Resize):
-
Compaction:指的是将要被删除的对象移动到数组末端,其他所有的对象向左移动。处在末端的那些要被删除对象的内存块,可以被未来增加的对象重用,以减少内存分配
-
Resize:指的是将数组中所有的元素(那些不需要被删除的元素)复制到另外一个数组中,然后将旧的数组删除。另一方面,当要添加一个新元素时,同样也需要将所有的元素(数组中已存在的元素及新添加的元素)复制到新的数组中去。这是最慢的方法,但这也是绝对内存安全的,因为不存在无用的内存分配
SparseArray相比HashMap的优劣
HashMap:
优点:
-
只有一个单独的数组来存储哈希、键以及值,它使用大数组技术以避免冲突发生,因此HashMap速度快
-
可以导出到其他Java平台上使用HashMap
缺点:
-
使用大数组技术对内存不利,因为它分配的内存超过了实际所需
-
循环使用速度慢内存低效的迭代模式
for (Iterator iter = map.keySet().iterator(); iter.hasNext();) {
Object value = iter.next();
}
SparseArray:
优点:
-
内存高效的,因为它在执行期间进行的内存分配,处于一个可接受的增长范围内
-
循环可以通过索引进行迭代内存高效
for (int i = 0; i < map.size(); i++) {
Object value = map.get(map.keyAt(i));
}
缺点:
- SparseArray不能在其他Java平台上使用
注:作为开发者,我们应该努力在每个平台上都实现性能最优,而非在不同平台上重用相同的代码。因为,从内存的角度看,相同的代码在不同平台上会带来不同的影响。这也是为什么着重建议,在我们所工作的平台上对代码进行分析,然后根据结果来最终决定采用何种方式的原因所在。
适合使用SparseArray的场景:
是否使用SparseArray,取决于内存管理策略以及CPU性能,节省内存空间需要以牺牲CPU计算量作为代价。
-
所处理的对象数量在一千以内,并且没有打算对数据进行大量的添加或删除
-
数据的组织形式为Map,数据量不大,但存在大量的迭代
9 ArrayMap
ArrayMap对象是Android对Map接口的一个实现,它比HashMap更加内存高效。它的主要目的是让你能够像HashMap一样使用对象作为Map的key。
它的实现和用法与SparseArray类似,包括对内存的使用和消耗计算量的方式都是类似的。
10 循环
循环有三种方式:
-
Iterator循环
-
while循环
-
for循环
循环执行速度:Iterator < while < for < for each
for循环的三种方式对比:
// 速度最慢,dummies数组在每次循环中都需要重新计算一次数组的长度dummies.length
// dummies.length的额外消耗是因为每次循环时即时编译器都需要解释该语句
private void classicCycle(Dummy[] dummies) {
int sum = 0;
for (int i = 0; i < dummies.length;i++) {
sum += dummies[i].dummy;
}
}
// dummies.length放在循环外,只计算一次数组长度
private void fasterCycle(Dummy[] dummies) {
int sum = 0;
int length = dummies.length;
for (int i = 0; i < length; i++) {
sum += dummies[i].dummy;
}
}
// for each循环最快
private void enhancedCycle(Dummy[] dummies) {
int sum = 0;
for (Dummy dummy : dummies) {
sum += a.dummy;
}
}
11 枚举
枚举对开发者非常友好,数量有限的元素、描述性的名字,因此增强了代码的可读性且还支持多态性;但枚举(new Enum(String name, int ordinal))
在应用程序生成时会被转换为对象,需要分别为枚举的name参数分配String,为ordinal参数分配一个integer,以及一个array和一个包装类,更糟糕的是,枚举需要被复制到每个使用到它的进程中,因此在多进程应用程序中,枚举会增加额外的消耗。
Android提供了简化从枚举到整型值的转换:@IntDef
,该注解还能利用flag属性让常量可以组合使用。
@IntDef
注解将所有的整型值集合起来。例如,以注解的方式将这些整型值转换为类似枚举的东西,却不会带来任何内存性能上的问题。
public static final int RECTANGLE = 0;
public static final int TRIANGLE = 1;
public static final int SQUARE = 2;
public static final int CIRCLE = 3;
@IntDef({RECTANGLE, TRIANGLE, SQUARE, CIRCLE})
publci @interface Shape {}
public abstract void setShape(@Shape int mode);
@Shape
public abstract int getShape();
注:枚举影响着内存的整体性能,因为它们会带来非必要的内存呢分配。要尽量避免使用枚举,将它们替换为static final整型值。如果你需要限制整型值的数量,那么你可以创建一个自己的注解,来使用这些整型值。
12 常量(静态变量)
在Java编译器中有一个特殊的方法叫做,该方法负责处理类的初始化,但仅作用于静态变量和静态代码块,按照它们在类中的顺序,对它们进行初始化。
如果静态变量还有final关键字,情况就不太一样了。这种情况下,就不是通过方法进行初始化,它们会被存储在DEX文件中。这能带来两方面的好处,既不需要更多的内存分配,也不需要额外的操作对它进行分配。但这仅适用于基本类型和字符串常量,所以不应该为对象这样做。
注:代码中的常量应该用static和final进行修饰,这样才能充分节约内存,避免在Java编译器的方法中初始化。
13 字符串
- 创建字符串:
// 错误:
// ①字符串"example"本身就是一个对象,他的内存必须被分配
// ②new String的操作也带来了一个新对象
String string = new String("example");
// 符合内存对性能的要求
String string = "example";
- 字符串拼接:
通常在字符串操作时,经常会这样使用:
String string = "This is ";
string += "a string";
但实际并不是这样,对于此类操作,StringBuffer和StringBuilder相对于String更加高效,因为它们是以字符数组的形式工作:
StringBuffer stringBuffer = new StringBuffer("This is ");
stringBuffer.append("a string");
如果需要进行大量的字符串连接操作,这会是一个更好的方案。甚至可以说,这在任何时候都是一个最佳实践方案。记住StringBuffer和StringBuilder之间的不同点:StringBuffer是线程安全的,所以它更慢,但是可以在多线程环境下使用;而StringBuilder不是线程安全的,所以它更快,但是它只能在单线程中使用。
另一个要记住的是,StringBuilder和StringBuffer默认都有16个字符的初始化容量。当它们的容量不够用,需要增加时,会初始化并分配一个双倍大小的新对象,旧对象会在下一次垃圾回收事件中被回收。为避免这个不必要的内存浪费,如果已经知道所处理字符的大小,那么我们可以在实例化StringBuffer或StringBuilder时,为其指定一个初始容量:
StringBuffer stringBuffer = new StringBuffer(64);
stringBuffer.append("This is ");
stringBuffer.append("a string");
stringBuffer.append....
14 本地变量
方法内的某个对象在方法的整个执行过程中都没有被修改过。这意味着,该对象可以被放置在方法外部。这样,它只需被分配一次,并且不会被回收,改善了内存管理:
// dateFormat对象是不变的,可以放在方法外创建分配内存
public static format(Date date) {
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM--dd'T'HH:mm:ss.SSSZ");
return dateFormat.format(date);
}
// 正确创建对象
private DateFormat dateFormat = new SimpleDateFormat("yyyy-MM--dd'T'HH:mm:ss.SSSZ");
public String format(Date date) {
return dateFormat(date);
}
15 数组VS集合
集合能够根据需求自动扩大或者减小,并且提供了大量非常有用的方法,可用于添加、删除、获取、改变、移动对象,但这也使得集合的使用代价高昂。如果所处理对象的数量是固定不变的,那么原始的数组比集合更加内存高效。
16 StrictMode
StrictMode可用于找出内存及网络相关的问题。StrictMode启用后,在后台运行,当有问题发生时,它会按照我们制定的策略来通知我们。
if (BuildConfig.DEBUG) {
StrictMode.VmPolicy policy = new StrictMode.VmPolicy.Builder()
.detectAll()
.penaltyLog()
.build();
StrictMode.setVmPolicy(policy);
}
-
detectActivityLeaks:检查Activity泄漏
-
detectLeakedClosableObjects:检查是否存在未关闭的可关闭对象
-
detectLeakdedRegistractionObjects:检查当Context被销毁时,ServiceConnection或BroadcastReceiver是否存在泄漏
-
detectSqlLiteObjects:检查是否有SQLite对象在使用后,未被关闭
-
detectAll:检查每个可疑的行为
-
penaltyDeath:当检测到问题时,进程会被杀气且应用程序崩溃
-
penaltyDropBox:当检测到问题时,相关日志会被发送到DropBoxManager,DropBoxManager收集这些日志,用于调试
-
penaltyLog:当问题发生时,日志会被记录到Logcat
17 对象池模式和享元模式
对象池模式:
当我们要大量创建耗费资源的对象时,该模式特别有用。
该模式的背后思想是,避免对一个将来可能会被重用的对象进行垃圾回收,节省了创建对象所花费的时间。在该模式中,需要处理以下三种类型的对象:
-
ReusableObject:可重用对象,被客户使用,被对象池管理
-
Client:客户,需要一个可重用对象来做一些事情,所以它需要向对象池请求一个对象,并在事情完成后将对象归还
-
ObjectPool:对象池,持有所有可重用对象,负责供给和回收这些对象
ObjectPool应该是一个单例对象,以便集中管理所有的可重用对象;ObjectPool包含的对象数量有一个上限,当一位客户在请求一个可重用对象时,如果对象池已满,并且所有可重用对象都在使用中,那么该请求会被延迟,直到有其他客户归还了一个可重用对象。
对象池模式使用注意点:
-
对象在使用完毕后,客户应当尽快归还它
-
刚被使用完的对象,在被传递给另一个请求它的客户之前,需要被恢复成某一个一致的状态,以便能够清晰地管理这些对象
-
如果某个可重用对象在被客户释放后,仍与其他对象保持着引用关系,就会导致一个内存泄漏。所以在大部分情况下,可重用对象需要被恢复成创建时的状态
-
如果条件允许,可以在创建对象池时,预先分配一定数量的可重用对象,这样可以为未来的对象访问节省一些时间
对象池模式代码:
public abstract class ObjectPool<T> {
// 使用两个SparseArray数组保存对象集合,防止这些对象在借出后被系统回收
private SparseArray<T> freePool;
private SparseArray<T> lentPool;
private int maxCapacity;
public ObjectPool(int initialCapacity, int maxCapacity) {
initialize(initialCapacity);
this.maxCapacity = maxCapacity;
}
public ObjectPool(int maxCapacity) {
this(maxCapacity / 2, maxCapacity);
}
public T acquire() {
T t = null;
synchronized(freePool) {
int freeSize = freePool.size();
for (int i = 0; i < freeSize; i++) {
int key = freePool.keyAt(i);
t = freePool.get(key);
if (t != null) {
this.lentPool.put(key, t);
this.freePool.remove(key);
return t;
}
}
if (t == null && lentPool.size() + freeSize < maxCapacity) {
t = create();
lentPool.put(lentPool.size() + freeSize, t);
}
}
return t;
}
public void release(T t) {
if (t == null) {
return null;
}
int key = lentPool.indexOfValue(t);
restore(t);
this.freePool.put(key, t);
this.lentPool.remove(key);
}
protected abstract T create();
protected void restore(T t) {}
private void initialize(final int initialCapacity) {
lentPool = new SparseArray<>();
freePool = new SparseArray<>();
for (int i = 0; i < initialCapacity; i++) {
freePool.put(i, create());
}
}
}
享元模式:
对象池模式和享元模式的区别:
-
对象池模式:面对需要大量分配高成本对象时,通过对象重用尽量减少内存分配以及垃圾回收对系统产生的影响
-
享元模式:通过节省所有对象共享的状态,以减少载入内存的量;即为所有对象只创建一个实例,实现内部状态的重用,减少复制它所带来的消耗
享元模式的客户请求对象可分为两种类型的状态:
-
Internal state:内部状态。由所有能够唯一标识一个对象的字段组成,并且这些字段不与其他对象共享
-
External state:外部状态。在所有可交换对象之间能够共享的字段集合
注:享元模式适用于大量细粒度对象的复用。该模式只需使用少量对象即可实现大量对象的复用,前提是这些对象的粒度小,变化小,对象之间较为相似。内部状态存储与对象内部,一般在构造时或者通过setter函数设置,这些状态不随外界环境变化而变化。外部状态由客户保存,一般是在请求的时候,将外部状态通过参数传入对象中,这些状态随着外界环境变化而变化。
享元模式代码:
public interface Courier<T> {
void equip(T param);
}
// 享元对象
public class PackCourier implements Courier<Pack> {
private Van van;
// id为内部状态且用于唯一标识一个对象,用在Factory中实现对象复用
public PackCourier(int id) {
super(id);
van = new Van(id);
}
// pack为外部状态
public void equip(Pack pack) {
van.load(pack);
}
}
// 客户,通过courier.equip(pack)将外部状态pack传入享元对象courier
public class Delivery extends Id {
private Courier<Pack> courier;
public Delivery(int id) {
super(id);
courier = new Factory().getCourier(0);
}
public void deliver(Pack pack, Destination destination) {
courier.equip(pack);
}
}
public class Factory {
private static SparseArray<Courier> pool;
public Factory() {
if (pool == null)
pool = new SparseArray<>();
}
public Courier getCourier(int type) {
Courier courier = pool.get(type);
if (courier == null) {
courier = create(type);
pool.put(type, courier);
}
return courier;
}
private Courier create(int type) {
Courier courier = null;
switch(type) {
case 0:
courier = new PackCourier(0);
}
return courier;
}
}
// 每个Delivery都是由同一个Courier完成操作
for (int i = 0; i < DEFAULT_COURIER_NUMBER; i++) {
new Delivery(i).deliver(new Pack(i), new Destination(i));
}