热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Android性能优化检测App卡顿

在移动APP性能评测-流畅度评测中,我们介绍了如何准确客观评价APP的流畅度,最终采用SM指标来评价应用的流畅度,在知道如何评价流畅度之后

在移动APP性能评测-流畅度评测中,我们介绍了如何准确客观评价APP的流畅度,最终采用SM指标来评价应用的流畅度,在知道如何评价流畅度之后,我们应该如何来检测出APP中的UI卡顿就是我们面临的一个新的问题;在Android性能优化-App卡顿中介绍了Google官方提供的检测卡顿的方法,除此之外还有那边比较好的方法来检测应用卡顿?目前主流的方法主要有:
1.利用UI线程Looper打印的日志,典型代表就是BlockCanary;
2.采用Choreographer;
BlockCanary:blockcanary是国内开发者MarkZhai开发的一套性能监控组件,它对主线程操作进行了完全透明的监控,并能输出有效的信息,帮助开发分析、定位到问题所在,迅速优化应用;
BlockCanary核心原理:通过自定义一个Printer,设置到主线程ActivityThread的MainLooper中。MainLooper在dispatch消息前后都会调用Printer进行打印。从而获取前后执行的时间差值,判断是否超过设置的阈值。如果超过,则会将记录的栈信息及cpu信息发通知到前台。和利用UI线程Looper打印日志原理一样;
下面通过Blockcanary来简单介绍它是如何来检测应用卡顿的,然后简单介绍通过Choreographer来检测应用卡顿;

Blockcanary检测APP卡顿

GitHub地址:BlockCanary
Blog in Chinese: BlockCanary.
blockcanary源码学习随笔
BlockCanary原理图如下图所示:

 

BlockCanary原理图.png


其中最核心的两步是在调用msg.target.dispatchMessage(msg),进行消息的分发前记录时间T1,调用msg.target.dispatchMessage(msg)进行消息分发后记录时间T2,如果T2-T1大于设置的卡顿阈值就会打印当前方法调用堆栈以及显示其他相关提示或打印日志;
blockcanary充分的利用了Loop的机制,在MainLooper的loop方法中执行dispatchMessage前后都会执行printer的println进行输出,并且提供了方法设置printer。通过分析前后打印的时差与阈值进行比对,从而判定是否卡顿。下面我们来看一下Looper中的loop方法;

Looper.java

public static void loop() {// 获取一个Looper对象final Looper me &#61; myLooper();if (me &#61;&#61; null) {throw new RuntimeException("No Looper; Looper.prepare() wasn&#39;t called on this thread.");}// 获取Looper中的消息队列final MessageQueue queue &#61; me.mQueue;// 死循环&#xff0c;对消息队列里面的消息进行遍历for (;;) {// 通过queue.next()取出消息&#xff0c;消息是在Handler.sendMessage方法中存到消息队列里的Message msg &#61; queue.next(); // might blockif (msg &#61;&#61; 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 loggerfinal Printer logging &#61; me.mLogging;if (logging !&#61; null) {//用户设置自己的Printer&#xff0c;在消息分发前调用Printer打印相关信息&#xff0c;此时获取消息分发前的时间T1&#xff1b;logging.println(">>>>> Dispatching to " &#43; msg.target &#43; " " &#43;msg.callback &#43; ": " &#43; msg.what);}final long slowDispatchThresholdMs &#61; me.mSlowDispatchThresholdMs;final long traceTag &#61; me.mTraceTag;if (traceTag !&#61; 0 && Trace.isTagEnabled(traceTag)) {Trace.traceBegin(traceTag, msg.target.getTraceName(msg));}final long start &#61; (slowDispatchThresholdMs &#61;&#61; 0) ? 0 : SystemClock.uptimeMillis();final long end;try {// 调用msg.target.dispatchMessage(msg)&#xff0c;进行消息的分发。这里的msg.target就是发送这条消息的Handler对象。// 这样Handler发送的消息最终又交回到它的dispatchMessage方法来处理。不同的是&#xff0c;Handler的dispatchMessage// 方法是在创建Handler时所使用的Looper中执行的&#xff0c;这样就成功将代码逻辑切换到指定线程中去执行了。msg.target.dispatchMessage(msg);end &#61; (slowDispatchThresholdMs &#61;&#61; 0) ? 0 : SystemClock.uptimeMillis();} finally {if (traceTag !&#61; 0) {Trace.traceEnd(traceTag);}}if (logging !&#61; null) {//消息分发完成后&#xff0c;调用用户自己设置的Printer.println&#xff08;&#xff09;方法&#xff0c;此时获取消息分发之后时间T2&#xff1b;logging.println("<<<<printer* at the beginning and ending of each message dispatch, identifying the* target Handler and message contents.** &#64;param printer A Printer object that will receive log messages, or* null to disable message logging.* 用户可以设置自己的Printer&#xff0c;这样在知道消息分发前后的时间&#xff0c;* 通过前后的时差与阈值进行对比&#xff0c;从而确定是否发生了卡顿*/public void setMessageLogging(&#64;Nullable Printer printer) {mLogging &#61; printer;}

通过设置Printer我们可以检测msg.target.dispatchMessage(msg)执行时间&#xff0c;这样就可以知道部分UI线程是否有耗时操作了。
BlockCanary的LooperMonitor的println方法如下&#xff1a;

LooperMonitor
&#64;Overridepublic void println(String x) {if (!mPrintingStarted) {//dispatchMesage前执行的println//记录开始时间mStartTimestamp &#61; System.currentTimeMillis();mStartThreadTimestamp &#61; SystemClock.currentThreadTimeMillis();mPrintingStarted &#61; true;//开始采集栈及cpu信息&#xff0c;最终会调用Stacksampler.start()方法&#xff1b;startDump();} else {//dispatchMesage后执行的println//获取结束时间final long endTime &#61; System.currentTimeMillis();mPrintingStarted &#61; false;//判断耗时是否超过阈值if (isBlock(endTime)) {notifyBlockEvent(endTime);}//最终会调用Stacksampler.stop()方法&#xff1b;stopDump();}}//判断是否超过阈值private boolean isBlock(long endTime) {return endTime - mStartTimestamp > mBlockThresholdMillis;}//回调监听private void notifyBlockEvent(final long endTime) {final long startTime &#61; mStartTimestamp;final long startThreadTime &#61; mStartThreadTimestamp;final long endThreadTime &#61; SystemClock.currentThreadTimeMillis();HandlerThreadFactory.getWriteLogThreadHandler().post(new Runnable() {&#64;Overridepublic void run() {mBlockListener.onBlockEvent(startTime, endTime, startThreadTime, endThreadTime);}});}

其中在startDump方法最终会调用Stacksampler.start()方法&#xff1b;stopDump最终会调用Stacksampler.stop()方法&#xff1b;相关方法如下&#xff1a;

Stacksamplerpublic void start() {//在mRunable进行信息采集&#xff1b; HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);//通过一个HandlerThread延时执行了mRunnableHandlerThreadFactory.getTimerThreadHandler().postDelayed(mRunnable,BlockCanaryInternals.getInstance().getSampleDelay());}public void stop() {//取消handler消息&#xff0c;如果未超时就不会采集相关信息HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);}

在开始进行msg.target.dispatchMessage(msg)消息分发前通过HandlerThread发送一个延时runable&#xff0c;在msg.target.dispatchMessage(msg)消息分发后会remove该runable&#xff0c;如果指定的时间消息分发没有完成&#xff0c;说明应用发生了卡顿&#xff0c;这之后开始执行mRunable&#xff0c;在mRunable进行相关信息采集及提示APP发生卡顿&#xff1b;以上就是BlockCanary监测卡顿的核心原理&#xff1b;

利用Choreographer监测APP卡顿

Android系统每隔16ms发出VSYNC信号&#xff0c;触发对UI进行渲染。开发者可以使用Choreographer#postFrameCallback设置自己的callback与Choreographer交互&#xff0c;你设置的FrameCallCack&#xff08;doFrame方法&#xff09;会在下一个frame被渲染时触发。理论上来说两次回调的时间周期应该在16ms&#xff0c;如果超过了16ms我们则认为发生了卡顿&#xff0c;我们主要就是利用两次回调间的时间周期来判断&#xff0c;

Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {&#64;Overridepublic void doFrame(long l) {//移除消息Handler.removeMessage();//发送延时消息Hnadler.sendMessageAtTime(...)Choreographer.getInstance().postFrameCallback(this);}});

发送的延时消息在执行的时间没有被remove掉&#xff0c;说明发生了卡顿&#xff0c;这时候可以进行卡顿相关信息的采集&#xff0c;如果在渲染下一帧的时候该消息还没有被处理&#xff0c;这时候将该消息remove掉&#xff0c;此场景说明未发生卡顿&#xff1b;该检测卡顿的思想和BlockCanary类似&#xff1b;
最后&#xff0c;我们可以结合上述原理以及自己需求开发出一个适合自己的卡顿监测方案&#xff0c;也可以参考已有开源方案。

其它

为什么主线程Looper.loop进行消息分发耗时就代表APP卡顿&#xff1f;
答&#xff1a;为了保证应用的平滑性&#xff0c;每一帧渲染时间不能超过16ms&#xff0c;达到60帧每秒&#xff1b;如果UI渲染慢的话&#xff0c;就会发生丢帧&#xff0c;这样用户就会感觉到不连贯性&#xff0c;我们称之为Jank&#xff08;APP卡顿&#xff09;&#xff1b;VSync信号由SurfaceFlinger实现并定时发送&#xff08;每16ms发送&#xff09;&#xff0c;Choreographer.FrameDisplayEventReceiver收到信号后&#xff0c;调用onVsync方法组织消息发送到主线程处理。Choreographer主要功能是当收到VSync信号时&#xff0c;去调用使用通过postCallBack设置的回调函数&#xff0c;在postCallBack调用doFrame&#xff0c;在doFrame中渲染下一帧&#xff1b;FrameDisplayEventReceiver相关代码如下&#xff1a;

Choreographer.java/*** FrameDisplayEventReceiver继承自DisplayEventReceiver接收底层的VSync信号开始处理UI过程。* VSync信号由SurfaceFlinger实现并定时发送。FrameDisplayEventReceiver收到信号后&#xff0c;* 调用onVsync方法组织消息发送到主线程处理。这个消息主要内容就是run方法里面的doFrame了&#xff0c;* 这里mTimestampNanos是信号到来的时间参数。*/private final class FrameDisplayEventReceiver extends DisplayEventReceiverimplements Runnable {private boolean mHavePendingVsync;private long mTimestampNanos;private int mFrame;public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {super(looper, vsyncSource);}&#64;Overridepublic void onVsync(long timestampNanos, int builtInDisplayId, int frame) {mTimestampNanos &#61; timestampNanos;mFrame &#61; frame;// 发送Runnable(callback参数即当前对象FrameDisplayEventReceiver)到FrameHandler&#xff0c;请求执行doFrameMessage msg &#61; Message.obtain(mHandler, this);msg.setAsynchronous(true);// 此处mHandler为FrameHandler&#xff0c;该Handler对应的Looper是主线程的LoopermHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);}&#64;Overridepublic void run() {mHavePendingVsync &#61; false;doFrame(mTimestampNanos, mFrame);}}

在mHandler.sendMessageAtTime发送消息之后&#xff0c;最终会在主线程的Looper.loop()方法中调用msg.target.dispatchMessage(msg);Looper.loop相关代码可以参考在本文上边进行查看&#xff1b;然后在Handler.dispatchMeassange分发消息&#xff0c;如下所示&#xff1a;

Handler.javapublic void dispatchMessage(Message msg) {// Message的callback实际上就是Handler的post方法所传递的Runnable参数// 这里首先检查是否有由Runnable封装的消息&#xff0c;如果有&#xff0c;首先处理&#xff1b;if (msg.callback !&#61; null) {handleCallback(msg);} else {// 其次处理mCallbackif (mCallback !&#61; null) {// 如果mCallback的handleMessage方法返回true&#xff0c;那么handler中的handleMessage方法是不会被执行的if (mCallback.handleMessage(msg)) {return;}}handleMessage(msg);}}private static void handleCallback(Message message) {//在此执行FrameDisplayEventReceiver中的run方法&#xff0c;最终执行doFrame渲染下一帧&#xff1b;message.callback.run();}

通过以上流程可以发现&#xff0c;Android渲染每一帧都是通过消息机制来实现的&#xff0c;最终都会在主线Looper.loop()方法中开始渲染下一帧&#xff0c;因为Looper.loop方法在进行消息分发时是串行执行的&#xff0c;这样如果上一个消息分发时间过长即msg.target.dispatchMessage(msg)执行时间过长&#xff0c;就会导致在VSYNC到来时进行下一帧渲染延迟执行&#xff0c;就不能保证该帧在16ms内完成渲染&#xff0c;从而导致丢帧&#xff1b;所以主线程Looper.loop方法中msg.target.dispatchMessage(msg)执行时间过长就会导致APP卡顿&#xff1b;因此通过检测msg.target.dispatchMessage(msg)执行时间就可以检测APP卡顿&#xff1b;
Android消息机制的重要性&#xff1a;
1.在卡顿监测会用到消息机制&#xff1b;主要是发送一个延时消息来监测是否&#xff0c;在执行时间内没有remove该消息就代码APP发生卡顿&#xff1b;
2.ANR监测也是通过发送一个延时消息来监测是否发生ANR&#xff1b;ANR是APP卡顿的极端情况&#xff1b;
3.View监测事件是否长按也用到消息机制&#xff0c;在发生Down的时候会发送一个延时消息&#xff0c;在Up的时候会将该消息Remove掉&#xff0c;如果指定的时间没有发生UP就会触发长按事件&#xff1b;
4.Choreographer在渲染每一帧的时候也是通过发送一个消息&#xff0c;然后在Looper.loop中处理下一个消息时才会去渲染下一帧&#xff1b;
5.Activity生命周期的控制也是在ActivityThread发送不同的消息来切换Activity生命周期&#xff1b;
6.消息机制可以将一个任务切换到其它指定的线程&#xff0c;如AsyncTask&#xff1b;
以上这些场景都用到Android消息机制&#xff0c;还有很多其他未知的场景可能也会用到Android消息机制&#xff0c;所以消息机制在Android中具有很重要的地位&#xff1b;

参考资料
鸿洋&#xff1a;Android UI性能优化 检测应用中的UI卡顿
BlockCanary GitHub地址
Blog in Chinese: BlockCanary.
blockcanary源码学习随笔
Android Choreographer 源码分析 讲的很好很重要



作者&#xff1a;htkeepmoving
链接&#xff1a;https://www.jianshu.com/p/9e8f88eac490
来源&#xff1a;简书
著作权归作者所有。商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处。


推荐阅读
  • 深入理解Java反射机制
    本文将详细介绍Java反射的基础知识,包括如何获取Class对象、反射的基本过程、构造器、字段和方法的反射操作,以及内省机制的应用。同时,通过实例代码加深对反射的理解,并探讨其在实际开发中的应用。 ... [详细]
  • 字符、字符串和文本的处理之Char类型
    .NetFramework中处理字符和字符串的主要有以下这么几个类:(1)、System.Char类一基础字符串处理类(2)、System.String类一处理不可变的字符串(一经 ... [详细]
  • 本文详细介绍了使用Java语言来测量程序运行时间的方法,包括代码示例和实现步骤,旨在帮助开发者更好地理解和应用时间测量技术。 ... [详细]
  • 3144:[Hnoi2013]切糕TimeLimit:10SecMemoryLimit:128MBSubmit:1261Solved:700[Submit][St ... [详细]
  • 本教程旨在指导开发者如何在Android应用中通过ViewPager组件实现图片轮播功能,适用于初学者和有一定经验的开发者,帮助提升应用的视觉吸引力。 ... [详细]
  • HDU1085 捕获本·拉登!
    问题描述众所周知,本·拉登是一位臭名昭著的恐怖分子,他已失踪多年。但最近有报道称,他藏匿在中国杭州!虽然他躲在杭州的一个洞穴中不敢外出,但近年来他因无聊而沉迷于数学问题,并声称如果有人能解出他的题目,他就自首。 ... [详细]
  • 本文详细解析了Java中流的概念,特别是OutputStream和InputStream的区别,并通过实际案例介绍了如何实现Java对象的序列化。文章不仅解释了流的基本概念,还探讨了序列化的重要性和具体实现步骤。 ... [详细]
  • GCC(GNU Compiler Collection)是GNU项目下的一款功能全面且高效的多平台编译工具,广泛应用于Linux操作系统中。本文将详细介绍GCC的特点及其基本使用方法。 ... [详细]
  • ZOJ 2760 - 最大流问题
    题目链接:How Many Shortest Paths。题目描述:给定一个包含n个节点的有向图,通过一个n*n的矩阵来表示。矩阵中的a[i][j]值为-1表示从节点i到节点j无直接路径;否则,该值表示从i到j的路径长度。输入起点vs和终点vt,计算从vs到vt的所有不共享任何边的最短路径数量。如果起点和终点相同,则输出无穷大。 ... [详细]
  • A1166 峰会区域安排问题(25分)PAT甲级 C++满分解析【图论】
    峰会是指国家元首或政府首脑之间的会议。合理安排峰会的休息区是一项复杂的工作,理想的情况是邀请的每位领导人都是彼此的直接朋友。 ... [详细]
  • 优雅地记录API调用时长
    本文旨在探讨如何高效且优雅地记录API接口的调用时长,通过实际案例和代码示例,帮助开发者理解并实施这一技术,提高系统的可观测性和调试效率。 ... [详细]
  • 本文探讨了六项Java特性,它们虽然强大,但在不当使用时可能会给应用程序带来严重问题。文章基于作者Nikita Salnikov Tarnovski多年的应用性能调优经验,提供了对这些特性的深入分析。 ... [详细]
  • 基于Flutter实现风车加载组件的制作_Android
    Flutter官方提供了诸如 CircularProgressIndicator和 LinearProgressIndicator两种常见的加载指示组件,但是说实话,实在太普通,所 ... [详细]
  • 今天老师上课讲解的很好,特意记录下来便于以后复习。多态的简单理解*1.什么是多态性?*(1)同一个动作与不同的对象产生不同的行为*(2)多态指的 ... [详细]
  • iOS 小组件开发指南
    本文详细介绍了iOS小部件(Widget)的开发流程,从环境搭建、证书配置到业务逻辑实现,提供了一系列实用的技术指导与代码示例。 ... [详细]
author-avatar
书友70030711
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有