乐趣区

关于android:使用-Android-Profiler-和-友盟UAPM-解决-Android-卡顿问题

本⽂出⾃于“「2021 友盟 + 挪动应⽤性能挑战赛 」”中的参赛作品,该⽂章表述了作者如何借助 友盟 + U-APM⼯具进⾏了性能优化。

作为⼀款倒计时⽇历 APP,咱们须要对每个⽇期实时显示倒计时并准确到秒。然而咱们的 app 在滑动刷新数据时,会呈现卡顿。

卡顿在很⼤水平上取决于设施的 CPU 和其余耗费 CPU 工夫的过程。于是咱们尝试使⽤了 友盟 + U-APM 内存剖析对 APP 进⾏剖析:

通过观察内存的散布,⼤局部程序的运⾏都处于可预测的范畴内,咱们须要更加细粒度地进⾏测试。

启动 Android Profiler Tool Window , 关上 CPU Profiler 并抉择正确的工夫线。连贯咱们的测试设施并再次进⾏滑动刷新,能够看到 Profile 线程被增加到应⽤过程并耗费了额定的 CPU 工夫。看看 Logcat

I/Choreographer: Skipped 147 frames! The application may be doing too much

查看 CPU Profiler 工夫线:

图表上⽅有⼀个视图示意⽤户与应⽤程序的交互。所有⽤户输⼊事件在此处显示为紫⾊圆圈。能够看到⼀个圆圈,代表咱们为刷新数据⽽执⾏的滑动。

在事件下⽅,有⼀个 CPU 工夫线,它以图形⽅式显示了与可⽤ CPU 总工夫相干的应⽤程序和 其余过程的 CPU 使⽤率。还能够查看应⽤程序正在使⽤的线程数。

底部能够看到属于应⽤过程的线程流动工夫线。每个线程处于由颜⾊批示的三种状态之⼀:流动(绿⾊)、期待(⻩⾊)或睡眠(灰⾊)。

在列表顶部,能够找到应⽤程序的主线程。在我的设施 (Nexus 5X) 上,它使⽤的 CPU 工夫⼤ 约 5 秒。咱们能够记录⼀个⽅法跟踪来查看。

在滑动之前单击“记录”按钮以刷新操作并在数据刷新实现后⽴即停⽌记录:

咱们将从第⼀个选项卡中显示的图表开始剖析。横轴代表工夫的流逝。

调⽤者及其被调⽤者(从上到下)显示在垂直轴上。⽅法调⽤也通过颜⾊辨别,具体取决于是调 ⽤零碎 API、第三⽅ API 还是咱们本地函数。每个⽅法调⽤的总工夫是⽅法⾃身工夫及其被调⽤者工夫的总和:

从这张图表中,能够推断出性能问题出在 generateItems ⽅法外部。

为了证实咱们的推断,咱们⼜使⽤了 友盟 + U-APM 进⾏了线上测试,测试后果和前⽂雷同,下图是 友盟 + U-APM 的卡顿分析测试,图中展现了次要是哪些⽅法造成了 CPU 的⾼耗时,导致了卡顿状况的发⽣:

⽕焰图揭示了哪些⽅法占⽤了贵重的 CPU 工夫,并聚合了雷同的调⽤堆栈:

发现了两个可疑的地⽅,getRemainingTime 整个⽅法执⾏工夫达到 2 秒以上,
LocalDateTime.format 占⽤ CPU 工夫 1 秒以上:

此工夫还包含线程未处于活动状态的时间段。另外,能够切换要在线程工夫中显示的计时信息。

最初⼀个选项卡中的图表显示按 CPU 工夫耗费降序排列的⽅法调⽤列表。该图表提供具体的计时信息(以微秒为单位):

从图表中获取耗费过多 CPU 工夫的⽅法的计时信息。将它们与调⽤堆栈中的两个⽅法相关联:

class  method total time (in ms) percentage of recorded duration
Sample1Activity refreshData 3027   89.28
Sample1Activity generateItems  3025   89.22
Sample1Activity getRemainingTime   1723   50.83
LocalDateTime  format 1003   29.59

能够看到 getRemainingTime LocalDateTime.format 耗费了超过 80% 的工夫。为了解决卡顿问题,咱们须要从这⾥⼊⼿。

那么该怎么办?聪慧的读者可能曾经提出了⼏种解决⽅案。因为咱们执⾏了⼤量计算,所以咱们将只为多数以后显示和筹备显示的项⽬调⽤ getRemainingTimeLocalDateTime.format ⽅法。

为了实现它,咱们须要更新 Item 属性,保留必要的数据以便稍后执⾏格式化:

data class Item(val now: LocalDateTime, val offset: Int)

这须要在 generateItemsbindItem 函数中应⽤以下更改:

private fun generateItems(): List<Item> {val now = LocalDateTime.now()
return List(1_000) {Item(now, it + 1) }
}


private fun bindItem(holder: ViewHolderBinder<Item>, item: Item) = with(val date = item.now.plusDays(item.offset.toLong()).toLocalDate().atS
val remainingTime = getRemainingTime(item.now, date)

咱们内联了 createItem 函数,当初所有函数都在 bindItem ⽅法外部。
在咱们的代码批改⽣效之后,重新启动 CPU Profiler 并记录⽅法的运⾏状况。
为了查看咱们的优化是否胜利,咱们须要查看 Call Chart

将⿏标移到 generateItems 函数上,会发现当初耗时约为 0.3 秒。

这⽐优化前缩小了 13 倍以上的 CPU 工夫。为了确保咱们的更改不会对 bindItem ⽅法的耗时造成负⾯增益,咱们切换到⽕焰图标来查看 bindItem 的总耗时。
如图所示,它最多耗费 0.1 秒:

此外,咱们能够进⾏滚动测试,以确保咱们的代码优化不会影响整体应⽤程序的性能,并且在滚动过程中加以记录。

测试发现滚动之后不会再呈现卡顿和掉帧的状况了。胜利!代码已优化!

总结

Android Profiler 和 友盟 + U-APM 都是很好的测试⼯具。如果咱们谋求晦涩的⽤户体验,那么使⽤这些优良的 debug ⼯具是⼗分必要的。在本⽂中,我次要关注性能调优。

然而,本⽂中未涵盖的 Android Profiler 的 Memory Profiler 和 Network Profiler,友盟 + U-A PM 的内存剖析、OOF 异样和内存占⽤也同样值得钻研。记录内存调配对查找内存透露有很⼤帮忙,例如代码中有没有回收 bitmap。

⽆论如何,使⽤失当的剖析⼯具能够带来多项优化成绩,期待读者们的⾃⾏摸索。

本⽂出⾃于“「2021 友盟 + 挪动应⽤性能挑战赛 」中的参赛作品,该⽂章表述了作者如何 友盟 + U-APM⼯具进⾏了性能优化。

退出移动版