『网易实习』周记(四)
上一周自己也在研究这个卡顿检测,然后最近才有时间去总结一下,所以上一周的blog就没有写出来了,这周一起总结。
本周知识清单:
- Android 卡顿检测及优化
- Monkey 随机测试Android卡顿机制
- ANR日志分析
Android卡顿检测及优化
首先明白何为卡顿:Android 通过从应用生成帧并将其显示在屏幕上来呈现界面。如果您的应用存在界面呈现缓慢的问题,系统会不得不跳过一些帧。发生这种情况时,用户会看到屏幕上不断闪烁,这种情况称为卡顿。 **为什么会出现卡顿:**出现卡顿通常是因为界面线程(在大多数应用中是主线程)上存在一些减速或阻塞异步调用 **界面呈现:**从应用生成帧并将其显示在屏幕上的动作。要确保用户能够流畅地与您的应用互动,您的应用呈现每帧的时间不应超过 16ms,以达到每秒 60 帧的呈现速度(为什么是 60fps?)。如果您的应用存在界面呈现缓慢的问题,系统会不得不跳过一些帧,这会导致用户感觉您的应用不流畅。我们将这种情况称为卡顿。
帧率
即 Frame Rate,单位 fps,是指 gpu 生成帧的速率,60fps,Android中更帧率相关的类是SurfaceFlinger。 SurfaceFlinger surfaceflinger作用是接受多个来源的图形显示数据,将他们合成,然后发送到显示设备。比如打开应用,常见的有三层显示,顶部的statusbar底部或者侧面的导航栏以及应用的界面,每个层是单独更新和渲染,这些界面都是有surfaceflinger合成一个刷新到硬件显示。 在显示过程中使用到了bufferqueue,surfaceflinger作为consumer方,比如windowmanager管理的surface作为生产方产生页面,交由surfaceflinger进行合成。 推荐阅读:Android 图形组件
VSync
Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,VSync是Vertical Synchronization(垂直同步)的缩写,是一种在PC上很早就广泛使用的技术,可以简单的把它认为是一种定时中断。而在Android 4.1(JB)中已经开始引入VSync机制,用来同步渲染,让UI和SurfaceFlinger可以按硬件产生的VSync节奏进行工作。 安卓系统中有 2 种 VSync 信号: 1、屏幕产生的硬件 VSync: 硬件 VSync 是一个脉冲信号,起到开关或触发某种操作的作用。 2、由 SurfaceFlinger 将其转成的软件 Vsync 信号:经由 Binder 传递给 Choreographer。 除了Vsync的机制,Android还使用了多级缓冲的手段以优化UI流程度,例如双缓冲(A+B),在显示buffer A的数据时,CPU/GPU就开始在buffer B中准备下一帧数据:但是不能保证每一帧CPU、GPU都运行状态良好,可能由于资源抢占等性能问题导致某一帧GPU掉链子,vsync信号到来时buffer B的数据还没准备好,而此时Display又在显示buffer A的数据,导致后面CPU/GPU没有新的buffer着手准备数据,导致卡顿(jank)。
识别卡顿
使用dumpsys gfxinfo
输出中会包含录制阶段所发生的动画帧的相关性能信息。以下命令使用 gfxinfo 收集指定软件包名称的界面性能数据:
adb shell dumpsys gfxinfo package-name
测试:
gzs21620-@gih-d-15094 godlike % adb shell dumpsys gfxinfo com.netease.gl
Applications Graphics Acceleration Info:
Uptime: 155730654 Realtime: 155730654
** Graphics info for pid 16002 [com.netease.gl] **
Stats since: 155608165526070ns
Total frames rendered: 2336
Janky frames: 296 (12.67%)
Janky frames (legacy): 507 (21.70%)
50th percentile: 16ms
90th percentile: 22ms
95th percentile: 36ms
99th percentile: 101ms
Number Missed Vsync: 107
Number High input latency: 2429
Number Slow UI thread: 167
Number Slow bitmap uploads: 6
Number Slow issue draw commands: 175
Number Frame deadline missed: 296
Number Frame deadline missed (legacy): 131
一些参数的简单说明:
- Graphics info for pid 16002 [com.netease.gl] 代表当前dump的一些信息
- Total frames rendered: 2336 总共渲染了2336帧
- Janky frames: 296 (12.67%) 丢帧数量
- Janky frames (legacy): 507 (21.70%) 非法的帧数量(指绘制时间大于等于16.67ms)
- 50th percentile: 16ms 50%的帧能在16ms绘制完成
- 99th percentile: 101ms 99%的帧能在101ms绘制完成
- Number Missed Vsync: 107 垂直同步失败的帧107
- Number Slow UI thread: 167 因UI线程上的工作导致超时的帧数
- Number Slow bitmap uploads: 6 因bitmap的加载耗时的帧数
- Number Slow issue draw commands: 175 因绘制导致耗时的帧数
使用systrace
上面使用的dumpsys是能发现问题或者判断问题的严重性,但无法定位真正的原因。如果要定位原因,应当配合systrace工具使用。 关于systrance的使用推荐阅读:**界面卡顿检测 ** 个人觉得这种方式很不适合全局查找,若只是查找某一个已知道的问题就还行,若是工程里面全局去查找就不合适了。
使用BlockCanary
BlockCanary是国内开发者MarkZhai开发的一套性能监控组件,它对主线程操作进行了完全透明的监控,并能输出有效的信息,帮助开发分析、定位到问题所在,迅速优化应用。 其特点有: 1、非侵入式,简单的两行就打开监控,不需要到处打点,破坏代码优雅性。2、精准,输出的信息可以帮助定位到问题所在(精确到行),不需要像Logcat一样,慢慢去找。3、目前包括了核心监控输出文件,以及UI显示卡顿信息功能 BlockCanary官方地址:https://github.com/markzhai/AndroidPerformanceMonitor
原理
熟悉Message/Looper/Handler系列的同学们一定知道Looper.java中这么一段:
private static Looper sMainLooper;
...
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
public static Looper getMainLooper() {
synchronized (Looper.class) {
return sMainLooper;
}
}
即整个应用的主线程,只有这一个looper,不管有多少handler,最后都会回到这里。如果再细心一点会发现在Looper的loop方法中有这么一段
public static void loop() {
...
for (;;) {
...
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
msg.target.dispatchMessage(msg);
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
...
}
}
是的,就是这个Printer - mLogging,它在每个message处理的前后被调用,而如果主线程卡住了,不就是在dispatchMessage里卡住了吗? 核心流程图: 该组件利用了主线程的消息队列处理机制,通过
Looper.getMainLooper().setMessageLogging(mainLooperPrinter);
并在mainLooperPrinter中判断start和end,来获取主线程dispatch该message的开始和结束时间,并判定该时间超过阈值(如2000毫秒)为主线程卡慢发生,并dump出各种信息,提供开发者分析性能瓶颈。
...
@Override
public void println(String x) {
if (!mStartedPrinting) {
mStartTimeMillis = System.currentTimeMillis();
mStartThreadTimeMillis = SystemClock.currentThreadTimeMillis();
mStartedPrinting = true;
} else {
final long endTime = System.currentTimeMillis();
mStartedPrinting = false;
if (isBlock(endTime)) {
notifyBlockEvent(endTime);
}
}
}
private boolean isBlock(long endTime) {
return endTime - mStartTimeMillis > mBlockThresholdMillis;
}
...
使用Choreographer
Android 主线程运行的本质,其实就是 Message 的处理过程,我们的各种操作,包括每一帧的渲染操作 ,都是通过 Message 的形式发给主线程的 MessageQueue ,MessageQueue 处理完消息继续等下一个消息。Choreographer 的引入,主要是配合 Vsync ,给上层 App 的渲染提供一个稳定的 Message 处理的时机,也就是 Vsync 到来的时候 ,系统通过对 Vsync 信号周期的调整,来控制每一帧绘制操作的时机. 目前大部分手机都是 60Hz 的刷新率,也就是 16.6ms 刷新一次,系统为了配合屏幕的刷新频率,将 Vsync 的周期也设置为 16.6 ms,每个 16.6 ms , Vsync 信号唤醒 Choreographer 来做 App 的绘制操作 ,这就是引入 Choreographer 的主要作用。 Choreographer 两个主要作用 1、承上:负责接收和处理 App 的各种更新消息和回调,等到 Vsync 到来的时候统一处理。比如集中处理 Input(主要是 Input 事件的处理) 、Animation(动画相关)、Traversal(包括 measure、layout、draw 等操作) ,判断卡顿掉帧情况,记录 CallBack 耗时等。 2、启下:负责请求和接收 Vsync 信号。接收 Vsync 事件回调(通过 FrameDisplayEventReceiver.onVsync );请求 Vsync(FrameDisplayEventReceiver.scheduleVsync) . 使用Choreographer 计算帧率 Choreographer 处理绘制的逻辑核心在 Choreographer.doFrame 函数中,申请VSync信号接收到后是走 doFrame()方法,Choreographer的postFrameCallback()通常用来计算丢帧情况,使用方式如下:
Choreographer.getInstance().postFrameCallback(mBlockCanaryCore.monitor);
public class FrameMonitor implements Choreographer.FrameCallback {
private static final int DEFAULT_BLOCK_THRESHOLD_MILLIS = 300;
private long mBlockThresholdMillis = DEFAULT_BLOCK_THRESHOLD_MILLIS;
private long mStartTimestamp = 0;
private long mStartThreadTimestamp = 0;
private BlockListener mBlockListener = null;
private boolean mPrintingStarted = false;
private final boolean mStopWhenDebugging;
@Override
public void doFrame(long frameTimeNanos) {
if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
return;
}
if (!mPrintingStarted) {
mStartTimestamp = System.currentTimeMillis();
mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
mPrintingStarted = true;
startDump();
} else {
final long endTime = System.currentTimeMillis();
mPrintingStarted = false;
if (isBlock(endTime)) {
notifyBlockEvent(endTime);
}
stopDump();
}
Choreographer.getInstance().postFrameCallback(this);
}
public interface BlockListener {
void onBlockEvent(long realStartTime,
long realTimeEnd,
long threadTimeStart,
long threadTimeEnd);
}
public FrameMonitor(BlockListener blockListener, long blockThresholdMillis, boolean stopWhenDebugging) {
if (blockListener == null) {
throw new IllegalArgumentException("blockListener should not be null.");
}
mBlockListener = blockListener;
mBlockThresholdMillis = blockThresholdMillis;
mStopWhenDebugging = stopWhenDebugging;
}
private boolean isBlock(long endTime) {
return endTime - mStartTimestamp > mBlockThresholdMillis;
}
private void notifyBlockEvent(final long endTime) {
final long startTime = mStartTimestamp;
final long startThreadTime = mStartThreadTimestamp;
final long endThreadTime = SystemClock.currentThreadTimeMillis();
HandlerThreadFactory.getWriteLogThreadHandler().post(new Runnable() {
@Override
public void run() {
mBlockListener.onBlockEvent(startTime, endTime, startThreadTime, endThreadTime);
}
});
}
private void startDump() {
if (null != BlockCanaryInternals.getInstance().stackSampler) {
BlockCanaryInternals.getInstance().stackSampler.start();
}
if (null != BlockCanaryInternals.getInstance().cpuSampler) {
BlockCanaryInternals.getInstance().cpuSampler.start();
}
}
private void stopDump() {
if (null != BlockCanaryInternals.getInstance().stackSampler) {
BlockCanaryInternals.getInstance().stackSampler.stop();
}
if (null != BlockCanaryInternals.getInstance().cpuSampler) {
BlockCanaryInternals.getInstance().cpuSampler.stop();
}
}
public long getBlockThresholdMillis() {
return mBlockThresholdMillis;
}
public void setBlockThresholdMillis(long blockThresholdMillis) {
mBlockThresholdMillis = blockThresholdMillis;
}
}
Choreographer可以复制监控卡顿但是,我们的Stack信息的dump是如何方案呢?
dumpStack方案
这里不完全参照blockCanary的实现,但是大家都是为了解决能够抓取到问题发生的堆栈,这里先说一下对于主线程堆栈dump需要关注的问题。不同的抓取策略也是为了解决这个问题,此处先不考虑对性能带来的影响 假设此时发生了卡顿,那么在调用getStackTrace的时候,这时候虚拟机中所跟踪的堆栈中会把当前记录的一些堆栈返回。通过在发生卡顿的时候,dump出当前的堆栈,记录下来,再追溯问题的时候直接看存储下来的堆栈信息,那么定位问题就会方便很多,而实际情况下并不能如此理想,因为从VM中取出的堆栈dalvik.system.VMStack#getThreadStackTrace返回的数据是未知的,不能保证里面到底有多少内容,可能只有一部分,这样就可能会遗漏真正的问题所在,可以参考下图~
可以看到真正有问题的函数其实是FunctionA-1,而如果捞出来的堆栈只有FunctionA-2或者A-3的话,当然可以优化A-3,但是会漏掉真正发生问题的函数。所以对于堆栈的抓取,基于VMStack抓取堆栈的方式下,笔者思考了两种方案来解决这样的问题,这两种应该也是市面上基于VMStack方式的大概方案,再深入往VM中去研究感觉可以有,但是不推荐,因为成本高,且回报的话不太会有预期中的高。
周期性Dump
通过每个一段时间从VM中获取主线程的堆栈,在发生卡顿的时候,过滤出时间,然后直接取出这段时间内的堆栈来进行问题排查。 周期Dmp.png 在实现的时候需要注意的一些小细节:
起止Dump
这里可以“忽略”多线程的特性,因为我们关注的仅仅是主线程,那么只需要在消息分发之初dump一次堆栈,然后再消息处理之后再dump一次堆栈,这样既能在dump出来的堆栈中发现可能存在的问题,同时又能自行推断这中间的执行过程来观测代码中出现的问题。当然不可缺少一个代码耗时检测的小工具~ blockCanary就是这种实现方案
public void doFrame(long frameTimeNanos) {
if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
return;
}
if (!mPrintingStarted) {
mStartTimestamp = System.currentTimeMillis();
mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
mPrintingStarted = true;
startDump();
} else {
final long endTime = System.currentTimeMillis();
mPrintingStarted = false;
if (isBlock(endTime)) {
notifyBlockEvent(endTime);
}
stopDump();
}
Choreographer.getInstance().postFrameCallback(this);
}
基于WatchDog原理的方案及代码实现
**原理:**参考系统的WatchDog原理,我们启动一个卡顿检测线程,该线程定期的向UI线程发送一条延迟消息,执行一个标志位加1的操作,如果规定时间内,标志位没有变化,则表示产生了卡顿。如果发生了变化,则代表没有长时间卡顿,我们重新执行延迟消息即可。
public class WatchDog {
private final static String TAG = "WatchDog";
private static final int TICK_INIT_VALUE = 0;
private volatile int mTick = TICK_INIT_VALUE;
public final int DELAY_TIME = 100;
private Handler mHandler = new Handler(Looper.getMainLooper());
private HandlerThread mWatchDogThread = new HandlerThread("WatchDogThread");
private Handler mWatchDogHandler;
private Runnable mDogRunnable = new Runnable() {
@Override
public void run() {
if (null == mHandler) {
Log.e(TAG, "handler is null");
return;
}
mHandler.post(new Runnable() {
@Override
public void run() {
mTick++;
}
});
try {
Thread.sleep(DELAY_TIME);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (TICK_INIT_VALUE == mTick) {
StringBuilder sb = new StringBuilder();
StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
Log.d(TAG,"👇👇👇👇👇👇👇👇👇👇 发生卡顿 👇👇👇👇👇👇👇👇👇👇");
for (StackTraceElement s : stackTrace) {
sb.append(s.toString() + "\n");
}
Log.d(TAG, sb.toString());
Log.d(TAG,"👆👆👆👆👆👆👆👆👆👆 卡顿信息 👆👆👆👆👆👆👆👆👆👆");
} else {
mTick = TICK_INIT_VALUE;
}
mWatchDogHandler.postDelayed(mDogRunnable, DELAY_TIME);
}
};
public void startWork(){
mWatchDogThread.start();
mWatchDogHandler = new Handler(mWatchDogThread.getLooper());
mWatchDogHandler.postDelayed(mDogRunnable, DELAY_TIME);
}
}
推荐阅读: Android 卡顿方案研究 “终于懂了” 系列:Android屏幕刷新机制—VSync、Choreographer 全面理解!
第三方公司的开源框架
卡顿优化
由上面的分析可知对象分配、垃圾回收(GC)、线程调度以及Binder调用 是Android系统中常见的卡顿原因,因此卡顿优化主要以下几种方法,更多的要结合具体的应用来进行: 1、布局优化
- 通过减少冗余或者嵌套布局来降低视图层次结构。比如使用约束布局代替线性布局和相对布局。
- 用 ViewStub 替代在启动过程中不需要显示的 UI 控件。
- 使用自定义 View 替代复杂的 View 叠加。
2、减少主线程耗时操作
- 主线程中不要直接操作数据库,数据库的操作应该放在数据库线程中完成。
- sharepreference尽量使用apply,少使用commit,可以使用MMKV框架来代替sharepreference。
- 网络请求回来的数据解析尽量放在子线程中,不要在主线程中进行复制的数据解析操作。
- 不要在activity的onResume和onCreate中进行耗时操作,比如大量的计算等。
3、减少过度绘制 过度绘制是同一个像素点上被多次绘制,减少过度绘制一般减少布局背景叠加等方式,如下图所示右边是过度绘制的图片。 4、列表优化
- RecyclerView使用优化,使用DiffUtil和notifyItemDataSetChanged进行局部更新等。
5、对象分配和回收优化 自从Android引入 ART 并且在Android 5.0上成为默认的运行时之后,对象分配和垃圾回收(GC)造成的卡顿已经显著降低了,但是由于对象分配和GC有额外的开销,它依然又可能使线程负载过重。 在一个调用不频繁的地方(比如按钮点击)分配对象是没有问题的,但如果在在一个被频繁调用的紧密的循环里,就需要避免对象分配来降低GC的压力。
Monkey 随机测试Android卡顿
概述:Monkey 是一个命令行工具,可以在任何模拟器实例或设备上运行。它会将伪随机用户事件流发送到系统中,从而对您正在开发的应用软件进行压力测试。 Monkey 包含许多选项,主要分为以下四个类别:
- 基本配置选项,例如设置要尝试的事件数。
- 操作限制条件,例如将测试对象限制为单个软件包。
- 事件类型和频率。
- 调试选项。
做卡顿检测的时候总不能每次都去点击人工模拟吧,得有个命令行工具帮我们人工模拟,像手机发射一些随机事件所一些测试
Monkey 的基本用法
您可以使用开发计算机上的命令行启动 Monkey,也可以通过脚本启动。由于 Monkey 在模拟器/设备环境中运行,因此您必须从该环境中通过 shell 启动它。为此,您可以在每个命令前面加上 adb shell,或者直接进入 shell 并输入 Monkey 命令。 基本语法如下:
$ adb shell monkey [options] <event-count>
如果未指定任何选项,Monkey 将以静默(非详细)模式启动,并将事件发送到目标上安装的任何(及所有)软件包。下面是一个更典型的命令行,它会启动您的应用并向其发送 500 个伪随机事件:
$ adb shell monkey -p your.package.name -v 500
例如:
adb shell monkey -p com.netease.gl --throttle 200 --pct-touch 30 --pct-motion 20 --pct-nav 20 --pct-appswitch 20 --ignore-crashes --ignore-timeouts 500
推荐阅读:UI/Application Exerciser Monkey
ANR日志分析
最近在公司做ANR的排查,关于ANR的排查极力推荐
ANR的日志分析
写在最后:笔记只是记录学习的知识,可能必不全面,但是也是对自己的知识的一种输出,记录着自己的一点一滴,追风赶月莫停留,平芜尽处是春山。
|