android

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

2022-11-27  本文已影响0人  我爱田Hebe

浅谈

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

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

卡顿问题是一个老生常谈的话题了,一个App的好坏,卡顿也许会占一半,它直接决定了用户的留存问题,各大app排行版上,那些知名度较高,但是排行较低的,可能就要思考思考是不是和你app本身有关系了。

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

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

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

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

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

下面我们就来讲解下卡顿方面的知识。

什么是卡顿:

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

卡顿是如何发生的

卡顿产生的原因一般都比较复杂,如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信号种类:

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的绘制流程用一张图来表达整个绘制流程:

UI绘制全路径分析卡顿原因:

接下来,我们逐个分析,看看都会有哪些原因可能造成卡顿:

1.渲染流程

2.系统负载

如何监控卡顿

线下监控:

我们知道卡顿问题的原因错综复杂,但最终都可以反馈到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配置:

请注意与检测每种方法相关的开销会影响运行时性能,并可能影响性能分析数据。对于生命周期相对较短的方法,这一点甚至更为明显。此外,如果您的应用在短时间内执行大量方法,则探查器可能会很快超过其文件大小限制,并且可能无法记录任何进一步的跟踪数据。

使用方式

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中的耗时情况:

优点

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 ",它们打印的日志是不一样的,我们就可以由此来判断消息执行的前后时间点。

具体的实现可以归纳为如下步骤

这里我们使用blockcanary来做测试:

BlockCanary

APM是一个非侵入式的性能监控组件,可以通过通知的形式弹出卡顿信息。它的原理就是我们刚刚讲述到的卡顿监控的实现原理。 使用方式

implementation 'com.github.markzhai:blockcanary-android:1.5.0'
// 注意在主进程初始化调用
BlockCanary.install(this, new AppBlockCanaryContext()).start();
public class AppBlockCanaryContext extends BlockCanaryContext {
    ...
    ...
     /**
    * 指定判定为卡顿的阈值threshold (in millis),  
    * 你可以根据不同设备的性能去指定不同的阈值
    *
    * @return threshold in mills
    */
    public int provideBlockThreshold() {
        return 1000;
    }
    ....
}

try {
    Thread.sleep(4000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

可以看到一个和LeakCanary一样效果的阻塞可视化堆栈图

那有了BlockCanary的方法耗时监控方式是不是就可以解百愁了呢,呵呵。有那么容易就好了

根据原理:我们拿到的是msg执行前后的时间和堆栈信息,如果msg中有几百上千个方法,就无法确认到底是哪个方法导致的耗时,也有可能是多个方法堆积导致

这就导致我们无法准确定位哪个方法是最耗时的。如图中:堆栈信息是T2的,而发生耗时的方法可能是T1到T2中任何一个方法甚至是堆积导致。

那如何优化这块

这里我们采用字节跳动给我们提供的一个方案:基于 Sliver trace 的卡顿监控体系

Sliver trace

整体流程图

主要包含两个方面:

同时基于我们的需要设置相应的卡顿阈值,以 Message 的执行耗时为衡量。对主线程消息调度流程进行拦截,在消息开始分发执行时埋点,在消息执行结束时计算消息执行耗时,当消息执行耗时超过阈值,则认为产生了一次卡顿。

之后,将 trace 文件和堆栈一同上报,这样的特征堆栈提取策略保证了堆栈聚合的可靠性和准确性,保证了上报到平台后堆栈的正确合理聚合,同时提供了进一步分析问题的 trace 文件。

可以看到字节给的是一整套监控方案,和前面BlockCanary不同之处就在于,其是定时存储堆栈,缓存,然后使用diff去重的方式,并上传到服务器,可以最大限度的监控到可能发生比较耗时的方法。

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

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

2.主线程耗时操作

3.过度绘制

过度绘制是同一个像素点上被多次绘制,减少过度绘制一般减少布局背景叠加等方式,如下图所示右边是过度绘制的图片。

4.列表

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

5.对象分配和回收优化

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

减少小对象的频繁分配和回收操作。

好了,关于卡顿优化的问题就讲到这里,下篇文章会对卡顿中的ANR情况的处理,这里做个铺垫。

参考

Android卡顿检测及优化

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

“终于懂了” 系列:Android屏幕刷新机制—VSync、Choreographer 全面理解!

深入探索Android卡顿优化(上)

西瓜卡顿 & ANR 优化治理及监控体系建设

作者:小余的自习室
链接:https://juejin.cn/post/7161757546875715615

上一篇 下一篇

猜你喜欢

热点阅读