Hi,我是小余。本文已收录到GitHub Androider-Planet中。这儿有 Android 进阶成长常识系统,重视大众号 [小余的自习室] ,在成功的路上不走失!

浅谈

前段时刻有个客户问我,为啥你们项目都搞了好几年了,为啥线上还会常常反应卡顿,呃呃呃。。

项目维护几年了,为啥还这么卡?

所以依据自己的了解以及网上大佬们的思路总结了一篇关于卡顿优化这块的文章。

卡顿问题是一个陈词滥调的话题了,一个App的好坏,卡顿或许会占一半,它直接决议了用户的留存问题,各大app排行版上,那些知名度较高,可是排行较低的,或许就要思考思考是不是和你app自身有关系了。

卡顿一直是功能优化中相对重要的一个点,因为其涉及了UI制作废物收回(GC)、线程调度以及BinderCPU,GPU方面等JVM以及FrameWork相关常识

假如能做好卡顿优化,那么也就直接证明你对Android FrameWork的了解之深。

下面两篇是笔者之前总结的两篇关于启动优化和内存优化的文章

Android 功能优化(一): 启动优化理论与实践

Android功能优化(二):内存优化你一定要了解的常识点

下面咱们就来解说下卡顿方面的常识。

什么是卡顿:

对用户来讲便是界面不流畅,滞顿。 场景如下

  • 1.视频加载慢,画面卡顿,卡死,黑屏
  • 2.声音卡顿,音画不同步。
  • 3.动画帧卡顿,交互呼应慢
  • 4.滑动不跟手,列表自动更新,滚动不流畅
  • 5.网络呼应慢,数据和画面展示慢、
  • 6.过渡动画僵硬。
  • 7.界面不行交互,卡死,等等现象。

卡顿是怎么产生的

卡顿产生的原因一般都比较杂乱,如CPU内存大小,IO操作,锁操作,低效的算法等都会引起卡顿

站在开发的视点看: 一般咱们讲,屏幕改写率是60fps,需求在16ms内完结一切的作业才不会形成卡顿

为什么是16ms,不是17,18呢?

下面咱们先来理清在UI制作中的几个概念:

SurfaceFlinger:

SurfaceFlinger作用是承受多个来历的图形显现数据Surface,组成后发送到显现设备,比方咱们的主界面中:或许会有statusBar,侧滑菜单,主界面,这些View都是独立Surface烘托和更新,最终提交给SF后,SF依据Zorder,透明度,大小,方位等参数,组成为一个数据buffer,传递HWComposer或许OpenGL处理,终究给显现器

项目维护几年了,为啥还这么卡?

在显现进程中运用到了bufferqueue,surfaceflinger作为consumer方,比方windowmanager办理的surface作为出产方产生页面,交由surfaceflinger进行组成。

项目维护几年了,为啥还这么卡?

VSYNC

Android系统每隔16ms发出VSYNC信号,触发对UI进行烘托,VSYNC是一种在PC上很早就有运用,能够了解为一种守时中止技术。

tearing 问题:

前期的 Android 是没有 vsync 机制的,CPU 和 GPU 的配合也比较紊乱,这也形成闻名的 tearing 问题,即 CPU/GPU 直接更新正在显现的屏幕 buffer 形成画面撕裂。 后续 Android 引入了双缓冲机制,可是 buffer 的切换也需求一个比较适宜的机遇,也便是屏幕扫描完上一帧后的机遇,这也便是引入 vsync 的原因。

新近一般的屏幕改写率是 60fps,所以每个 vsync 信号的距离也是 16ms,不过随着技能的更迭以及厂商关于流畅性的追求,越来越多 90fps 和 120fps 的手机问世,相对应的距离也就变成了 11ms 和 8ms。

VSYNC信号品种:

  • 1.屏幕产生的硬件VSYNC:硬件VSYNC是一种脉冲信号,起到开关和触发某种操作的作用。
  • 2.由SurfaceFlinger将其转成的软件VSYNC信号,经由Binder传递给Choreographer

Choreographer:

编舞者用于注册VSYNC信号并接收VSYNC信号回调,当内部接收到这个信号时终究会调用到doFrame进行帧的制作操作

Choreographer在系统中流程

项目维护几年了,为啥还这么卡?

怎么经过Choreographer核算掉帧状况:原理便是:

经过给Choreographer设置FrameCallback,在每次制作前后看时刻差是16.6ms的多少倍,即为前后掉帧率。

运用办法如下:

//Application.java
public void onCreate() {
     super.onCreate();
     //在Application中运用postFrameCallback
     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);
  }
  @Override
  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);
  }
}

UI制作全路径剖析:

有了前面几个概念,这儿咱们让SurfaceFlinger结合View的制作流程用一张图来表达整个制作流程:

项目维护几年了,为啥还这么卡?

  • 出产者:APP方构建Surface的进程。
  • 消费者:SurfaceFlinger

UI制作全路径剖析卡顿原因:

接下来,咱们逐个剖析,看看都会有哪些原因或许形成卡顿:

1.烘托流程

  • 1.Vsync 调度:这个是起始点,可是调度的进程会经过线程切换以及一些委派的逻辑,有或许形成卡顿,可是一般或许性比较小,咱们也根本无法介入;

  • 2.音讯调度:首要是 doframe Message 的调度,这便是一个一般的 Handler 调度,假如这个调度被其他的 Message 堵塞产生了时延,会直接导致后续的一切流程不会被触发

  • 3.input 处理:input 是一次 Vsync 调度最先履行的逻辑,首要处理 input 事情。假如有很多的事情堆积或许在事情分发逻辑中参加很多耗时事务逻辑,会形成其时帧的时长被拉大,形成卡顿,能够测验经过事情采样的计划,削减 event 的处理

  • 4.动画处理:首要是 animator 动画的更新,同理,动画数量过多,或许动画的更新中有比较耗时的逻辑,也会形成其时帧的烘托卡顿。对动画的降帧和降杂乱度其实处理的便是这个问题;

  • 5.view 处理:首要是接下来的三大流程,过度制作、频频改写、杂乱的视图作用都是此处形成卡顿的首要原因。比方咱们平时所说的下降页面层级,首要处理的便是这个问题;

  • 6.measure/layout/draw:view 烘托的三大流程,因为涉及到遍历和高频履行,所以这儿涉及到的耗时问题均会被放大,比方咱们会降不能在 draw 里边调用耗时函数,不能 new 目标等等;

  • 7.DisplayList 的更新:这儿首要是 canvas 和 displaylist 的映射,一般不会存在卡顿问题,反而或许存在映射失利导致的显现问题;

  • 8.OpenGL 指令转换:这儿首要是将 canvas 的指令转换为 OpenGL 的指令,一般不存在问题

  • 9.buffer 交流:这儿首要指 OpenGL 指令集交流给 GPU,这个一般和指令的杂乱度有关

  • 10.GPU 处理:望文生义,这儿是 GPU 对数据的处理,耗时首要和使命量和纹理杂乱度有关。这也便是咱们下降 GPU 负载有助于下降卡顿的原因;

  • 11.layer 组成:Android P 修改了 Layer 的核算办法 , 把这部分放到了 SurfaceFlinger 主线程去履行, 假如后台 Layer 过多, 就会导致 SurfaceFlinger 在履行 rebuildLayerStacks 的时分耗时 , 导致 SurfaceFlinger 主线程履行时刻过长。 能够选择下降Surface层级来优化卡顿。

项目维护几年了,为啥还这么卡?

  • 12.光栅化/Display:这儿暂时忽略,底层系统行为; Buffer 切换:首要是屏幕的显现,这儿 buffer 的数量也会影响帧的全体推迟,不过是系统行为,不能干预。

2.系统负载

  • 内存:内存的吃紧会直接导致 GC 的添加乃至 ANR,是形成卡顿的一个不行忽视的因素;
  • CPU:CPU 对卡顿的影响首要在于线程调度慢、使命履行的慢和资源竞争,比方
    • 1.降频会直接导致运用卡顿

    • 2.后台活动进程太多导致系统繁忙,cpu \ io \ memory 等资源都会被占用, 这时分很简单出现卡顿问题 ,这种状况比较常见,能够运用dumpsys cpuinfo查看其时设备的cpu运用状况:

    • 3.主线程调度不到 , 处于 Runnable 状况,这种状况比较少见

    • 4.System 锁:system_server 的 AMS 锁和 WMS 锁 , 在系统反常的状况下 , 会变得非常严峻 , 如下图所示 , 许多系统的关键使命都被堵塞 , 等候锁的开释 , 这时分假如有 App 发来的 Binder 恳求带锁 , 那么也会进入等候状况 , 这时分 App 就会产生功能问题 ; 假如此刻做 Window 动画 , 那么 system_server 的这些锁也会导致窗口动画卡顿

项目维护几年了,为啥还这么卡?

  • GPU:GPU 的影响见烘托流程,可是其实还会直接影响到功耗和发热;
  • 功耗/发热:功耗和发热一般是不分居的,高功耗会引起高发热,从而会引起系统保护,比方降频、热缓解等,直接的导致卡顿

怎么监控卡顿

线下监控:

咱们知道卡顿问题的原因错综杂乱,但终究都能够反应到CPU运用率上来

1.运用dumpsys cpuinfo指令

这个指令能够获取其时设备cpu运用状况,咱们能够在线下经过重度运用运用来检测或许存在的卡顿点

A8S:/ $ dumpsys cpuinfo
Load: 1.12 / 1.12 / 1.09
CPU usage from 484321ms to 184247ms ago (2022-11-02 14:48:30.793 to 2022-11-02 1
4:53:30.866):
  2% 1053/scanserver: 0.2% user + 1.7% kernel
  0.6% 934/system_server: 0.4% user + 0.1% kernel / faults: 563 minor
  0.4% 564/signserver: 0% user + 0.4% kernel
  0.2% 256/ueventd: 0.1% user + 0% kernel / faults: 320 minor
  0.2% 474/surfaceflinger: 0.1% user + 0.1% kernel
  0.1% 576/vendor.sprd.hardware.gnss@2.0-service: 0.1% user + 0% kernel / faults
: 54 minor
  0.1% 286/logd: 0% user + 0% kernel / faults: 10 minor
  0.1% 2821/com.allinpay.appstore: 0.1% user + 0% kernel / faults: 1312 minor
  0.1% 447/android.hardware.health@2.0-service: 0% user + 0% kernel / faults: 11
75 minor
  0% 1855/com.smartpos.dataacqservice: 0% user + 0% kernel / faults: 755 minor
  0% 2875/com.allinpay.appstore:pushcore: 0% user + 0% kernel / faults: 744 mino
r
  0% 1191/com.android.systemui: 0% user + 0% kernel / faults: 70 minor
  0% 1774/com.android.nfc: 0% user + 0% kernel
  0% 172/kworker/1:2: 0% user + 0% kernel
  0% 145/irq/24-70900000: 0% user + 0% kernel
  0% 575/thermald: 0% user + 0% kernel / faults: 300 minor
...

2.CPU Profiler

这个东西是AS自带的CPU功能检测东西,能够在PC上实时查看咱们CPU运用状况。 AS供给了四种Profiling Model装备:

  • 1.Sample Java Methods:在运用程序依据Java的代码履行进程中,频频捕获运用程序的调用仓库 获取有关运用程序依据Java的代码履行的时刻和资源运用状况信息。
  • 2.Trace java methods:在运转时对运用程序进行检测,以在每个办法调用的开端和结束时记载时刻戳。收集时刻戳并进行比较以生成办法盯梢数据,包括时序信息和CPU运用率。

请注意与检测每种办法相关的开支会影响运转时功能,并或许影响功能剖析数据。关于生命周期相对较短的办法,这一点乃至更为明显。此外,假如您的运用在短时刻内履行很多办法,则探查器或许会很快超越其文件大小约束,并且或许无法记载任何进一步的盯梢数据。

  • 3.Sample C/C++ Functions:捕获运用程序本机线程的示例盯梢。要运用此装备,您必须将运用程序部署到运转Android 8.0(API等级26)或更高版别的设备。
  • 4.Trace System Calls:捕获细粒度的详细信息,使您能够查看运用程序与系统资源的交互办法 您能够查看线程状况的确切时刻和持续时刻,可视化CPU瓶颈在一切内核中的方位,并添加自定义盯梢事情进行剖析。在对功能问题进行故障排除时,此类信息或许至关重要。要运用此装备,您必须将运用程序部署到运转Android 7.0(API等级24)或更高版别的设备。

运用办法

Debug.startMethodTracing("");
// 需求检测的代码片段
...
Debug.stopMethodTracing();

长处:**有比较全面的调用栈以及图画化办法时刻显现,包括一切线程的状况

缺陷:自身也会带来一点的功能开支,或许会带偏优化方向**

火焰图:能够显现其时运用的办法仓库:

项目维护几年了,为啥还这么卡?

3.Systrace

Systrace在前面一篇剖析启动优化的文章解说过

这儿咱们简略来复习下:

Systrace用来记载其时运用的系统以及运用(运用Trace类打点)的各阶段耗时信息包括制作信息以及CPU信息等

运用办法

Trace.beginSection("MyApp.onCreate_1");
alt(200);
Trace.endSection();

在指令行中:

python systrace.py -t 5 sched gfx view wm am app webview -a "com.chinaebipay.thirdcall" -o D:\trac1.html

记载的办法以及CPU中的耗时状况:

项目维护几年了,为啥还这么卡?
长处

  • 1.轻量级,开支小,CPU运用率能够直观反映
  • 2.右侧的Alerts能够依据咱们运用的问题给出详细的建议,比方说,它会告知咱们App界面的制作比较慢或许GC比较频频。

4.StrictModel

StrictModel是Android供给的一种运转时检测机制,用来协助开发者自动检测代码中不规范的当地。 首要和两部分相关: 1.线程相关 2.虚拟机相关

基础代码:

private void initStrictMode() {
    // 1、设置Debug标志位,只是在线下环境才运用StrictMode
    if (DEV_MODE) {
        // 2、设置线程战略
        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                .detectCustomSlowCalls() //API等级11,运用StrictMode.noteSlowCode
                .detectDiskReads()
                .detectDiskWrites()
                .detectNetwork() // or .detectAll() for all detectable problems
                .penaltyLog() //在Logcat 中打印违规反常信息
//              .penaltyDialog() //也能够直接跳出警报dialog
//              .penaltyDeath() //或许直接崩溃
                .build());
        // 3、设置虚拟机战略
        StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                .detectLeakedSqlLiteObjects()
                // 给NewsItem目标的实例数量约束为1
                .setClassInstanceLimit(NewsItem.class, 1)
                .detectLeakedClosableObjects() //API等级11
                .penaltyLog()
                .build());
    }
}

线上监控:

线上需求自动化的卡顿检测计划来定位卡顿,它能记载卡顿产生时的场景。

自动化监控原理

项目维护几年了,为啥还这么卡?

选用阻拦音讯调度流程,在音讯履行前埋点计时,当耗时超越阈值时,则以为是一次卡顿,会进行仓库抓取和上报作业

首要,咱们看下Looper用于履行音讯循环的loop()办法,关键代码如下所示:

/**
 * Run the message queue in this thread. Be sure to call
 * {@link #quit()} to end the loop.
 */
public static void loop() {
    ...
    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        // This must be in a local variable, in case a UI event sets the logger
        final Printer logging = me.mLogging;
        if (logging != null) {
            // 1
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }
        ...
        try {
             // 2 
             msg.target.dispatchMessage(msg);
            dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
        ...
        if (logging != null) {
            // 3
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }

在Looper的loop()办法中,在其履行每一个音讯(注释2处)的前后都由logging进行了一次打印输出。能够看到,在履行音讯前是输出的”>>>>> Dispatching to “,在履行音讯后是输出的”<<<<< Finished to “,它们打印的日志是不相同的,咱们就能够由此来判断音讯履行的前后时刻点。

详细的完结能够概括为如下进程

  • 1、首要,咱们需求运用Looper.getMainLooper().setMessageLogging()去设置咱们自己的Printer完结类去打印输出logging。这样,在每个message履行的之前和之后都会调用咱们设置的这个Printer完结类。
  • 2、假如咱们匹配到”>>>>> Dispatching to “之后,咱们就能够履行一行代码:也便是在指定的时刻阈值之后,咱们在子线程去履行一个使命,这个使命便是去获取其时主线程的仓库信息以及其时的一些场景信息,比方:内存大小、电脑、网络状况等。
  • 3、假如在指定的阈值之内匹配到了”<<<<< Finished to “,那么说明message就被履行完结了,则标明此刻没有产生咱们以为的卡顿作用,那咱们就能够将这个子线程使命取消掉。

这儿咱们运用blockcanary来做测验:

BlockCanary

APM是一个非侵入式的功能监控组件,能够经过通知的方法弹出卡顿信息。它的原理便是咱们刚刚讲述到的卡顿监控的完结原理。 运用办法

  • 1.导入依靠
implementation 'com.github.markzhai:blockcanary-android:1.5.0'
  • Application的onCreate办法中开启卡顿监控
// 注意在主进程初始化调用
BlockCanary.install(this, new AppBlockCanaryContext()).start();
  • 3.继承BlockCanaryContext类去完结自己的监控装备上下文类
public class AppBlockCanaryContext extends BlockCanaryContext {
	...
	...
	 /**
    * 指定判定为卡顿的阈值threshold (in millis),  
    * 你能够依据不同设备的功能去指定不同的阈值
    *
    * @return threshold in mills
    */
    public int provideBlockThreshold() {
        return 1000;
    }
	....
}
  • 4.在Activity的onCreate办法中履行一个耗时操作
try {
    Thread.sleep(4000);
} catch (InterruptedException e) {
    e.printStackTrace();
}
  • 5.成果:

能够看到一个和LeakCanary相同作用的堵塞可视化仓库图

项目维护几年了,为啥还这么卡?

那有了BlockCanary的办法耗时监控办法是不是就能够解百愁了呢,呵呵。有那么简单就好了

依据原理:咱们拿到的是msg履行前后的时刻和仓库信息,假如msg中有几百上千个办法,就无法承认到底是哪个办法导致的耗时,也有或许是多个办法堆积导致

这就导致咱们无法精确定位哪个办法是最耗时的。如图中:仓库信息是T2的,而产生耗时的办法或许是T1到T2中任何一个办法乃至是堆积导致。

项目维护几年了,为啥还这么卡?

那怎么优化这块

这儿咱们选用字节跳动给咱们供给的一个计划:依据 Sliver trace 的卡顿监控系统

Sliver trace

全体流程图

项目维护几年了,为啥还这么卡?
首要包括两个方面:

  • 检测计划: 在监控卡顿时,首要需求翻开 Sliver 的 trace 记载能力,Sliver 采样记载 trace 履行信息,对抓取到的仓库进行 diff 聚合和缓存。

一起依据咱们的需求设置相应的卡顿阈值,以 Message 的履行耗时为衡量。对主线程音讯调度流程进行阻拦,在音讯开端分发履行时埋点,在音讯履行结束时核算音讯履行耗时,当音讯履行耗时超越阈值,则以为产生了一次卡顿。

  • 仓库聚合战略: 当卡顿产生时,咱们需求为此次卡顿预备数据,这部分作业是在端上子线程中完结的,首要是 dump trace 到文件以及过滤聚合要上报的仓库。分为以下几步:
    • 1.拿到缓存的主线程 trace 信息并 dump 到文件中。
    • 2.然后从文件中读取 trace 信息,依照数据格式,从最近的办法栈向上追溯,找到其时 Message 包括的全部 trace 信息,并将其时 Message 的完好 trace 写入到待上传的 trace 文件中,删除其余 trace 信息。
    • 3.遍历其时 Message trace,依照(Method 履行耗时 > Method 耗时阈值 & Method 耗时为该层仓库中最耗时)为条件过滤出每一层函数调用仓库的最长耗时函数,构成最终要上报的仓库链路,这样特征仓库中的每一步都是最耗时的,且最底层 Method 为最终的耗时大于阈值的 Method。

之后,将 trace 文件和仓库一同上报,这样的特征仓库提取战略确保了仓库聚合的可靠性和精确性,确保了上报到平台后仓库的正确合理聚合,一起供给了进一步剖析问题的 trace 文件。

能够看到字节给的是一整套监控计划,和前面BlockCanary不同之处就在于,其是守时存储仓库,缓存,然后运用diff去重的办法,并上传到服务器,能够最大限度的监控到或许产生比较耗时的办法。

开发中哪些习惯会影响卡顿的产生

1.布局太乱,层级太深。

  • 1.1:经过削减冗余或许嵌套布局来下降视图层次结构。比方运用约束布局代替线性布局和相对布局。
  • 1.2:用 ViewStub 代替在启动进程中不需求显现的 UI 控件。
  • 1.3:运用自定义 View 代替杂乱的 View 叠加。

2.主线程耗时操作

  • 2.1:主线程中不要直接操作数据库,数据库的操作应该放在数据库线程中完结。
  • 2.2:sharepreference尽量运用apply,少运用commit,能够运用MMKV结构来代替sharepreference。
  • 2.3:网络恳求回来的数据解析尽量放在子线程中,不要在主线程中进行仿制的数据解析操作。
  • 2.4:不要在activity的onResume和onCreate中进行耗时操作,比方很多的核算等。
  • 2.5:不要在 draw 里边调用耗时函数,不能 new 目标

3.过度制作

过度制作是同一个像素点上被屡次制作,削减过度制作一般削减布局背景叠加等办法,如下图所示右边是过度制作的图片。

项目维护几年了,为啥还这么卡?

4.列表

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

5.目标分配和收回优化

自从Android引入 ART 并且在Android 5.0上成为默认的运转时之后,目标分配和废物收回(GC)形成的卡顿已经显著下降了,可是因为目标分配和GC有额外的开支,它仍然又或许使线程负载过重。 在一个调用不频频的当地(比方按钮点击)分配目标是没有问题的,但假如在在一个被频频调用的紧密的循环里,就需求避免目标分配来下降GC的压力。

削减小目标的频频分配和收回操作。

好了,关于卡顿优化的问题就讲到这儿,下篇文章会对卡顿中的ANR状况的处理,这儿做个衬托。

假如喜欢我的文章,欢迎重视我的大众号

项目维护几年了,为啥还这么卡?

参考

Android卡顿检测及优化

一文读懂直播卡顿优化那些事儿

“总算懂了” 系列:Android屏幕改写机制—VSync、Choreographer 全面了解!

深化探究Android卡顿优化(上)

西瓜卡顿 & ANR 优化治理及监控系统建造