一、前言
性能优化是一个持续的过程,好比我们身体,不能等生大病了才明白健康的重要,平时我们就该注意锻炼身体,保证一个健康的身体状况。对程序而言也是如此,平时开发的就要注意性能方面的问题,不能等到后期出现体验不好的时候再进行优化。对硬件而言,我们开发的程序的影响在两个方面:CPU和内存。下面就从这两个方面入手分享一些在开发中应该注意的事项。
二、内存
针对JVM而言,内存的管理由虚拟机帮我们完成,GC在回收内存的过程中可能会暂停当前的所有线程,然后进行内存回收,这样就可能造成UI显示丢帧,出现卡顿,如果再频繁的进行内存分配和回收,这样就有可能出现内存抖动。
如果我们开发中使用的对象得不到回收,造成内存泄漏,当内存泄漏达到一定数量后就可能会出现OOM,导致应用程序奔溃。
2.1 内存按需使用
2.1.1 图片的按需使用
开发中进程会使用图片,比如一张1024* 1024像素的图片,但是我们的控件只需要200*200像素大小。如果不做任何处理,将这张原始大小图片显示到控件上,将会消耗4M内存( 假设bitmap configuration ARGB_8888),在UI体验没有太大差别的情况下我们只需约0.153M的内存即可,而不是使用4M内存。
图片的按需使用需要改变inSampleSize,取合适值对图片进行压缩处理。当然我们也可以使用一些第三方的库,比如Glide、Picasso、Fresso,这些库已经从多级缓存的角度来处理了图片,保证UI显示过程中快速和高效的渲染。
2.1.2 切记在onDraw中new对象
在Android中,View的onDraw()方法复制绘制UI,若果View频繁的刷新,那么onDraw()方法会频繁的调用,如果在该方法中new 对象,那么产生很多短生命周期对象,可能因为频繁的内存申请和回收,造成内存抖动,从而影响体验。
2.2 使用经过优化的数据容器:SparseArray、SparseBooleanArray、SparseIntArray、SparseLongArray
上面的数据容器功能和HashMap一致,在了解这几个数据容器之前我们先简单来了解一下HashMap的基础使用:
private fun testHashMap(){
val hashMap = HashMap<Int,String>()
hashMap[1] = "data1"
hashMap[2] = "data2"
hashMap[3] = "data3"
}
上面示例已int作为key,String作为value,添加三个数据,下面了解public V put(K key, V value)实现。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
static class Node<K,V> implements Map.Entry<K,V> {
...
}
put(K key, V value)的具体实现我们先不分析,单从上面的中我们可以看出,key和value最终会包装到Node对象中,总的来说每次调用put()方法,都会创建新的对象Map.Entry,这样造成一定的内存浪费,同时int是基本数据类型,使用中需要转为Intergr类型,存在自动装箱和拆箱,有一定的性能开销。因此针对上面的基本数据类型的自动装箱和拆箱以及创建新的Map.Entry,Android提供了新的容器SparseArray、SparseBooleanArray、SparseIntArray、SparseLongArray来避免这些问题。下面了解SparseArray这对这些问题是如何做优化处理。
public class SparseArray<E> implements Cloneable {
private static final Object DELETED = new Object();
private boolean mGarbage = false;
private int[] mKeys;
private Object[] mValues;
private int mSize;
......
}
SparseArray直接使用int[]数组来存储int型的key,Object[]来存储value,就避免了int类型的key的自动拆箱和装箱以及创建Map.Entry实例来存储。针对value 类型是boolean,int,long基本类型的数据,还提供了SparseBooleanArray、SparseIntArray、SparseLongArray来避免boolean,int,long的自动拆箱和装箱。
public class SparseBooleanArrayimplements Cloneable {
private int[] mKeys;
private boolean[] mValues;
private int mSize;
......
}
public class SparseIntArray implements Cloneable {
private int[] mKeys;
private int[] mValues;
private int mSize;
......
}
public SparseLongArray implements Cloneable {
private int[] mKeys;
private long[] mValues;
private int mSize;
......
}
注意:
虽然SparseArray做了基本数据类型的自动装箱和拆箱以及创建新的Map.Entry,但是因为内部采用数组来存储key和value,因此针对大量数据删除和查询操作有一定性能影响,内部的分二分查询效率会低于HashMap,同时删除数据后的数组位置移动,也会消耗一定的性能。
因此SparseArray、SparseBooleanArray、SparseIntArray、SparseLongArray适用于一下场景:
- int类型作为key的情况
- 数据量小于1000一下
在上述两个场景下才能发挥他们的优势,其他的情况建议使用HashMap
2.3 内存缓存
内存缓存的目的是为了减少频繁的分配和释放内存导致的内存抖动问题,达到一种空间换时间的效果。这个方案在资源读写中比较常见,在这种情况下一般会使用bbyte[]来存储每次读取的数据,这块的实现在okio中就有对应的实现,Segment中生命byte[8192]的内存空间,SegmentPool按照约定大小来缓存Segment,从而达到内存空间的复用。但是这种方案必须有大小限制,避免OOM问题。
2.3.1 单列的合理利用
单列能够保证我们的程序在整个运行期间只有一个实例,但它因此也会常驻内存,不容易被GC回收,在开发中如果满足以下场景,我们可以考虑使用单列:
- 需要频繁实例化然后销毁的对象
- 创建对象耗时过的或者消耗资源过多,但同时又经常用到
- 有状态的工具类对象
这里举个例子,网络框架OkHttp中的OkHttpClient,每个OkHttpClient对象都有自己的线程池和连接池,并且在开发会经常用到,因此就需要使用单列模式。
另外单列在使用过程只有一个对象,因此我们需要考虑线程同步问题。
2.3.2 线程池的使用
我们知道每次都new Thread 会有很大的性能开销,因此我们需要根据功能需求来使用线程池。
我们可以使用已经提供的几种方式来创建线程池:
- Executors.newCachedThreadPool
- Executors.newFixedThreadPool
- Executors.newSingleThreadExecutor
- Executors.newScheduledThreadPool
同时也可以用ThreadPoolExecutor相关的构造方法来创建线程。下面描述ThreadPoolExecutor的构造方法的参数含义,了解后可根据需要自行选择.
- int corePoolSize 该池中核心线程数最大值
- int maximumPoolSize 该线程池中线程总数最大值
- long keepAliveTime 非核心线程闲置超时时长
- TimeUnit keepAliveTime的单位
- BlockingQueue workQueue 线程池中的任务队列
- ThreadFactory threadFactory 创建线程的工厂
- RejectedExecutionHandler handler 饱和策略
关于更多ThreadPoolExecutor的用法,可以参考网上其他文章。
三、CPU
CUP作为系统的运算和控制核心,程序在运行过程中,减少CPU的计算时间,能够让我们的程序快速的响应。
3.1 合理的布局
合理布局注意以下几点:
- 能实同等功能的情况下,选择消费性能少的布局:FrameLayout、LinearLayout
- 减少布局嵌套层级:布局层级越多,消耗的性能也就越大,可以使用RelativeLayout、ConstraintLayout减少嵌套层级
- 结合inclue,merge,ViewStub结合来提升布局重用、减少布局层级、延迟加载
3.2 合理使用数据结构
这里举个开发中的常用数据容器ArrayList和LinkedList,其他的根据需求来定。
这个两个List都能存储数据,完成数据的增、删操作,但使用过程中有一定的区别。ArrayList使用数组来存储数据,这种情况随机的访问就比较快,根据索引就得到对应的值,但是随机的增删就影响性能,因为需要做数组的复制和移动操作。而LinkedList采用的是列表结构,针对数据的随机增删只需要改变对应Node的上下应用关系即可。但是访问就比较耗时了,需要循环遍历,效率相对就要差一些。