卡顿优化

卡顿优化

相关参考:https://androidperformance.com/2019/07/27/Android-Hardware-Layer/

用户对于内存占用过高、网络消耗过大等可能不易察觉,但是app卡顿会很直观的体现在交互上,需要重点针对

卡顿产生的原因很多,代码质量不好、可用内存过低、绘制复杂、IO占用等都会导致卡顿,且线上用户发生的卡顿线下不易复现,与使用场景强相关

工具选择

自动化检测卡顿方案

由于卡顿与场景强相关,线下工具复现并不好实现,需要线上自动化监控

方案原理

Android 的消息处理机制,一个线程只有一个 LoopermLogging 对象再每个message处理前后都会调用,主线程发生卡顿,说明 dispatchMessage 在执行耗时操作。当匹配到 >>>>> Dispatching 我们可以在子线程(HanderThread)中执行一个任务,当达到我们设定的阈值时获取主线程的堆栈来定位耗时函数,当匹配到 <<<<< Finished 时将任务取消掉。

Looper 代码参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Looper.java

for (;;) {
// ...

final 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);
}
}

相关三方库 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
final void appNotResponding(ProcessRecord app, ActivityRecord activity,
ActivityRecord parent, boolean aboveSystem, final String annotation) {
// ...
//1、写入event log
EventLog.writeEvent(EventLogTags.AM_ANR, app.userId, app.pid,
app.processName, app.info.flags, annotation);
// ...
// 2、收集需要的log,anr、cpu等,StringBuilder凭借
StringBuilder info = new StringBuilder();
info.setLength(0);
info.append("ANR in ").append(app.processName);
if (activity != null && activity.shortComponentName != null) {
info.append(" (").append(activity.shortComponentName).append(")");
}
info.append("\n");
info.append("PID: ").append(app.pid).append("\n");
if (annotation != null) {
info.append("Reason: ").append(annotation).append("\n");
}
if (parent != null && parent != activity) {
info.append("Parent: ").append(parent.shortComponentName).append("\n");
}
ProcessCpuTracker processCpuTracker = new ProcessCpuTracker(true);
// ...
// 3、dump堆栈信息,包括java堆栈和native堆栈,保存到文件中
File tracesFile = ActivityManagerService.dumpStackTraces(
true, firstPids,
(isSilentANR) ? null : processCpuTracker,
(isSilentANR) ? null : lastPids,
nativePids);
String cpuInfo = null;
// ...
// 4、输出ANR 日志
Slog.e(TAG, info.toString());
if (tracesFile == null) {
// 5、没有抓到tracesFile,发一个SIGNAL_QUIT信号
Process.sendSignal(app.pid, Process.SIGNAL_QUIT);
}
StatsLog.write(StatsLog.ANR_OCCURRED, ...)
// 6、输出到drapbox
mService.addErrorToDropBox("anr", app, app.processName, activity, parent, annotation, cpuInfo, tracesFile, null);
// ...
synchronized (mService) {
mService.mBatteryStatsService.noteProcessAnr(app.processName, app.uid);
//7、后台ANR,直接杀进程
if (isSilentANR) {
app.kill("bg anr", true);
return;
}

//8、错误报告
makeAppNotRespondingLocked(app,
activity != null ? activity.shortComponentName : null,
annotation != null ? "ANR " + annotation : "ANR",
info.toString());

//9、弹出ANR dialog,会调用handleShowAnrUi方法
Message msg = Message.obtain();
msg.what = ActivityManagerService.SHOW_NOT_RESPONDING_UI_MSG;
msg.obj = new AppNotRespondingDialog.Data(app, activity, aboveSystem);

mService.mUiHandler.sendMessage(msg);
}
}

【注意】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 命令检测

    1. 开启监控:adb shell am trace-ipc start
    2. 停止监控并保存到指定文件:adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt
    3. 导出监控文件:adb pull /data/local/tmp/tmp/ipc-trace.txt C:\Users\benbe\Desktop\ipc-trace.txt
  • 通过 ART-Hook 方式 hook 常用 IPC 方法

    1. 通过 PackageManager 获取进程名
    2. 获取 DeviceID 信息
    3. AMS 相关
    4. View.getWindowVisibleDisplayFrame()

    上诉无论那种操作,最终都会调用到 BinderProxy 类里的 transact 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    try {
    DexposedBridge.findAndHookMethod(Class.forName("android.os.BinderProxy"), "transact",
    int.class, Parcel.class, Parcel.class, int.class, new XC_MethodHook() {
    @Override
    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 对于相应方法进行实现

监控耗时盲区

  1. 耗时盲区监控难点

    • 只知道在主线程做了耗时操作,但并不知道具体在做什么
    • 线上盲区无从追查
  2. 线下耗时盲区监控

    • TraceView or Systrace
      • 特别适合一段时间内的盲区监控
      • 线程具体做了什么,一目了然
  3. 线上监控

    • 参考方案:监听消息 mLogging 方案

      【缺点】有所有方法的 Msg,但是没有 Msg 具体堆栈,并不知道 Msg 是被谁抛出来的

    • 参考方案:通过 AOPHandler 方法

      【缺点】可以知道发送 Msg 的堆栈,但是这种方案不清楚 Msg 的具体执行时间

    • 推荐方案:使用统一的 Handler,定制 sendMessageAtTimedispatchMessage 方法,同时可以定制 gradle 插件,编译时动态替换所有 Handler的父类,这样就能让所有 Handler 发送和执行的调用栈执行时间都能捕捉到,参考:监控所有消息的发送栈和执行时间

卡顿优化总结

  • 耗时操作:异步、延迟
  • 布局优化:异步 InflateX2C、重绘解决
  • 内存:降低内存占用,减少 GC 时间