面试题之性能优化
ANR
Application Not Responding
(界面无反应,一般的相应时间是五秒)。
造成ANR的主要原因
- 主线程被
IO
操作阻塞 - 主线程中存在耗时的计算
Android中在主线程中的操作
-
Activity
的所有生命周期回调都是执行在主线程中的 -
Service
默认是执行在主线程中的 -
BroadcastReceiver
的onReceive
回调是执行在主线程中的 - 没有在子线程中使用
Looper
的Handler
的handleMessage
,post(Runnable)
是执行在主线程中的 -
AsyncTask
的回调中除了doInBackground
,其他都是执行在主线程中的
如何解决ANR
- 使用
AsyncTask
处理耗时的IO
操作 - 使用
handler
来处理工作线程的耗时任务 -
Activity
的onCreate
和onResume
回调中尽量避免耗时的代码
OOM
当前占用的内存加上我们申请的内存资源超过了
Dalvik
虚拟机的最大内存限制时就会抛出Out of memory
异常
如何避免OOM
-
Bitmap
- 图片显示(比如说如果加载网络图片的时候,我们可以优先加载缩略图,还有比如在列表中,我们可以在滚动的时候不去加载图片)
- 及时释放内存(当我们去创建
Bitmap
的时候,它会通过jni
调用nativeCreate()
方法,这样不仅会在java
中开辟一块内存,同时会在底层c
也开辟一块内存空间,当我们确认不用该图片时,可以调用recycle()
方法去释放内存,主要还是调用了nativeRecycle()
去释放C
中的内存,不过我们即使不去调用recycle()
释放内存,当我们程序的进程被杀死时,也会去释放该内存) - 图片压缩
- 捕获异常(创建
bitmap
时,我们可以使用try/catch
来捕获oom
异常)
-
ListView/RecyclerView
的优化 -
避免在自定义
View
的onDraw()
方法中执行对象的创建 -
谨慎使用多进程
缓存LruCache
LruCache
内部维护了一个LinkedHashMap
(双链表数据结构),在put
数据的时候会判断指定的内存大小是否已满。若已满,则会使用最近最少使用算法进行清理,LinkedHashMap
内部是一个数组加双向链表的形式来存储数据,也就是说当我们通过get
方法获取数据的时候,数据会从队列跑到队头来。反反复复,队尾的数据自然是最少使用到的数据。
具体使用
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
LruCache<String, Bitmap> cache = new LruCache<String, Bitmap>(maxMemory / 8) {
@Override
protected int sizeOf(@NonNull String key, @NonNull Bitmap value) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
我们在使用LruCache
的时候需要复写sizeOf()
方法,具体我们就从源码分析一波吧。
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
//构造方法
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
//测量元素大小
private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
//这里的map.get()方法就会进行数据排序
mapValue = map.get(key);
if (mapValue != null) {
//命中次数+1,并且返回mapValue
hitCount++;
return mapValue;
}
//未命中次数+1
missCount++;
}
//如果未命中,会尝试利用create方法创建对象
//create需要自己实现,若未实现则返回null
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
//创建了新对象之后,再将其添加进map中,与之前put方法逻辑基本相同
createCount++;
mapValue = map.put(key, createdValue);
if (mapValue != null) {
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
//每次加入数据时,都需要判断一下是否溢出
trimToSize(maxSize);
return createdValue;
}
}
public final V put(K key, V value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
V previous;
synchronized (this) {
//count为LruCahe的缓存个数,这里加一
putCount++;
//加上这个value的大小
size += safeSizeOf(key, value);
//存进LinkedHashMap中
previous = map.put(key, value);
if (previous != null) {
//如果之前存过这个key,则减掉之前value的大小
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
//进行内存判断
trimToSize(maxSize);
return previous;
}
//判断是否内存溢出
private void trimToSize(int maxSize) {
while(true) {
//这是一个无限循环,目的是为了移除value直到内存空间不溢出
Object key;
Object value;
synchronized(this) {
if (this.size < 0 || this.map.isEmpty() && this.size != 0) {
//如果没有分配内存空间,抛出异常
throw new IllegalStateException(this.getClass().getName() + ".sizeOf() is reporting inconsistent results!");
}
if (this.size <= maxSize || this.map.isEmpty()) {
//如果小于内存空间
return;
}
//否则将使用Lru算法进行移除(找到LinkedHashMap的头节点进行移除)
Entry<K, V> toEvict = (Entry)this.map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
this.map.remove(key);
this.size -= this.safeSizeOf(key, value);
//回收次数+1
++this.evictionCount;
}
this.entryRemoved(true, key, value, (Object)null);
}
}
public final V remove(K key) {
//判空
if (key == null) {
throw new NullPointerException("key == null");
}
V previous;
synchronized (this) {
//根据key移除value
previous = map.remove(key);
if (previous != null) {
//减掉size的大小
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, null);
}
return previous;
}
}
从上面的代码中我们可以分析出来其重点的代码就在trimToSize()
方法当中,每次LruCache
去put(value)
都调用该方法,在trimToSize()
中对大于了存储空间的值找到LinkedHashMap
的头节点进行移除(最少使用的值),这里我们需要记住的是LinkedHashMap
的get
方法(每次get对节点进行顺序排列,将使用的数据重新排列到节点尾部)。
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMapEntry<K,V> last;
//accessOrder为true且当前节点不是尾节点则进行访问顺序排序
if (accessOrder && (last = tail) != e) {
LinkedHashMapEntry<K,V> p =
(LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
//下面是排序过程(就是将当前的数值放置节点尾部)
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
原来如此!LinkedHashMap
在这个方法中实现了按访问顺序排序,这也就是为什么我们的LruCache
底层是使用的LinkedHashMap
作为数据结构。
UI卡顿
在
Android
中通常流畅的动画保持在16ms
绘制一帧,也就是我们常说的60fps
(1000ms
/16ms
约等于60),如果绘制时间超过了16ms
,就会给人一种卡顿的现象。
UI卡顿原因分析
- 在
UI
线程做轻微的耗时操作,导致UI
线程卡顿 - 布局
layout
过于复杂,无法在16ms
内完成渲染 - 同一时间内动画执行次数过多,导致
CPU
和GPU
负载过重 -
View
的过度绘制,导致某些像素在同一帧时间内被绘制多次,从而导致CPU
和GPU
负载过重 -
View
频繁的触发measure
,layout
导致累计耗时过多,整个View
频繁的重新渲染 - 内存频繁的触发
GC
操作,导致GC
暂时阻塞渲染操作 - 冗余资源及逻辑等导致加载和执行过慢
ANR
UI卡顿解决办法
- 布局优化
减少布局嵌套,可以结合实际使用include
标签,merge
标签 - 列表及
Adapter
的优化
比如说在列表滚动时候不要进行图片加载操作 - 背景和图片等内存分配优化
背景最好不要过度绘制,图片最好压缩 - 避免
ANR
(不要在主线程中进行耗时操作)
内存泄露
某个不再使用的对象被其他实例所引用,导致其该被回收而无法被回收。
Android中常见的内存泄露
- 单例
长生命周期类持有短生命周期类的引用,比如单例的构造方法中传入了Activity
的Context
,我们需要传入的是Application
的Context
-
Handler
非静态内部类持有外部类的引用,比如在Activity
中直接申明了一个非静态的Handler
,解决办法:1.将申明的Handler
变成static
2.创建一个静态的内部类Handler
持有Activity
的弱引用 3.在Activity
的onDestroy()
方法中remove
掉message
. - 开启线程
匿名内部类持有外部类的引用,比如说new Thread
,new AsyncTask
等,解决办法: 将其写成静态的非匿名内部类 WebView
内存管理
内存管理机制的特点
- 更少的占用内存
- 在合适的时候,合理的释放系统资源
- 在系统内存紧张的情况下,能释放掉大部分不重要的资源,来为
Android
系统提供可用的内存 - 能够很合理的在特殊生命周期中,保存或者还原重要数据,以至于系统能够正确的重新恢复该应用
内存优化方法
- 当
Service
完成任务后,尽量停止它(可以使用IntentService
,在IntentService
内有一个工作线程来处理耗时操作,启动IntentService
的方式和启动传统的Service
一样,同时,当任务执行完后,IntentService
会自动停止) - 在
UI
不可见的时候,释放掉一些只有UI
使用的资源 - 在系统内存紧张的时候,尽可能多的释放掉一些非重要的资源
- 避免滥用
Bitmap
导致的内存浪费 - 使用针对内存优化过的数据容器(少用枚举常量,它消耗的资源是常量的两倍多)
- 避免使用依赖注入框架
- 使用
ZIP
对齐的Apk
- 使用多进程(比如定位,推送,
WebView
可以单独开启一个进程)
冷启动优化
冷启动的定义
冷启动就是在启动应用前,当前系统中没有该应用的任何进程信息。
热启动的定义
热启动:用户使用返回键退出应用,然后马上又重新启动应用。其实就是重新启动应用的时候,当前系统后台中拥有该应用的进程。
冷启动时间的计算
这个时间值是从应用启动(创建进程)开始计算,到完成视图的第一次绘制(当第一个
Activity
内容对用户可见)为止
冷启动的流程
Application
的构造器方法->attachBaseContext()
->onCreate()
->Activity
的构造方法->onCreate()
->配置主题中背景等属性->onStart()
->onResume()
->测量布局绘制显示在界面上
-
Zygote
进程中fork
创建出一个新的进程 - 创建和初始化
Application
类,创建MainActivity
类 -
inflate
布局,当onCreate/onStart/onResume
方法都执行完成 -
contentView
的measure/layout/draw
显示在界面上
冷启动时间优化
- 减少
onCreate()
方法的工作量 - 不要让
Application
参与业务操作和进行耗时操作 - 不要以静态变量的方式在
Application
中保存数据 - 减少第一个
Activity
的布局嵌套
其他优化
-
Android
不用静态变量存储重要数据- 静态变量等数据可能会由于进程已经被杀死而被重新的初始化
- 建议使用其他的方式传输数据:文件/
sp
/contentProvider
-
SharePreference
- 不能跨进程同步数据(
SharePreference
在多进程读写的时候,不能跨进程读写数据,因为每个进程都会维护一套SharePreference
的副本,每次修改和读取都是读取该进程的副本,只有在应用结束的时候,所有进程的副本才会同步至文件中) - 存储
SharePreference
的文件过大问题(内容都是通过key/value
的形式进行存储的,如果文件过大,特别消耗内存,可能造成主界面卡顿,还很有可能导致创建大量的临时变量,导致内存泄漏)
- 不能跨进程同步数据(
- 内存对象序列化
将对象的状态信息转化为可以存储或者传输的形式的过程
-
Serializeble
(Java
的序列化方式,在序列化的时候会产生大量的临时变量,从而引起频繁的GC
) -
Parcelable
(Android
特有的序列化方式,不能序列化磁盘数据)
-
- 避免在
UI
线程中进行繁重的操作
以上就是对Android
中的性能优化的一些个人见解和总结。