Android性能优化之CPU Profiler

8,558 阅读12分钟
原文链接: www.jianshu.com

低性能的APP常见的表现有启动/界面切换慢、动画掉帧、卡顿、耗电,甚至出现应用无响应、程序崩溃的现象。当我们着手解决这些性能问题时,面对的第一个问题就是需要找到合适的工具来检测这些问题,用肉眼观察来判断定位这类问题是不靠谱的。理想的检测工具要能做到两点:

  1. 一是可以定性的告诉我们应用是否有低性能问题,并且能定位到的点,指出哪个逻辑哪个方法使用系统资源低效,以便我们针对具体的问题给出对应的优化方案;
  2. 二是能定量说明问题点的严重程度,有具体的数字来衡量,这样我们对比优化前后的测量数据,就可以清晰地看到优化方案的收益和效果。
Android Profiler

为了满足上面两个需求,我们需要使用到systraceCPU Profiler这两个检测工具。

工具一:systrace
这个工具的作用从名字‘system trace’系统跟踪就能看出一二。我们主要是通过这个工具来记录分析系统级函数的执行情况。它从android内核收集CPU调度、存储器访问、应用线程等信息,生成一张html格式的报表。这个工具还会自动分析这些数据,高亮警示开发者注意这些可能有问题的地方。比如下图UI掉帧的问题。但是systrace只提供了系统方法调用信息,没有指明具体是因为调用我们APP程序哪个方法导致的,从systrace中只能定位出大概是在首个Activity创建的时候发生的。想要获取关于APP执行的详细信息就得使用今天的主角CPU Profiler了。

systrace

点击查看systrace更多详情。

工具二:CPU Profiler
CPU Profiler 可实时检查应用的 CPU 使用率和线程运行情况,并记录函数跟踪,以便我们优化和调试相关代码。简单点说就是我们可以通过CPU Profiler查看应用在某段时间里某个线程执行了哪些方法,并且还定量的展示了执行这些方法所耗费的时间及其方法的调用堆栈。稍有经验的程序员都知道应用启动慢、界面切换慢、动画不流畅卡顿等类似问题基本都是UI刷新不及时的表现,UI刷新不及时就是因为UI线程被其他逻辑方法长时间占用导致。呐,CPU Profiler简直就是为了解决这个问题而生的,轻松的分析出在卡顿(或者其他)过程中主线程都执行了哪些耗时操作。

Google官方提供的Android开发工具Android Studio附带了很多开发调试工具,这其中就包括了一系列的性能分析工具。在Android Studio 3.0之前这些工具叫Android Monitor tools,Android Studio 3.0开始成为Android Profiler tools。我现在使用的Android Studio版本是3.2.1,这篇文章主要介绍的是3.2.1版本Android Profiler tools中的CPU Profiler - CPU分析器。下面我们来看下CPU Profiler的使用方法。

一、CPU Profiler概览

1、打开CPU Profile界面
  1. 点击 View > Tool Windows > Android Profiler(也可以点击工具栏中的 Android Profiler )。
  2. 在 Android Profiler 工具栏中点击“+”号选择您想要分析的设备和应用进程。 如果您通过 USB 连接了某个设备但该设备未在设备列表中列出,请确保您已启用 USB 调试
  3. 点击 CPU 时间线中的任意位置即可打开 CPU Profiler。Android Profiler 界面是CPU、内存、网络、电量4项性能数据共享时间线视图,此共享时间线视图只显示时间线图表。 要详细分析,需要点击性能数据对应的图表,打开详情视图。
Android Profiler 共享时间线视图

打开 CPU Profiler 后,可以看到类似下图的一些内容。它将立即开始显示应用的 CPU 使用率和线程 Activity。

CPU Profiler
  1. Event 时间线: 显示应用中在其生命周期不同状态间转换的 Activity,并表明用户与设备的交互,包括触摸事件、按键点击的事件。 如需了解有关 Event 时间线的更多信息,包括如何启用它,请阅读 启用高级分析

  2. CPU 时间线: 显示应用的实时 CPU 使用率(以占总可用 CPU 时间的百分比表示)以及应用使用的总线程数。 此时间线还显示其他进程的 CPU 使用率(如系统进程或其他应用),以便您可以将其与您的应用使用率进行对比。 通过沿时间线的水平轴移动鼠标,您还可以检查历史 CPU 使用率数据。

  3. 线程活动时间线: 列出属于应用进程的每个线程并使用下面列出的颜色沿时间线标示它们的状态。 在记录一个函数跟踪后,可以从此时间线中选择一个线程以在跟踪窗格中检查其数据。

    • 绿色区域: 表示线程处于活动状态或准备使用 CPU。 即,它正在“运行中”或处于“可运行”状态。
    • 黄色区域: 表示线程处于活动状态,但它正在等待一个 I/O 操作(如磁盘或网络 I/O),然后才能完成它的工作。
    • 灰色区域: 表示线程正在休眠且没有消耗任何 CPU 时间。 当线程需要访问尚不可用的资源时偶尔会发生这种情况。 线程进入自主休眠或内核将此线程置于休眠状态,直到所需的资源可用。
  4. Record按钮:现在当我们与应用交互时,可以通过 CPU Profiler 监控 CPU 使用率和线程状态了。 不过,如想要了解应用执行代码的详细信息, 我们需要记录和检查函数跟踪。

2、记录和检查函数跟踪

要开始记录函数跟踪,从下拉菜单中选择 SampledInstrumented 记录配置,或选择您创建的自定义记录配置,然后点击 Record 。 与应用交互并在完成后点击 Stop recording。 分析器将自动选择记录的时间范围,并默认在函数跟踪窗格中显示主线程函数跟踪信息,如下图所示。如果您想检查另一个线程的函数跟踪,只需从线程活动时间线中选中它,函数跟踪窗格就会切换成所选线程的函数跟踪信息。

跟踪函数
  1. 选择时间范围: 用于确定您要在跟踪窗格中检查所记录时间范围的哪一部分。 当您首次记录函数跟踪时,CPU Profiler 将在 CPU 时间线中自动选择您的记录的完整长度。 如果您想仅检查所记录时间范围一小部分的函数跟踪数据,您可以点击并拖动突出显示的区域边缘以修改其长度。
  2. 时间戳: 用于表示所记录函数跟踪的开始和结束时间(相对于分析器从设备开始收集 CPU 使用率信息的时间)。 在选择时间范围时,您可以点击时间戳以自动选择完整记录,如果您有多个要进行切换的记录,则此做法尤其有用。
  3. 跟踪窗格: 用于显示您所选的时间范围和线程的函数跟踪数据。 仅在您至少记录一个函数跟踪后此窗格才会显示。 在此窗格中,您可以选择想如何查看每个堆叠追踪(使用跟踪标签),以及如何测量执行时间(使用时间引用下拉菜单)。
  4. 选择后,可通过 Top Down 树、Bottom Up 树、调用图表或火焰图的形式显示您的函数跟踪。 您可以在下文中了解每个跟踪窗格标签的更多信息。
  5. 从下拉菜单中选择以下选项之一,以确定如何测量每个函数调用的时间信息:
    • Wall clock time:壁钟时间表示实际经过的时间。
    • Thread time:线程时间 = 实际经过的时间 - 线程未消耗 CPU 资源的时间。 对于任何给定函数,其线程时间始终少于或等于其壁钟时间。 线程时间可以更好地了解到线程的实际 CPU 使用率中有多少是给定函数消耗的。

二、CPU Profiler 使用示例

上面把 CPU Profiler 工具的基本界面介绍了一下。下面我们通过两个例子来进一步了解下 CPU Profiler 的用法。

示例1:界面切换卡顿

问题:首次打开播放页时界面卡顿,待优化。
分析:界面卡顿,应该数主线有耗时操作导致UI刷新不及时。所以我们是用CPU Profiler来定位问题。

  1. AS(Android Studio)连接上手机,手机上运行要分析APP来到MainActivity,再在AS的Profiler窗口选择要调试的进程,打开CPU Profiler。
  2. 然后在AS上点击 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:应用冷启动慢

问题:杀掉进程后,再次启动应用慢,待优化。
分析:有了上面的经验,分析这个问题也一样,先打开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工具找到这个文件双击即可打开。

public class DymApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        
        SimpleDateFormat date = new SimpleDateFormat("dd_MM_yyyy_hh_mm_ss");
        String logDate = date.format(new Date());
        
        // 在Application创建的时候开始函数跟踪
        // 传入的参数是函数跟踪信息文件名,加时间戳保证文件不会被覆盖
        Debug.startMethodTracing("sample-" + logDate);
    }
}

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_activity);

        try {
            // TODO: 2018/12/7 怎么又是这个倒霉的sleep,为了测试APP启动卡顿问题
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 在首页Activity创建完成停止函数跟踪
        Debug.stopMethodTracing();
    }
}

函数跟踪日志生成了,分析就和之前没什么两样了。

CPU Profiler 中还有一些信息也是很有用的,比如Top Down 和 Bottom Up ,这里没有讲到,大家可以自己去看下,有什么不明白的,也可以在下面留言讨论。

三、结束

总的来说就是 CPU Profiler 可以让我们查看应用进程中的每个线程,某段时间内执行了哪些函数,以及在其执行期间每个函数消耗的 CPU 资源(文章中耗时只得就是占用CPU的时间)。 还可以使用函数跟踪来识别调用方被调用方。 据此可以确定哪些函数负责调用常常会消耗大量特定资源的任务,并尝试优化应用代码以避免不必要的开支。

大家努力,最大限度减少应用的 CPU 使用率,向德芙巧克力一样,在各种新旧设备上都能提供纵享丝滑的用户体验。