卡顿是什么

从表面现象来说,卡顿就是直观的感受反应变慢,画面停顿,触摸响应不及时,从深层次说,卡顿由一系列的原因造成,包括CPU、内存、磁盘 I/O都可能导致卡顿。
还记得上次帧率的公式吗?显卡的处理能力= 分辨率×帧率 如果处理能力不行,分辨率不变就会导致帧率变低,帧率变低就会导致卡顿现象。

卡顿排查工具

官方已经给我们提供了分析工具Traceview,它利用 Android Runtime 函数调用的 event 事件,将函数运行的耗时和调用关系写入 trace 文件中,它可以用来查看整个过程有哪些函数调用,但是工具本身带来的性能开销过大,无法反映真实的情况。在 Android 5.0 之后,新增了startMethodTracingSampling方法,可以使用基于样本的方式进行分析,以减少分析对运行时的性能影响。systrace Android 4.1 新增的性能分析工具,我们通常使用它跟踪系统的 I/O 操作、CPU 负载、Surface 渲染、GC 等事件,系统有很多地方都调用该工具的方法Trace.beginSection和Trace.endSection来做监控,如果想监控我们自己的应用也可以给应用的方法添加这个代码,方法可以通过插桩的方式,这样就实现了在 systrace 基础上增加应用程序耗时的监控,但想想,为什么Matrix可以自动应用程序的耗时分析呢?其实原理上和帧率监控相呼应,在Looper分发消息时,计算分发耗时,如果大于预定好的阀值,就开始收集信息上报,信息内容包括(进程信息、cpu信息、方法栈信息等)

造成卡顿的常见因素

  • 界面绘制层级过高
  • UI线程耗时操作
  • GC频繁

这几个常见的因素,在我们日常开发中经常会关注的问题,也有了很好的解决办法,这里就不详细描述,接下来直接看Matrix的源码,看看它都做了哪些优化。

Matrix卡顿分析源码

直接看卡顿分析的Tracer类,我们还看onAlive方法,它是在plugin调用start方法时被调用的函数,可以理解为start函数,它将自己注册为观察者。观察的谁呢?
image.png
其实就是观察下面几个方法,等待的就是主线程的Looper消息回调。
image.png
那么现在可以看EvilMethodTracer的dispatchBegin函数,看它做了什么?

  1. public void dispatchBegin(long beginNs, long cpuBeginMs, long token) {
  2. super.dispatchBegin(beginNs, cpuBeginMs, token);
  3. //插入方法结点,如果出现了方法执行过慢的问题,就从该结点开始收集方法执行记录
  4. indexRecord = AppMethodBeat.getInstance().maskIndex("EvilMethodTracer#dispatchBegin");
  5. }

再看doFrame函数,好像没什么用

  1. @Override
  2. public void doFrame(String focusedActivity, long startNs, long endNs, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) {
  3. queueTypeCosts[0] = inputCostNs;
  4. queueTypeCosts[1] = animationCostNs;
  5. queueTypeCosts[2] = traversalCostNs;
  6. }

再看dispatchEnd函数

  1. @Override
  2. public void dispatchEnd(long beginNs, long cpuBeginMs, long endNs, long cpuEndMs, long token, boolean isVsyncFrame) {
  3. super.dispatchEnd(beginNs, cpuBeginMs, endNs, cpuEndMs, token, isVsyncFrame);
  4. //判断是否是dev环境,如果是dev就使用当前时间,如果不是就返回0
  5. long start = config.isDevEnv() ? System.currentTimeMillis() : 0;
  6. //计算消息分发耗时
  7. long dispatchCost = (endNs - beginNs) / Constants.TIME_MILLIS_TO_NANO;
  8. try {
  9. //如果大于阀值,默认是700
  10. if (dispatchCost >= evilThresholdMs) {
  11. //取出该方法
  12. long[] data = AppMethodBeat.getInstance().copyData(indexRecord);
  13. long[] queueCosts = new long[3];
  14. //深拷贝数组,将doFrame中记录的信息,copy到queueCosts数组中
  15. System.arraycopy(queueTypeCosts, 0, queueCosts, 0, 3);
  16. //获取activity的name
  17. String scene = AppMethodBeat.getVisibleScene();
  18. //添加任务,开始收集其他数据并上报
  19. MatrixHandlerThread.getDefaultHandler().post(new AnalyseTask(isForeground(), scene, data, queueCosts, cpuEndMs - cpuBeginMs, dispatchCost, endNs / Constants.TIME_MILLIS_TO_NANO));
  20. }
  21. } finally {
  22. indexRecord.release();
  23. if (config.isDevEnv()) {
  24. //如果dev环境,计算cpu使用率,并输出
  25. String usage = Utils.calculateCpuUsage(cpuEndMs - cpuBeginMs, dispatchCost);
  26. MatrixLog.v(TAG, "[dispatchEnd] token:%s cost:%sms cpu:%sms usage:%s innerCost:%s",
  27. token, dispatchCost, cpuEndMs - cpuBeginMs, usage, System.currentTimeMillis() - start);
  28. }
  29. }
  30. }

继续看AnalyseTask任务,直接看run方法调用的函数analyse

  1. void analyse() {
  2. // 进程信息
  3. int[] processStat = Utils.getProcessPriority(Process.myPid());
  4. //继承cup使用
  5. String usage = Utils.calculateCpuUsage(cpuCost, cost);
  6. LinkedList<MethodItem> stack = new LinkedList();
  7. //方法栈数据收集
  8. if (data.length > 0) {
  9. //从插入结点开始收集并整理方法执行记录
  10. TraceDataUtils.structuredDataToStack(data, stack, true, endMs);
  11. TraceDataUtils.trimStack(stack, Constants.TARGET_EVIL_METHOD_STACK, new TraceDataUtils.IStructuredDataFilter() {
  12. @Override
  13. public boolean isFilter(long during, int filterCount) {
  14. return during < filterCount * Constants.TIME_UPDATE_CYCLE_MS;
  15. }
  16. @Override
  17. public int getFilterMaxCount() {
  18. return Constants.FILTER_STACK_MAX_COUNT;
  19. }
  20. @Override
  21. public void fallback(List<MethodItem> stack, int size) {
  22. MatrixLog.w(TAG, "[fallback] size:%s targetSize:%s stack:%s", size, Constants.TARGET_EVIL_METHOD_STACK, stack);
  23. Iterator iterator = stack.listIterator(Math.min(size, Constants.TARGET_EVIL_METHOD_STACK));
  24. while (iterator.hasNext()) {
  25. iterator.next();
  26. iterator.remove();
  27. }
  28. }
  29. });
  30. }
  31. StringBuilder reportBuilder = new StringBuilder();
  32. StringBuilder logcatBuilder = new StringBuilder();
  33. long stackCost = Math.max(cost, TraceDataUtils.stackToString(stack, reportBuilder, logcatBuilder));
  34. String stackKey = TraceDataUtils.getTreeKey(stack, stackCost);
  35. MatrixLog.w(TAG, "%s", printEvil(scene, processStat, isForeground, logcatBuilder, stack.size(), stackKey, usage, queueCost[0], queueCost[1], queueCost[2], cost)); // for logcat
  36. //上报问题
  37. try {
  38. TracePlugin plugin = Matrix.with().getPluginByClass(TracePlugin.class);
  39. if (null == plugin) {
  40. return;
  41. }
  42. JSONObject jsonObject = new JSONObject();
  43. jsonObject = DeviceUtil.getDeviceInfo(jsonObject, Matrix.with().getApplication());
  44. jsonObject.put(SharePluginInfo.ISSUE_STACK_TYPE, Constants.Type.NORMAL);
  45. jsonObject.put(SharePluginInfo.ISSUE_COST, stackCost);
  46. jsonObject.put(SharePluginInfo.ISSUE_CPU_USAGE, usage);
  47. jsonObject.put(SharePluginInfo.ISSUE_SCENE, scene);
  48. jsonObject.put(SharePluginInfo.ISSUE_TRACE_STACK, reportBuilder.toString());
  49. jsonObject.put(SharePluginInfo.ISSUE_STACK_KEY, stackKey);
  50. Issue issue = new Issue();
  51. issue.setTag(SharePluginInfo.TAG_PLUGIN_EVIL_METHOD);
  52. issue.setContent(jsonObject);
  53. plugin.onDetectIssue(issue);
  54. } catch (JSONException e) {
  55. MatrixLog.e(TAG, "[JSONException error: %s", e);
  56. }
  57. }

整体分析下来,经过帧率和启动耗时的分析后,卡顿现象的分析是不是就游刃有余,很容易就理解了呢。好了本次就先分析到这里。