浅谈
前段时间有个客户问我,为啥你们项目都搞了好几年了,为啥线上还会常常反馈卡顿,呃呃呃。。
于是根据自己的明白以及网上大佬们的思绪总结了一篇关于卡顿优化这块的文章。
卡顿标题是一个老生常谈的话题了,一个App的好坏,卡顿大概会占一半,它直接决定了用户的留存标题,各大app排行版上,那些着名度较高,但是排行较低的,大概就要思索思索是不是和你app自己有关系了。
卡顿不绝是性能优化中相对紧张的一个点,由于其涉及了UI绘制、垃圾接纳(GC)、线程调理以及Binder,CPU,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.javapublic 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 cpuinfoLoad: 1.12 / 1.12 / 1.09CPU usage from 484321ms to 184247ms ago (2022-11-02 14:48:30.793 to 2022-11-02 14: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: 1175 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 minor 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是一个非侵入式的性能监控组件,可以通过关照的情势弹出卡顿信息。它的原理就是我们刚刚报告到的卡顿监控的实现原理。 使用方式:
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();}
可以看到一个和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 优化管理及监控体系建立
作者:小余的自习室
链接:https://juejin.cn/post/7161757546875715615
|