卡顿优化
相关参考:https://androidperformance.com/2019/07/27/Android-Hardware-Layer/
用户对于内存占用过高、网络消耗过大等可能不易察觉,但是app卡顿会很直观的体现在交互上,需要重点针对
卡顿产生的原因很多,代码质量不好、可用内存过低、绘制复杂、IO占用等都会导致卡顿,且线上用户发生的卡顿线下不易复现,与使用场景强相关
工具选择
自动化检测卡顿方案
由于卡顿与场景强相关,线下工具复现并不好实现,需要线上自动化监控
方案原理
Android
的消息处理机制,一个线程只有一个 Looper
,mLogging
对象再每个message处理前后都会调用,主线程发生卡顿,说明 dispatchMessage
在执行耗时操作。当匹配到 >>>>> Dispatching
我们可以在子线程(HanderThread
)中执行一个任务,当达到我们设定的阈值时获取主线程的堆栈来定位耗时函数,当匹配到 <<<<< Finished
时将任务取消掉。
Looper
代码参考如下:
1 | // Looper.java |
相关三方库 BlockCanary
:https://github.com/markzhai/AndroidPerformanceMonitor
该库优点:非侵入式,简单方便的定位到代码的某一行
该库缺点:捕获到的卡顿堆栈可能不准确,并不是真正的卡顿点
修改方案:采用高频采集堆栈的形式,如卡顿阈值设置500ms,每100ms采集一次堆栈信息,当真的操作500ms阈值,将其中采集的堆栈信息保存起来在合适的时机上报
再优化:由于上述高频采集会产生很多无效堆栈,且上报数据量过大会造成服务端压力过大,对问题分析也并不友好。由于一个卡顿下多个堆栈大概率重复,找出重复的堆栈进行保存和上传即可
ANR异常
场景(ActivityManagerService
)
类型 | TimeOut | 是否弹框提示 | 备注 |
---|---|---|---|
Activity | 10 | 提示 | Activity切换时会设置TimeOut |
BroadCast | 10,60 | 无感知场景不会有提示 | 系统默认前后台广播,厂商会额外定制广播 |
Service | 10 | 无感知场景不会有提示 | 创建Servier时会设置TimeOut |
Provider | 10 | 无感知场景不会有提示 | 创建Provider时会设置TimeOut |
Input | 5 | 提示 | 厂商或平台会调整此阈值,MTK调整至8秒 |
Finalizer | 10 | Crash | 默认10S,厂商调整至20S~120S |
ANR触发原理
简单来说就是埋炸弹,如在启动服务时,会发送一条延迟消息,这里称为定时炸弹消息,当服务能够正常启动会把这条炸弹消息移除,如果时间到了还没有移除,说明服务没有在指定的时间内正常启动,执行该定时炸弹消息,最终会执行到 AppErrors#appNotResponding
1 | final void appNotResponding(ProcessRecord app, ActivityRecord activity, |
【注意】input的超时检测机制有点不同,需要等到下一次input事件,才会检测上一次的input事件是否超时,相当于需要主动扫雷
线下解决套路
导出系统默认 anr
记录信息,adb pull data/anr/traces.txt C:\Users\benbe\Desktop\anr.txt
线上ANR监控
通过
FileOberver
监控traces
文件变化【注:高版本中有权限问题,监控不到该文件变化】通过
WatchDog
监控ANR
原理:开启一个子线程无限循环,将一个变量值变更后往主线程发送一个还原消息,睡眠一定的间隔后检测这个变量值是否被还原,如果没有被还原说明发送了
ANR
问题:会有漏检的情况,主线程卡顿1秒后才开启新一轮的监控,如果主线程到达卡顿阈值就不卡了,此时这个变量值已被还原,
WatchDog
就以非卡顿的场景进行处理了,需要针对这个问题进行优化,优化:假设原本卡顿阈值为5秒,那么我们可以将检测间隔设定为1秒,当累计卡顿5秒才触发监控ANR的流程,这样就能让误差降低到1s内
BlockCanary
和 WatchDog
的区别
BlockCanary
:监控的是每条Msg
的执行耗时WatchDog
: 开启子线程看最终结果,如果值没被还原则认为有问题- 前者适合监控卡顿,后者适合补充
ANR
监控
死锁监控
死锁引起的 ANR ,如何找出死锁原因?
参考:获取锁调用链
单点卡顿问题检测
上诉提到了卡顿和ANR的解决方案,但是还是不够,例如,此时有很多消息在执行,但是又没有达到卡顿和ANR的阈值,但是你自己的消息就是执行不到,造成交互不流畅。
卡顿点:主线程IPC、DB
下面以 IPC 检测为例,其它的如DB、IO、Looper 消息过多和 View 绘制等也是类似处理
检测指标
- IPC 调用类型
- 调用耗时、次数
- 调用堆栈、发生线程
检测方案
IPC 操作前后加埋点
adb
命令检测- 开启监控:
adb shell am trace-ipc start
- 停止监控并保存到指定文件:
adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt
- 导出监控文件:
adb pull /data/local/tmp/tmp/ipc-trace.txt C:\Users\benbe\Desktop\ipc-trace.txt
- 开启监控:
通过
ART-Hook
方式 hook 常用 IPC 方法- 通过
PackageManager
获取进程名 - 获取
DeviceID
信息 AMS
相关View.getWindowVisibleDisplayFrame()
上诉无论那种操作,最终都会调用到
BinderProxy
类里的transact
方法1
2
3
4
5
6
7
8
9
10
11
12
13try {
DexposedBridge.findAndHookMethod(Class.forName("android.os.BinderProxy"), "transact",
int.class, Parcel.class, Parcel.class, int.class, new XC_MethodHook() {
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
LogUtils.i( "BinderProxy beforeHookedMethod " + param.thisObject.getClass().getSimpleName()
+ "\n" + Log.getStackTraceString(new Throwable()));
super.beforeHookedMethod(param);
}
});
} catch (ClassNotFoundException e) {
e.printStackTrace();
}- 通过
界面秒开实现
更多参考启动优化和布局优化
- 通过
SysTrace
获取布局初始化的执行情况 - 异步
Inflate
、X2C 、绘制优化 - 提前获取页面数据
- 监控界面打开耗时:通过在
onCreate
里手动埋点,在UI可交互式统计界面耗时,也可使用AOP
的方式 Hook 对于相应方法进行实现
监控耗时盲区
耗时盲区监控难点
- 只知道在主线程做了耗时操作,但并不知道具体在做什么
- 线上盲区无从追查
线下耗时盲区监控
TraceView
orSystrace
- 特别适合一段时间内的盲区监控
- 线程具体做了什么,一目了然
线上监控
参考方案:监听消息
mLogging
方案【缺点】有所有方法的
Msg
,但是没有Msg
具体堆栈,并不知道Msg
是被谁抛出来的参考方案:通过
AOP
切Handler
方法【缺点】可以知道发送
Msg
的堆栈,但是这种方案不清楚Msg
的具体执行时间推荐方案:使用统一的
Handler
,定制sendMessageAtTime
和dispatchMessage
方法,同时可以定制gradle
插件,编译时动态替换所有Handler
的父类,这样就能让所有Handler
发送和执行的调用栈执行时间都能捕捉到,参考:监控所有消息的发送栈和执行时间
卡顿优化总结
- 耗时操作:异步、延迟
- 布局优化:异步
Inflate
、X2C
、重绘解决 - 内存:降低内存占用,减少
GC
时间