为什么能看到卡顿
先了解几个概念:
- 屏幕刷新频率: 一秒内屏幕的刷新效率,如目前市场上常见的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查找
- 使用adb 链接手机
- 使用Android Sdk tools Systrace
- 使用python ./systrace.py -t 5 -o looktrace.html(python 2.7 )
python systrace.py [options] [categories] options
category
- 用chrome浏览器打开looktrace.html,地址栏输入chrome://tracing/ 来查看报告。
UI thread
不同的颜色会有不同的状态 下面列举下不同颜色的不同状态:
- 绿色:表示正在运行
即使是绿色,也要看是否频率是否是对的, 1. 频率是否正确,是否是运行在打核上Cpu0
- 蓝色:表示可以运行,但是cpu的在执行其他线程
检测是否有很多子线程任务
- 紫色:表示休眠
表示有io 操作
白色:表示休眠
可能是线程上的互斥锁,如Binder阻塞、/sleep/Wait
我们根据每个线程的状态颜色结合自己代码,判断哪些是在卡顿,哪些是可以优化的,Systrace 只能范围性帮助我们找到cpu调度,具体根据项目的具体情况,
Trace view 精确卡顿位置
Systrace 确定了范围,我们想要在猜测的范围内找到相对精确的范围我们可以使用Trace view
- 使用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 包名
- 打开looktrace looktrace
我们通过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();
}
}
当出现卡顿的时候,我们可以通过收集的堆栈信息来找到卡顿的原因
找到过度绘制减少卡顿
找到过度绘制
- 进入开发者
- 启用Gpu的调试层。显示过度绘制。显示ui边界
- 根据颜色确定绘制次数
- 蓝色:绘制一次
- 绿色:绘制两次
- 粉色:过度绘制3次
- 红色:过度绘制4次或者四次以上
减少绘制导致的卡顿
过度绘制会导致卡顿,众人皆知,怎么样才能确定过度绘制了呢?
- 使用Layout Inspector 查看布局层级
- 使用merge 去除同一属性布局,merge 本身没有任何意义,只是让写法合法
- 当一个view不知道是否需要展示的时候我们可以使用viewStub来引向布局,当viewStub 是Gone 是不占用任何资源,
VIEW.Gone 虽然不占用位置,但是会被创建对象
- 移除布局中不需要的背景
- 使视图扁平化减少嵌套,可以进可能使用ConstraintLayout
- 尽量少使用透明度设置
因为透明的绘制需要多次渲染,很容易导致过度绘制
- 特定场景使用AsyncLayoutInflater 来异步加载布局
LayoutInflater 正常加载xml 的布局其实是io 的一个操作,如果某个布局比较复杂,会呈现加载比较慢,AsyncLayoutInflat 是异步的情况去加载布局,减少主线程的耗时