1.系统卡顿的根本原因
系统卡顿的根本原因是因为丢帧导致的,即本该在一个Vsync周期内显示的画面帧,由于种种原因并没有即时显示到屏上,而是过了好几个Vsync信号周期后才显示出来。
出现这种情况大体分为两个原因:
1.1页面太复杂,导致View绘制的时候耗时太多,超过了16.6ms
1.2主线程存在耗时的任务,导致在Vsync信号开始之后,没有及时开始执行绘制的任务
2.View的绘制过程
首先在performLaunchActivity的时候会创建PhoneWindow,然后在setContentView的时候创建DecorView,并加载XML布局文件到DecorView的contentParent这个父布局上,在onResume的时候WindowManagerGlobal在addView方法中创建ViewRootImpl,然后执行ViewRootImpl的setView方法,然后ViewRootImpl会执行requestLayout方法。requestLayout方法是View绘制的入口方法,我们经常调用的view的invalidate或者requestLayout方法,都会执行到ViewRootImpl的requestLayout方法。
requestLayout方法会执行到scheduleTraversals方法,在scheduleTraversals里面,会添加同步屏障消息,以便及时响应界面绘制的异步消息。然后会给Choreographer注册一个TraversalRunnable回调,并让Choreographer去做Vsync垂直同步信号的监听,在Choreographer内部监听到Vsync垂直同步信号之后,会回调到TraversalRunnable的回调接口,然后移除同步屏障消息,执行performTraversals,这里才会真正开始执行测量、布局、绘制的流程。其中绘制过程是ActivityThread(CPU) + RenderThread(GPU)一起完成,然后把绘制好的buffer通过queuebuffer添加到缓冲队列,交给SurfaceFlinger。SurfaceFlinger在收到缓冲buffer,并监听到Vsync信号后,开始调用HWC或者OpenGL ES做合成处理,然后把合成的缓冲帧通过HWC给到显示驱动做显示。
3.vsync垂直同步信号
Vsync信号是由显示设备发出来的,经过HWC传递给SurfaceFlinger,SurfaceFlinger会把信号传给自己内部处理,同时还会传给各个应用进程的Choreographer。大部门显示设备的刷新频率是60HZ,也就是每个信号的间隔为16.6ms,刷新率和Vsync的频率是一致的。Vsync信号会触发View的测量、布局、绘制;也会触发SurfaceFlinger的合成缓冲帧。Vsync机制加上triple buffer缓存,可以最大程度保证屏幕的刷新率和软件绘制的帧率保持一致,尽量保证了系统的流畅性。
4.Choreographer监控丢帧情况
使用Choregrapher的postFrameCallback,去注册监听每一帧的开始刷新的时间,重而发现丢帧的情况。监听接口回调的时间单位是纳秒,也就是1/1000000ms。
// Application.java
public void onCreate() {
super.onCreate();
Choreographer.getInstance().postFrameCallback(new FPSFrameCallback(System.nanoTime()));
}
public class FPSFrameCallback implements Choreographer.FrameCallback {
private static final String TAG = "FPS_TEST";
private long mLastFrameTimeNanos = 0;
private long mFrameIntervalNanos;
public FPSFrameCallback(long lastFrameTimeNanos) {
mLastFrameTimeNanos = lastFrameTimeNanos;
mFrameIntervalNanos = (long)(1000000000 / 60.0);
}
public void doFrame(long frameTimeNanos) {
// 初始化时间
if (mLastFrameTimeNanos == 0) {
mLastFrameTimeNanos = frameTimeNanos;
}
final long jitterNanos = frameTimeNanos - mLastFrameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if(skippedFrames > 30){
// 丢帧30以上打印日志
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
}
mLastFrameTimeNanos = frameTimeNanos;
// 注册下一帧回调
Choreographer.getInstance().postFrameCallback(this);
}
}
实际上在Choreographer内部,也同样是有这样一套判断是否掉帧的代码。
5.布局优化
5.1减少过度绘制
5.2减少布局层级
5.3使用include和merge标签
5.4使用viewStub懒加载View
6.耗时优化
6.1使用IdelHandler处理非紧急任务
6.2使用AsyncLayoutInflater去加载复杂的布局
6.3使用线程池去处理耗时任务
7.内存影响和优化
每次GC的时候,需要做一次GCRoot引用链的可达标记,这会挂起进程,暂停所有线程,在ART虚拟机中,这个时间大概是3ms。因此如果内存不足,会引起频繁的GC,那么就会导致主线程绘制受影响,从而导致丢帧卡顿。
7.1内存分析方式
7.1.1查看系统日志
在系统logcat日志齐全的情况下,可以查看是否是因为低内存导致的问题,如下会打印lowmemorykiller相关的tag:
或者看频繁存在GC的打印信息:
7.1.2 adb命令生成hprof文件分析内存状态
.hprof是Java堆内存的一个快照,内部记录了当时堆内存的状态,包括所有存活的对象和它们的引用关系。
.hprof文件可以用adb命令生成:adb dumpheap 包名 生成文件位置
在生成hprof文件的时候,最好是在卡顿前和卡顿后各采集一次,做对比。
然后使用Android Studio的Profiler打开这个.hprof文件
7.1.3直接使用Android Studio的Profiler
直接在Android Studio的Profiler选择MEMORY,然后选择Capture heap dump,然后选择Record开始采集:
然后就会得到如下的堆栈分析界面:
其中的Leaks表示的是存在内存泄漏的点,点击reference然后jump to source可以跳转到泄露点代码的位置。
直接跳转到泄露点的位置这种情况,是一种比较理想的情况,实际排查内存泄露的时候,可能会更复杂一点。包括是否是完全的复现的场景,或者不会直接显示leaks内存泄露点。
7.2垃圾判断算法
java虚拟机判断垃圾的方法,是从一系列GCRoot开始做引用链的可达搜索,不在引用链上的对象就会被判断为垃圾。
GCRoot包括:虚拟机栈引用的对象;本地native方法引用的对象;方法区中静态变量引用的对象;存活线程中存在的对象。
7.3几种垃圾回收算法
标记清除算法:先做可达性的标记算法,把GCRoot没有直接或者间接引用的对象标记为垃圾对象;然后把垃圾对象直接清除。
优点是算法简单,不需要移动存活的对象;缺点是容易产生内存碎片,导致频繁的GC。
复制算法:把内存分成两块,只使用其中一块内存。垃圾回收的时候,把当前内存块中存活的对象直接复制到另外一块空闲的内存,然后清除掉当前的内存块,调整两个内存块的角色,完成垃圾回收。
优点是不会存在碎片内存;缺点是内存使用缩小了一半,对象高存活的时候,会导致大量频繁复制。
标记压缩算法:首先从GCRoot做可达性标记,然后把存活的对象压缩到内存的一端,然后清楚边界外的所有空间。
优点是避免了内存碎片的产生,有不需要两块内存空间,性价比高;缺点是压缩内存,需要移动存活的对象,存在一定的效率降低。
7.4分代回收策略
在ART虚拟机中采用内存分代的策略,把堆内存划分为新生代和老年代。
新生代:
新创建的对象默认放入新生代的内存区域,新生代内存的对象存活率低,回收率高,一次GC平均回收70%~90%的对象,所以在新生代中的垃圾回收算法使用的是复制算法。
新生代中的内存区域,还可以被细分成一个Eden区和两个Survive区,比例是8:1:1。Eden用来存在新创建的对象,GC的时候会把Eden中存活的对象和Survive区中的对象一起拷贝到另一个空闲的Survive区中。
老年代:
对象在多次GC后仍然没有被回收(15次),那就会移入老年代内存。老年代内存的大小一般会比新生代内存更大,内部的对象存活时间长,存活率也高,因此采用的垃圾回收算法是标记-压缩算法。
当新创建的对象很大,且新生代的内存不足的情况下,对象会被直接放入老年代。
7.5.优化内存的方法和实践
7.5.1防止内存泄漏
6.5.1.1避免使用非静态的内部类,非静态的内部类会持有外部类的引用,导致外部类不能被回收。
典型的就是在Activity或者Fragment中使用匿名内部类的方式实现Handler的实例。由于Handler持有Activity的引用,在Message延迟等原因没有执行的情况下,Activity就不能被回收,存在风险。
还有就是在Activity中去注册广播、系统回调之后,没有在ondestroy的时候去注销监听,也是会引起内存泄露的。
但是Activity的View设置匿名函数的监听并不会导致内存泄漏,是因为在Activity的ondestroy的时候会主动让View去释放掉监听的引用。
6.5.1.2静态变量的引用
在单例模式中,我们会创建一个静态的变量去持有这个实例。当实例中引入的Context是Activity的时候,会导致Activity被一直强引用,导致内存泄漏。这种情况,可以使用Application Context的去做为单例模式中的Context参数。
7.5.2使用软引用和弱引用
在恰当的时候使用软引用和弱引用,可以降低内存的占用,降低GC的频率。
7.5.3避免在ondraw中创建对象
在自定义View的时候,避免在onDraw中去创建对象,或者去加载图片。因为onDraw会频繁调用,频繁创建对象或者加载资源,会容易造成内存的抖动。
7.5.4对图片的优化处理
图片是很占用内存的,内存占用 = width * height *像素点占用字节,如果是RGB_8888类型,占用4字节,如果RGB_4444则是占用2字节。因此一张800 x 600像素的图片,加载到内存 = 1080 * 720 * 4 = 3037KB = 2.96M的内存。
现在对图片的处理,很多时候会用到Gilde框架处理,确实也挺强大,但是很多时候我们还是需要自己去处理图片问题,特别是图片的缓存和内存的管理。
我们可以使用Android SDK提供的LruCache去构建一个图片缓存容器,它可以用图片的名称或者绝对路径做key,然后图片自己做value来缓存。当图片的缓存达到上限的时候,就移除掉最久没有使用的图片,释放内存。