Android卡顿检测及优化

前言

以前在项目中作过一些Android卡顿以及性能优化的工做,可是一直没时间总结,趁着这段时间把这部分总结一下。html

卡顿

在应用开发中若是留意到log的话有时候可能会发下下面的log信息:java

I/Choreographer(1200): Skipped 60 frames!  The application may be doing too much work on its main thread.
复制代码

在大部分Android平台的设备上,Android系统是16ms刷新一次,也就是一秒钟60帧。要达到这种刷新速度就要求在ui线程中处理的任务时间必需要小于16ms,若是ui线程中处理时间长,就会致使跳过帧的渲染,也就是致使界面看起来不流畅,卡顿。若是用户点击事件5s中没反应就会致使ANR。python

帧率

即 Frame Rate,单位 fps,是指 gpu 生成帧的速率,60fps,Android中更帧率相关的类是SurfaceFlinger。android

SurfaceFlinger surfaceflinger做用是接受多个来源的图形显示数据,将他们合成,而后发送到显示设备。好比打开应用,常见的有三层显示,顶部的statusbar底部或者侧面的导航栏以及应用的界面,每一个层是单独更新和渲染,这些界面都是有surfaceflinger合成一个刷新到硬件显示。 web

在这里插入图片描述
在显示过程当中使用到了bufferqueue,surfaceflinger做为consumer方,好比windowmanager管理的surface做为生产方产生页面,交由surfaceflinger进行合成。

在这里插入图片描述
VSync

Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,VSync是Vertical Synchronization(垂直同步)的缩写,是一种在PC上很早就普遍使用的技术,能够简单的把它认为是一种定时中断。而在Android 4.1(JB)中已经开始引入VSync机制,用来同步渲染,让UI和SurfaceFlinger能够按硬件产生的VSync节奏进行工做。shell

安卓系统中有 2 种 VSync 信号: 一、屏幕产生的硬件 VSync: 硬件 VSync 是一个脉冲信号,起到开关或触发某种操做的做用。 二、由 SurfaceFlinger 将其转成的软件 Vsync 信号:经由 Binder 传递给 Choreographer。数据库

除了Vsync的机制,Android还使用了多级缓冲的手段以优化UI流程度,例如双缓冲(A+B),在显示buffer A的数据时,CPU/GPU就开始在buffer B中准备下一帧数据:可是不能保证每一帧CPU、GPU都运行状态良好,可能因为资源抢占等性能问题致使某一帧GPU掉链子,vsync信号到来时buffer B的数据还没准备好,而此时Display又在显示buffer A的数据,致使后面CPU/GPU没有新的buffer着手准备数据,致使卡顿(jank)。 浏览器

在这里插入图片描述

卡顿缘由

从系统层面上看主要如下几个方面的缘由会致使卡顿: 1. SurfaceFlinger 主线程耗时性能优化

SurfaceFlinger 负责 Surface 的合成 , 一旦 SurfaceFlinger 主线程调用超时 , 就会产生掉帧 . SurfaceFlinger 主线程耗时会也会致使 hwc service 和 crtc 不能及时完成, 也会阻塞应用的 binder 调用, 如 dequeueBuffer \ queueBuffer 等.网络

2. 后台活动进程太多致使系统繁忙

后台进程活动太多,会致使系统很是繁忙, cpu \ io \ memory 等资源都会被占用, 这时候很容易出现卡顿问题 , 这也是系统这边常常会碰到的问题。 dumpsys cpuinfo 能够查看一段时间内 cpu 的使用状况:

在这里插入图片描述
3.主线程调度不到 , 处于 Runnable 状态

当线程为 Runnable 状态的时候 , 调度器若是迟迟不能对齐进行调度 , 那么就会产生长时间的 Runnable 线程状态 , 致使错过 Vsync 而产生流畅性问题。

四、System 锁

system_server 的 AMS 锁和 WMS 锁 , 在系统异常的状况下 , 会变得很是严重 , 以下图所示 , 许多系统的关键任务都被阻塞 , 等待锁的释放 , 这时候若是有 App 发来的 Binder 请求带锁 , 那么也会进入等待状态 , 这时候 App 就会产生性能问题 ; 若是此时作 Window 动画 , 那么 system_server 的这些锁也会致使窗口动画卡顿

在这里插入图片描述
五、Layer过多致使 SurfaceFlinger Layer Compute 耗时

Android P 修改了 Layer 的计算方法 , 把这部分放到了 SurfaceFlinger 主线程去执行, 若是后台 Layer 过多, 就会致使 SurfaceFlinger 在执行 rebuildLayerStacks 的时候耗时 , 致使 SurfaceFlinger 主线程执行时间过长。

在这里插入图片描述
从应用层来看如下会致使卡顿:

一、主线程执行时间长 主线程执行 Input \ Animation \ Measure \ Layout \ Draw \ decodeBitmap 等操做超时都会致使卡顿 。

  • 一、Measure \ Layout 耗时\超时

在这里插入图片描述

  • 二、draw耗时

在这里插入图片描述

  • 三、Animation回调耗时

在这里插入图片描述

  • 四、View 初始化耗时

在这里插入图片描述

  • 五、List Item 初始化耗时

在这里插入图片描述

  • 六、主线程操做数据库

二、主线程 Binder 耗时

Activity resume 的时候, 与 AMS 通讯要持有 AMS 锁, 这时候若是碰到后台比较繁忙的时候, 等锁操做就会比较耗时, 致使部分场景由于这个卡顿, 好比多任务手势操做。

在这里插入图片描述

三、WebView 性能不足

应用里面涉及到 WebView 的时候, 若是页面比较复杂, WebView 的性能就会比较差, 从而形成卡顿

在这里插入图片描述

四、帧率与刷新率不匹配

若是屏幕帧率和系统的 fps 不相符 , 那么有可能会致使画面不是那么顺畅. 好比使用 90 Hz 的屏幕搭配 60 fps 的动画。

在这里插入图片描述

卡顿检测

卡顿检测可使用如下多种方法同时进行: 一、使用dumpsys gfxinfo 二、使用Systrace获取相关信息 三、使用LayoutInspect 检测布局层次 四、使用BlockCanary 五、利用Choreographer。 六、使用严格模式(StrictMode )。

一、使用dumpsys gfxinfo

在开发过程当中发现有卡顿发生时可使用下面的命令来获取卡顿相关的信息:

adb shell dumpsys gfxinfo [PACKAGE_NAME]
复制代码

输入这个命令后可能会打印下面的信息:

Applications Graphics Acceleration Info:
Uptime: 102809662 Realtime: 196891968
** Graphics info for pid 31148 [com.android.settings] **
Stats since: 524615985046231ns
Total frames rendered: 8325
Janky frames: 729 (8.76%)
90th percentile: 13ms
95th percentile: 20ms
99th percentile: 73ms
Number Missed Vsync: 294
Number High input latency: 47
Number Slow UI thread: 502
Number Slow bitmap uploads: 44
Number Slow issue draw commands: 135
复制代码

上面参数说明:

Graphics info for pid 31148 [com.android.settings]: 代表当前dump的为设置界面的帧信息,pid为31148 Total frames rendered: 8325 本次dump搜集了8325帧的信息

Janky frames :729 (8.76%)出现卡顿的帧数有729帧,占8.76%

Number Missed Vsync: 294 垂直同步失败的帧

Number Slow UI thread: 502 因UI线程上的工做致使超时的帧数

Number Slow bitmap uploads: 44 因bitmap的加载耗时的帧数

Number Slow issue draw commands: 135 因绘制致使耗时的帧数

二、使用systrace

上面使用的dumpsys是能发现问题或者判断问题的严重性,但没法定位真正的缘由。若是要定位缘由,应当配合systrace工具使用。

systrace使用

Systrace能够帮助分析应用是如何设备上运行起来的,它将系统和应用程序线程集中在一个共同的时间轴上,分析systrace的第一步须要在程序运行的时间段中抓取trace log,在抓取到的trace文件中,包含了这段时间中想要的关键信息,交互状况。

在这里插入图片描述
图1显示的是当一个app在滑动时出现了卡顿的现象,默认的界面下,横轴是时间,纵向为trace event,trace event 先按进程分组,而后再按线程分组.从上到下的信息分别为Kernel,SurfaceFlinger,应用包名。经过配置trace的分类,能够根据配置状况记录每一个应用程序的全部线程信息以及trace event的层次结构信息。

Android studio中使用systrace

一、在android设备的 设置 -- 开发者选项 -- 监控 -- 开启traces。 二、选择要追中的类别,而且点击肯定。

完成以上配置后,开始抓trace文件

$ python systrace.py --cpu-freq --cpu-load --time=10 -o mytracefile.html
复制代码

分析trace文件 抓到trace.html文件后,经过web浏览器打开

检查Frames 每一个应用程序都有一排表明渲染帧的圆圈,一般为绿色,若是绘制的时间超过16.6毫秒则显示黄色或红色。经过“W”键查看帧。

在这里插入图片描述
trace应用程序代码 在framework中的trace marker并无覆盖到全部代码,所以有些时候须要本身去定义trace marker。在Android4.3以后,能够经过Trace类在代码中添加标记,这样将可以看到在指定时间内应用的线程在作哪些工做,固然,trace 的begin和end操做也会增长一些额外的开销,但都只有几微秒左右。 经过下面的例子来讲明Trace类的 用法。

public class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {

    ...

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        Trace.beginSection("MyAdapter.onCreateViewHolder");
        MyViewHolder myViewHolder;
        try {
            myViewHolder = MyViewHolder.newInstance(parent);
        } finally {
            Trace.endSection();
        }
        return myViewHolder;
    }

   @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        Trace.beginSection("MyAdapter.onBindViewHolder");
        try {
            try {
                Trace.beginSection("MyAdapter.queryDatabase");
                RowItem rowItem = queryDatabase(position);
                mDataset.add(rowItem);
            } finally {
                Trace.endSection();
            }
            holder.bind(mDataset.get(position));
        } finally {
            Trace.endSection();
        }
    }

…

}
复制代码

3 、使用BlockCanary

BlockCanary是国内开发者MarkZhai开发的一套性能监控组件,它对主线程操做进行了彻底透明的监控,并能输出有效的信息,帮助开发分析、定位到问题所在,迅速优化应用。 其特色有: 一、非侵入式,简单的两行就打开监控,不须要处处打点,破坏代码优雅性。 二、精准,输出的信息能够帮助定位到问题所在(精确到行),不须要像Logcat同样,慢慢去找。 三、目前包括了核心监控输出文件,以及UI显示卡顿信息功能

BlockCanary基本原理

android应用程序只有一个主线程ActivityThread,这个主线程会建立一个Looper(Looper.prepare),而Looper又会关联一个MessageQueue,主线程Looper会在应用的生命周期内不断轮询(Looper.loop),从MessageQueue取出Message 更新UI。

public static void loop() {
    ...
    for (;;) {
        ...
        // This must be in a local variable, in case a UI event sets the logger
        Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }
        msg.target.dispatchMessage(msg);
        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }
        ...
    }
}
复制代码

BlockCanary主要是检测msg.target.dispatchMessage(msg);以前的>>>>> Dispatching to 和以后的<<<<< Finished to的间隔时间。 应用发生卡顿,必定是在dispatchMessage中执行了耗时操做。经过给主线程的Looper设置一个Printer,打点统计dispatchMessage方法执行的时间,若是超出阀值,表示发生卡顿,则dump出各类信息,提供开发者分析性能瓶颈。

四、使用Choreographer

Android 主线程运行的本质,其实就是 Message 的处理过程,咱们的各类操做,包括每一帧的渲染操做 ,都是经过 Message 的形式发给主线程的 MessageQueue ,MessageQueue 处理完消息继续等下一个消息。

在这里插入图片描述
Choreographer 的引入,主要是配合 Vsync ,给上层 App 的渲染提供一个稳定的 Message 处理的时机,也就是 Vsync 到来的时候 ,系统经过对 Vsync 信号周期的调整,来控制每一帧绘制操做的时机. 目前大部分手机都是 60Hz 的刷新率,也就是 16.6ms 刷新一次,系统为了配合屏幕的刷新频率,将 Vsync 的周期也设置为 16.6 ms,每一个 16.6 ms , Vsync 信号唤醒 Choreographer 来作 App 的绘制操做 ,这就是引入 Choreographer 的主要做用。

Choreographer 两个主要做用

一、承上:负责接收和处理 App 的各类更新消息和回调,等到 Vsync 到来的时候统一处理。好比集中处理 Input(主要是 Input 事件的处理) 、Animation(动画相关)、Traversal(包括 measure、layout、draw 等操做) ,判断卡顿掉帧状况,记录 CallBack 耗时等。

二、启下:负责请求和接收 Vsync 信号。接收 Vsync 事件回调(经过 FrameDisplayEventReceiver.onVsync );请求 Vsync(FrameDisplayEventReceiver.scheduleVsync) .

使用Choreographer 计算帧率

Choreographer 处理绘制的逻辑核心在 Choreographer.doFrame 函数中,从下图能够看到,FrameDisplayEventReceiver.onVsync post 了本身,其 run 方法直接调用了 doFrame 开始一帧的逻辑处理:

在这里插入图片描述
Choreographer周期性的在UI重绘时候触发,在代码中记录上一次和下一次绘制的时间间隔,若是超过16ms,就意味着一次UI线程重绘的“丢帧”。丢帧的数量为间隔时间除以16,若是超过3,就开始有卡顿的感知。 使用Choreographer检测帧的代码以下:

public class MyFrameCallback implements Choreographer.FrameCallback {
        private String TAG = "性能检测";
        private long lastTime = 0;

        @Override
        public void doFrame(long frameTimeNanos) {
            if (lastTime == 0) {
                //代码第一次初始化。不作检测统计。
                lastTime = frameTimeNanos;
            } else {
                long times = (frameTimeNanos - lastTime) / 1000000;
                int frames = (int) (times / 16);

                if (times > 16) {
                    Log.w(TAG, "UI线程超时(超过16ms):" + times + "ms" + " , 丢帧:" + frames);
                }

                lastTime = frameTimeNanos;
            }

            Choreographer.getInstance().postFrameCallback(mFrameCallback);
        }
    }
复制代码

卡顿优化

由上面的分析可知对象分配垃圾回收(GC)、线程调度以及Binder调用 是Android系统中常见的卡顿缘由,所以卡顿优化主要如下几种方法,更多的要结合具体的应用来进行:

一、布局优化

  • 经过减小冗余或者嵌套布局来下降视图层次结构。好比使用约束布局代替线性布局和相对布局。
  • 用 ViewStub 替代在启动过程当中不须要显示的 UI 控件。
  • 使用自定义 View 替代复杂的 View 叠加。

二、减小主线程耗时操做

  • 主线程中不要直接操做数据库,数据库的操做应该放在数据库线程中完成。
  • sharepreference尽可能使用apply,少使用commit,可使用MMKV框架来代替sharepreference。
  • 网络请求回来的数据解析尽可能放在子线程中,不要在主线程中进行复制的数据解析操做。
  • 不要在activity的onResume和onCreate中进行耗时操做,好比大量的计算等。

三、减小过分绘制 过分绘制是同一个像素点上被屡次绘制,减小过分绘制通常减小布局背景叠加等方式,以下图所示右边是过分绘制的图片。

在这里插入图片描述
四、列表优化

  • RecyclerView使用优化,使用DiffUtil和notifyItemDataSetChanged进行局部更新等。

五、对象分配和回收优化

自从Android引入 ART 而且在Android 5.0上成为默认的运行时以后,对象分配和垃圾回收(GC)形成的卡顿已经显著下降了,可是因为对象分配和GC有额外的开销,它依然又可能使线程负载太重。 在一个调用不频繁的地方(好比按钮点击)分配对象是没有问题的,但若是在在一个被频繁调用的紧密的循环里,就须要避免对象分配来下降GC的压力。

  • 减小小对象的频繁分配和回收操做。

参考文献

一、source.android.google.cn/devices/gra… 二、www.bradcypert.com/what-is-and… 三、ashishb.net/tech/demyst… 四、devblogs.microsoft.com/xamarin/tip… 五、developer.android.com/training/te… 六、www.androidperformance.com/2019/09/05/… 七、developer.android.com/tools/help/… 八、zhuanlan.zhihu.com/p/87954949

相关文章
相关标签/搜索