卡顿优化总结

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,这里才会真正开始执行测量、布局、绘制的流程。然后把绘制好的内容交给GPU,然后GPU会把数据写进back buffer,在下一个Vsync信号开始的时候,Display会把双缓存交换,然后把frame buffer的数据显示到屏幕上。

3.vsync垂直同步信号

vsync信号是由SurfaceFlinger发送出来的,它是一种模拟的脉冲信号,不是实际硬件的脉冲信号,频率为60HZ,也就是每个信号的间隔为16.6ms。Vsync信号会触发View的测量、布局、绘制,并把绘制好的数据给到GPU渲染back buffer;同时Vsync信号也会触发Display交换缓存buffer,并且读取frame buffer数据,把绘制的数据刷新到屏幕上。
Vsync加双buffer的机制,保证了屏幕数据的完整性;同时,也协调了CPU、GPU和Display之间的工作关系,让刷新更高效。

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);
    }
}

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来缓存。当图片的缓存达到上限的时候,就移除掉最久没有使用的图片,释放内存。

  • 23
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值