三、构建布局

应用的图形设计和导航定义了它的外观和感觉,可能是成功的关键,但是在处理目标用户的安卓屏幕大小和 SDK 级别碎片化时,构建一个稳定、快速加载和高效的用户界面真的很重要。缓慢、无响应或不可用的图形用户界面可能会导致不好的评论,无论它看起来如何。这就是为什么在每个应用的开发过程中,您必须牢记创建高效布局和视图的重要性。

在这一章中,我们将详细介绍你的用户界面的优化细节,然后使用有用的工具来了解如何提高屏幕性能和效率,以满足你的应用用户的期望。

穿行

了解设备屏幕背后的一些关键概念以及在开发安卓应用时对提高稳定性和性能非常有用的代码非常重要。让我们从了解设备如何刷新屏幕上的内容以及人眼如何感知它们开始。我们将通过开发者可以面临的限制和常见问题,发现谷歌团队在安卓发展过程中引入了哪些解决方案,以及开发者可以使用哪些解决方案来最大限度地提高他们开发过程的输出。

渲染性能

让我们在观看我们的应用时,对人脑中的内容进行一个概述,以便更好地了解如何改善用户体验我们的应用的性能。人脑从我们的眼睛接收模拟连续图像进行处理。但是数字世界是由模拟现实世界的少量后续帧组成的。这个棘手系统背后的基本机制基于一个主要的物理定律:单位时间内处理的帧越多,人脑感知运动的效率就越高。我们的大脑每秒感知运动的最小帧数在 10 到 12 帧之间。

那么,一个设备创建最流畅的应用的最合适的每秒帧数是多少?为了给出这个问题的答案,我们将只看一看不同的行业如何处理这个问题:

  • 电视和戏剧电影:电视广播和电影在这个领域有三种标准帧率。它们是 24 FPS(适用于美国 NTSC 和电影院)、25 FPS(适用于欧洲 PAL/SECAM)和 30 FPS(适用于家庭电影和摄像机)。使用这些帧速率,可能会出现运动模糊:这是大脑处理后续图像速度过快时视觉敏锐度的损失。
  • 慢动作和新电影制作人:在这些方面最常用的帧率是 48 FPS——是电影的两倍。这是新电影人改善动作电影流畅性的路径。这种帧速率也用于降低场景的速度,因为以 24 FPS 播放的 48 FPS 录像机场景具有与电影相同的感知水平,但速度只有电影的一半。

应用帧速率如何?我们要实现的目标是在应用的整个生命周期中保持 60 FPS 的速度。这意味着屏幕应该在一秒钟内刷新 60 次,或者每 16.6667 毫秒刷新一次。

有很多事情会导致这个 16 ms 的期限得不到尊重;例如,当视图层次被重绘太多次,占用太多 CPU 周期时,就会发生这种情况。如果发生这种情况,框架会被删除,用户界面不会被刷新,在绘制下一个框架之前,用户会看到相同的图形。这是您需要避免的,以便为用户提供流畅流畅的用户体验。

有一个技巧可以加快用户界面的绘制速度,达到 60 FPS:当您构建布局并使用Activity.setContentView()方法将其添加到活动中时,许多其他视图会被添加到层次结构中,以创建所需的用户界面。在图 1 中,有一个完整的视图层次结构,但是我们添加到活动的 XML 布局文件中的唯一视图位于两个较低的级别:

Rendering performance

图 1:完整层次视图的一个例子

我们现在感兴趣的是层次结构顶层的视图;该视图被称为德景 ,它保存了由主题定义的活动背景。然而,这个默认背景经常被你的布局背景所覆盖。这意味着它会影响图形处理器的工作,降低渲染速度,从而降低帧速率。所以诀窍就是避免画这个背景,从而提高性能。

移除此drawable背景的方法是将属性添加到活动主题或使用以下主题(即使对于兼容性主题,也可以使用相同的属性):

<resources>
    <style name="Theme.NoBackground" parent="android:Theme">
      <item name="android:windowBackground">@null</item>
    </style>
</resources>

这在您每次处理全屏活动时都很有帮助,这些活动用不透明的儿童视图覆盖了整个去核心视图屏幕。尽管如此,将活动布局背景移到窗口装饰视图是一个很好的做法。其主要原因是 DecorView 的背景是在任何其他布局之前绘制的:这意味着用户将立即看到背景,无论其他 UI 组件加载操作花费多长时间,并且不会给人以应用未加载的错误感觉。为此,只需将背景drawable作为上一个主题 XML 文件的windowBackground属性,并将其从活动的根布局中移除:

<resources>
    <style name="Theme.NoBackground" parent="android:Theme">
      <item name="android:windowBackground"> 
        @drawable/background</item>
    </style>
</resources>

总的来说,这第二个变化不是一个适当的改进,而只是一个技巧,给用户一个更流畅的应用的感觉;无论是在去核心视图还是活动布局根中,背景图都与 GPU 消耗相对应。

屏幕撕裂和 VSYNC

当我们谈论刷新时,有两个主要方面需要考虑:

  • 帧率:这个是关于设备 GPU 能够在屏幕上绘制一整帧的次数,以每秒帧数来指定。我们的目标是保持 60 FPS,这是安卓设备的标准,我们将了解原因。
  • 刷新率:这个是指一秒钟内屏幕更新的次数,以赫兹为单位。大多数安卓设备屏幕的刷新率为 60 Hz。

虽然第二个是固定不变的,但第一个,如前所述,取决于很多因素,但首先取决于开发人员的技能。

这些值可能不会同步。所以,显示即将更新,但是要绘制的内容由单个屏幕绘制中两个不同的后续帧决定,导致屏幕上出现明显的剪切,直到下一个屏幕绘制,如图图 2 所示。这个事件也被称为屏幕撕裂,它可以影响每一个带有 GPU 系统的显示器。图像上不连续的线条称为 撕裂点,是本次屏幕撕裂的结果:

Screen tearing and VSYNC

图 2:屏幕撕裂的一个例子

这种现象的主要原因可以在用于绘制帧的单个数据流中找到:每一个新帧都会覆盖前一个帧,这样屏幕上就只有一个缓冲区可以读取和绘制。这样,当屏幕即将刷新时,它会从缓冲区中读取要绘制的帧的状态,但它可能仍在完成,尚未完成。于是,图 2 的截屏。

这个问题最常用的解决方案是对帧进行双缓冲。该解决方案具有以下实现:

  • 所有绘图操作都保存在后台缓冲区中
  • 当这些操作完成时,整个后缓冲区被复制到另一个内存位置,称为前缓冲区

复印操作与屏幕速率同步。为了避免屏幕撕裂,屏幕仅从前缓冲区读取,所有背景绘制操作都可以在不影响屏幕操作的情况下执行。但是,在从后缓冲区到前缓冲区的复制操作过程中,是什么阻止了屏幕更新呢?这叫 VSYNC 。这代表垂直同步,最早在安卓 4.1 果冻豆(API Level 16)中推出。

VSYNC 不是解决问题的办法:如果帧速率至少等于刷新率,它就能正常工作。我们来看看图 3;帧率为 80 FPS,刷新率为 60 Hz。一个新的框架总是可用于绘图,然后屏幕上将没有滞后:

Screen tearing and VSYNC

图 3:帧速率高于刷新率的 VSYNC 示例

但是,如果帧率低于刷新率会怎么样呢?让我们看一下下面的例子,逐步描述 40 FPS GPU 和 60 Hz 刷新率屏幕的情况:即帧率是刷新率的 2/3,导致每 1.5 次屏幕刷新更新一帧:

  1. 在瞬间 0,屏幕第一次刷新,帧 1 落入前缓冲区,GPU 开始准备后缓冲区的第二帧。
  2. 屏幕第二次刷新时,第一帧被绘制到屏幕上,而第二帧不能被复制到前缓冲区,因为图形处理器仍在完成对它的绘制操作:它仍处于该操作的 2/3。
  3. 在第三次刷新时,第二帧已经被复制到前缓冲区,因此它必须等待下一次刷新显示在屏幕上。GPU 开始准备第三帧。
  4. 在第四步中,第 2 帧被绘制在屏幕上,因为它位于前缓冲区,并且 GPU 仍在准备第三帧。
  5. 第五次刷新与第二次类似:第三帧无法显示,因为需要新的刷新,所以第二帧连续第二次显示。

这里描述的内容见图 4 :

Screen tearing and VSYNC

图 4:帧速率低于刷新率的 VSYNC 示例

毕竟只是四次屏幕刷新已经画出了两帧。但每次帧率低于刷新率时都会出现这种情况:即使帧率为 59 FPS,屏幕上显示的实际帧数也是每秒 30 帧,因为 GPU 需要等待新的刷新发生后才能在后台缓冲区开始新的绘制操作。这导致了滞后和 jank,并抵消了任何图形设计的努力。这种行为对开发人员来说是透明的,并且没有 API 来控制或更改它,因此在我们的应用中保持高帧率并遵循性能提示和技巧来实现 60 FPS 目标是极其重要的。

硬件加速

安卓平台的进化历史在图形渲染方面也有增量的提升。这方面最大的改进是在 Android 3.0 蜂巢(API Level 11)中引入了硬件加速。设备屏幕变得越来越大,平均设备像素密度也在增长,因此中央处理器和软件不再足以满足用户界面和性能方面不断增长的需求。随着平台行为的这种变化,视图及其所有由Canvas对象进行的绘制操作都使用 GPU,而不是 CPU。

硬件加速最初是可选的,应该在清单文件中声明启用,但是随着下一个主要版本(安卓 4.0 冰淇淋三明治,应用编程接口级别 14),它被默认启用。它在平台中的引入带来了一个新的绘图模型。基于软件的绘图模型基于以下两个步骤:

  • 失效:当由于视图层次结构需要更新或者仅仅是视图属性的改变而调用View.invalidate()方法时,失效会在整个层次结构中传播。这个步骤也可以由非主线程使用View.postInvalidate()方法调用,失效发生在下一个循环周期。
  • 重绘:每个视图都是重绘的,CPU 消耗很大。

使用新的硬件加速绘图模型,由于视图被存储,重绘不会立即执行。因此,步骤变成如下:

  • 失效:由于在基于软件的绘图模型中,一个视图需要更新,所以View.invalidate()方法在整个层次中传播。
  • 存储:在这种情况下,只重绘受失效影响的视图并存储以备将来重用,减少了运行时计算。
  • 重绘:每个视图都使用存储的图形进行更新,因此不受无效影响的视图使用其最后存储的图形进行更新。

每个视图都可以渲染并保存到屏幕外的位图中以备将来使用。可以使用Canvas.saveLayer()方法完成,然后使用Canvas.restore()将保存的位图拉回画布。应该谨慎使用它,因为它在屏幕外绘制了一个不需要的位图,根据提供的边界尺寸增加了计算绘制成本。

从安卓 3.0 蜂巢(API Level 11)开始,可以选择使用哪种类型的图层,同时使用View.setLayerType()方法为每个视图创建屏幕外位图。此方法需要以下参数之一作为第一个参数:

  • View.LAYER_TYPE_NONE:没有应用层,所以视图不能保存为屏下位图。这是默认行为。
  • View.LAYER_TYPE_SOFTWARE:此强制基于软件的绘图模型渲染所需视图,即使启用了硬件加速。它可以在以下情况下使用:
    • 需要将颜色过滤器、混合模式或透明度应用于视图,并且应用不使用硬件加速
    • 硬件加速已启用,但它无法应用渲染图形图元
  • View.LAYER_TYPE_HARDWARE:如果视图层次结构启用了硬件加速,则硬件专用管道渲染图层;否则行为将与View.LAYER_TYPE_SOFTWARE相同。

用于性能目的的正确图层类型是硬件图层类型:在调用其View.invalidate()方法之前,不需要重新绘制视图;否则,使用图层位图不会产生额外成本。

我们在本节中讨论的内容可以帮助我们在处理动画时保持 60 FPS 的目标;硬件加速层可以使用纹理来避免视图在每次属性改变时被无效和重绘。这是可能的,因为更改的不是视图的属性,而是图层的属性。可以在不涉及整个层次失效的情况下更改的属性如下:

  • alpha
  • x
  • y
  • translationX
  • translationY
  • scaleX
  • scaleY
  • rotation
  • rotationX
  • rotationY
  • pivotX
  • pivotY

这些都是谷歌用安卓 3.0 蜂巢(API Level 11)发布的属性动画中涉及到的相同属性,只是对硬件加速的支持。

提高动画性能和减少不必要计算的一个好方法是在开始动画之前启用硬件层,并在动画完成后立即禁用它,以释放已使用的视频内存:

view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "rotationY", 
180);
animator.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
        view.setLayerType(View.LAYER_TYPE_NONE, null);
    }
});
animator.start();

类型

考虑在每次制作视图动画、更改其 alpha 或仅设置不同的 alpha 时使用View.LAYER_TYPE_HARDWARE。这一点非常重要,谷歌改变了View.setAlpha()方法的行为,从 Android 6.0 棉花糖(API Level 23)开始自动应用硬件层,所以如果你的应用的目标 SDK 是 23 或者更高,你就不需要这么做了。

透支

布局构建满足 UI 需求往往是一个误导性的任务:一旦完成了我们的布局,简单的检查一下我们刚刚做的是否符合图形设计者认为的不够。我们的目标是验证用户界面不会影响我们的应用性能。忽略我们如何在布局中构建视图是一种常见的做法,但有一点需要记住:系统不知道哪些视图对用户可见,哪些视图对其他人不可见。这意味着无论如何每个视图都是绘制的,不管它是被覆盖的、隐藏的还是不可见的。

请记住,如果视图不可见、隐藏或被另一个视图或布局覆盖,则视图生命周期不会终止:从计算和内存的角度来看,即使没有显示,其计算工作也会继续影响最终的布局性能。因此,一个好的做法是在用户界面设计步骤中限制使用的视图数量,以防止性能显著下降。

从系统的角度来看,屏幕上的每个像素需要更新的次数等于每次帧更新的重叠视图数。这种现象叫做透支。开发商的目标是尽可能限制透支。

我们如何减少屏幕上绘制的视图数量?这个问题的答案取决于我们的应用 UI 是如何设计的。但是为了实现这个目标,有一些简单的规则可以遵循:

  • The window background adds a layer to be drawn every update. Background removal can free one level from the overdrawing amount. This can be done for the DecorView, as discussed earlier in this chapter, by deleting it from the used theme of our activity directly in the XML style file. Otherwise, it can be done at runtime by adding the following to the activity code:

    java @Override public void onWindowFocusChanged(boolean hasFocus) { if (hasFocus) getWindow().setBackgroundDrawable(null); }

    这可以应用于层次结构的每个视图;这背后的想法是消除不必要的背景,以限制系统每次必须处理和绘制的级别数量。

  • 扁平化视图层次是降低透支风险的好方法;使用层次查看器在设备 GPU 上透支,如下文所述,是实现这一目标的关键一步。在这个扁平化的操作中,你可能会不经意间由于 RelativeLayout 管理而陷入透支的问题:视图可能会重叠,使得这个任务效率低下。

  • 安卓以不同的方式管理位图和 9 补丁:对 9 补丁的特殊优化让系统避免绘制它们的透明像素,这样它们就不会继续透支,而每个位图像素都会。所以使用 9 补丁作为背景可以帮助限制透支的表面。

多窗口模式

在新的 Android N 版本中增加的新功能之一,在撰写本书时的预览版中,被称为多窗口模式。这是为了让用户能够同时在屏幕上并排显示两个活动。在分析这个特性的性能效果之前,让我们先简单了解一下它。

概述

该分割模式在纵向和横向模式下均可用。你可以在图 5 中看到人像模式,在图 6 中看到风景模式:

Overview

图 5:纵向安卓 N 分割模式

Overview

图 6:风景中的安卓 N 分割模式

从用户角度来看,这是与多个应用或活动交互的方式,而无需离开当前屏幕并打开最近的应用屏幕。中心的分割条可以移动,关闭分割模式。这种行为适用于智能手机,而制造商可以在更大的设备上启用 自由格式模式,让用户通过简单的滑动手势为两种活动选择合适的屏幕比例。还可以将对象从一个活动拖放到另一个活动。

在电视设备上,这是通过使用 画中画模式来完成的,如图 7 所示。在这种情况下,视频内容继续播放,而用户可以导航应用。然后,视频活动仍然可见,但在屏幕的较小部分:屏幕右上角是一个 240 x 135 dp 的窗口:

Overview

图 7:安卓 N 画中画模式

由于窗口的尺寸较小,活动应该只显示视频内容,避免显示任何其他内容。除此之外,请确保画中画窗口不会遮挡任何背景活动。

现在让我们来看看典型的活动生命周期有什么不同,以及系统如何同时处理屏幕上的两个活动。当多窗口模式处于活动状态时,最近使用的活动处于恢复状态,而另一个处于暂停状态。当用户与第二个交互时,这将进入恢复状态,第一个将进入暂停状态。这就是为什么不需要修改活动生命周期,然后在新的 SDK 中状态和以前一样。但请记住,在多窗口模式开启时,暂停状态下的活动应继续,不要限制应用的用户体验。

配置

开发者可以通过在应用的清单文件内部添加新的属性,选择设置活动支持多窗口或画中画模式。新属性如下:

android:resizeableActivity=["true" | "false"]
android:supportsPictureInPicture=["true" | "false"]

它们的默认值是真的,所以如果我们的应用以安卓为目标,并且我们希望支持多窗口或画中画模式,就没有必要指定它们。画中画模式被认为是多窗口模式上的特例:那么,只有android:resizableActivity设置为true才考虑其属性。

这些属性可以放在清单文件中的<activity><application>节点中,如以下代码片段所示:

<activity
    android:name=".BuildingLayoutActivity"
    android:label="@string/app_name"
    android:resizeableActivity="true"
    android:supportsPictureInPicture="true"
    android:theme="@style/AppTheme.NoActionBar">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" 
        />
    </intent-filter>
</activity>

开发人员还可以在清单文件中添加更多的配置信息,以便在这些特定的新模式中定义所需的行为。为此,我们可以向<activity>节点添加一个新节点,称为<layout>。这个新节点支持四个属性,如下所示:

  • defaultWidth:自由模式下活动的默认宽度
  • defaultHeight:自由模式下活动的默认高度
  • gravity:活动首次以自由形式显示在屏幕上时的重力
  • minimalSize:这指定了在分屏和自由形式模式下用于活动的最小期望高度或宽度

因此,清单文件中的前一个活动声明如下:

<activity
    android:name=".MyActivity"
    android:label="@string/app_name"
    android:resizeableActivity="true"
    android:supportsPictureInPicture="true"
    android:theme="@style/AppTheme.NoActionBar">
    <layout
        android:defaultHeight="450dp"
        android:defaultWidth="550dp"
        android:gravity="top|end"
        android:minimalSize="400dp" />
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" 
        />
    </intent-filter>
</activity>

管理

新的 SDK 为Activity类提供了新的方法来知道模式之一是否被启用,以及处理不同状态之间的切换。这些方法如下:

  • Activity.isMultiWindow():此返回活动当前是否处于多窗口模式。
  • Activity.inPictureInPicture():此返回活动当前是否处于画中画模式。如前所述,这是多窗口模式的特例;所以,如果这是返回true,那么Activity.isMultiWindow()方法就是返回true
  • Activity.onMultiWindowChanged():这个是活动进入或离开多窗口模式时调用的新回调。
  • Activity.onPictureInPictureChanged():这个是活动进入或离开画中画模式时调用的新回调。

也为Fragment类定义了具有相同签名的方法,以便为该组件提供相同的灵活性。

开发人员还可以在这些特定模式中的一种模式下开始新的活动。这可以通过使用为此目的添加的新意图标志来实现;这是Intent.FLAG_ACTIVITY_LAUNCH_TO_ADJACENT,可以通过以下方式使用:

Intent intent = new Intent();
intent.setClass(this, MyActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_LAUNCH_TO_ADJACENT);
// Other settings here...
startActivity(intent);

这个的效果取决于屏幕上活动的当前状态:

  • 分割模式激活:创建活动并放置在旧活动旁边,它们共享屏幕。如果除了启用多窗口模式(也启用自由形式模式)之外,我们还可以使用ActivityOptions.setLaunchBounds()方法为两个定义的尺寸或全屏(传递空对象而不是Rect对象)指定初始尺寸,如下所示:

    java Intent intent = new Intent(); intent.setClass(this, MyActivity. class); intent.setFlags(Intent.FLAG_ACTIVITY_LAUNCH_TO_ADJACENT); // Other settings here... Rect bounds = new Rect(500, 300, 100, 0); ActivityOptions options = ActivityOptions.makeBasic(); options.setLaunchBounds(bounds); startActivity(intent, options.toBundle());

  • 分割模式未激活:标志无效,活动全屏打开。

拖放

如所述,新的多窗口模式允许拖放功能在共享屏幕的两个活动之间传递视图。这可以通过使用以下新方法来实现,这些新方法是专门为此功能添加的。我们需要使用Activity.requestDropPermissions()方法请求权限来开始拖放手势,然后获取与我们想要传递的DragEvent对象相关联的DropPermission对象。完成后,应调用View.startDragAndDrop()方法,将View.DRAG_FLAG_GLOBAL标志作为参数传递,以启用多个应用之间的拖放功能。

绩效影响

从性能角度来看,所有这些如何改变系统的行为?屏幕上暂停的活动与之前创建最终帧的过程一致。想想一个对话框覆盖的可见活动:它仍然在屏幕上,当内存问题发生时,它不会被系统杀死。然而,在多窗口模式的情况下,如前所述,活动需要继续做它在与其他活动交互之前正在做的事情。因此,系统将不得不同时处理两个视图层次结构,从而需要更大的努力来准备每一帧。如果我们计划启用这种新模式,我们需要更加小心地创建活动布局。因此,密切关注下一个最佳实践部分甚至下一个部分中表达的概念将会很好。

最佳实践

我们将解释一些有用的方法来直接在代码中实现之前设定的目标,以尽可能地限制应用滞后的原因,探索如何减少我们视图的透支,如何展平我们的布局,以及如何改善用户体验——特别是常见的情况,以及如何适当地开发我们自己的自定义视图和布局来构建高性能 ui。

提供布局概述

每次调用方法或使用LayoutInflater对象膨胀视图时,相关的布局 XML 文件都会被加载和解析,并且每个大写的 XML 节点都对应于一个必须由系统实例化的View对象,该对象将成为所有ActivityFragment生命周期的用户界面层次结构的一部分。这会影响应用使用期间的内存分配。让我们来了解一下安卓平台 UI 系统的关键概念。

如上所述,布局资源中每个大写的 XML 节点都将使用其名称和属性进行实例化。ViewGroup类定义了一种特殊的视图,可以将其他ViewViewGroup类作为一个容器来管理,描述了如何测量和定位子视图。因此,我们将布局称为扩展ViewGroup类的每个类。安卓平台提供了不同的ViewGroup子类用于我们的布局。以下是主要直接子类的简要概述,通常在构建布局 XML 资源文件时使用,只是为了解释它们如何管理嵌套视图:

  • LinearLayout:每个子对象分别在水平或垂直状态下,被画在先前添加的一行或一列旁边。
  • RelativeLayout:每个子视图相对于其他兄弟视图或父视图进行定位。
  • FrameLayout:这个用来屏蔽一个屏幕区域,管理一堆视图,上面画着最近添加的视图。
  • AbsoluteLayout:这个因为灵活性差,在 API 级被弃用。事实上,您必须提供确切的位置(通过指定其所有子项的 xy 坐标)。它唯一的直接子类是WebView
  • GridLayout:这个把它的孩子放在一个格子里,所以它的使用仅限于某些你应该把孩子放在细胞里的情况。

分级布局管理

让我们对每次系统被要求绘制布局时发生的情况有一个概述。该过程由两个自上而下的后续步骤组成:

  • 测量:
    • 根布局测量自身
    • 根布局要求它的所有子布局测量它们自己
    • 任何子布局都需要递归地对其子布局执行相同的操作,直到层次结构结束
  • 定位:
    • 当布局中的所有视图都存储了自己的度量时,根布局会定位其所有子视图
    • 任何子布局都需要递归地对其子布局执行相同的操作,直到层次结构结束

每当View属性发生变化时(如ImageView的图像或TextView的文本或外观),视图本身调用View.invalidate()方法,该方法以自下而上的方式传播其请求,直到根布局:前面的过程可以一次又一次地重复,因为视图需要再次测量自己(例如,只是为了更改文本)。这会影响绘制用户界面的加载时间。层次结构越复杂,用户界面加载越慢。因此开发尽可能平坦的布局非常重要。

AbsoluteLayout不再使用,FrameLayoutGridLayout有自己的特定用途,LinearLayoutRelativeLayout可以互换:这意味着开发者可以选择使用其中一个。但两者各有优缺点。当您开发一个简单的布局时,例如在图 8 中,您可以选择使用不同类型的方法来构建布局创建:

Hierarchical layout management

图 8:布局示例

  • The first one is based on LinearLayout and it's good for readability but bad for performance, as you need to nest LinearLayout every time there is a change of orientation in positioning the children:

    ```java <?xml version="1.0" encoding="utf-8"?>

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:src="@mipmap/ic_launcher" />
    
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="TextView" />
    
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
    
        <ImageButton
            android:id="@+id/imagebutton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ 
              common_ic_googleplayservices" />
        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Button" />
    </LinearLayout>
    

    ```

    该布局的视图层次如图图 9 :

    Hierarchical layout management

    图 9:使用线性布局构建的视图层次示例

  • The second one is based on RelativeLayout and in this particular case you don't need to nest any other ViewGroup, as every child position can be related to others or to the parent:

    ```java <?xml version="1.0" encoding="utf-8"?>

    <ImageView 
        android:id="@+id/imageview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:src="@mipmap/ic_launcher" />
    
    <TextView 
        android:id="@+id/textview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/imageview"
        android:layout_centerHorizontal="true"
        android:text="TextView" />
    
    <ImageButton 
        android:id="@+id/imagebutton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/textview"
        android:layout_weight="1"
        android:src="@drawable 
          /common_ic_googleplayservices" />
    
    <Button 
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_below="@id/textview"
        android:layout_toRightOf="@id/imagebutton"
        android:text="Button" />
    

    ```

    该备选布局的层次结构如图 10 所示:

    Hierarchical layout management

    图 10:使用 RelativeLayout 构建的视图层次结构示例

对比两种方式,很容易看出第一种情况下三个层次有六个视图,第二种情况下只有两个层次有五个视图。

典型的情况是混合方法,因为不总是可以相对于其他方法来定位视图。

为了在创建各种布局的同时实现性能目标,并避免过度绘制,层次结构应该尽可能平坦,以便系统在需要时在最短的时间内再次绘制每个视图。因此,建议尽可能使用相对布局,而不是线性布局。

在长时间的应用开发过程中,一个常见的坏方法是在删除不再需要的视图后,在我们的 XML 文件中留下冗余的布局。这白白增加了视图层次结构的复杂性。正如第 2 章高效调试以及本章后面几页中所讨论的,使用 LINT 和层次查看器可以方便地避免这种情况。

不幸的是,最常用的视图组是线性布局,因为它很容易理解和管理。所以,新的安卓开发者首先会接触它。为此,谷歌决定提供一个新的 ViewGroup,从 Android 4.0 冰淇淋三明治开始,如果使用正确,在处理网格时,可以减少特定情况下的冗余。我们在谈论交通拥堵。显然,可以使用 LinearLayouts 创建网格,但是最终的布局至少有三个层次。它也可以使用只有两个层次的相对布局来创建,但是最终的布局不是那么容易管理,视图之间有太多的引用。网格布局通过定义自己的行和列以及单元格来管理空间。下面的 XML 布局显示了如何使用网格布局创建与图 11 中相同的布局:

Hierarchical layout management

图 11:使用网格布局构建的视图层次示例

<?xml version="1.0" encoding="utf-8"?>
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:columnCount="2"
    android:orientation="vertical">

    <ImageView android:id="@+id/imageview"
        android:layout_columnSpan="2"
        android:layout_gravity="center_horizontal"
        android:src="@mipmap/ic_launcher" />

    <TextView android:id="@+id/textview"
        android:layout_columnSpan="2"
        android:layout_gravity="center_horizontal"
        android:text="TextView" />

    <ImageButton android:id="@+id/imagebutton"
        android:layout_column="0"
        android:layout_row="2"
        android:src="@drawable/common_ic_googleplayservices" />

    <Button android:id="@+id/button"
        android:layout_column="1"
        android:layout_row="2"
        android:text="Button" />
</GridLayout>

可以注意到,如果你想让android:layout_heightandroid:layout_width标记属性成为LayoutParams.WRAP_CONTENT,就不需要指定它们,因为这是两者的默认值。GridLayoutLinearLayout非常相似,因此转换起来非常简单。

重用布局

安卓软件开发工具包提供了一个有用的标签,当你想在其他布局中重用你的用户界面的一部分,或者当你想在不同的设备配置中只改变那部分用户界面时,可以在特定的情况下使用。这个<include/>标签允许您添加另一个布局文件,只需指定它的引用标识。如果您想要重用上一个示例的标题,只需创建如下所示的可重用布局 XML 文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <ImageView android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:src="@mipmap/ic_launcher" />

    <TextView android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="TextView" />
</LinearLayout>

然后将<include/>标签放入布局中您想要的位置,替换导出的视图:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <include layout="@layout/merge_layout" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <ImageButton
            android:id="@+id/imagebutton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@id/textview"
            android:src="@drawable/common_ic_googleplayservices" />

        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Button" />
    </LinearLayout>
</LinearLayout>

这样,对于不同的配置,您不需要在所有布局中复制/粘贴相同的视图;您只需为所需的配置定义@layout/content_building_layout文件,就可以在每个所需的布局中进行。但是这样做,您可以通过添加一个ViewGroup作为可重用布局的根节点来引入布局冗余,就像前面的例子一样。其视图层次与图 9 相同,三级六视图。这就是为什么安卓软件开发工具包提供了另一个有用的标签,有助于消除冗余布局,保持更平坦的层次结构。只需用<merge />标签替换可重用的根布局。可重用布局如下:

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:src="@mipmap/ic_launcher" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="TextView" />
</merge>

这样,整个最终布局具有两级层次结构,没有冗余布局,因为系统将<merge />标签中的视图直接包含在其他视图中,而不是包含在<include />标签中。实际上,对应的布局层次结构与图 10 相同。

在处理这个标签时,你需要记住它有两个主要的限制:

  • 它只能用作 XML 布局文件中的根
  • 每次调用LayoutInflater.inflate()方法时,您必须提供一个作为父视图的视图并将其附加到该视图上:

    java LayoutInflater.from(parent.getContext()).inflate(R.layout. merge_layout, parent, true);

查看存根

可以将ViewStub类添加为布局层次结构中指定布局引用的节点,但是不会为其绘制视图,直到在运行时使用ViewStub.inflate()View.setVisibility()方法对其布局进行膨胀:

<ViewStub xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/viewstub"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom"
    android:inflatedId="@+id/panel_import"
    android:layout="@layout/viewstub_layout" />

在运行时调用以下方法之前,ViewStub指向的布局不会膨胀:

((ViewStub) findViewById(R.id.viewstub)).setVisibility(View.VISIBLE);
// or
View newView = ((ViewStub) findViewById(R.id.viewstub)).inflate();

膨胀的布局取代了层次结构中的ViewStub,并且ViewStub不再可用。上述方法调用此方法后,ViewStub无法再访问;而是使用android:inflatedId属性中的 ID。

这个类很有用,尤其是在处理复杂的布局层次结构时,但是您可以将一些视图的加载推迟到以后需要的时候,这样可以减少第一次加载时间,并从不必要的分配中释放内存。

调整视图并查看回收

有一个特殊的ViewGroup子类需要一个Adapter类来管理它的所有孩子:这个类叫做AdapterViewAdapterView常用的专业有:

  • ListView
  • ExpandableListView
  • GridView
  • Gallery
  • Spinner
  • StackView

Adapter类负责定义AdapterView的子视图数量,并在其Adapter.getView()方法中膨胀每个子视图,而AdapterView定义子视图在屏幕上的位置以及对用户交互的反应。

根据开发人员选择如何处理模型,平台提供了不同的Adapter实现:

  • ArrayAdapter:使用将toString()方法结果映射到每一行
  • CursorAdapter:用于处理数据库中的数据
  • SimpleAdapter:使用绑定复选框、文本视图和图像视图

其中的每一个都扩展了BaseAdapter,这也被广泛用于创建自定义适配器。以下是BaseAdapter实现的一个例子:

public class SampleObjectAdapter extends BaseAdapter {
    private SampleObject[] sampleObjects;

    public SampleObjectAdapter(SampleObject[] sampleObjects) {
        this.sampleObjects = sampleObjects;
    }

    @Override
    public int getCount() {
        return sampleObjects.length;
    }

    @Override
    public SampleObject getItem(int position) {
        return sampleObjects[position];
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
// Non optimized code: this executionis slow and we want it to be
//faster
        convertView = LayoutInflater.from(parent.getContext()) .inflate(R.layout.adapter_sampleobject, parent, false);
        SampleObject sampleObject = getItem(position);
        ImageView icon = (ImageView) convertView.findViewById(R.id.icon);
        TextView title = (TextView) convertView.findViewById(R.id.title);
        TextView description = (TextView) convertView.findViewById(R.id.description);
        icon.setImageResource(sampleObject.getIcon());
        title.setText(sampleObject.getTitle());
        description.setText(sampleObject.getDescription());
        return convertView;
    }
}

描述每行的布局如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">

    <ImageView android:id="@+id/icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView android:id="@+id/title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/icon" />

    <TextView android:id="@+id/description"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/title"
        android:layout_toRightOf="@id/icon" />
</RelativeLayout>

要使用此Adapter,只需按以下方式将其设置为ListView:

ListView listview = (ListView) findViewById(R.id.listview);
listview.setAdapter(new SampleObjectAdapter(sampleObjects));

最常见的用法是用于ListView。让我们来看看当用户滚动一个ListView时会发生什么;对于每个需要添加的新行,都会调用Adapter.getView()方法。一个新的视图被膨胀,并且行布局的每个视图每次都用View.findViewById()方法引用。这些操作只能由主线程执行,因为它是唯一可以处理用户界面的线程。这影响了运行时的计算,并经常导致滞后滚动,降低性能。然后,行布局层次结构的复杂性可能会涉及并强调这种行为。

视图持有者模式

为了避免对Adapter.getView()内部的View.findViewById()方法的这种计算量大的调用,使用视图保持器设计模式是一个很好的实践。

A ViewHolder是一个静态类,目的是存储布局组件视图,使其可用于后续调用;相同的视图被重用,不需要为布局的每个视图调用View.findViewById()方法。

之前的SampleObjectAdapter变成如下:

@Override
public View getView(int position, View convertView, ViewGroup parent)
{
    SampleObjectViewHolder viewHolder;
    if (convertView == null) {
        convertView = LayoutInflater.from(parent.getContext()) .inflate(R.layout.adapter_sampleobject, parent, false);
        viewHolder = new SampleObjectViewHolder();
        viewHolder.icon = (ImageView) convertView.findViewById(R.id.icon);
        viewHolder.title = (TextView) convertView.findViewById(R.id.title);
        viewHolder.description = (TextView) convertView.findViewById(R.id.description);
        convertView.setTag(viewHolder);
    } else {
        viewHolder = (SampleObjectViewHolder) convertView.getTag();
    }
    SampleObject sampleObject = getItem(position);
    viewHolder.icon.setImageResource(sampleObject.getIcon());
    viewHolder.title.setText(sampleObject.getTitle());
    viewHolder.description.setText(sampleObject.getDescription());
    return convertView;
}

static class SampleObjectViewHolder {
    TextView title;
    TextView description;
    ImageView icon;
}

这是可能的,因为Adapter.getView()方法提供一个旧的参考视图作为convertView参数,只是为了重复使用。这就是神奇之处:当它为空时,一个视图被膨胀,每个包含的视图都存储在ViewHolder对象中以备后用,并且ViewHolder对象被设置为刚刚初始化的convertView的标签。这样,当它不为空时,Adapter类给我们相同的前一个实例,这样我们就可以从convertView中检索ViewHolder并使用它的属性视图。

类型

在处理BaseAdapter时,强烈建议使用ViewHolder模式,以避免频繁调用View.findViewById()方法,这可能会影响运行时的计算。

模式的使用由开发者决定;新的安卓开发者多年来倾向于不使用它,这增加了安卓平台性能的坏名声,因为滚动 a ListView或 a GridView时会出现滞后。这就是为什么谷歌引入了一个新的视图来创建列表和网格,管理子视图本身的回收,因此得名RecyclerView;它可以从安卓 2.1éclair 开始使用,因为它在支持包库 v7 中可用。在使用这个新的高度灵活的对象时,开发人员不能跳过使用ViewHolder对象。

在这两种情况下,为行布局中的ImageView显示正确尺寸的图像作为占位符,而不是它们的原始尺寸,以避免中央处理器和图形处理器的处理非常重要,这通常会导致OutOfMemoryError

从计算的角度来看,这种模式不足以创建流畅的应用;如前所述,只有主线程负责接触视图和处理 UI。此外,每个处理任务都应该在工作线程中执行,以便让主线程快速访问视图。阅读第 5 章多线程了解更多关于此主题的信息。

自定义视图和布局

在我们的 UI 应用开发中,我们经常面临缺少具有布局所需功能的视图,或者我们需要从头开始创建具有一些伟大功能的视图。幸运的是,安卓平台让我们开发了各种视图,让我们可以构建所需的用户界面。这样做有很多自由度,所以如果你对如何开发自定义视图不够小心,你可能会损坏内存和 GPU,带来灾难性的结果。根据我们到目前为止所说的,让我们了解一个视图在 Android 中是如何工作的,它是如何被测量和绘制的,以及如何优化这个过程。

尽管您可以向自定义视图添加尽可能多的属性来改善其外观,但最重要的是如何在屏幕上绘制一切。有两个主要选项可以做到这一点:

  • 您可以用所有需要的视图包装一个布局,以获得一个可重用的对象,其中每个持有的视图都由视图层次结构处理。不需要指定绘制什么和如何绘制,只需要一个经典的布局,根据需要排列所需的视图。
  • 您可以创建自己的视图,指定要绘制什么以及如何绘制,覆盖每次调用View.invalidate()方法使视图无效时执行的View.onDraw()方法,该方法通知系统需要再次绘制视图。

使用第二种方法,您将处理两个要绘制的主要对象:

  • Canvas:这是画东西的物体。用这个你可以指定画什么;一个Canvas对象可以画什么,由它上面调用的方法来表示。这些是主要的Canvas绘画方法:
    • drawARGB()
    • drawArc()
    • drawBitmap()
    • drawCircle()
    • drawColor()
    • drawLine()
    • drawOval()
    • drawPaint()
    • drawPath()
    • drawPicture()
    • drawPoints()
    • drawRect()
    • drawText()
  • Paint:这是用来告诉Canvas怎么画即将要画的东西的对象。以下是用于更改对象属性的一些Paint方法:
    • setARGB()
    • setAlpha()
    • setColor()
    • setLetterSpacing()
    • setShader()
    • setStrikeThruText()
    • setTextAlign()
    • setTextSize()
    • setTypeFace()
    • setUnderlineText()

当您覆盖View.onDraw()方法时,您将不得不使用Canvas对象作为该方法的参数,以使您的绘图出现在屏幕上(或您的视图边界中)。用于定制图纸的Paint对象需要单独处理。

每个视图都需要能够添加到ViewGroups中,该视图负责在测量完孩子后放置他们的孩子。然后,告诉父视图哪个尺寸有视图的方法是View.onMeasure()方法。这是定制视图开发中至关重要的一步,因为每个视图都必须有自己的宽度和高度;事实上,忘记调用View.onMeasure()内部的View.setMeasuredDimension()会导致抛出异常。

每次由于视图的边界被改变或由于它需要比原来更多或更少的空间而需要再次测量视图时,您需要调用View.requestLayout()方法:它要求父级再次计算其所有子级的位置并再次重新绘制它们,而不是仅仅使视图本身无效。这相当于整个视图层次结构的失效。如前所述,这可能非常昂贵,应尽可能避免。

得益于平台的功能,定制视图创建可以带来真正有趣的结果,但所有这些自由都必须得到控制,最重要的是,要有度量。一个很好的做法是,仅用布局中的视图来检查图形处理器的性能,以此来验证您的视图计时,然后,在更广泛的背景下,当它与其他视图站在一起时,控制它的行为。

了解了的工作原理后,让我们确定并分类开发人员在开发自定义视图时可能出现的性能错误:

  • 不需要时刷新视图图形
  • 绘制不可见的像素:这就是我们以前所说的透支
  • 在绘图过程中,通过执行不必要的操作来消耗内存资源

每一个都可以阻止 GPU 达到 60 FPS 的目标。让我们更深入地探讨它们:

  • View invalidation is widely used among newcomers just because this is the fastest way to have a refreshed and updated view at any time.

    类型

    在开发自定义视图时,请注意不要调用不必要的方法,这些方法会一次又一次地强制重新绘制整个层次结构,从而消耗宝贵的框架绘制周期。始终检查对View.invalidate()View.requestLayout()的调用是在何时何地进行的,只是因为这会影响整个 UI,降低 GPU 及其帧率。

  • To avoid overdraw in a custom view, you could use a Canvas API that lets you draw just a desired portion of the custom view. This can be very helpful while designing a stack view or any other view with overlapping portions. The API we are referring to is the Canvas.clipRect() method. For example, if your view needs to draw multiple overlapping objects on the screen, our goal is to properly clip each view to avoid unnecessary overdraw and draw just the visible part of each one of them.

    例如,图 12 显示了不需要完全绘制重叠卡片的堆叠视图:

    Custom views and layouts

    图 12:带有重叠部分的定制视图示例

    下面的代码片段显示了如何避免透支:

    ```java @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); for (int i = 0; i < cards.length; i++) {

        // Calculate the horizontal position of the beginning 
        // of the card
        left = calculateHorizontalSpacing(i, cards[i]);
    
        // Calculate the vertical position of the beginning of 
        // the card
        top = calculateVerticalSpacing(i, cards[i]);
    
        // Save the canvas
        canvas.save();
    
        // Specify what is the area to be drawn
        canvas.clipRect(left, top, visibleWidth, visibleHeight);
    
        // Draw the card: only the selected portion of the view // will be drawn
        drawCard(canvas, cards[i], left, top);
    
        //Resore the canvas to go on
        canvas.restore();
    }
    

    } ```

  • In our View.onDraw() method implementation, we shouldn't place any allocation, nor in any method called by View.onDraw(). This is because, when an allocation is done inside that method, the object needs to be created and initialized. Then, when the execution of View.onDraw() is over, the garbage collector frees memory because no one is using that. Furthermore, the view is redrawing 60 times a second if it's animated. Hence, the importance of avoiding allocations in View.onDraw() method.

    类型

    切勿在View.onDraw()方法内部(或其调用的其他方法内部)分配对象,以免加重该方法的执行负担,该方法在视图生命周期内可以多次调用;垃圾收集器可能会释放内存太多次,导致口吃。最好在第一次创建视图时实例化它们。

屏幕缩放

新的 Android N 预览版引入了一个特殊的可访问性功能,如果我们不遵守前面介绍的最佳实践,这可能会给我们的应用带来压力。我们说的是显示尺寸,可以从设备设置可及性部分内部进行更改,如图图 13 :

Screen zoom

图 13:辅助功能中的显示大小设置

当用户更改设置时,会显示一个预览,看起来像图 14 :

Screen zoom

图 14:默认和最大尺寸的显示尺寸变化效果

现在让我们快速浏览一下当用户在设备上设置这个新功能时会发生什么。如果应用是使用新的 Android N 版本作为目标进行编译的,则应用进程将由典型的运行时更改框架进行通知。否则,所有的进程都被终止,活动被重新创建,就像改变方向一样。但娱乐是用不同的屏幕宽度,用 dp 表示。出于这个原因,我们应该测试这个特定的用例,以检查我们的应用性能没有受到这个新特性的影响。

这是不使用 px 测量和选择更适合的 dp 测量的进一步激励。

除此之外,如第 6 章联网所述,我们应该改变应用的任何密度相关行为,例如图像格式缓存或对后端的请求。

调试工具

我们现在知道创建灵活高效的 UI 背后的问题以及如何解决这些问题。但是,我们怎么知道我们做得好不好呢?而且,如何衡量自己努力的产出质量?让我们来看一下各种工具,您不仅可以使用它们来衡量我们的产品,还可以发现其他问题,修复它们,并在应用的整个生命周期中提高其性能。

设计视图

在开发过程中,创建 XML 布局文件是一项被低估的活动:如果在开发步骤中布局设计得很好,应用就不需要特别努力来提高性能。在编写 XML 文件时,集成开发环境允许我们在布局编辑器中以预览模式观看我们正在设计的内容。这包含文本设计视图,如图 15 :

The Design view

图 15:设计视图

设计视图包含一个名为组件树的特殊视图,它显示了我们正在制作的视图层次结构。在图 16 中,层次视图对应于图 19 中的层次视图。这是一种实用的视觉方式来评估我们的布局深度:

The Design view

图 16:设计视图中的视图层次预览

正如本章中所讨论的一样,我们的目标是展平层次深度以限制计算,并尽可能快地创建要在屏幕上显示的视图。

设计视图是突出我们可以在开发过程中限制层次深度的案例的正确工具;如果我们在分析和开发过程中注意细节,我们可以显著减少恢复应用性能损失的工作量。

层级查看器

分析视图层次结构、调试用户界面和分析我们的布局的主要工具是层次结构查看器。它在安卓设备监视器中,提供了一个完整的可视化工具。如图 17 所示,该工具包含许多视图来帮助我们分析用户界面:

Hierarchy Viewer

图 17:层次查看器

树形视图

中心面板包含树状图,其中有视图层次的缩放部分。可以选择每个视图来打开详细信息,其中包含与所选视图相关的以下信息以及所有其他层次较低的信息:

  • 包含的视图数量
  • 测量时间
  • 布局时间
  • 绘制时间

这意味着树形视图最左边视图中的时间告诉我们整个 UI 创建过程需要多长时间,因为这是我们布局的根。这是必须始终考虑的参数;如前几页所述,我们的目标是将该值保持在 16 毫秒以下图 18 显示了选择了图像视图树形视图的示例:

Tree View

图 18:层次查看器中的树形视图

类型

检查布局创建时间应该是每次测试过程的一部分。测量、布局和绘制步骤最多必须在 16.67 毫秒内完成。等级查看器中的树形视图帮助我们测量计时。

使用树形视图,我们布局的深度很简单:这非常有助于理解我们在哪里重载了我们活动的布局,以及我们可能会在哪里意外添加透支。

查看属性

左侧面板包含两个视图:

  • Windows :这里可以找到所有连接的设备和仿真器的列表,以及所有可调试进程的附属列表,选中的那个用粗体显示。可以选择其中一个,点击图标后,相关视图加载到树形视图中,整个面板切换到视图属性
  • View Properties: This contains a list of view properties useful to debug the view:

    View properties

    图 19:在层次查看器中查看属性

树概述

在安卓设备监视器的右侧,树形视图整体显示视图层次,位于树形视图中的缩放部分灰显以便高亮显示。这个视图向我们展示了我们构建的视图层次结构的复杂性。参见图 20 了解树形图的外观:

Tree overview

图 20:层次查看器中的树概述

布局视图

树形视图的下,有一个名为布局视图的视图,它显示了模拟设备屏幕上显示的布局的每个视图所覆盖的区域,因此您可以在树形视图中选择一个特定的视图,并简化对布局中单个视图的搜索。图 21 显示布局视图,按照本章使用的例子:

Layout View

图 21:层次查看器中的布局视图

在设备工具上

当您想要调试和配置您的用户界面时,在真实设备上这样做很重要。安卓系统在开发者选项设置中提供了很多可以在设备上使用的灵活工具。

调试 GPU 透支

为了调试设备上的透支,安卓提供了一个有用的工具,可以在开发者选项中启用。在硬件加速渲染部分,有调试 GPU 透支选项。启用后,屏幕会根据屏幕上每一个像素的透支级别,通过添加覆盖颜色(如果有透支)进行不同的着色,如下所示:

  • 本色:无透支
  • 蓝色 : 1X 透支
  • 绿色 : 2X 透支
  • 粉色 : 3X 透支
  • 红色 : 4X+透支

例,来看看图 22 。左边的屏幕没有优化,右边的优化了。所以,这个工具确实有助于发现我们布局中的透支。作为开发人员,我们的目标是尽可能减少覆盖,以减少透支并提高 GPU 计时和渲染速度。要完成的主要操作是检查布局的背景和相对布局中的重叠视图:

Debugging GPU overdraw

图 22:优化前后的透支比较

配置图形处理器渲染

该工具向开发人员显示帧渲染操作需要多长时间,定义它们是否在 16 毫秒的限制内完成。从渲染的角度来看,这是对我们的应用进行基准测试的好方法。

不管名字如何,所有观察到的进程都是由中央处理器执行的:在中央处理器提交渲染操作之后,图形处理器以异步方式工作。

要启用它,只需在设备的开发者设置监控部分中选择轮廓图形处理器渲染。有两种选择:

  • 在屏幕上显示为条形:这显示了屏幕上的结果,快速浏览一下我们的应用相对于每帧 16 毫秒目标的渲染性能是很有用的
  • 在 adb shell dumpsys gfxinfo 中:这存储了要使用adb命令读取的基准结果

图 23 显示了是如何在屏幕上显示的。每个竖线对应于一帧在屏幕上呈现的时间。每一条新线都在前一条线的右边。绿色的水平线表示 16 毫秒的目标:如果越过了这条线,就会有东西减缓我们的帧渲染操作:

Profile GPU rendering

图 23:图形处理器渲染工具

这个工具提供了更多关于渲染每一帧时会发生什么的信息。竖条分为四个彩色部分。它们中的每一个表示完成不同的子渲染操作所花费的时间,从下到上描述如下:

  • 蓝条-绘制:这个代表绘制视图所花费的时间。当View.onDraw()方法中需要太多工作时,这变得更长。
  • 紫色条–准备:这个代表准备和传输要在屏幕上显示的资源到渲染线程所花费的时间。
  • 红条–流程:这是处理 OpenGL 操作所花费的时间。
  • 橙色条–执行:这个是 CPU 等待 GPU 完成工作所花费的时间。当图形处理器过载时,时间会变长。

adb shell dumbsys方法有助于比较我们的优化结果,并证明我们做得好不好。使用以下命令调用时,结果会打印在终端中:

adb shell dumbsys gfxinfo <PACKAGE_NAME>

跟踪如下所示:

Applications Graphics Acceleration Info:
Uptime: 297209064 Realtime: 578485201

** Graphics info for pid 15111 [com.packtpub.androidhighperformanceprogramming] **

Recent DisplayList operations
 DrawRenderNode
 Save
 ClipRect
 DrawRoundRect
 RestoreToCount
 Save
 ClipRect
 Translate
 DrawText
 RestoreToCount
 DrawRenderNode
 Save
 ClipRect
 DrawRoundRect
 RestoreToCount
 Save
 ClipRect
 Translate
 DrawText
 RestoreToCount

Caches:
Current memory usage / total memory usage (bytes):
 TextureCache         30937728 / 75497472
 LayerCache                  0 / 50331648 (numLayers = 0)
 Garbage layers              0
 Active layers               0
 RenderBufferCache           0 /  8388608
 GradientCache               0 /  1048576
 PathCache                   0 / 33554432
 TessellationCache        2976 /  1048576
 TextDropShadowCache         0 /  6291456
 PatchCache                576 /   131072
 FontRenderer 0 A8     1048576 /  1048576
 FontRenderer 0 RGBA         0 /        0
 FontRenderer 0 total  1048576 /  1048576
Other:
 FboCache                    0 /        0
Total memory usage:
 31989856 bytes, 30.51 MB

Profile data in ms:
 com.packtpub.androidhighperformanceprogramming/com.packtpub.androidhighperformanceprogramming.BuildingLayoutActivity/android.view.ViewRootImpl@257c51f4 (visibility=0)
 Draw    Prepare Process Execute
 0.32    0.12    3.06    3.68
 0.37    0.45    2.64    0.42
 0.53    0.09    2.59    0.76
 0.33    0.22    2.59    0.42
 0.32    0.08    2.74    0.44
 0.34    0.20    2.58    0.40
 0.65    0.21    3.04    0.51
 0.36    0.61    2.80    0.41
 0.39    0.32    2.38    0.36
 0.45    0.11    2.78    0.37
 0.36    0.10    2.97    0.51
 0.48    0.49    6.95    0.75
 0.66    0.31    4.20    1.75
 0.30    0.17    2.84    1.22
 0.29    0.15    2.13    0.44
View hierarchy:
 com.packtpub.androidhighperformanceprogramming/com.packtpub.androidhighperformanceprogramming.BuildingLayoutActivity/android.view.ViewRootImpl@257c51f4
 26 views, 45.09 kB of display lists

Total ViewRootImpl: 1
Total Views:        26
Total DisplayList:  45.09 kB

这种渲染性能基准测试提供了比视觉基准测试更多的信息,例如显示列表操作、内存使用、每个渲染操作的确切时间(这将在视觉基准测试中显示为一个条),以及关于视图层次结构的信息。

安卓棉花糖(API Level 23)在之前的打印跟踪中添加了新的有用信息:

Stats since: 133708285948ns
Total frames rendered: 18
Janky frames: 1 (5.55%)
90th percentile: 17ms
95th percentile: 19ms
99th percentile: 22ms
Number Missed Vsync: 0
Number High input latency: 0
Number Slow UI thread: 1
Number Slow bitmap uploads: 1
Number Slow issue draw commands: 2

这更有效地解释了我们的应用帧渲染的真实性能。

安卓棉花糖中增加了另一个有用的高级功能,叫做 framestats 。它列出了详细的帧计时,并将数据添加到之前的打印中(行数已经减少,以限制使用的空间)。终端将列的名称添加为第一行,然后列出所有其他列的值,这样第一个值对应于第一个名称,第二个值对应于第二个名称,依此类推:

---PROFILEDATA---
Flags,IntendedVsync,Vsync,OldestInputEvent,NewestInputEvent,HandleInputStart,AnimationStart,PerformTraversalsStart,DrawStart,SyncQueued,SyncStart,IssueDrawCommandsStart,SwapBuffers,FrameCompleted,
0,133733327984,133849994646,9223372036854775807,0,133858052707,133858119755,133858280669,133858382079,133859178269,133859218497,133994699099,134289051517,134294121146,
1,133849994646,134283327962,9223372036854775807,0,134298506898,134298579812,134298753298,134301580193,134302094783,134302130821,134302130821,134307073077,134315631711,
0,135349994586,135349994586,9223372036854775807,0,135363372921,135363455055,135363522941,135363598369,135363991438,135364050104,135364221077,135367243259,135371662551,
---PROFILEDATA---

让我们解释一下这些价值观代表什么。每个时间戳都以纳秒为单位,添加的列如下:

  • Flags:如果是0,需要考虑与该行相关的帧时序;否则,就不应该。如果帧是正常性能的例外,它可以是非零的。
  • IntendedVsync:这是起点。如果用户界面线程被占用,它可以不同于Vsync值。
  • Vsync:VSYNC 的时间值。
  • OldestInputEvent:最早输入事件的时间戳。
  • NewestInputEvent:最新输入事件的时间戳。
  • HandleInputStart:向应用分派输入事件的时间戳。
  • AnimationStart:动画开始的时间戳。
  • PerformTrasversalsStart:减去DrawStart得到布局和测量时序的时间戳。
  • DrawStart:绘图开始的时间戳。
  • SyncQueued:同步请求发送到RenderThread的时间戳。
  • SyncStart:绘图同步开始的时间戳。
  • IssueDrawCommandsStart:GPU 开始绘图操作的时间戳。
  • SwapBuffers:前后缓冲区交换的时间。
  • FrameCompleted:帧完成的时间。

该数据报告时间戳,因此计时需要通过减去两个时间戳来计算。结果可以向我们显示关于渲染性能的重要信息。例如,如果IntendedVsyncVsync不同,则一帧丢失,并可能发生 jank。

这个新的dumbsys命令可以通过在终端上运行以下命令来执行:

adb shell dumbsys gfxinfo <PACKAGE_NAME> framestats

Systrace

Systrace 工具有助于分析渲染执行时间。它是安卓设备监视器的一部分,可以通过选择设备标签中的相关图标进行访问。之后,显示带有系统选项的对话框,如图图 24 :

Systrace

图 24: Systrace 选项

这个工具从要跟踪的设备上的所有进程收集信息,并将跟踪保存到一个 HTML 文件中,在该文件中,一个图形用户界面突出显示观察到的问题,提供关于如何修复它们的重要信息。

结果类似于图 25 中的情况。透视图分为三个主视图:上侧包含轨迹本身,下侧包含另一部分突出显示对象的细节,而右侧视图,称为 警报区域,包含当前轨迹中报告的警报摘要。主要的上半部分描述了关于内核的细节,包含了所有的 CPU 信息;关于 SurfaceFlinger 、这款安卓排字机的流程;然后是在信息收集期间活动的每一个进程,即使该进程是一个系统进程。每个进程都包含评估期间运行的每个线程的详细信息:

Systrace

图 25: Systrace 示例

让我们来了解一下如何分析痕迹:单个过程的每一个绘制的帧都在 Frames 行中用带圆圈的 F 表示,如图 26 :

  • 绿色框架表示它们没有问题
  • Yellow and red frames indicate the drawing time exceeded the 16 ms target, producing a lag:

    Systrace

    图 26:框架细节

每一个错误的 F 都是可以选择的,以查看事件的详细描述。以下是 Systrace 为红色框架报告的内容示例:

|

警报

|

调度延迟

| | --- | --- | | 运转 | 6.401 毫秒 | | 没有计划,但可以运行 | 16.546 毫秒 | | 不间断睡眠|醒来 | 19.402 毫秒 | | 睡着的 | 27.143 毫秒 | | 阻塞输入输出延迟 | 1.165 毫秒 | | 基本框架 |   | | 描述 | 制作此帧的工作被推迟了几毫秒,导致 jank。确保用户界面线程上的代码不会阻止其他线程上正在进行的工作,并且后台线程(例如,进行网络或位图加载)运行在android.os.Process#THREAD_PRIORITY_BACKGROUND或更低,因此它们不太可能中断用户界面线程。这些后台线程应该以 130 或更高的优先级出现在内核进程的调度部分。 |

如上所述,这个工具获取设备上运行的每个进程和线程的信息,但是如果我们想详细描述我们的应用执行的有限部分,以了解它在某个时间正在做什么工作,我们可以使用一个应用编程接口来告诉系统从哪里开始和结束跟踪。这个 API 可以从安卓果冻豆(API Level 18)上使用,它基于Trace类。只需调用静态方法来开始和结束跟踪,如下所示:

Trace.beginSection("Section name");
try {
    // your code
} finally {
    Trace.endSection();
}

这样,新的跟踪将包含一个新的行,其中包含您的部分的名称及其详细信息。

记得在同一个线程上调用Trace.beginSection()Trace.endSection()方法。

总结

在移动设备的当代理念中,应用是让用户访问我们的远程服务的主要方式,因此它应该是获得这些服务的主要手段。那么,用户感知我们的应用的方式是成功的根本途径,而它的用户体验和用户界面是成功的关键指标。因此,确保我们的应用呈现没有滞后是非常重要的。

我们在本章中所做的是了解设备如何渲染我们的应用,定义每帧 16 毫秒的目标,并概述硬件加速是安卓系统中主要的性能渲染改进。然后,我们分析了开发人员在构建应用用户界面时可能犯的主要错误,更详细地探索了如何通过展平层次视图、重用listview中的行视图以及定义开发自定义视图和布局的最佳实践来提高代码中的渲染速度。最后,我们浏览了平台提供的有用工具,以帮助我们找到改进优化并衡量我们的应用渲染性能。