一、性能分析---场景
启动卡,点击按钮卡,进入新页面卡等)、耗电,甚至出现应用无响应、程序崩溃的现象。当我们着手解决这些性能问题时,面对的第一个问题就是需要找到合适的工具来检测这些问题,用肉眼观察来判断定位这类问题是不靠谱的。理想的检测工具要能做到两点:
一、是可以定性的告诉我们应用是否有低性能问题,并且能定位到的点,指出哪个逻辑哪个方法使用系统资源低效,以便我们针对具体的问题给出对应的优化方案;
二、是可以定量说明问题点的严重程度,有具体的数字来衡量,这样我们对比优化前后的测量数据,就可以清晰地看到优化方案的收益和效果。
二、工具的使用---以卡顿为例
今天我们以性能优化中的卡顿问题来进行分析,和我使用到的一些工具,并指出工具的优缺点。
方式一:系统日志Displayed
2022-05-30 23:22:58.284 1879-1951/? I/ActivityTaskManager: Displayedcom.kang.cpuprofilerdemo/.MainActivity: +262ms
这样就是系统内部的ActivityTaskManager这个类中输出的应用启动时间,我试了下,只有冷启动的时候会输出,也就是说是应用从0开始到1创建的时间。
方式二:adb shell am命令。
1、卡顿的问题点:
清除后台全部进程,点击桌面我们开发的图标,等待了好长一段时间才看到app的画面(有开屏页就是等待一段时间才看到开屏页内容,没有开屏页的应用等待一段时间才看到的首页的内容),这对用户来说,是很不能接受和理解的,轻则吐槽,重则卸载。
2、Android启动基础知识:
首页我们要明白Android应用的启动方式分冷启动和热启动,有些说法还有温启动。
冷启动:后台没有我们的进程了(手动干掉或者系统回收了),点击桌面的app应用图标启动。
热启动:启动应用后,按Home退回桌面,Android后台还是存在这个应用的进程,缓存了相关的数据。这是再去点击桌面的app图标或者从后台进程点击启动。在热启动中,系统的所有工作就是讲activity带到前台,只要应用的所有activity仍驻留在内存中,应用就不必重复执行对象初始化、布局加载和绘制。
温启动:温启动包含了在冷启动期间发生的部分操作;同时,它的开销要比热启动高。有很多潜伏在状态可视为温启动:用户退出应用后又重新启动应用;
3、adb命令的使用
我们只分析冷启动的方式,因为热启动温启动都很快,没多大意义。
adb shell am命令是activity manager的意思,可以操作四大组件。
发送一个广播:adb shell am broadcast -a <广播动作>
adb shell am broadcast -a com.example.mybroadcast()
启动一个服务:adb shell am startservice <服务名称>
adb shell am startservice -n com.android.music/com.android.music.MediaPlaybackService
或 adb shell am startservice -a com.example.myservice //这里 -a 表示动作,就是你在 Androidmanifest 里定义的action。
手机连接上电脑后,连接adb,我们的adb要Android sdk的platform-tools目录下,所以需要配置环境变量才能全局使用。
使用:adb shell am start -S -W applicationId/首页全路径名
adb shell am start -S –W com.kang.cpuprofilerdemo/com.kang.cpuprofilerdemo.MainActivity
参数解释:
-S: force stop the target app before starting the activity
-W: wait for launch to complete
applicationId:就是我们项目module中的build.gradle中的applicationId
activity全路径名:就是项目中你的activity所在的全路径
(其他可以自己查看相关的options:adb shell am -h)
输出结果:
Status:操作成功
LaunchState:COLD冷启动方式
Activity:被启动的activity
ThisTime: 当前activity的启动时间,(不知道我这里输出结果为啥没有)
TotalTime: 启动过程中所有activity的启动时间,如MainActivity前面有SplashActivity
WaitTime: 应用进程的创建时间+TotalTime
结果分析:
可以看到我们这里的totalTime为2335ms,体验肯定很不好。一般我们就关注totalTime就行。
方式的优缺点总结:
这里2秒多的原因是因为我在mainActivity的onCreate()中Thread.sleep(2000)了,这种sleep导致的启动卡顿慢就比较容易看出来,但是在实际项目中,我们不可能看到一个totalTime就能发现问题点的。优点就是比较直观的发现确实是启动比较慢,但为什么慢?哪里慢?你是完全不知道的。
方式三:systrace工具(简单略过)
Androidstudio4.2.1 platform-tools33.0.2(被这个坑了)
说实话,我被这玩意整得眼睛痛,搞了一天都没搞出来,一堆问题。为啥呢?因为已经被抛弃了。
1、systrace介绍
从名字‘system trace’系统跟踪就能看出一二。我们主要是通过这个工具来记录分析系统级函数的执行情况。它从android内核收集CPU调度、存储器访问、应用线程等信息,生成一张html格式的报表。这个工具还会自动分析这些数据,高亮警示开发者注意这些可能有问题的地方。
2、systrace如何使用
有二种方式抓取systrace
(一)android Device monitor (DDMS)
这里就被坑了一下,我AS的菜单栏居然找不到这个工具了,以前的版本是在tools->android-DDMS就能找到,尼玛,现在as直接不展示给你找到了。不给你找到是有他的理由的,后面会讲。
那么怎么找到呢?在as的Terminal中输入monitor回车,就应该能出来了,反正我的是这样,这就耽误好一会时间了。(需要配置path环境变量,否则提示monitor不是内部命令)
打开之后就是下面的图:
然后这里又被坑了一下,因为我们的Android是大屏幕的板子,我就去ddms之后,显示了设备名称,但是一直不显示设备下的进程名,不显示进程我咋跟踪呀,又是坑了好久的时间。上面图是因为我连了手机的会显示。
DDMS无法看到进程:()
看这篇文章可以解决进程显示的问题,但尼玛文章里面讲到一个mprop的文件又不给我,又是一通找啊,写个文章就不能仔细点。
接下来又是坑,然后我们选中你要跟踪的进程,点击
那个智慧屏的设备就一直提示我:提示platform-tools版本过低,要我通过Android studio的sdk manager来升级platform-tools,结果我进去看我的版本是33.0.2,这明显大于18.0.1啊,啊西巴,真的是~~~~。
这里是因为我已经降低了到了33.0.0,一开始是33.0.2的。
platform-tools的下载地址:https://androidsdkmanager.azurewebsites.net/Platformtools
没降级之前,33.0.2的版本我进去Android sdk的platform-tools看到,居然尼玛没有systrace这个文件夹了。结果又是一通搜索,坑得一批,结果才发现Google的这个图:
这就是为什么我点击ddms去追踪的时候一直提示我要去升级的原因。
以上的坑都踩完之后就应该出现这个可视化配置:配置完之后就是去操作你的app,会自动记录你的trace,结果就是用浏览器看结果了。我后面的步骤就没去弄下去了,看着都烦,花费太多时间填这些坑。
(二)通过systrace.py工具
经过上面的ddms之后我就没走后面的步骤了,我就想换到这个systrace.py的脚本,用命令行的方式来抓trace.html.(systrace.py在platform-tools目录的systrace目录下)
配置环境:cmd的路径cd到C:\Users\sunbinkang\AppData\Local\Android\Sdk\platform-tools\systrace
接下来先安装win32
输入命令 pip install pypiwin32
如果pip版本过低则需要升级pip
输入命令 python -m pip install --upgrade pip(不升级也行好像)
安装six模块
输入命令 pip install six 如果报错可以试下先卸载six模块在重新安装,卸载命令是 pip uninstall six
还要安装python 2.7.16,我安装的是这个python版本
安装完上面的东西之后就好了。
输入命令:systrace.py应该会进入下面(这里python systrace.py 还可以配置一些参数)
你就在你手机上操作你的app,就会进行追踪了。按enter就可以结束追踪。输出文件就在C:\Users\sunbinkang\AppData\Local\Android\Sdk\platform-tools\systrace目录下
用chrome浏览器打开就行。
我这里就又遇到坑了,即使我们的Android板子抓到的trace.html是空白了,啊我要爆炸了。因为在你的终端看到了报错日志:ValueError: Invalid trace result format for HTML output和ValueError: Invalid trace result format for HTML output,但是我已经root了,
心态爆炸吧?然后又是一顿搜索一顿疑问,终究是没能解决这个问题,怀疑是Android板子的问题。怎么办呢?换工具呗,不整了。
如果是手机,上面的两种方式应该很容易就能得到跟踪的文件。
好了,具体systrace的使用就看这个:
怎么使用和怎么看跟踪的结果都有介绍,我就学Google,抛弃这个工具了,主要是被搞得心累了。整整一天没搞出来,都是坑。有一部分原因是我们用的第三方的Android板子。
方式三:Android Profile工具(AS版本4.2.1)
先上图,看上去是不是很清爽,很直观,这个工具结合了systrace和viewtrace的特点,使用cpu的版块就能达到systrace的作用。我们今天的重点就是CPU Profile。
(一)了解CPU Profile
点击 CPU 时间线中的任意位置即可打开 CPU Profiler。Android Profiler 界面是CPU、内存、网络、电量4项性能数据共享时间线视图,此共享时间线视图只显示时间线图表。 要详细分析,需要点击性能数据对应的图表,打开详情视图。
打开 CPU Profiler 后,可以看到类似上图的一些内容。它将立即开始显示应用的 CPU 使用率和线程 Activity。
标记1.Event 时间线: 显示应用中在其生命周期不同状态间转换的 Activity,并表明用户与设备的交互,包括触摸事件、按键点击的事件。 其中的5就代表我点击了页面的某个地方。
标记2.CPU 时间线: 显示应用的实时 CPU 使用率(以占总可用 CPU 时间的百分比表示)以及应用使用的总线程数。 此时间线还显示其他进程的 CPU 使用率(如系统进程或其他应用),以便您可以将其与您的应用使用率进行对比。 通过沿时间线的水平轴移动鼠标,您还可以检查历史 CPU 使用率数据。
标记3.选择cpu profiler mode:
有四种mode可以选择:
1、Sample Java Methods:采样跟踪
对 Java 方法采样:在应用的 Java 代码执行期间,频繁捕获应用的调用堆栈。分析器会比较捕获的数据集,以推导与应用的 Java 代码执行有关的时间和资源使用信息。如果应用在捕获调用堆栈后进入一个方法并在下次捕获前退出该方法,分析器将不会记录该方法调用。如果您想要跟踪生命周期如此短的方法,应使用下面的检测跟踪。
2、Trace Java Methods:检测跟踪
跟踪 Java 方法:在运行时检测应用,以在每个方法调用开始和结束时记录一个时间戳。系统会收集并比较这些时间戳,以生成方法跟踪数据,包括时间信息和 CPU 使用率。
3、Sample C/C++ Functions:
对 C/C++ 函数采样:捕获应用的原生线程的采样跟踪数据。要使用此配置,您必须将应用部署到搭载Android 8.0(API 级别 26)或更高版本的设备上。
4、Trace System Calls:
跟踪系统调用:捕获非常翔实的细节,以便您检查应用与系统资源的交互情况。您可以检查线程状态的确切时间和持续时间、直观地查看所有内核的 CPU 瓶颈在何处,并添加要分析的自定义跟踪事件。要使用此配置,您必须将应用部署到搭载 Android 7.0(API 级别 24)或更高版本的设备上。此跟踪配置在 systrace 的基础上构建而成。您可以使systrace 命令行实用程序指定除 CPU Profiler 提供的选项之外的其他选项。systrace 提供的其他系统级数据可帮助您检查原生系统进程并排查丢帧或帧延迟问题。
标记4.Record按钮:现在当我们与应用交互时,可以通过 CPU Profiler 监控 CPU 使用率和线程状态了。 不过,如想要了解应用执行代码的详细信息, 我们需要记录和检查函数跟踪。
标记5.屏幕点击就会出现这个橙色的小圆点,代表你的交互记录点
(二)记录和检查函数跟踪
要开始记录函数跟踪,从下拉菜单中选择 Sample Java Methods 记录配置,然后点击 Record 。与应用交互并在完成后点击 Stop recording。 分析器将自动选择记录的时间范围,并默认在函数跟踪窗格中显示主线程函数跟踪信息,如下图所示。如果您想检查另一个线程的函数跟踪,只需从线程活动时间线中选中它,函数跟踪窗格就会切换成所选线程的函数跟踪信息。
Stop Record之后就是下面的界面了:
1、选择时间范围:用于确定您要在跟踪窗格中检查所记录时间范围的哪一部分。 当您首次记录函数跟踪时,CPU Profiler 将在 CPU 时间线中自动选择您的记录的完整长度。 如果您想仅检查所记录时间范围一小部分的函数跟踪数据,您可以点击并拖动突出显示的区域边缘(就是你看到的那个蓝色的蒙层边缘)以修改其长度。快捷键(W、S、A、D可以实现缩小、方法、左移、右移)
2、时间戳:用于表示所记录函数跟踪的开始和结束时间(相对于分析器从设备开始收集 CPU 使用率信息的时间)。在选择时间范围时,您可以点击时间戳以自动选择完整记录,如果您有多个要进行切换的记录,则此做法尤其有用。
3、跟踪窗格:用于显示您所选的时间范围和线程的函数跟踪数据。仅在您至少记录一个函数跟踪后此窗格才会显示。在此窗格中,您可以选择想如何查看每个堆叠追踪(使用跟踪标签),以及如何测量执行时间(使用时间引用下拉菜单)。
4、选择后,可通过 Top Down 树、Bottom Up 树、调用图表或火焰图的形式显示您的函数跟踪。 您可以在下文中了解每个跟踪窗格标签的更多信息。
5、从下拉菜单中选择以下选项之一,以确定如何测量每个函数调用的时间信息:
Wall clock time:壁钟时间表示实际经过的时间。
Thread time:线程时间 = 实际经过的时间 - 线程未消耗 CPU 资源的时间。 对于任何给定函数,其线程时间始终少于或等于其壁钟时间。 线程时间可以更好地了解到线程的实际 CPU 使用率中有多少是给定函数消耗的。
(三)CPU Profiler 使用示例
下面我们通过两个例子来进一步了解下 CPU Profiler 的用法。
示例1:界面切换卡顿(老as版本的cpu profiler界面)
问题:首次打开播放页时界面卡顿,待优化。
分析:界面卡顿,应该是主线程有耗时操作导致UI刷新不及时。所以我们是用CPU Profiler来定位问题。
1.AS(Android Studio)连接上手机,手机上运行要分析APP来到MainActivity,再在AS的Profiler窗口选择要调试的进程,打开CPU Profiler。
2.然后在AS上点击 Record(或者右键点击Record) ,再在手机上操作跳转到PlayerActivity完成后点击 Stop recording。得到如下图所示信息。
3.仔细查看主线程Call Chart信息,发现播放页的onCreate()里的MultiscreenManager.init();耗用了绝大多数时间。仔细查看这一方法作用是初始化投屏相关功能。(初始话方法的具体逻辑去掉了,调用了sleep方法来模拟)
Call Chart 标签提供函数跟踪的图形表示形式,其中,水平轴表示函数耗费的时间,垂直轴显示其被调用者。 对系统 API 的函数调用显示为橙色,对应用自有函数的调用显示为绿色,对第三方 API(包括 Java 语言 API)的函数调用显示为蓝色。
解决方案:问题找到了解决就很容易了,投屏功能不是播放页启动的必须初始化项目,但是如果后置到用户点击投屏功能以后再去初始化投屏SDK,则会影响投屏功能的用户体验。这种问题有一个国际标准解决方案,把该任务添加到闲时任务系统中去。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_player);
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
MultiscreenManager.init();
return false;
}
});
}
CPU Profiler 检测时测量的函数耗时会大于真正线上包运行的时间。一方面是函数跟踪工具会加入额外的计时逻辑,另一方面它还会关闭虚拟机的JIT功能。所以我们一般是对比优化前后测量的两组数据来判断优化的效果。
修改后再测试下数据,onCreate()方法耗时明显降低,投屏sdk初始化逻辑放在主线程空闲的时候去执行的。还有一点注意下,可能出现从播放页打开到用户请求投屏时,主线程一直不空闲,也就是我们的初始化任务没有执行情况。所以加到闲时任务系统中的任务还得具备一个特点,就是如果显示任务没有执行,后续逻辑也要能正常运行。不过这个问题也很容易解决,就是在后续逻辑执行前先判断下是否初始化了。
示例2:应用冷启动慢(新as版本的cpu profiler界面)
问题:杀掉进程后,再次启动应用慢,待优化。
分析:有了上面的经验,分析这个问题也一样,先打开APP,选择要调试的进程,点击record按钮跟踪卡顿过程中函数调用信息。好了,问题来了,我们这里是要抓取应用冷启动过程函数调用信息,但是要用CPU Profiler工具抓取信息得先指定进程。应用启动前又没有进程信息可以指定。愁,挠头,程序员的头就是这么给挠秃顶的。
这个时候就该Debug 类出场了。我们可使用 Debug 类精确地控制设备何时开始和停止记录函数跟踪信息,来生成一份函数跟踪信息文件。然后再使用 Android Studio 或 Traceview 查看各个跟踪日志。
Traceview 有点过时了。如果我们使用的是3.2及其更新的Android Studio,就没有必要用Traceview了。
在开始生成跟踪日志之前,要确保应用有权限写入外部存储WRITE_EXTERNAL_STORAGE,以便将跟踪日志保存至该设备。创建跟踪日志,在想系统开始记录跟踪数据的位置调用 Debug.startMethodTracing(),要停止跟踪的位置请调用 Debug.stopMethodTracing()。系统将在getExternalFilesDir() 目录下生成 .trace 文件,一般都在 ~/sdcard/Android/data/$packname/files 目录中。用Android Studio的Device File Explorer工具找到这个文件双击即可打开。(这里我双击打不开的,会报错说没法解析,后面我是先下载到电脑桌面,拖进as打开的)
函数跟踪日志生成了,分析就和之前没什么两样了。
其实Debug下还有个 Debug.startMethodTracingSample(),追踪采样函数,对应的就是上面的Sample Java Methods.另外这几个函数的参数是多参数的,可以指定输出文件的大小,或者采样频率、输出路径等。
接下来就用一个自己的项目来分析冷启动慢的问题。
将这个文件右键点击save as...保存到电脑中,然后拖进as的编辑区域就可以打开了。
从上图可以看到总共这次追踪花了2.87s。接下来就分析怎么看上面这个图:
最左边是文件名,如果有多个追踪文件拖进as,都会在这显示,可以右键删除。
中间最上面区域:最上面的蓝色长方形是追踪的时间轴,可以通过键盘的W(缩小)、S(放大)、A(左移动)、D(右移动),可以根据实际情况缩小到对应的时间内,查看这个时间段内的method调用情况。
中间下面区域:显示的线程名,我们主要关心的main线程,点击可以展开,如下图:
对系统 API 的调用显示为橙色,对应用自有方法的调用显示为绿色,对第三方 API(包括 Java 语言 API)的调用显示为蓝色。 (as实际颜色显示有Bug,有时候会出现系统api也显示绿色这种情况)
最右边分析区域:
Summary:该时间段内的数据,longest running events(top 10)就是这段时间段内的耗时最长的前10的方法调用。
Top Down:调用列表,在该列表中展开方法或函数节点会显示它调用了的方法节点。从上往下的函数调用情况。
- Self Time —— 运行自己的代码所消耗的时间;
- Children Time —— 调用其他方法的时间;
- Total Time —— 前面两者时间之和。
Flame Chart:“火焰图”,提供一个倒置的调用图表,用来汇总完全相同的调用堆栈。也就是说,将具有相同调用方顺序的完全相同的方法或函数收集起来,并在火焰图中将它们表示为一个较长的横条 。横轴显示的是百分比数值。由于忽略了时间线信息,Flame Chart 可以展示每次调用消耗时间占用整个记录时长的百分比。 同时纵轴也被对调了,在顶部展示的是被调用者,底部展示的是调用者。此时的图表看起来越往上越窄,就好像火焰一样,因此得名: 火焰图。说白了就是将刚刚中间下面区域上下调用栈倒过来。
Bottom Up:方便地找到某个方法的调用栈。在该列表中展开方法或函数节点会显示哪个方法调用了自己。
Events:列出所有的函数,可以分页查看
所以从上面的这些不同形式的表格,我们可以分析我们的耗时方法数据。
很明显,在我们的MyApplication的onCreate中的initX5WebView是比较耗时的。
(四)结束
总的来说就是 CPU Profiler 可以让我们查看应用进程中的每个线程,某段时间内执行了哪些函数,以及在其执行期间每个函数消耗的 CPU 资源(文章中耗时指得就是占用CPU的时间)。 还可以使用函数跟踪来识别调用方和被调用方。 据此可以确定哪些函数负责调用常常会消耗大量特定资源的任务,并尝试优化应用代码以避免不必要的开支。
大家努力,最大限度减少应用的 CPU 使用率,向德芙巧克力一样,在各种新旧设备上都能提供纵享丝滑的用户体验。
本篇文章主要是从一个很小的冷启动的例子,分析了一个启动卡顿的问题,实际项目中,会更加复杂,需要我们花费很长的时间耐心去寻找分析。具体的一些冷启动卡顿优化解决办法,在下一篇文章会列举一些。