性能优化第一步

3,118 阅读7分钟
原文链接: linxiaotao.github.io

感谢

cpu-profiler

前言

对于有一定 Android 应用开发经验的同学来说,性能优化是避不开的话题,一般来说,Android 上我们常说的性能优化包括,内存优化和界面优化,当然还有电量优化、网络优化等等。性能优化知识的学习是一个长期的过程,也不可能通过一两篇文章就能说的清楚,需要在具有相关的理论知识的前提下,同时在日常开发中加以实践,才能在这方面的工作中得心应手。

同时作者也并不是这方面的大牛,还处于入门学习的阶段,所以也会遇到初学者经常碰到的问题,其中,我个人认为阻碍初学者在这方面深入学习的一个原因是,大部分的文章都偏向于理论知识和工具使用说明,很多同学包括作者在看完这些文章,都有着似懂非懂的感觉,只记住了工具的名称,但等到自己去实际使用时,却不知道如何下手。这也是这个系列文章存在的意义,希望能通过实际例子去使用工具,从而能掌握工具基本的使用,之后在实际开发中,通过经常使用来达到得心应手的地步。

例子

作者最近刚接手一个新项目,在首页列表滑动时就感到有点不顺畅,特别是在滑动到有 ViewPager 部分的时候,如果是熟悉的项目,可能会第一时间会去检查代码,但前面说到这个是刚接手的项目,同时首页的代码逻辑比较复杂,不花点时间熟悉下代码可能很难找出问题来,那在这种情况下,我们就只能通过外部工具来检查,快速定位问题。

Android Studio 提供了一个非常好用的工具:Android Profiler,基本可以满足大部分的场景了。所以下面的分析我们都基于 Android Profiler。

为了更直观的观察问题,我创建了个 Demo,里面包含了问题代码,首先我们先通过两个 gif 图来看下,没有问题代码之前和加了问题代码之后的运行情况:

正常 正常
问题代码 问题代码

上面的 gif 图中,我们先开启了 GPU 呈现模式分析 后面的文章中我们会说到这个,现在只需要了解横坐标表示应用绘制渲染的每一帧,纵坐标表示每一帧绘制渲染的时间,绿线表示 16.6 ms,也就是要想保持应用能够流畅使用,我们应该让页面绘制渲染时间尽量位于绿线之下。

从第一张动图中可以看到页面每一帧的绘制渲染时间都基本处于绿线之下,而第二章动图加了问题代码之后,出现了不少帧出现了绘制渲染时间过长的情况。

接下来,我们可以使用 Android Profiler 来对应用进行分析,在这个例子中,我们只使用 cpu-profiler 功能。具体介绍可以看官方文档,下面就官方提供的图片简单说下:

cpu-profiler cpu-profiler

这是 cpu-profiler 的主界面,具体各个部分的说明可以阅读官方文档,我们只需要知道这里记录了各个线程的活动状态:

  • 绿色:表示线程处于活动状态或准备使用 CPU。 即,它正在“运行中”或处于“可运行”状态。
  • 黄色:表示线程处于活动状态,但它正在等待一个 I/O 操作(如磁盘或网络 I/O),然后才能完成它的工作。
  • 灰色:表示线程正在休眠且没有消耗任何 CPU 时间。 当线程需要访问尚不可用的资源时偶尔会发生这种情况。 线程进入自主休眠或内核将此线程置于休眠状态,直到所需的资源可用。

可以看到标记 5 为一个记录按钮,用于开始和停止记录函数跟踪。这个是我们这篇文章所要着重讲的功能,接下来我们看下函数跟踪的功能面板,图片同样来源于官方文档:

method-traces method-traces

使用方式很简单,先点击记录按钮,接着在应用上操作一小会,接着再次按下按钮,结束记录。上图则是记录后的面版:

  • 标记 1和标记 2 表示这次函数跟踪的时间段
  • 标记 3 表示函数跟踪的结果,具体展示跟标记 4 所选的展示标签有关
  • 标记 4 则是不同的展示标签
  • 标记 5 则是函数调用的时间信息:
    • Wall clock time: 壁钟时间信息表示实际经过的时间
    • Thread time: 线程时间信息表示实际经过的时间减去线程没有消耗 CPU 资源的任意时间部分。 对于任何给定函数,其线程时间始终少于或等于其壁钟时间。 使用线程时间可以让您更好地了解线程的实际 CPU 使用率中有多少是给定函数消耗的

标记 4 中包含的标签包括:Call Chart、Flame Chart、Top Down 和 Bottom Up,具体的含义可以阅读官方文档,这里我们同样只是简单解释下:

  • Call Chart:函数调用情况的图形化界面。从上至下表示父函数和子函数的调用情况。

    这里我们的父函数指的是,调用其他函数的函数,而被调用的函数称为该父函数的子函数。

    call-chart

    可以看到,应用的执行开始于 ZygoteInit.main() 其中系统 API 的调用表示为橙色,第三方 API 包括 Java 的调用表示为蓝色,应用自有函数的调用表示为绿色。

  • Flame Chart:提供一个倒置的调用图表,其汇总相同的调用堆栈,简单来说就是将 Call Chart 倒置过来

    flame-chart

  • Top Down:显示一个函数调用列表,在该列表中展开函数节点会被调用的子函数

    top-down

  • Bottom Up:显示一个函数调用列表,在该列表中展开函数节点将显示函数的父函数

    bottom-up

简单了解每个选项的功能后,我们针对上面的 demo 来实践下,这里我们选择 bottom up 选项,从上面图看出,每个函数所展示的信息有:Total(函数总共花费的时间)、%(所占的百分比)、Self(函数自身花费的时间)、%(所占的百分比)、Children(函数所调用的子函数花费的时间),%(所占百分比)。可以看到 main() run() 这些方法的时间主要都是花费在调用子函数上。那么我们先找出本身最耗时的方法,使用 Self 进行排序:

max-self-method max-self-method

很清楚的看出, setConfiguration() 函数是除了 nativePollOnce() 之外最耗时的函数,而 nativePollOnce() 是系统函数,是 Handle 用于阻塞获取栈中下一个执行的消息,具体可以看另外一篇文章,Android消息机制-Handler 所以这个函数我们这里不需要关心它,所以我们打开 setConfiguration() 的父函数调用链,可以发现最终指到我们应用自身的函数,即 MainActivity.getResources() 这时候,我们就可以找到对应的类和函数,也可以使用快捷跳转方式: Jump To Source,我们看下具体的函数实现:

@Override                                                     
public Resources getResources() {                             
    Resources res = super.getResources();                     
    Configuration config = new Configuration();               
    config.setToDefaults();                                   
    res.updateConfiguration(config, res.getDisplayMetrics()); 
    return res;                                               
}

从代码中可以看出,耗时函数就出在 res.updateConfiguration(),而 getResources() 会在比如 View 的构造函数中或者 inflate 布局时候被调用,我们上面的例子实现的是,使用 ViewPager 去实现图片滑动浏览的效果,ViewPager 在滑动的时候会调用 instantiateItem() 函数去生成新的 item:

@NonNull                                                                                                         
@Override                                                                                                        
public Object instantiateItem(@NonNull ViewGroup container, int position) {                                      
    final TextView childView = (TextView) View.inflate(container.getContext(), R.layout.item_image, null);       
    childView.setText(String.valueOf(position));                                                                 
    container.addView(childView);                                                                                
    return childView;                                                                                            
}

这里会调用 inflate() 从 xml 布局文件中构建 View,而这个函数又会调用 getResources() 函数,最终调用到 setConfiguration() 上,这也是我们之所以在滑动的时候,会出现卡顿的原因。

小结

在上面的例子上,我们不需要去纠结为什么要重载 getResources() 而是通过这个例子达到能简单使用 cpu-profiler 的目的。

当然你也可以使用其他工具来检查,比如 method-trace 等等,但最终目的是一样的,找到耗时函数,从而去优化它。