卡顿的起源

271 阅读5分钟

为什么能看到卡顿

先了解几个概念:

  • 屏幕刷新频率: 一秒内屏幕的刷新效率,如目前市场上常见的60HZ,90HZ,120HZ ,刷新率的多少取决于手机的硬件配置
  • 逐行扫描:手机画面展示是通过像素点显示,像素点的渲染在手机上是从上到下,从左到右的顺序显示;比如60hz刷新率手机一次扫描需要的时间是1000 / 60 ≈ 16ms
  • 帧率:GPU一秒内绘制操作的帧数,单位 fps。根据我们的常识第一感觉,帧率越高就会越流畅,就像书本动画我们翻阅的越快感觉越流畅,Android 的屏幕帧数是60fbs,为什么不是更高,因为大脑和眼睛的所能感知的最高帧率是60(但是据说也有120fbs,生物学总是在进步的吗),所以再高对于人来说没有任何意义,一个设备的帧率不是永远不变的,是根据当前画面所决定,所以我我们说的60fbs 是设备在使用的过程中最大的帧率b

谁决定了流程度

手机厂商都在吹嘘自己手机的刷新率,如何丝滑,但是刷新率并不是决定丝滑的唯一因素,GPU 的帧率,屏幕的刷新率,cpu等,厂商系统,app 的质量都可以影响你使用的流畅度, 在cpu 等其他条件最优单一条件下,GPU 的帧率确实是影响视觉流程的首要元素,但是GPU的帧率呈现也受刷新率的影响,120fbs 遇到60hz 的刷新率ta的上限也是60fbs;所以想要手机流畅去买个水桶机硬件跟的上,软件去下载正版大厂的app保证app的质量部,我们在使用的时候尽量不要非常规操作,你的手机就会有最佳体验感!

找到Android 卡顿的原因

导致Android卡顿主要原因

想达到帧率为60fbs 一个逐行扫描必须为16ms 如果耗时太长就会出现丢帧的现象,如果在32ms内就会看到同一帧。当丢针时候就是你觉的卡顿时候,导致Android 卡顿的原因有很多,一般主要原因是:过多的ui绘制,大量的io 的操作或者大量的占用cpu,导致cpu 数据处理时长拉长,导致一次逐行扫描时间过长,视觉卡顿,关于Android 的渲染机制,从郭神那看到一篇文章讲的很好,在此分享文章地址

找到Android 的卡顿

使用Systrace查找

  1. 使用adb 链接手机
  2. 使用Android Sdk tools Systrace
  3. 使用python ./systrace.py -t 5 -o looktrace.html(python 2.7 )

python systrace.py [options] [categories] options

image.png

category

image.png

  1. 用chrome浏览器打开looktrace.html,地址栏输入chrome://tracing/ 来查看报告。

image.png UI thread

image.png

不同的颜色会有不同的状态 下面列举下不同颜色的不同状态:

  • 绿色:表示正在运行

即使是绿色,也要看是否频率是否是对的, 1. 频率是否正确,是否是运行在打核上Cpu0

  • 蓝色:表示可以运行,但是cpu的在执行其他线程

检测是否有很多子线程任务

  • 紫色:表示休眠

表示有io 操作

白色:表示休眠

可能是线程上的互斥锁,如Binder阻塞、/sleep/Wait

我们根据每个线程的状态颜色结合自己代码,判断哪些是在卡顿,哪些是可以优化的,Systrace 只能范围性帮助我们找到cpu调度,具体根据项目的具体情况,

Trace view 精确卡顿位置

Systrace 确定了范围,我们想要在猜测的范围内找到相对精确的范围我们可以使用Trace view

  1. 使用Trace api 打印标记
super.onCreate(savedInstanceState);
TraceCompat.beginSection("libo");

mActivity=this;
viewDataBinding = DataBindingUtil.setContentView(this, getLayoutId());
initLoadSir();
viewModel = getViewModel();
getLifecycle().addObserver(viewModel);
viewDataBinding.setVariable(initViewModeId(),viewModel);
viewDataBinding.setLifecycleOwner(this);
onViewCreate();
initLiveDataLister();
TraceCompat.endSection();

2. cd 到 Android Sdk tools Systrace并执行python ./systrace.py -t 5 -o looktrace.html -a 包名

  1. 打开looktrace looktrace

image.png 我们通过frames 查看每一帧的时间间隔。来判断是是否掉帧,判断到掉帧的范围。 可以通过ui thread 看下执行情况是否有耗时,

App 层面来监控卡顿

前面的工具使用主要是确定范围,猜测原因,如果需要精确到具体什么函数,就需要在代码层面去下手操作,Android ui 线程是通过loop 不断去取message转换handler 让其在ui 线程执行,看下Loop.looper源码

public static void loop() {
    final Looper me = myLooper();
 --》省略代码
    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) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }

        final long traceTag = me.mTraceTag;
        long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
        long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
        if (thresholdOverride > 0) {
            slowDispatchThresholdMs = thresholdOverride;
            slowDeliveryThresholdMs = thresholdOverride;
        }
        final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
        final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);

        final boolean needStartTime = logSlowDelivery || logSlowDispatch;
        final boolean needEndTime = logSlowDispatch;

        if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
            Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
        }

        final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
        final long dispatchEnd;
        try {
            msg.target.dispatchMessage(msg);
            dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
  
  if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);}

  
     ==> 省略部分代码
}

从代码中看到,handler msg.target.dispatchMessage 监控这个函数的执行时长就可以监控是否卡顿,我们看到,looper 也在使用的 Trace.traceBegin ,Trace.traceEnd 来监控性能和卡顿,监听的范围也是dispatchMessage 这个过程的时长, 如果我们也想监听这个时长怎么办? 我们注意到 在这个函数前后有两个打印: logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what);和logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);}我们可以通过这两个log 的时间差来判断是否卡顿,可以通过自定义设置 Looper.getMainLooper().setMessageLogging(logMonitor);来设置logging 来监听卡顿信息,下面贴下逻辑:

@Override
public void println(String x) {
  
    if (!mPrintingStarted) {// 第一打印为Start
        //记录开始时间
        mStartTimestamp = System.currentTimeMillis();
        mPrintingStarted = true;// 改为开始
        mStackSampler.startDump();//收集堆栈信息
    } else {
        final long endTime = System.currentTimeMillis();
        mPrintingStarted = false;// 改为结束
        if (isBlock(endTime)) {// 是否出现卡顿
            notifyBlockEvent(endTime);
        }
        mStackSampler.stopDump();
    }
}

当出现卡顿的时候,我们可以通过收集的堆栈信息来找到卡顿的原因

找到过度绘制减少卡顿

找到过度绘制

  1. 进入开发者
  2. 启用Gpu的调试层。显示过度绘制。显示ui边界
  3. 根据颜色确定绘制次数
  • 蓝色:绘制一次
  • 绿色:绘制两次
  • 粉色:过度绘制3次
  • 红色:过度绘制4次或者四次以上

减少绘制导致的卡顿

过度绘制会导致卡顿,众人皆知,怎么样才能确定过度绘制了呢?

  1. 使用Layout Inspector 查看布局层级
  2. 使用merge 去除同一属性布局,merge 本身没有任何意义,只是让写法合法
  3. 当一个view不知道是否需要展示的时候我们可以使用viewStub来引向布局,当viewStub 是Gone 是不占用任何资源,

VIEW.Gone 虽然不占用位置,但是会被创建对象

  1. 移除布局中不需要的背景
  2. 使视图扁平化减少嵌套,可以进可能使用ConstraintLayout
  3. 尽量少使用透明度设置

因为透明的绘制需要多次渲染,很容易导致过度绘制

  1. 特定场景使用AsyncLayoutInflater 来异步加载布局

LayoutInflater 正常加载xml 的布局其实是io 的一个操作,如果某个布局比较复杂,会呈现加载比较慢,AsyncLayoutInflat 是异步的情况去加载布局,减少主线程的耗时