原文:
zh.annas-archive.org/md5/09787EDC0EF698C9109E8B809C38277C
译者:飞龙
前言
在移动领域,性能高效的应用程序是成功的关键因素之一。如果应用运行缓慢,用户可能会放弃使用。学习如何构建在功能性和用户体验之间平衡速度和性能的应用可能是一个挑战;然而,现在比以往任何时候都更重要的是要找到这种平衡。
Android 高性能编程将使你思考如何从应用安装的任何硬件中榨取最大性能,从而你可以扩大影响并提高参与度。本书首先提供了对 Android 最新技术的介绍,以及性能在 Android 应用中的重要性。然后,我们将解释常规用于调试和剖析 Android 应用的 Android SDK 工具。我们还将学习一些高级主题,如构建布局、多线程、网络和安全性。电池寿命是应用程序中最大的瓶颈之一;本书将展示耗尽电池寿命的典型代码示例,如何预防这种情况,以及如何在各种情况下测量应用程序的电池消耗。
本书解释了构建优化和高效系统的技术,这些系统不会耗尽电池电量,导致内存泄漏,或随时间变慢。
本书内容涵盖
第一章,引言:为什么需要高性能?,对主题进行了介绍,包括当前 Android 技术的最新状态,以及性能在 Android 应用中的重要性。
第二章,高效调试,涵盖了 Android SDK(和一些外部工具)提供的常规用于调试和剖析 Android 应用的工具。
第三章,构建布局,将带你了解用于优化 Android 例程的技术,编写高效使用内存的应用程序,并解释从内存分配到垃圾回收的概念。
第四章,内存,提供了许多关于 UI 设计的见解,需要学习以创建高效的 UI,使其快速加载,不会给用户造成延迟感,并且能够高效更新。
第五章,多线程,解释了 Android 应用中所有不同的线程选项以及何时应用每种选项。一些高级技术,如 IPC,也将通过实际代码展示。
第六章,网络,展示了用于执行高效网络操作的技术,以及从服务器检索数据的技术,如指数回退或避免轮询。
第七章,安全,涵盖了保护安卓应用程序的技术,如何利用安卓原生提供的安全加密机制,以及如何获取关于连接的信息或只是通知连接变化。
第八章,优化电池消耗,提供了消耗电池寿命的典型代码示例,如何防止这种情况,以及如何在各种情况下从应用程序测量电池消耗;许多开发者在开发应用时不知道如何处理拍照或视频、处理预览和保存数据的行为。
第九章,安卓中的本地编码,这一章介绍了在安卓中本地代码和 C++的世界及其使用方法。
第十章,性能提示,帮助开发者在常见的编码情境中得到指导,错误的抉择可能会影响应用效率;这将是关于前几章未涉及主题的最佳实践指南。
你需要为这本书准备以下内容
你需要以下硬件来使用这本书:
-
运行 Windows、Linux 或 Mac OS X 的 PC/笔记本电脑
-
安卓手机。建议使用至少安装了安卓 5.0 的高端型号。
这本书适合谁
这个主题面向具有高级安卓知识,希望推动自己的知识并学习提高应用程序性能技巧的开发者。我们假设他们能够熟练使用整个安卓 SDK,并且已经这样做了很多年。他们还熟悉如 NDK 之类的框架,以使用对性能至关重要的本地代码。
约定
在这本书中,你会发现有多种文本样式区分不同类型的信息。以下是一些样式示例及其含义的解释。
文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 处理方式将如下显示:“如果你进入这个文件夹并调用adb
命令,你会在屏幕上看到可用选项的列表。”
一段代码的设置如下:
<resources>
<style name="Theme.NoBackground" parent="android:Theme">
<item name="android:windowBackground">@null</item>
</style>
</resources>
任何命令行输入或输出都会如下写出:
adb shell dumbsys gfxinfo <PACKAGE_NAME>
新术语和重要词汇会以粗体显示。你在屏幕上看到的词,例如菜单或对话框中的,会像这样出现在文本中:“为了在设备上调试过度绘制,安卓提供了一个有用的工具,可以在开发者选项中启用。”
注意
警告或重要提示会以这样的框形式出现。
提示
技巧和窍门会以这样的形式出现。
读者反馈
我们始终欢迎读者的反馈。让我们知道你对这本书的看法——你喜欢或不喜欢什么。读者的反馈对我们很重要,因为它帮助我们开发出你真正能从中获得最大收益的标题。
如果要给我们发送一般反馈,只需通过电子邮件<feedback@packtpub.com>
联系我们,并在邮件的主题中提及书籍的标题。
如果您在某个主题上有专业知识,并且有兴趣撰写或参与书籍编写,请查看我们位于www.packtpub.com/authors的作者指南。
客户支持
既然您已经拥有了 Packt 的一本书,我们有许多方法可以帮助您充分利用您的购买。
下载示例代码
您可以从您的账户www.packtpub.com
下载本书的示例代码文件。如果您在别处购买了这本书,可以访问www.packtpub.com/support
注册,我们会直接将文件通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
使用您的电子邮件地址和密码登录或注册我们的网站。
-
将鼠标悬停在顶部的支持标签上。
-
点击代码下载与勘误。
-
在搜索框中输入书籍的名称。
-
选择您要下载代码文件的书。
-
从下拉菜单中选择您购买本书的地方。
-
点击代码下载。
您还可以通过在 Packt Publishing 网站的书页上点击代码文件按钮来下载代码文件。通过在搜索框中输入书名可以访问此页面。请注意,您需要登录到您的 Packt 账户。
文件下载后,请确保您使用最新版本的软件解压或提取文件夹:
-
适用于 Windows 的 WinRAR / 7-Zip
-
适用于 Mac 的 Zipeg / iZip / UnRarX
-
适用于 Linux 的 7-Zip / PeaZip
本书代码捆绑包也托管在 GitHub 上,地址为github.com/PacktPublishing/Android-High-Performance-Programming
。我们还有其他丰富的书籍和视频代码捆绑包,可在github.com/PacktPublishing/
查看。请查看!
下载本书的色彩图像
我们还为您提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的色彩图像。色彩图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/AndroidHighPerformanceProgramming_ColorImages.pdf
下载此文件。
勘误
尽管我们已经竭尽全力确保内容的准确性,但错误仍然在所难免。如果您在我们的书中发现了一个错误——可能是文本或代码中的错误——如果您能向我们报告,我们将不胜感激。这样做可以避免其他读者产生困扰,并帮助我们在后续版本中改进这本书。如果您发现任何勘误信息,请通过访问www.packtpub.com/submit-errata
,选择您的书籍,点击Errata Submission Form链接,并输入您的勘误详情。一旦您的勘误信息被核实,您的提交将被接受,并且勘误信息将被上传到我们的网站或添加到该标题下的现有勘误列表中。
要查看之前提交的勘误信息,请访问www.packtpub.com/books/content/support
,并在搜索字段中输入书名。所需信息将在Errata部分出现。
盗版问题
在互联网上对版权材料的盗版是一个所有媒体都面临的持续问题。在 Packt,我们非常重视保护我们的版权和许可。如果您在互联网上以任何形式遇到我们作品的非法副本,请立即提供我们该位置地址或网站名称,以便我们可以寻求补救措施。
如果您怀疑有盗版材料,请通过<copyright@packtpub.com>
联系我们,并提供疑似盗版材料的链接。
我们感谢您帮助我们保护作者权益和我们为您提供有价值内容的能力。
问题反馈
如果您对这本书的任何方面有问题,可以通过<questions@packtpub.com>
联系我们,我们将尽力解决问题。
第一章:引言:为何要高绩效?
根据剑桥词典,性能的一个可接受的定义是:“一个人、机器等完成一项工作或活动的表现。”如果我们将其与“高”结合,可以定义为执行任务时的输出或效率。
软件中的高性能指的是开发者采用的策略,以创建能够高效执行流程的软件片段。当我们开发移动软件时,这影响到但不限于布局开发、能源和电池管理、安全问题、有效的多线程、编程模式和调试技术。
做事情与把事情做对之间存在很大差异。在一个有截止日期、预算和经理的现实世界中,软件工程师经常陷入技术债务。当系统在没有完整或不适当设计的情况下开发时,就会产生技术债务,将问题推向前而不是正确解决。这会产生滚雪球效应:在高级阶段,技术债务如此之高,以至于进一步开发成本非常高,这导致组织中的预算达到死点或天文数字的损失。
尽管截止日期有时无法避免,但在任何软件开发中采用有效的开发过程对于以合理的成本交付高质量产品至关重要。这也意味着开发技能在开发者中变得更加成熟,工程师可以开发的不仅仅是满足要求的软件,而是高效、健壮,并且可以在未来进一步扩展的软件(我们称之为“可维护性”)。
本书介绍了为安卓设备构建高性能软件的技术。
为何应用程序的性能对如此多人来说如此重要?
无论哪个行业,软件系统性能或质量的下降可能导致巨大的损失。今天的软件系统控制着我们的财务,控制着照顾我们健康或公共交通的机器。我们的生活几乎没有任何领域至少不是部分计算机化的。不仅是损失:在一个全球化和竞争激烈的世界中,生产低性能软件的公司很快就会被更高效和更便宜的竞争对手吞噬。
一段时间以来,软件开发中唯一使用的指标是“软件是否正确?它是否在执行它应该做的事情?”。在计算机系统时代的曙光时期,这种做法是有道理的,当时并不是每个流程都计算机化,我们还没有发展出软件工程的文化或质量控制的良好方法,以及团队组织等等。现在,每个人都要求更多。
图表是展示信息的绝佳方式。让我们分析一下智能手机的渗透率数据:
数据很明确。在 2008 年第四季度,世界上几乎所有地区的智能手机渗透率都低于 20%。如今,在 2015 年,大多数发达国家的渗透率接近 80%,而发展中国家接近 40%。预计到 2020 年,发达国家的渗透率将接近 100%,发展中国家的渗透率将超过 50%。有些国家的手机数量甚至超过了人口数量!
现在的移动用户不仅会在手机上检查电子邮件。有许多操作是在手机上完成的:娱乐业、银行业务和支付、旅游业和旅行、游戏……这让我们得出一个结论:软件不仅要正确无误,还必须是高效的。软件的失败将导致客户的不满,他们可能会选择使用性能更好的竞争对手产品。在极端情况下,性能不佳的软件可能导致我们的业务失去收入——想象一下一个无法进行支付流程的酒店预订应用程序。
手动测试和自动测试
自然会首先想到的一个问题是,测试在提高和改进应用程序性能方面起着核心作用。这在一定程度上是正确的,或者我们更愿意说:测试是一个为智能设计应用程序的好补充,但不是替代品。
如果我们只关注测试,主要有两种类型:手动测试和自动测试。与之前的情况一样,这两种测试是相互包含的,不应以牺牲另一种为代价来使用其中一种。手动测试涉及一个真实用户与一个应用程序及其一些定义的使用场景进行交互,但也有更多的自由意志和能力去离开预定义的测试路径,探索新的路径。
自动测试是开发者编写的,以确保应用程序在整个系统生命周期内的一致性。有几种不同类型:单元测试、集成测试或 UI 测试,读者对此应该很熟悉。良好的测试覆盖为新应用的变化提供了系统的健壮性,提高了对失败和性能问题的抵抗力。与之前的情况一样,我们不希望排除手动测试以支持自动测试,反之亦然(至少在机器能够通过图灵测试之前!)。
ANR 和软件中的延迟
ANR代表应用无响应,是安卓开发者面临的一系列噩梦之一。安卓操作系统会分析应用程序和线程的状态,当满足某些条件时会触发 ANR 对话框,阻止用户进行任何交互式体验。该对话框宣布应用程序停止响应,且不再有反应。用户可以选择是否关闭应用程序,或者继续等待直到应用程序再次变得响应(如果这种情况真的会发生):
什么原因会导致 ANR,我该如何避免它们?
Android 系统在两种不同情况下会触发 ANR:
-
当有事件在五秒内没有得到响应
-
如果一个 BroadcastReceiver 在执行后 10 秒仍在运行
这通常发生在UI 线程中执行操作时。通常,任何预期会耗时或操作消耗的操作都应该在单独的线程中执行,保持 UI 线程可用于用户交互,并且只在操作完成时通知 UI 线程。在第五章 多线程中,我们将展示一些多线程和线程通信的高级技术。也有不同的类可以在不同的线程中执行操作,每一个都有其自身的优缺点。通常,在开发应用程序时,请记住:ANR 对话框出现的频率与用户满意度成反比。
Android 架构
与任何其他开发框架一样,Android 定义了自己的架构和模块。Android 是一个基于 Linux 的操作系统,尽管 SDK 提供的众多抽象层几乎完全隐藏了 Linux 内核,实际上我们很少会直接在内核级别编程:
Dalvik 虚拟机
每个 Android 应用程序都在一个名为 Dalvik 的虚拟机中运行自己的进程。正如我们所见,程序通常是用 Java 编写的,然后编译成字节码。从字节码(.class
文件)之后,它们会被转换成 DEX 格式,通常使用 Android SDK 提供的特殊工具,名为dx。这种 DEX 格式更优化,与普通的 Java .class
文件相比,其内存占用更小,因为移动设备的计算能力不如桌面设备。这是通过对多个.class
文件的压缩和合并/优化来实现的。
注意
编码严格来说并非必须使用 Java,Android 同样允许在我们的应用程序中使用原生代码。因此,之前使用过的现有代码可以在这里复用。同样,在计算机视觉领域,有大量从 OpenCV 框架复用的代码。这是通过原生开发工具包(NDK)实现的,在第九章 Android 中的原生编码和第十章 性能优化技巧中探讨了这一点。
Dalvik 虚拟机还包括一些Java 虚拟机(JVM)的特性,比如垃圾回收(GC)。由于 GC 的非分代性质,它曾受到很多批评;它以让开发者抓狂而闻名。然而,自从 Android 2.3 以来,改进的并发垃圾回收器使得开发变得更加容易。
在 Dalvik 上运行的应用程序至少有 16MB 的总可用堆内存。这对于某些应用程序来说可能是一个真正的限制,因为我们可能需要处理大量的图像和音频资源。然而,像平板电脑或高端设备这样的新型设备具有更高的堆限制,允许使用高分辨率图形。我们预计由于移动硬件的快速发展,这种情况将在不久的将来得到改善。
内存管理
按内存的定义,在任何软件平台上它都是一种稀缺资源。但说到移动设备,这更是一个受限的资源。移动设备通常具有比其更大的同行更少的物理内存和处理能力,因此高效的内存管理对于提升用户体验和软件稳定性至关重要。
Dalvik 虚拟机与 Java 类似,会定期触发垃圾回收,但这并不意味着我们可以完全忽视内存管理。初级程序员常犯的错误之一就是产生内存泄漏。当内存中的对象无法再被运行中的代码访问时,就会发生内存泄漏。这些对象的大小可能差异很大(从整数到一个大位图或几个兆字节的机构),但一般来说,它们会影响软件的流畅性和完整性。我们可以使用自动化工具和框架来检测内存泄漏,同时应用一些编程技术来避免不必要地分配对象(同样重要的是,在不再需要时释放它们)。
安卓应用有一个最大可管理的 RAM 内存量。它因每个设备而异(是的,系统分化的另一个问题),可以通过在ActivityManager
上调用getMemoryClass()
函数来特别检查。早期设备的每个应用上限为 16MB。后来设备将其增加到 24MB 或 32MB,看到高达 48 或 64MB 的设备并不会令人惊讶。有几个因素促成了这一事实,比如屏幕尺寸。较大的屏幕通常意味着位图的分辨率更高;因此,随着它们的增加,内存需求也会增长。一些技术也可以绕过这个限制,比如使用 NDK 或向系统请求更大的堆。然而,这对于安卓应用来说被认为是拙劣的形式。
当一个进程启动时,它是由一个现有的或根进程Zygote分叉出来的。Zygote 每次系统启动时都会启动,并加载所有应用程序共有的资源。通过这种方式,安卓试图在应用程序之间共享所有公共资源,避免为相同的框架重复使用内存。
能耗
移动设备的电池容量有限,且不像标准电脑那样连接到永久电源。因此,高效使用电池和能源是生存的关键因素。如果你持续执行耗电操作或需要持续访问设备硬件,这将会影响用户体验,甚至可能导致应用程序被拒绝。
良好的能源管理需要对能源如何使用以及哪些操作可能迅速耗电有很好的了解。有一些工具和基准测试框架可以找出能源瓶颈和软件中能源消耗高于预期的部分。
移动消费电子产品,尤其是手机,由有限容量的电池供电。这意味着在这样的设备中,良好的能源管理至关重要。良好的能源管理需要对能源的使用地点和使用方式有很好的了解。为此,我们详细分析了最近一款手机 Openmoko Neo Freerunner 的功耗。我们不仅测量了整个系统的功耗,还精确地测量了设备主要硬件组件的功耗分解。我们为微基准测试以及一些真实的使用场景提供了这种功耗分解。这些结果通过 HTC Dream 和 Google Nexus One 两款设备的整体功耗测量得到了验证。
Java 语言
安卓大部分是用 Java 编写的。尽管最近出现了一些替代方案(例如,我们可以提到 Kotlin 和 Android,这是一个绝佳的组合),但 Java 可能仍将是安卓的首选语言。它成熟的环境、来自谷歌和其他公司的大量支持以及活跃的开发者社区,确保了它继续引领安卓开发。
正是这种对现有语言的共享使用吸引了开发者加入安卓生态系统。Java 有一些特定的特点和技巧,我们需要学习以有效地使用它。
原生开发工具包,或者在需要时如何使用原生代码进行开发
使用原生开发工具包(NDK)有时意味着应用程序的表现截然不同,有的只是完成任务而已。我们通常在以下情况下使用 NDK:
-
使用现有的 C/C++库:这是一个明显的优势,因为你能够使用强大的现有软件,如 OpenCV1、音频编码器等。
-
性能:对于一些关键的内循环,在 Android 编译器中**即时编译(JIT)**可用之前,C/C++相对于 Java 的边缘性能优势可能是决定性因素。
-
使用 NDK 实现 Java API 无法处理的事情:接近硬件的低级别操作,尤其是针对特定制造商硬件的影响,可能只能通过 C/C++实现。
-
混淆:编译后的代码在某种程度上比 Java 字节码更难以逆向工程。然而,安全性依赖于隐蔽性并不是理想的解决方案,但它可以补充您已有的系统。
应用程序响应性的三个限制
在任何软件系统中,有三个不同的阈值被认为是用户体验的限制:
-
0.1 秒被用户视为即时响应。在这种操作中,无需向用户显示任何视觉反馈或通知,这包括大多数正常场景中的操作(例如,点击按钮和显示对话框之间的间隔,或显示不同的活动)。
-
1.0 秒是用户流程中断的时间。在 0.1 到 1.0 秒之间,仍然无需提供任何反馈,但超过一秒后,用户会失去立即执行操作的感知。
-
10 秒是最终的限制,此时用户会失去对应用程序的集中精力和兴趣。在操作超过 10 秒的情况下,用户通常会失去对系统的兴趣,并在操作执行过程中拖延。这里的视觉反馈至关重要;如果没有它,用户会感到沮丧并拒绝我们的系统。
谷歌建议所有交互的响应时间保持在 100 到 200 毫秒以内。这是用户感知应用程序迟缓的阈值。尽管这并不总是可能的(考虑到下载大量数据,如媒体等),但我们将学习一些技术,以提供最佳用户体验。
软件质量商业价值
开发人员经常需要向非技术同行解释为什么做出某些决策,这些决策并不能立即带来价值(考虑到重构旧模块或开发一些测试覆盖率)。商业和工程部门之间存在明显的差距,需要调和。
当我们需要与其他部门讨论为了软件质量而做出的决策的商业价值时,我总是喜欢提到“金钱”这个词。从长远来看,做出某些决策等同于节省开支并直接为软件提供价值。它们可能不会立即产生结果,或者产生一个实体物品(尽管软件可以是实体),但它们将来肯定会带来一些好处。我可以想起几次在恰当的时刻重构软件使得可持续扩展的工件与因许多糟糕设计决策而产生的单体架构之间的区别,没有人能够维护最终意味着金钱和财务成本。以下图表揭示了因软件质量不佳随时间给公司带来的损失和后果:
这张图表摘自 David Chappell 的文档,它解释了软件质量不佳可能导致财务损失的一些例子。因业务流失而造成的价值损失可能让我们想起索尼因网络攻击而关闭 PlayStation 网络的事件。如果软件得到了恰当的设计和保护,网络可能还能够继续运行,但糟糕的设计导致公司损失了大量的金钱。每当公司需要为客户因软件系统糟糕导致的问题进行赔偿时,都会发生因客户赔偿而导致的财务损失。当客户不再愿意购买声名狼藉公司的服务时,明显的财务损失就会发生!因诉讼而导致的财务损失在很多情况下是不可避免的,特别是涉及到隐私问题或数据被盗时(这可能非常昂贵!)。
总结
在阅读了本章之后,读者应该对我们将在本书中一起探索的不同领域有一个更准确的认识。我们也希望我们的论点足够有说服力,并且我们将在整本书中进一步发展这些论点。
读者应该能够论证在自身组织环境中性能的重要性,并且应该了解一些关于高效 Android 开发的关键词。不要感到压力,这只是一个开始。
第二章:高效调试
每个开发者在早期都会熟悉“bug”这个词,并且这种关系将贯穿他们的整个职业生涯。一个bug是软件系统中的一个错误或缺陷,会导致一个意外和不正确的结果。
关于这个词的词源有一些讨论。它最初是用来描述硬件系统中的技术故障,第一次使用这个词的引用来自托马斯·爱迪生。计算机先驱格蕾丝·霍珀(Grace Hopper)在 1946 年追踪到计算机 Mark II 的故障是由一只被困在继电器中的飞蛾引起的。这个物理 bug 最终不仅代表了困在机器内引起故障的物理 bug,也代表了逻辑 bug 或软件错误。
在这个背景下,调试是寻找软件系统中的 bug 或故障的过程。调试包括众多因素,如阅读日志、内存转储和分析、性能分析以及系统监控。在开发阶段,或者在生成系统中检测到 bug 时,开发者将调试软件应用程序以检测缺陷并继续修复它。
如果你是一个安卓开发者,谷歌提供了一套丰富的工具,我们可以用来调试我们的应用程序。本书将基于 Android Studio 套件和谷歌的官方 SDK 进行编写——尽管在过程中还有其他外部工具也可能很有帮助。
安卓调试桥
安卓调试桥,更广为人知的是ADB,是 Android 的一个核心工具。它包含在 Android SDK 的/platform-tools 文件夹中。如果你进入这个文件夹并调用adb
命令,你将在屏幕上看到可用的选项列表。
提示
如果你现在还没有这样做,这是一个可以提高生产力的技巧,可能在你使用 ADB 的第一分钟就会得到回报。将你的 Android SDK 存储位置添加到你的PATH
环境变量中。从这一刻起,你将能够从系统的任何部分调用该文件夹内包含的所有工具。
使用adb
,我们可以执行多种操作,包括显示设备、截图或者连接和断开与不同设备的连接。本书的目的不是详尽地审查一个工具的每一项操作,但在这里,我们列出了一些adb
最常见和有用的功能:
# | 命令 | 描述 |
---|---|---|
1 | adb logcat *:E|D|I | 在控制台中启动logcat ,通过错误、调试信息或信息消息进行过滤 |
2 | adb devices | 列出所有连接到adb 的设备 |
3 | adb kill-server``adb start-server | 杀死并重启adb 服务器。当adb 出现卡顿或功能异常时,这是一个有用的消息 |
4 | adb shell | 在目标设备或模拟器上启动一个远程 shell |
5 | adb bugreport | 将dumpsys 、dumpstate 和logcat 的所有内容打印到屏幕上 |
6 | adb help | 打印adb 所有可执行命令的列表 |
adb
一个有趣的事实是,作为一个命令行工具,它可以用于脚本编写并包含在如 Jenkins 等持续集成(CI)系统中。通过使用adb
shell,我们可以执行设备上的任何命令。例如,考虑一个有用的脚本,它能够截取设备屏幕的截图:
adb shell screencap -p /sdcard/screenshot.png
adb pull /sdcard/screenshot.png
adb shell rm /sdcard/screen.png
在这本书中,我们将探索adb
的许多可能性。
Dalvik 调试监控服务器
Dalvik 调试监控服务器也称为DDMS。这个工具在adb
之上运行,并提供了一个具有大量功能的图形界面,包括线程和堆信息、logcat、短信/电话模拟、位置数据等。以下是 DDMS 启动时的样子:
屏幕有不同的部分:
-
左上部分显示了活动设备和设备上运行的不同进程。
-
右上部分展示了各种选项,默认选项为文件资源管理器。在底部,显示了LogCat。
DDMS 中还有更多可用的选项,让我们详细探索它们。首先,我们之前看到的左上部分:
-
图标开始调试选定的进程。
-
图标将在每次为选定进程触发 GC 时更新堆。
-
下一个图标,
,将 HPROF 转储到文件中。HPROF是一种二进制格式,包含应用程序堆的快照。有一些工具可以可视化它们,例如 jhat。稍后,我们将展示如何转换此文件并将其可视化的示例。
-
选项将触发我们应用程序的垃圾回收(对前一个条目很有用)。
-
图标更新 DDMS 中的线程。在处理多线程应用程序时,这将非常方便。
-
使用
图标,我们可以开始分析线程并显示有关它们的准确信息。稍后将展示一个完整的示例。
-
要停止正在运行的进程,我们可以使用
图标。
-
要对应用程序进行截图,可以使用
图标来实现。
-
使用
,我们可以获取视图层次结构的快照并将其发送到 UI 自动化工具。
-
选项利用 Android 的 systrace 捕获系统范围的跟踪。
-
图标用于开始捕获 OpenGL 跟踪信息。
捕获和分析线程信息
现在我们来看看如何处理线程调试。传统的设置断点并等待线程被调用的方法在这里效果不佳,因为多线程应用程序可能有多个线程同时运行且相互独立。因此,我们希望能够独立地可视化和访问它们。
在列表左侧选择一个进程,并点击 图标。如果你现在点击右侧的线程部分,你会看到这个部分已经用当前进程的线程信息进行了更新:
注意事项
一些开发者对于进程和线程是什么感到困惑,以防万一:进程提供了执行程序所需的资源(虚拟地址空间、可执行代码、安全上下文等)。进程是执行进程的实例(在某些上下文中也称为任务)。同一个程序可以有多个进程与之关联,且这些进程在机器重启时会消失。线程是进程的一个子集。一个进程可以由多个线程组成,多个线程在多处理器系统中利用并行性。同一进程中的所有线程共享一个地址空间和堆栈或文件描述符,以及其他内容。
我们可以在屏幕上看到每个线程的不同信息:它们都有一个 ID、线程 ID(Tid)、状态、utime(累计执行用户代码的时间,通常以“jiffies”,即 10 毫秒为单位)、stime(累计执行系统代码的时间,也是以 jiffies 为单位)以及一个名称。如果我们点击其中一个进程,我们将在紧随其下的部分可视化该进程的堆栈跟踪。
我们已经提到线程可以被分析。这通常用于调试内存泄漏。在开始分析之前,请记住以下几点:
-
API 级别低于 7(Android 2.1)的设备需要有一个 SD 卡,因为分析数据将保存在那里。
-
API 级别 7 以上的设备不需要有 SD 卡。
点击 图标。在 API 级别 19(Android 4.4)以上的 Android 设备上,如果你选择基于跟踪的分析,系统会提示你选择采样频率。激活后,DDMS 将捕获有关所选进程的信息,你只需与应用程序进行交互。准备好后,再次点击图标(此时图标将变为
)以停止分析器并转储获取的信息。会出现如下屏幕:
每行代表一个独立线程的执行,随着向右移动,时间增加。每个方法的执行以不同的颜色显示。
在这个新界面的底部部分是一个分析面板。这个表格显示了包括和排除 CPU 时间,以百分比和绝对值表示。排除时间是指我们在方法中花费的时间,而包括时间是我们在方法及其所有被调用函数中花费的时间。因此,调用方法被称为父方法,而方法被称为子方法。
注意
分析器有一个众所周知的问题:虚拟机复用线程 ID。如果一个线程停止而另一个开始,它们可能会得到相同的 ID。这可能导致混淆数据,因此在分析时请确保你正确处理线程。
堆分析和可视化
我们已经学会了如何使用 DDMS 来调试线程。现在我们将学习如何正确分析应用程序的内存堆:即已分配内存所在的内存部分。在调试内存泄漏时,这非常重要。
让我们使用堆转储来追踪问题。点击 图标来转储 HPROF 文件,并选择你想要保存文件的位置。现在对文件运行
hprof-conv
命令。hprof-conv
是一个 Android 实用工具,它将 .hprof
文件从 Dalvik 格式转换为 J2SE HPROF 格式,这样就可以使用标准工具打开它。你可以在 /platform-tools
目录下找到它。要运行它,你需要输入以下命令:
hprof-conv dump.hprof converted-dump.hprof
现在你将拥有一个可以被一些标准工具理解的文件。为了读取这个文件,我们将使用 MAT,这是一个可以从 www.eclipse.org/mat/downloads.php
下载的独立版本。
MAT 是一个非常复杂且强大的工具。点击 文件 并打开 堆转储。你将进入一个与以下类似的界面:
如果我们点击其中一个组,将显示一组选项。其中一个特别有趣的是 直方图。在直方图中,可以按实例数量、使用的总内存量或存活的总内存量过滤类。
如果我们右键点击其中一个类,并选择 列出对象 选项以及其引用,将在堆中生成一个类的列表。稍后可以根据使用情况进行排序。通过选择一个,我们可以显示保持对象存活的引用链。我们本身无法判断这是否意味着存在内存泄漏,但了解该领域的程序员可以确定其中某个值是否不应该再存活:
我们还可以在 DDMS 中可视化堆。如果我们选择一个进程并点击 图标,堆部分将更新有关应用程序当前所有存活的不同数据类型和对象的信息。还可以手动触发 GC,以便 DDMS 更新最新的信息。
在这里,我们可以看到每种类型的对象数量,它们的总大小(包括最小和最大对象的大小,这对于识别OutOfMemoryExceptions
发生时非常有用),以及每个对象的中位数和平均大小:
分配跟踪器
分配跟踪器是 Android 提供的一个工具,它记录应用程序的内存分配,并列出配置周期内所有已分配对象的调用堆栈、大小和分配代码。这比内存堆更进一步,允许我们识别正在创建的单独内存片段。它有助于识别可能低效分配内存的代码位置,以及在同一时间段内被分配和释放的相同类型的对象。
要开始使用分配跟踪器工具,请在左侧选择您的进程,在右侧窗格中选择分配跟踪器部分,然后点击停止跟踪按钮。将打开一个类似于以下窗口的界面:
信息量可能非常大,因此底部有一个过滤器,您可以指定想要获取哪些信息。如果您点击其中一行,分配对象的位置将在屏幕上打印出来。请注意,在我们的特定情况下,我们显示的是关于 Google Maps API 中包含的对象的信息,类名以字母表示。这意味着代码已经被混淆。
使用 ProGuard 混淆代码是一种基本的安全机制。ProGuard 不仅优化代码,去除冗余,还使潜在的攻击者难以查看我们的代码,最终无法对其进行操作。此外,每一行代表一个内存分配事件。每一列代表有关分配的信息,例如对象类型、线程及其大小。
网络使用情况
在 Android 4.0 中,设置中的数据使用情况功能可以长期监控应用程序如何使用网络资源。从 Android 4.0.3 开始,可以实时监控应用程序的网络资源使用情况。还可以通过在使用前为网络套接字应用标签来区分流量来源。
要显示应用程序的网络使用情况,请从左侧选择一个进程。然后移至网络统计标签页,点击开始按钮。你可以选择跟踪速度:每 100、250 或 500 毫秒。然后,与你的应用程序进行交互。将显示与以下类似的屏幕:
屏幕底部按标签显示网络信息,并通过总计收集。可以查看总共发送和接收的字节和包的数量,以及它们的图形表示。
如果你还没有这样做,建议使用TrafficStats
类在每个线程上设置标签。setThreadStatsTag()
函数将建立一个标签标识符。tagSocket()
和untagSocket()
函数将手动标记单个套接字。以下是一个典型的例子:
TrafficStats.setThreadStatsTag(0xF00000);
try {
// make your network request
} finally {
TrafficStats.clearThreadStatsTag();
}
模拟器控制
DDMS 的最后一个标签页是所谓的模拟器控制。通过选择我们的 adb 设备之一并启动它,将显示带有一些附加选项的标签页:
使用模拟器控制,我们可以以多种方式修改手机网络:
-
可以选择不同的数据与语音配置(如家庭网络、漫游、未找到、被拒绝等)
-
可以定义互联网连接的速度和延迟
-
可以模拟来自特定电话号码的来电或短信
-
我们可以向模拟器发送虚假位置信息。这可以手动完成,也可以通过上传 GPX/KML 文件完成
系统状态
DDMS 的最后部分是系统信息标签页。在这里,可以找到最多三个不同的信息类别:CPU 负载、当前时间的内存使用情况以及帧渲染时间(在进行游戏基准测试和调试时,这个尤其重要):
UI 调试
我们到目前为止关注的是 Android 的内存、线程和系统方面。还有一个更直观的方面也可以显著提高我们应用程序的性能:即用户界面(UI)。Android 提供了一个名为层次查看器的工具,用于调试和优化为 Android 设计的任何 UI。层次查看器提供了应用程序布局层次的可视化表示,并带有关于可以在布局中找到的每个节点的性能信息。它提供了一个所谓的像素完美窗口,其中包含放大显示信息,以便在需要仔细查看像素时使用。
要运行层次查看器,我们首先需要连接我们的设备或模拟器。请注意,出于安全原因,只有运行开发版本 Android 系统的设备才能与层次查看器一起工作。连接后,从/tools
目录启动hierarchyviewer
程序。如果你还没有把这个目录作为系统PATH
的一部分,现在是设置的好时机。
你将看到一个类似下面的屏幕。对于连接到系统的每个设备,你将看到一个附加的运行进程列表。选择一个进程,并点击加载视图层次:
打开一个新的屏幕,显示实际的层次查看器。层次查看器如下所示:
层次查看器包含以下元素:
-
在右上角,树状图概览提供了对
ViewHierarchy
应用的鸟瞰视角。 -
树视图可以通过鼠标拖动和缩放。当我们点击一个项目时,这个项目会被高亮显示,我们可以访问其属性。
-
在树视图下面的属性窗格,提供了视图所有属性的摘要。
-
布局视图显示了一个布局的线框图。当前选中的视图轮廓是红色的。如果我们点击一个轮廓,它将被选中,并且在属性窗格中可以访问其属性。
使用层次查看器进行性能分析
层次查看器提供了一个强大的分析器,用于分析和优化应用程序。要进行性能分析,请点击图标,选择分析节点。如果你的视图层次很大,可能需要一些时间才能初始化。
在这一点上,你的层次中的所有视图都将出现三个点:
-
左边的点代表渲染管道的绘制过程
-
中间的点代表布局阶段
-
右边的点代表执行阶段
视图内的每个点颜色有不同的含义:
-
绿色点表示该视图的渲染速度至少比其他视图的一半快。通常,绿色可以被视为高性能视图。
-
黄色点表示该视图的渲染速度比层次中下半部分的视图快。这是相对的,但黄色可能需要我们查看这个视图。
-
红色意味着视图是层次中最慢的一半。通常,我们想要查看这些值。
应用层次查看器探查器后,我们如何解释结果?最重要的一点是,探查器总是在相对的条件下进行测量,即针对我们自己的布局。这意味着一个节点可能总是红色,但如果应用程序运行良好,它不一定就是慢的。相反的情况也适用:一个节点可能是绿色,但如果整个应用程序没有响应,性能可能就是灾难性的。
层次查看器应用了一个称为栅格化的过程来获取信息。对于有图形编程背景的开发者来说,如视频游戏开发,栅格化可能听起来很熟悉,它是将图形基元(例如,一个圆)转换为屏幕上的像素的过程。这通常由 GPU 完成,但在这个情况下,由于我们处理的是软件栅格化,所以由 CPU 完成。这也使得层次查看器的输入相对准确。
为了识别层次查看器的问题,需要应用一些规则:
-
叶子节点或只有少数子节点的视图组中的红点可能指向一个问题。
-
如果一个视图组有很多子节点,且在测量阶段有红点,请查看各个子节点。
-
具有红点的根视图不一定意味着有问题。这种情况经常发生,因为这是所有当前视图的父视图。
Systrace
Systrace 是 Google SDK 中包含的一个工具,用于分析应用程序的性能。它从内核级别捕获并显示应用程序的执行时间(捕获的信息如 CPU 调度程序、应用程序线程和磁盘活动)。分析完成后,它会生成一个包含所有编译信息的 HTML 文件。
要使其工作,请点击 DDMS 视图中的Systrace按钮 ()。会出现如下屏幕:
在这个屏幕上,我们可以为 Systrace 输入一些参数:
-
目标文件将存储为 HTML 文件的路径。
-
跟踪持续时间:默认值为 5 秒。30 秒是一个能获取足够信息的好值。
-
跟踪缓冲区大小:跟踪时缓冲区应该有多大。
-
我们可以选择将启用应用程序跟踪的进程,因此通常我们在这里选择自己的应用程序。
-
我们需要从列表中选择一些我们想要与之交互的标签。
当一切选择完毕后,按下确定按钮,与应用程序进行一段时间的交互。当 systracing 完成后,将在您提供的位置存储一个 HTML 文件。这个文件看起来如下:
安卓设备调试选项
当我们调试 Android 设备时,需要激活开发者模式。默认情况下此模式是隐藏的,如果需要将设备连接到 ADB 或使用其某些选项,我们需要手动激活它。Android 的创造者们很好地隐藏了这个选项。
让我们看看如何激活这个选项以更好地了解 Android 调试,以及我们如何玩转不同的调试配置。
如前所述,设备中的开发者选项默认是隐藏的。这样做的原因很可能是为了使其仅供高级用户使用,而不是普通用户。普通用户无需访问此部分的功能;这样做可能会激活可能损坏设备的选项。
在标准 ROM 中,我们需要进入关于部分,向下滚动直到看到版本号条目,然后快速连续点击五次。会显示一个小对话框,告诉我们现在已成为开发者:
由于定制 ROM 的个性化,其他一些设备上的设置可能会有所不同。以下是一些知名制造商以及如何激活调试选项的说明:
-
三星:设置 | 关于设备 | 版本号
-
LG:设置 | 关于手机 | 软件信息 | 版本号
-
HTC:设置 | 关于 | 软件信息 | 更多 | 版本号
激活开发者选项后,我们将会看到(不同制造商可能会有所不同)在系统部分名为开发者选项的选项。如果我们点击它,将显示选项。我们需要激活开发者选项的开关,这样我们就可以访问整个设置:
同样,每个制造商的选项可能会有所不同。然而,以下是 Android 中的默认选项的全面列表:
-
获取错误报告:此选项将收集关于设备当前状态的信息,并将其作为电子邮件发送。这可能需要一些时间,因为可能会收集大量信息。
-
桌面备份密码:这为完整的桌面备份设置一个密码,默认情况下这些备份是没有密码保护的。
-
保持唤醒:设备在充电时将一直保持唤醒状态,这对于调试来说非常方便。
-
总是保持唤醒:与上一个类似,但在这种情况下,无论设备是否在充电,设备都将保持唤醒状态。如果开发者忘记激活它,这可能会很危险,因为即使在开发后,设备也会保持唤醒状态。
-
HDCP 检查:HDCP代表高带宽数字内容保护。我们可以设置此选项为从不检查数字保护,总是检查数字保护,以及仅在 DRM 内容情况下检查。
-
启用蓝牙 HCI 嗅探日志:激活此选项后,所有 HCI 蓝牙包将被保存在一个文件中。
-
进程统计:此部分包含关于设备进程的极客统计数据。它显示了过去两小时一直在后台运行的应用程序,以及它们的一些特定信息(例如平均/最大 RAM 使用量、运行时间和运行中的服务):
-
USB 调试:当连接 USB 时,此选项允许设备使用 ADB 调试应用程序。这应该是开发者首先激活的选项。
-
错误报告快捷方式:此选项在电源菜单中显示一个按钮,可以按此按钮来获取错误报告。
-
允许模拟位置:激活此选项后,可以模拟位置信息。
-
启用视图属性检查:激活此选项后,我们能够在 Android 系统管理器中查看属性检查。
-
选择调试应用:通过此选项,我们能够选择要调试的应用程序,无需输入冗长的
adb
命令。 -
等待调试器:此选项将正在调试的应用(在上一个选项中选择)附加到调试器。
-
通过 USB 验证应用:此选项默认是禁用的,除非 USB 调试选项处于激活状态。手动安装的任何内容都将被验证,以避免安装恶意软件。
-
无线显示认证:使用此选项帮助认证 Alliance Wi-Fi Display 规范。
-
启用 Wi-Fi 详细日志记录:此选项为所有 Wi-Fi 操作启用更全面的日志记录。
-
积极的 Wi-Fi 到蜂窝网络切换:此选项人为地降低 Wi-Fi 的接收信号强度指示(RSSI),以鼓励 Wi-Fi 状态机决定切换连接。
-
始终允许 Wi-Fi 漫游扫描:默认情况下,已经连接到 Wi-Fi 网络的 Android 设备在遇到更强的 SSID 时不会漫游。激活此选项后,设备将永久性地寻找新的 Wi-Fi。
-
记录器缓冲区大小:此选项改变每个记录器缓冲区的大小(默认为 256 K)。
-
显示触摸:如果激活此选项,每次与屏幕互动时都会有视觉反馈。
-
指针位置:这与上一个类似:指针将在屏幕上用两条垂直线标出。在屏幕顶部,将显示数字信息。
-
显示表面更新:当屏幕更新时,整个表面会闪烁(不建议癫痫患者使用)。
-
显示布局边界:在调试布局时,这是最有用的选项之一。激活后,你应该能看到所有视图边界的活泼蓝色和紫色显示:
-
强制 RTL 布局方向:这将强制布局方向从右至左,而不是默认的左至右。一些用户可能喜欢从右至左的布局,但对于某些语言(如阿拉伯语或希伯来语),布局会自动设置为这种方式。我们可以使用此模式来测试应用程序在此配置下的行为是否正常。
-
窗口动画缩放:您可以设置每个窗口的动画速度(介于 0.5 倍和 10 倍之间)或取消激活。
-
过渡动画缩放:您可以设置每个过渡的动画速度(介于 0.5 倍和 10 倍之间)或取消激活。
-
动画师动画缩放:您可以设置每个动画师的动画速度(介于 0.5 倍和 10 倍之间)或取消激活。
-
模拟辅助显示:此设置允许开发人员模拟辅助显示的不同屏幕尺寸。
-
强制 GPU 渲染:使用硬件 2D 渲染。这可以使你的应用程序看起来很棒,也可能降低性能。仅用于调试目的。
-
显示 GPU 视图更新:每个使用 GPU 硬件绘制的元素都将被一个红色方块覆盖。
-
显示硬件层更新:此选项指示硬件层更新的任何时间。
-
调试 GPU 过度绘制:使用颜色代码可视化元素的过度绘制,具体取决于它们被绘制的频率:这可以用来研究应用程序可能进行不必要的渲染工作的地方。屏幕将开始显示大量颜色,但不要惊慌!我们可以轻松地读懂它们的含义:
-
真实色彩:真实色彩意味着在执行过程中没有发生过度绘制。
-
蓝色:发生了单次过度绘制。
-
绿色:在应用程序的上下文中发生了两次过度绘制。
-
粉色:过度绘制发生了三次。
-
红色:发生了四次或更多次的过度绘制。
-
-
强制 4x MSAA:启用 4x MSAA(即多采样抗锯齿)。这将使你的应用程序运行更快,同时提高图像质量。
-
禁用硬件覆盖:使用硬件覆盖,每个应用程序都获得自己的视频内存部分,无需检查碰撞和剪辑。此选项将禁用硬件覆盖。
-
模拟色彩空间:使用此选项,我们可以强制 Android 仅模拟特定颜色组合的屏幕(例如,单色、红绿色、红黄色等)。
-
使用 NuPlayer(实验性):NuPlayer 是一个支持在线视频内容的视频播放器。它有很多错误,因此默认情况下是禁用的。启用此选项后,NuPlayer 将被激活。
-
禁用 USB 音频路由:此选项禁用了 USB 音频路由自动重定向到外部外围设备。
-
启用严格模式:StrictMode 是一种开发者模式,它可以检测开发者可能遇到的问题,并通知他们以便修复。StrictMode 通常会捕获如在错误线程中进行网络访问等操作。
-
显示 CPU 使用情况:激活此选项后,会在屏幕顶部叠加有关 CPU 使用情况的信息。
-
分析 GPU 渲染:当激活这个工具时,它会提供 UI 帧的速度和节奏的视觉表示。这仅从 Android 4.1 开始可用。在下面的屏幕中,我们看到了一个 分析 GPU 渲染 工具的例子,这里有一些关于如何理解它的说明:
-
水平轴表示经过的时间,垂直轴表示每帧的时间(以毫秒为单位)。
-
每个垂直条形图对应一个渲染的帧。条形越高,渲染所需的时间就越长。
-
绿色线条代表 16 毫秒。每次帧超过绿色线条,你的应用程序就会丢失一帧,这可能导致用户感觉到图像出现卡顿。
-
每种颜色的线条都有其含义:条形图的蓝色部分表示用于创建和更新视图显示列表的时间。如果这部分条形很高,可能存在大量的自定义视图绘制或者在
onDraw
方法中有很多工作。 -
紫色部分是花费在将资源传输到渲染线程上的时间(仅限 Android 4.1)。条形图的红色部分表示 Android 的 2D 渲染器发送命令到 OpenGL 以绘制和重绘显示列表所花费的时间。
-
橙色部分表示 CPU 等待 GPU 完成的时间。如果这个条形太长,说明 GPU 在执行操作上花费了太多时间。
-
-
启用 OpenGL 跟踪:允许在您选择的日志文件中跟踪 OpenGL。
-
不保留活动:这个设置会在你离开主视图时立即关闭每个应用程序。不用说,必须小心使用这个设置,因为它会改变每个应用程序的状态。
-
后台进程限制:使用此选项,我们可以限制同时运行的后台进程的数量。
-
显示所有 ANR:当应用程序因 应用程序无响应 错误而受阻时,即使这在后台发生,也会显示每个 ANR。
Android Instant Run
在撰写本文时,谷歌发布了 Android Studio 2.2 预览版。这(正如其名所示)是 Android Studio 的第二个主要版本,它包含了许多修复、性能改进以及一个名为 Android Instant Run 的强大工具。这个工具允许我们在代码中进行更改,并立即在我们的设备或模拟器中显示这些更改。当我们进行调试时,这是一个无价的功能,因为我们不需要重新编译应用程序,再次启动它,并重新连接到 adb
。
要激活此选项,我们需要进入首选项,然后查找构建、执行、部署 | 立即运行。勾选启用立即运行以在部署时热交换代码/资源更改(默认启用);如果你运行的是正确版本的 Gradle 插件,你将能够激活它:
要运行应用程序,选择运行以使 Android Studio 正常运行。现在有趣的部分来了:在对源代码进行编辑或修改之后,再次点击运行将只将更改部署到设备或模拟器。
目前,立即运行不支持以下几项操作:
-
添加、移除或更改注解
-
添加、移除或更改实例字段
-
添加、移除或更改静态字段
-
添加或移除静态方法签名
-
更改静态方法签名
-
添加或移除实例方法
-
更改实例方法签名
-
更改当前类继承的父类
-
更改实现的接口列表
-
更改类的静态初始化器
-
添加、移除或更改字符串(允许,但需要重新启动宿主活动)
GPU 分析工具
GPU 分析工具也是 Android Studio 2.0 中包含的一个实验性工具。这个工具旨在帮助我们理解导致渲染结果中特定问题的原因,并检查 GPU 的状态。
GPU 调试工具(其中包括 GPU 分析工具)默认情况下未安装。为此,我们需要从 SDK 管理器的 SDK 工具部分进行安装。
要在我们的应用程序中使用此分析工具,我们需要在应用程序中加载跟踪库。我们可以在 Java 代码或 C++ 代码中执行此操作(考虑到许多用于图形的代码因性能更佳而运行在 C++ 中,这是有意义的)。无论你使用哪种方法,都需要将库复制到项目中以便加载。库位于 <sdkDir>/extras/android/gapid/android/<abi>/libgapii.so
。
我们还需要将一些其他相关文件夹复制到 jniLibs
目录中。可以在 <projectDir>/app/src/main/jniLibs
中找到它。如果它尚不存在,你应该创建它(在后续章节中会有介绍 NDK 以及如何处理本地代码的内容)。与 SDK 管理器文件夹一样,jniLibs
应该包含你计划支持的每个 ABI 的一个文件夹。如果你不知道你计划支持哪些 ABI,可以复制所有文件夹。最终的项目目录结构应该如下所示:<projectDir>/app/src/main/jniLibs/<abi>/libgappii.so
。
为了在本地代码中加载库,我们需要创建一个类似于以下代码段的代码:
#include <android/log.h>
#include <dlfcn.h>
#define PACKAGE_NAME "" // Fill this in with the actual package // name
#define GAPII_SO_PATH "/data/data/" PACKAGE_NAME "/lib/libgapii.so"
struct GapiiLoader {
GapiiLoader() {
if (!dlopen(GAPII_SO_PATH, RTLD_LOCAL | RTLD_NOW)) {
__android_log_print(ANDROID_LOG_ERROR, "GAPII", "Failed loading " GAPII_SO_PATH);
}
}
};
GapiiLoader __attribute__((used)) gGapiiLoader;
为了将其加载到主类中,必须使用以下代码段:
static {
System.loadLibrary("gapii");
}
提示
下载示例代码
本书前言中提到了下载代码包的详细步骤。本书的代码包也托管在 GitHub 上,地址为github.com/PacktPublishing/Android-High-Performance-Programming
。我们还有其他丰富的书籍和视频代码包,可在github.com/PacktPublishing/
查看。请查看!
运行跟踪
当我们在应用程序中添加了跟踪库时,它将在启动时阻塞,直到能够连接到 Android Studio 的跟踪接收器。这意味着当你完成分析器的工作后,需要移除跟踪库,因为它会导致无用的渲染。
要开始跟踪,只需运行并部署你的应用程序。首先会提示一个空白屏幕,等待跟踪接收器连接。要启用它,请转到 DDMS 的 CPU/GPU 标签页,并点击 GPU 标签页左侧的红色跟踪按钮()。
开始跟踪后,应用程序将解锁,我们可以与之交互。完成跟踪后,我们需要再次点击跟踪按钮以停止跟踪过程。文件写入后,它将被打开。
ClassyShark
ClassyShark 是由谷歌的开发者倡导者 Boris Farber 开发的一款独立的 Android 诊断工具。ClassyShark 可以作为 Android 可执行文件浏览器,是浏览 Android 类及其内部结构(类接口和成员、依赖关系、dex 结构和计数等)的有价值工具。ClassyShark 已根据 Apache 2.0 许可发布,可以从github.com/google/android-classyshark
免费下载。
在分析 Android APK 内部内容时,ClassyShark 是一个有用的工具,它可以早期诊断由于多 dex 或 dexing 问题、添加的依赖关系和子库、循环依赖以及本地代码问题可能发生的问题。
开始使用
要开始使用 ClassyShark,最快的方法是从 GitHub 网站下载最新的.jar
文件(在撰写本书时,可以从以下 URL 下载 6.6 版本:github.com/google/android-classyshark/releases
)。下载最新版本,然后从控制台使用以下命令运行:
java –jar route/to/ClassyShark.jar
这将启动应用程序。你会看到一个如下面的屏幕:
现在是打开一个示例 APK 来查看其组成并开始使用 ClassyShark 的时候了。点击图标,将显示一个选择 APK 的屏幕。从你的项目中选择一个 APK(如果你使用的是 Android Studio,它们通常在
build/output/apk
文件夹中)。为此,任何 APK 文件都是有效的。
注意
如果你想要自动化 ClassyShark,或者你更习惯于命令行,也可以通过运行以下命令直接打开 APK:
java –jar ClassyShark.jar –open nameOfApk.jar
打开文件后,你将能够看到类似于以下截图的内容:
-
在左侧,我们可以看到一个包含 APK 文件文件夹和资源的树状结构(包括
classes.dex
内的所有文件)。 -
在右侧,我们可以看到 APK 源代码组成的摘要:
-
类的数量
-
字符串的数量
-
APK 内声明了多少个字段
-
APK 中的方法数量
-
注意
限制数量在应用程序开发时尤其是一个重要的上限。特别是,我们可以引用 APK 上的大量方法,但我们只能调用前 65,536 个。再也没有空间存放调用指令了。这个问题曾一度引发了争议和讨论,关于如何解决它,大多数解决方案都会影响应用程序的性能。
如果我们浏览classes.dex
文件,将看到属于 APK 的所有源代码(请参考被 ProGuard 混淆的类),包括像 Android Support、第三方库等库的源代码。为了使它更有趣,尝试选择属于您自己应用程序的一个类,然后点击它。你应该能够显示一个类似于以下对话框:
请注意,这里展示了所有文件的字段、方法和构造函数。对于所有图形和统计信息的爱好者,点击方法计数标签会显示一个交互式饼图。点击饼图上的任何部分,将展示一个子部分。我们还可以展开每个组的树状结构。这样,我们可以轻松地追踪 ClassyShark 中的许多问题,例如缺少库,引用来自其他子库的方法等。
我们之前提到了 Android 的 65 K 限制。这个问题的常见解决方案之一是使用 multidexing:这意味着包含几个.dex
文件,每个文件包含不超过 65 K 的方法。虽然这解决了限制问题,但它可能导致一些性能问题。
使用 ClassyShark,我们可以准确地确定一个方法被包含在哪个.dex
文件中。当包含多个.dex
文件时,它们都将被显示出来,如下面的截图(来自 I/O 调度应用程序)所示:
总结
调试 Android 应用程序是一门科学,开发者需要掌握。大多数调试工具都有一个学习曲线,以便能够有效地使用它们,并了解在特定情况下需要使用哪个工具。Android 提供了一套工具,需要一些时间来熟悉,由于 Android 作为一个移动平台的特殊性,一些工具需要具备特定的调试知识,如线程和内存管理。
阅读本章节后,用户将了解到在开发 Android 应用程序时可能遇到的全部问题(如 ANRs、内存泄漏、错误的线程处理等),以及必须使用哪些工具来进行分析并解决问题。使用高级技术,如性能分析,将帮助我们找到程序中的错误、内存泄漏和错误的线程处理;这些仅通过使用应用程序是无法轻易发现的。
第三章:构建布局
应用的图形设计和导航定义了它的外观和感觉,这可能是成功的关键,但在处理目标用户的 Android 屏幕尺寸和 SDK 级别碎片化时,构建稳定、快速加载和高效的 UI 非常重要。无论外观如何,一个缓慢、无响应或无法使用的图形 UI 可能会导致差评。这就是为什么在每一个应用的开发过程中,你都必须牢记创建高效布局和视图的重要性。
在本章中,我们将详细介绍 UI 的优化细节,以及了解如何提高屏幕性能和效率的有用工具,以满足应用用户的期望。
演练
理解设备屏幕背后的关键概念和代码对提高开发 Android 应用时的稳定性和性能非常重要。让我们从了解设备如何刷新屏幕内容以及人眼如何感知它们开始。我们将探讨开发者可能面临的限制和常见问题,了解谷歌团队在 Android 发展过程中引入的解决方案,以及开发者可以使用哪些解决方案来最大化他们的开发成果。
渲染性能
让我们先了解一下人脑在观看我们的应用时的工作原理,以便更好地理解如何改善用户体验我们应用的性能。人脑接收来自眼睛的模拟连续图像以进行处理。但数字世界是由离散的帧数来模拟真实世界的。这一巧妙系统背后的基本机制基于一个主要的物理定律:单位时间内处理的帧数越多,人脑对运动的感知效率越高。人脑感知运动的最小帧数每秒在 10 到 12 帧之间。
设备要创建最流畅的应用,每秒最合适的帧数是多少?为了回答这个问题,我们来看看不同行业是如何处理这个问题的:
-
电视和戏剧电影:在这个领域,电视广播和电影使用了三种标准的帧率。它们分别是 24 FPS(美国 NTSC 和电影院使用)、25 FPS(欧洲 PAL/SECAM 使用)和 30 FPS(家用电影和摄像机使用)。使用这些帧率时,可能会出现运动模糊:即当大脑处理后续图像过快时,视觉清晰度会降低。
-
慢动作与新电影制作人:这类用途最常使用的帧率是 48 FPS——这是电影帧率的两倍。新电影制作人采用这种方法来提高动作电影的流畅性。这种帧率也用于放慢场景,因为以 24 FPS 播放的 48 FPS 录制的场景具有与电影相同的感知水平,但速度减半。
那么应用程序的帧率又如何呢?我们的目标是让应用程序在其整个生命周期内保持 60 FPS。这意味着屏幕应该每秒刷新 60 次,或者每 16.6667 毫秒刷新一次。
有很多因素可能导致这个 16 毫秒的截止时间不被遵守;例如,当视图层次结构被重绘太多次,占用了过多的 CPU 周期时,这种情况就可能发生。如果发生这种情况,帧就会被丢弃,UI 就不会刷新,用户将看到同样的画面,直到下一个帧被绘制。这正是你需要避免的,以提供流畅的用户体验给你的用户。
有一个技巧可以加快 UI 绘制并达到 60 FPS:当你构建布局并通过Activity.setContentView()
方法将其添加到活动中时,为了创建所需的 UI,会有许多其他视图被添加到层次结构中。在图 1中,有一个完整的视图层次结构,但我们添加到活动 XML 布局文件中的视图只属于下面两层:
图 1:完整层次结构视图示例
目前我们关注的是层次结构顶层的视图;这个视图被称为DecorView,它包含了由主题定义的活动背景。然而,这个默认背景通常会被你的布局背景覆盖。这意味着它会影响 GPU 的工作量,降低渲染速度,从而降低帧率。因此,诀窍就是避免绘制这个背景,从而提高性能。
移除这个drawable
背景的方法是在活动的主题中添加属性,或者使用以下主题(即使对于兼容主题,该属性也是可用的):
<resources>
<style name="Theme.NoBackground" parent="android:Theme">
<item name="android:windowBackground">@null</item>
</style>
</resources>
每当你处理全屏活动,用不透明的子视图覆盖整个 DecorView 屏幕时,这都很有帮助。不过,将活动布局背景移动到窗口 DecorView 是一个好习惯。这样做的主要原因是 DecorView 的背景是在任何其他布局之前绘制的:这意味着无论其他 UI 组件加载操作需要多长时间,用户都会立即看到背景,而不会错误地认为应用程序没有在加载。为此,只需将背景drawable
作为先前主题 XML 文件中的windowBackground
属性,并将其从活动根布局中移除:
<resources>
<style name="Theme.NoBackground" parent="android:Theme">
<item name="android:windowBackground">
@drawable/background</item>
</style>
</resources>
总的来说,这第二个变化并不是一个适当的改进,而只是一个让用户感觉应用程序更流畅的技巧;背景绘图与 GPU 消耗相对应,无论它是 DecorView 还是活动布局的根。
屏幕撕裂与 VSYNC
当我们谈论刷新时,需要考虑两个主要方面:
-
帧率:这是指设备 GPU 能够在屏幕上绘制一整帧的次数,以每秒帧数(frames per second)表示。我们的目标是保持 60 FPS,这是 Android 设备的标准,我们将会了解为什么。
-
刷新率:这指的是屏幕每秒更新的次数,以赫兹为单位。大多数 Android 设备屏幕的刷新率为 60 Hz。
虽然第二个是固定且不可更改的,但如前所述,第一个取决于许多因素,但首先取决于开发者的技能。
这些值可能不同步。因此,显示器即将更新,但决定要绘制的内容是由两个不同的后续帧在单个屏幕绘制中决定的,直到下一次屏幕绘制,这会导致屏幕上出现明显的割裂,如图图 2所示。这个事件也被称为屏幕撕裂,它会影响每个带有 GPU 系统的显示器。图像上的不连续线条称为撕裂点,它们是这种屏幕撕裂的结果:
图 2:屏幕撕裂的一个示例
这种现象的主要成因可以追溯到用于绘制帧的单一流数据:每个新帧都以这样的方式覆盖前一个帧,以至于只有一个缓冲区可以读取以在屏幕上绘制。这样,当屏幕即将刷新时,它会从缓冲区读取要绘制的帧的状态,但它可能还在完成中并未完全完成。因此,如图图 2所示的撕裂屏幕。
解决这个问题的最常用方案是对帧进行双缓冲处理。这个解决方案有以下实现:
-
所有绘图操作都保存在后台缓冲区中
-
当这些操作完成后,整个后台缓冲区(back buffer)会被复制到另一个内存位置,称为前台缓冲区(front buffer)。
复制操作与屏幕速率同步。屏幕只从前台缓冲区读取以避免屏幕撕裂,所有后台绘图操作都可以在不影响屏幕操作的情况下执行。但是,在从后台缓冲区到前台缓冲区的复制操作过程中,是什么防止屏幕更新的呢?这称为VSYNC。这代表垂直同步,最早在 Android 4.1 Jelly Bean(API 级别 16)中引入。
VSYNC 并不是这个问题的解决方案:如果帧率至少等于刷新率,它工作得很好。看看图 3;帧率为 80 FPS,而刷新率为 60 Hz。总是有新帧可供绘制,因此屏幕上不会有延迟:
图 3:帧率高于刷新率的 VSYNC 示例
但是,如果帧率低于刷新率会发生什么呢?让我们看一下以下示例,逐步描述 40 FPS GPU 和 60 Hz 刷新率屏幕上发生的情况:即帧率是刷新率的 2/3,导致每 1.5 个屏幕刷新更新一次帧:
-
在 0 时刻,屏幕第一次刷新,第一帧落入前景缓冲区,GPU 开始在后台缓冲区准备第二帧。
-
在屏幕第二次刷新时,第一帧被绘制到屏幕上,而第二帧无法复制到前景缓冲区,因为 GPU 仍在完成它的绘图操作:它仍在这一操作的 2/3 处。
-
在第三次刷新时,第二帧已被复制到前景缓冲区,因此它必须等待下一次刷新才能显示在屏幕上。GPU 开始准备第三帧。
-
在第四步中,由于第二个帧在前景缓冲区,而 GPU 仍在准备第三个帧,因此会在屏幕上绘制第二个帧。
-
第五次刷新与第二次相似:由于需要新的刷新,第三帧无法显示,因此第二个帧连续第二次显示。
这里所描述的内容在图 4中展示:
图 4:帧率低于刷新率的 VSYNC 示例
毕竟,在四个屏幕刷新中只绘制了两帧。但是,每当帧率低于刷新率时,这种情况就会发生:即使帧率是 59 FPS,实际每秒显示在屏幕上的帧数也只有 30,因为 GPU 需要等待新的刷新开始,然后才能在后台缓冲区开始新的绘图操作。这导致了滞后和抖动,并抵消了任何图形设计上的努力。这种行为对开发者来说是透明的,并且没有 API 可以控制或更改它,因此保持应用程序中的高帧率以及遵循性能技巧以达到 60 FPS 目标至关重要。
硬件加速
安卓平台的演变历史在图形渲染方面也有逐步的改进。这一领域最大的改进是在 Android 3.0 Honeycomb(API 级别 11)中引入了硬件加速。设备屏幕变得越来越大,平均设备像素密度在增长,因此 CPU 和软件已不再足以满足 UI 和性能需求的增长。随着平台行为的这一变化,由Canvas
对象进行的视图及其所有绘图操作都开始使用 GPU 而不是 CPU。
硬件加速最初是可选的,应在清单文件中声明以启用,但从下一个主要版本(Android 4.0 Ice Cream Sandwich,API 级别 14)开始,默认启用。它在平台上的引入带来了一种新的绘图模型。基于软件的绘图模型基于以下两个步骤:
-
失效:当由于需要更新视图层次结构或仅更改视图属性而调用
View.invalidate()
方法时,失效会通过整个层次结构传播。这一步也可以由非主线程使用View.postInvalidate()
方法调用,失效在下一个循环周期发生。 -
重绘:每个视图都会在 CPU 大量消耗的情况下重新绘制。
在新的硬件加速绘图模型中,由于视图被存储,重绘不会立即执行。因此,步骤变为以下:
-
失效:与基于软件的绘图模型一样,视图需要更新,因此
View.invalidate()
方法会传播到整个层次结构中。 -
存储:在这种情况下,仅重绘由失效影响的视图,并将其存储以供将来重用,从而减少运行时计算。
-
重绘:每个视图都使用存储的绘图进行更新,因此未受失效影响的视图使用其最后一次存储的绘图进行更新。
每个视图都可以被渲染并保存到离屏位图中以供将来使用。可以通过使用Canvas.saveLayer()
方法来实现,然后使用Canvas.restore()
将保存的位图绘制回画布。应谨慎使用,因为它会绘制一个不需要的位图,根据提供的边界尺寸增加计算绘图成本。
从 Android 3.0 Honeycomb(API 级别 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
这些属性与谷歌在 Android 3.0 Honeycomb(API 级别 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();
提示
每当你在动画化视图、改变其透明度或只是设置不同的透明度时,请考虑使用View.LAYER_TYPE_HARDWARE
。这一点非常重要,谷歌从 Android 6.0 Marshmallow(API 级别 23)开始,自动应用硬件层,因此如果你的应用程序的目标 SDK 是 23 或更高,你就不需要手动操作。
过度绘制
满足 UI 要求的布局构建通常是一个容易误导的任务:完成布局后,仅仅检查我们所做的是否与图形设计师的想法一致是不够的。我们的目标是验证用户界面不会影响我们应用程序的性能。通常我们会忽略在布局内部如何构建视图,但有一个非常重要的点需要记住:系统不知道哪些视图对用户可见,哪些不可见。这意味着无论如何都会绘制每个视图,无论它是否被覆盖、隐藏或不可见。
注意
请记住,如果视图不可见、隐藏或被另一个视图或布局覆盖,视图的生命周期并没有结束:从计算和内存的角度来看,它的计算工作仍然会影响最终布局的性能,即使它没有显示。因此,一个良好的实践是在 UI 设计阶段限制使用的视图数量,以防止性能显著下降。
从系统角度来看,屏幕上的每一个像素都需要在每一帧更新时被更新多次,更新的次数等于重叠视图的数量。这种现象称为过度绘制。开发者的目标是尽可能限制过度绘制。
我们如何减少屏幕上绘制的视图数量?这个问题的答案取决于我们应用程序用户界面的设计方式。但为了实现这一目标,有一些简单的规则可以遵循:
-
窗口背景在每次更新时都会增加一个绘制层。移除背景可以减少一个过度绘制的层次。这可以通过本章前面讨论的 DecorView 来完成,直接在 XML 样式文件中删除我们活动使用的主题中的它。否则,也可以在运行时通过在活动代码中添加以下内容来完成:
@Override public void onWindowFocusChanged(boolean hasFocus) { if (hasFocus) getWindow().setBackgroundDrawable(null); }
这可以应用于层次结构中的每个视图;这一想法是为了消除不必要的背景,以限制系统每次必须处理和绘制的层数。
-
展平视图层次结构是减少过度绘制风险的好方法;使用下面几页将要介绍的层次查看器和设备上的 GPU 过度绘制是达到此目标的关键步骤。在这种展平操作中,由于 RelativeLayout 管理,您可能会无意中遇到过度绘制问题:视图可能会重叠,使得这项任务效率低下。
-
Android 以不同的方式管理位图和 9-patches:9-patches 的一种特殊优化让系统避免绘制其透明像素,因此它们不会像每个位图像素那样继续过度绘制。因此,使用 9-patches 作为背景可以帮助限制过度绘制的面积。
多窗口模式
在新版本的 Android N 中,新增了一项功能,即在本书撰写时处于预览状态的多窗口模式。这是关于让用户在屏幕上同时并排显示两个活动。在分析其性能影响之前,我们先快速了解一下这个功能。
概览
这种分屏模式在竖屏和横屏模式下都可用。您可以在图 5中看到竖屏模式的样子,在图 6中看到横屏模式的样子:
图 5:Android N 竖屏分屏模式
图 6:Android N 横屏分屏模式
从用户的角度来看,这种方式可以在不离开当前屏幕和打开最近应用屏幕的情况下与多个应用或活动进行交互。位于屏幕中央的分割条可以移动以关闭分屏模式。这种行为适用于智能手机,而制造商可以在更大的设备中启用自由形态模式,让用户通过简单的滑动手势为两个活动选择合适的屏幕比例。也可以将对象从一个活动拖放到另一个活动。
在电视设备上,这是通过使用画中画模式实现的,如图图 7所示。在这种情况下,视频内容继续播放,而用户可以浏览应用程序。然后,视频活动仍然可见,但只占用屏幕的一小部分:它是位于屏幕右上角的 240 x 135 dp 窗口:
图 7:Android N 画中画模式
由于窗口尺寸较小,活动应该只显示视频内容,避免显示其他任何内容。此外,要确保画中画窗口不会遮挡后台活动的内容。
现在我们来检查与典型的活动生命周期有何不同,以及系统如何同时处理屏幕上的两个活动。在多窗口模式激活时,最近使用的活动处于恢复状态,而另一个活动处于暂停状态。当用户与第二个活动交互时,这将进入恢复状态,而第一个活动将进入暂停状态。这就是为什么无需修改活动生命周期,在新 SDK 中状态与之前相同的原因。但请记住,在多窗口模式开启时,处于暂停状态的活动应继续不限制用户体验。
配置
开发者可以选择通过在应用程序的清单文件中添加新的属性来设置活动支持多窗口或画中画模式。这些新属性包括以下内容:
android:resizeableActivity=["true" | "false"]
android:supportsPictureInPicture=["true" | "false"]
它们的默认值为 true,因此如果我们应用的目标是支持 Android N 的多窗口或画中画模式,则无需指定它们。画中画模式被视为多窗口模式的一个特例:此时,只有当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()
方法为两个定义的尺寸或全屏(传递 null 对象而不是Rect
对象)指定初始尺寸,如下所示: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。
提供的布局概览
每当调用Activity.setContentView(int layoutRes)
方法或使用LayoutInflater
对象膨胀视图时,相关的布局 XML 文件将被加载和解析,每个大写的 XML 节点对应一个View
对象,系统必须实例化该对象,并且在整个Activity
或Fragment
生命周期中,它将是 UI 层次结构的一部分。这影响了应用程序使用期间的内存分配。让我们来了解 Android 平台 UI 系统的关键概念。
如前所述,布局资源中每个大写的 XML 节点将根据其名称和属性进行实例化。ViewGroup
类定义了一种特殊的视图,可以作为容器管理其他View
或ViewGroup
类,描述如何测量和定位子视图。因此,我们将把扩展了ViewGroup
类的每个类都称为布局。Android 平台提供了不同的ViewGroup
子类,供我们在布局中使用。以下是主要直接子类的简要概述,通常在构建布局 XML 资源文件时使用,仅解释它们如何管理嵌套视图:
-
LinearLayout
:每个子元素在水平或垂直方向上紧邻之前添加的元素绘制,分别对应行或列。 -
RelativeLayout
:每个子元素相对于其他兄弟视图或父视图定位。 -
FrameLayout
:这用于封锁屏幕区域,以管理以最近添加的视图为顶部的视图堆栈。 -
AbsoluteLayout
:在 API 级别 3 中已弃用,因为其灵活性较差。实际上,你必须为所有子元素提供确切的位置(通过指定所有子元素的x或y坐标)。其唯一的直接子类是WebView
。 -
GridLayout
:这会将子元素放置在网格中,因此其使用限于将子元素放入单元格的特定情况。
分层布局管理
让我们了解一下每次系统被要求绘制布局时会发生什么。这个过程由两个相继的从上到下的步骤组成:
-
测量:
-
根布局测量自身
-
根布局请求所有子元素进行自我测量
-
任何子布局都需要对其子元素递归执行相同的操作,直到层次结构的末尾
-
-
定位:
-
当布局中的所有视图都有自己的测量存储时,根布局定位其所有子元素
-
任何子布局都需要对其子元素递归执行相同的操作,直到层次结构的末尾
-
每当更改View
属性(例如ImageView
的图像或TextView
的文本或外观)时,视图本身会调用View.invalidate()
方法,该方法从下到上传播其请求,直到根布局:前面的过程可能需要一次又一次地重复,因为视图需要再次测量自己(例如,仅为了更改文本)。这会影响绘制 UI 的加载时间。你的层次结构越复杂,UI 加载越慢。因此,尽可能开发扁平的布局非常重要。
虽然AbsoluteLayout
不再使用,而FrameLayout
和GridLayout
有其特定的用途,但LinearLayout
和RelativeLayout
是可以互换的:这意味着开发者可以选择使用其中之一。但两者都有优缺点。当你开发如图8所示的简单布局时,你可以选择使用不同类型的方 法来构建布局。
图 8:布局示例
-
第一种基于
LinearLayout
,它有利于提高可读性,但性能不佳,因为每次需要对子视图进行方向定位更改时,你都需要嵌套LinearLayout
:<?xml version="1.0" encoding="utf-8"?> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:src="img/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> </LinearLayout>
此布局的视图层次结构如图9所示:
图 9:使用 LinearLayout 构建的视图层次结构示例
-
第二种基于
RelativeLayout
,在这种情况下,你不需要嵌套任何其他ViewGroup
,因为每个子视图的位置可以与其他视图或父视图相关:<?xml version="1.0" encoding="utf-8"?> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <ImageView android:id="@+id/imageview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:src="img/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" /> </RelativeLayout>
这个替代布局的层次结构如图10所示:
图 10:使用 RelativeLayout 构建的视图层次结构示例
通过比较这两种方法,可以很容易看出,在第一种方法中有六个视图分布在三个层次级别中,而在第二种方法中,五个视图只分布在两个级别中。
通常的情况是采用混合方法,因为不可能总是相对于其他视图来定位视图。
注意
为了在创建各种布局时实现性能目标并避免过度绘制,视图层次结构应尽可能扁平,以便系统在需要时以最短的时间重新绘制每个视图。因此,建议在可能的情况下使用 RelativeLayout,而不是 LinearLayout。
在长期的应用开发过程中,一个常见的错误做法是在删除不再需要的视图后,在 XML 文件中留下多余的布局。这无谓地增加了视图层次结构的复杂性。正如在第二章高效调试以及本章后续页面中讨论的那样,通过使用 LINT 和层次结构查看器,可以方便地避免这一点。
不幸的是,最常用的 ViewGroup 是 LinearLayout,因为它相对简单易懂易管理。因此,新的 Android 开发者首先会接触它。出于这个原因,谷歌决定从 Android 4.0 冰淇淋三明治开始提供一个全新的 ViewGroup,如果使用得当,可以在处理网格时减少特定情况下的冗余。我们所说的是 GridLayout。显然,可以使用 LinearLayout 创建网格,但生成的布局至少有三层层次结构。也可以使用 RelativeLayout 仅两层层次结构创建,但生成的布局管理起来并不那么容易,视图之间的引用太多。GridLayout 只需定义自己的行和列以及单元格,就可以管理其空间。以下 XML 布局展示了如何使用 GridLayout 创建与 图 11 相同的布局:
图 11:使用 GridLayout 构建视图层次结构示例
<?xml version="1.0" encoding="utf-8"?>
<GridLayout
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="img/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="img/common_ic_googleplayservices" />
<Button android:id="@+id/button"
android:layout_column="1"
android:layout_row="2"
android:text="Button" />
</GridLayout>
可以注意到,如果你希望 android:layout_height
和 android:layout_width
标签属性为 LayoutParams.WRAP_CONTENT
,则无需指定,因为这正是它们的默认值。GridLayout
与 LinearLayout
非常相似,因此从后者转换过来相当简单。
重用布局
Android SDK 提供了一个有用的标签,在特定情况下使用,当你想在其他布局中重用 UI 的一部分,或者当你想在不同的设备配置中只更改 UI 的这一部分时。这个 <include/>
标签允许你添加另一个布局文件,只需指定其引用 ID。如果你想重用上一个示例的头部,只需创建如下可重用布局 XML 文件:
<LinearLayout
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="img/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
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="img/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 相同,有三层和六个视图。这就是为什么 Android SDK 提供了另一个有用的标签 <merge />
,它可以帮助移除冗余布局并保持更扁平的层次结构。只需用 <merge />
标签替换可重用的根布局。可重用布局将变成以下这样:
<?xml version="1.0" encoding="utf-8"?>
<merge >
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:src="img/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()
方法时,你必须提供一个视图作为父视图并附加到它:LayoutInflater.from(parent.getContext()).inflate(R.layout. merge_layout, parent, true);
ViewStub
ViewStub
类可以作为布局层次结构中的节点添加,并指定一个布局引用,但在运行时使用ViewStub.inflate()
或View.setVisibility()
方法加载其布局之前,不会为它绘制任何视图:
<ViewStub
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。
当你处理复杂的布局层次时,这个类特别有用,但你可以将某些视图的加载推迟到稍后的时间,并在需要时加载,从而减少首次加载时间并释放不必要的内存分配。
AdapterView
和视图回收
有一个特殊的ViewGroup
子类,它需要一个Adapter
类来管理其所有子项:这个类被称为AdapterView
。AdapterView
的常见专用类型包括:
-
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
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()
方法引用行布局中的每一个视图。这些操作只能由主线程执行,因为它是唯一可以处理 UI 的线程。这会影响运行时的计算,经常导致滚动出现延迟,性能下降。此外,行布局层次的复杂性可能会加剧这种行为。
ViewHolder
模式
为了避免在Adapter.getView()
内部对View.findViewById()
方法的计算密集型调用,使用ViewHolder
设计模式是一个好习惯。
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
参数,以便重复使用。而其神奇之处在于:当convertView
为空时,会填充一个视图,并将每个包含的视图存储在ViewHolder
对象中以便后续重用,同时将ViewHolder
对象设置为刚刚初始化的convertView
的标签。这样,当convertView
不为空时,Adapter
类会给我们提供相同的实例,以便我们可以从convertView
中检索ViewHolder
并使用其属性视图。
提示
在使用BaseAdapter
时,强烈建议使用ViewHolder
模式,以避免频繁调用View.findViewById()
方法,这可能会影响运行时的计算。
模式的使用由开发者自行决定;多年来,新的 Android 开发者往往不使用它,导致 Android 平台性能因在滚动ListView
或GridView
时出现延迟而声誉不佳。这正是谷歌引入一个名为RecyclerView
的新视图来创建列表和网格,并自行管理子视图回收的原因之一;它可以从 Android 2.1 Éclair 开始使用,因为它包含在支持包库 v7 中。在使用这个高度灵活的新对象时,开发者不能跳过使用ViewHolder
对象。
在这两种情况下,使用正确的尺寸显示行布局中ImageView
的占位图像,而不是其原始尺寸,以避免 CPU 和 GPU 处理,这通常会导致OutOfMemoryError
。
从计算的角度来看,这一模式还不足以创建一个流畅的应用程序;如前所述,只有主线程负责处理视图和 UI 交互。此外,每个处理任务都应该在工作线程中执行,以便主线程能够快速访问视图。关于这个话题,请阅读更多关于第五章,多线程的内容。
自定义视图和布局
在我们的 UI 应用开发中,我们经常遇到缺乏具有我们所需布局功能的视图,或者我们需要从头开始创建一个具有某些出色功能的视图。幸运的是,Android 平台允许我们开发各种类型的视图,以便构建所需的 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
中,这些ViewGroups
负责在测量它们后放置其子视图。然后,告诉父视图视图大小的方法是View.onMeasure()
。在自定义视图开发中,这是一个关键步骤,因为每个视图都必须有自己的宽度和高度;实际上,如果在View.onMeasure()
中忘记调用View.setMeasuredDimension()
,会导致抛出异常。
每当视图因为边界改变或需要比之前更多或更少的空间而需要重新测量时,你需要调用View.requestLayout()
方法:它不是仅使视图本身无效,而是要求父视图重新计算所有子视图的位置并重新绘制它们。这相当于使整个视图层次结构无效。如前所述,这可能会非常昂贵,应尽可能避免。
幸亏平台的强大功能,自定义视图的创建可以带来非常有趣的结果,但所有这些自由都必须受到控制和衡量。检查视图的定时,通过查看仅包含视图的布局中的 GPU 性能,然后在一个更广泛的背景下,控制它在与其他视图一起时的行为,这是一种好习惯。
了解这一点后,让我们识别和分类开发者在开发自定义视图时可能犯的性能错误:
-
在不需要时刷新视图绘制
-
绘制不可见的像素:这是我们之前所说的过度绘制。
-
在绘制过程中通过进行不必要的操作消耗内存资源
每一个都可能导致 GPU 无法达到 60 FPS 的目标。让我们更深入地探讨它们:
-
视图无效化是新手广泛使用的方法,因为这是在任何时候刷新和更新视图的最快方式。
提示
在开发自定义视图时,要小心不要调用那些会导致整个层次结构一次又一次重新绘制的非必要方法,这会消耗宝贵的帧绘制周期。一定要检查
View.invalidate()
和View.requestLayout()
的调用时机和位置,因为这可能会影响整个 UI,减慢 GPU 和其帧率。 -
为了在自定义视图中避免过度绘制,你可以使用 Canvas API,它允许你只绘制自定义视图的所需部分。在设计堆叠视图或其他具有重叠部分的视图时,这会非常有帮助。我们指的是
Canvas.clipRect()
方法。例如,如果你的视图需要在屏幕上绘制多个重叠对象,我们的目标是要正确剪辑每个视图以避免不必要的过度绘制,只绘制每个对象的可见部分。例如,图 12 显示了一个堆叠视图,其中重叠的卡片不需要完全绘制:
图 12:具有重叠部分的自定义视图示例
下面的代码片段展示了如何避免过度绘制:
@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(); } }
-
在我们的
View.onDraw()
方法实现中,我们不应该在任何由View.onDraw()
调用的方法中放置任何分配。这是因为,当在该方法内部进行分配时,需要创建并初始化对象。然后,当View.onDraw()
的执行结束时,垃圾回收器会释放内存,因为没有人在使用它。此外,如果视图是动画的,视图每秒会重绘 60 次。因此,避免在View.onDraw()
方法中进行分配的重要性。提示
不要在
View.onDraw()
方法(或由它调用的其他方法)内部分配对象,以免增加此方法的执行负担,该方法在视图生命周期内可能会被多次调用;垃圾回收器可能会频繁释放内存,导致卡顿。更好的做法是在视图首次创建时实例化它们。
屏幕缩放
新的 Android N 预览版引入了一项特殊功能,如果我们不遵循之前介绍的最佳实践,可能会对我们的应用程序造成压力。我们所说的是显示大小,它可以在设备的设置中的辅助功能部分进行更改,如图图 13所示:
图 13:辅助功能中的显示大小设置
当用户更改设置时,会显示预览,看起来像图 14:
图 14:默认和最大尺寸的显示大小更改效果
现在我们快速了解一下用户在设备上设置此新功能时会发生什么。如果应用程序以新的 Android N 版本为目标进行编译,那么应用程序进程将通过典型的运行时更改框架得到通知。否则,所有进程将被杀死,活动将被重新创建,就像改变方向时一样。但这次重建会使用不同的屏幕宽度,以 dp 表示。因此,我们应该测试这个特定的用例,以确保我们的应用程序性能不受此新功能的影响。
这是我们不使用 px 测量单位,而选择更合适的 dp 单位的进一步原因。
此外,如第六章网络通信中所解释,我们应该改变应用程序的任何与密度相关的行为,比如图像格式缓存或对后端的服务请求。
调试工具
现在我们知道了创建灵活高效 UI 背后的问题以及如何解决它们。但是,我们如何知道我们做得好不好呢?此外,我们如何衡量我们辛勤工作的输出质量?让我们了解你可以使用各种工具来衡量我们的产品,同时发现并解决其他问题,以提高应用程序在整个生命周期内的性能。
设计视图
在开发过程中,创建 XML 布局文件是一个被低估的活动:如果在开发阶段布局设计得当,应用程序无需特别努力就能提升性能。在编写 XML 文件时,IDE 允许我们在布局编辑器中以预览模式查看我们所设计的内容。这包括文本和设计视图,如图15所示:
图 15:设计视图
设计视图包含一个名为组件树的特殊视图,它在我们构建视图层次时显示该层次。在图 16中,层次视图与图 19中的相同。这是一种实用的视觉方法,用于评估我们布局的深度:
图 16:设计视图中的视图层次预览
如本章所讨论,我们的目标是扁平化层次深度,以限制计算并加速创建要在屏幕上尽可能快显示的视图。
注意
设计视图是在开发过程中限制层次深度的正确工具;如果在分析和开发过程中注意细节,我们可以显著减少恢复应用程序丢失性能所需的工作量。
层次查看器
分析视图层次、调试 UI 以及分析布局的主要工具是层次查看器。它位于 Android 设备监视器中,提供了一个完整的可视化工具。如图17所示,该工具包含许多视图,帮助我们分析 UI:
图 17:层次查看器
树状视图
中间面板包含带有视图层次结构缩放部分的树状视图。每个视图可以被选中,以打开与所选视图以及所有层次结构中较低视图相关的详细信息:
-
包含的视图数量
-
测量时间
-
布局时间
-
绘制时间
这意味着树状视图最左边的视图告诉我们整个 UI 创建过程所需的时间,因为它是我们布局的根。这是必须始终考虑的参数;正如前几页所讨论的,我们的目标是保持这个值低于 16 毫秒。图 18展示了一个选中了ImageView的树状视图示例:
图 18:层次查看器中的树状视图
提示
每次测试过程都应检查布局创建时间。测量、布局和绘制步骤最多需要在 16.67 毫秒内完成。层次查看器中的树状视图可以帮助我们测量这些时间。
使用树状视图,我们布局的深度非常直观:这有助于我们了解在活动布局中过度设计的地方以及可能不小心添加的过度绘制。
视图属性
左侧面板包含两个视图:
-
窗口:在这里,你可以找到所有已连接设备和模拟器的列表以及所有可调试进程的子列表,选中的进程以粗体显示。可以选择其中一个,点击图标后,相关的视图将加载到树视图中,整个面板切换到视图属性。
-
视图属性:这包含了一系列用于调试视图的视图属性:
图 19:层次查看器内的视图属性
树状图概览
在 Android 设备监视器的右侧,树状图概览显示了整个视图层次结构,而树视图中放大的部分被灰色处理以突出显示。这个视图向我们展示了我们构建的视图层次结构的复杂性。看图 20以了解树状图概览的外观:
图 20:层次查看器内的树状图概览
布局视图
在树状图概览下方,有一个名为布局视图的视图,它显示了模拟设备屏幕上显示的布局中每个视图所覆盖的区域,这样你可以在树视图中选择一个特定的视图,简化在布局中查找单个视图的过程。图 21展示了本章示例所用的布局视图:
图 21:层次查看器内的布局视图
设备工具
当你想调试和评估你的用户界面时,在真实设备上进行操作是非常重要的。Android 系统在开发者选项设置中提供了许多灵活的工具,可以在设备上使用。
调试 GPU 过度绘制
为了在设备上调试过度绘制,Android 提供了一个有用的工具,可以在开发者选项内启用。在硬件加速渲染部分中,有调试 GPU 过度绘制的选项。启用后,屏幕会根据每个像素的过度绘制级别以不同的颜色显示,如果存在过度绘制,则通过添加覆盖颜色来指示:
-
真彩色:无过度绘制
-
蓝色:1 倍过度绘制
-
绿色:2 倍过度绘制
-
粉色:3 倍过度绘制
-
红色:4 倍以上过度绘制
例如,让我们看看图 22。左侧屏幕未经优化,而右侧屏幕则进行了优化。因此,这个工具对于查找我们布局中的过度绘制非常有帮助。作为开发者的我们的目标是尽可能减少叠加,以减少过度绘制并提高 GPU 计时和渲染速度。需要执行的主要操作是检查我们布局的背景和 RelativeLayouts 内部重叠的视图:
图 22:优化前后的过度绘制比较
分析 GPU 渲染
这个工具向开发者展示了帧渲染操作需要多长时间,并定义了这些操作是否在 16 毫秒限制内完成。从渲染的角度来看,这是评估我们应用程序性能的一个很好的方法。
尽管名字如此,所有观察到的过程都是由 CPU 执行的:GPU 以异步方式工作,在 CPU 提交渲染操作之后。
要启用它,只需在设备的开发者设置中的监控部分选择分析 GPU 渲染。有两个选项:
-
以条形图显示在屏幕上: 这显示了屏幕上的结果,有助于快速查看我们的应用程序针对每帧 16 毫秒目标的渲染性能。
-
在 adb shell dumpsys gfxinfo 中: 这会存储基准测试结果,以便使用
adb
命令读取
图 23展示了它在屏幕上的显示方式。每个垂直条对应于一帧在屏幕上渲染的时间。每新一行都在前一行的右侧。水平绿色线表示 16 毫秒的目标:如果超过这个时间,说明有东西在拖慢我们的帧渲染操作:
图 23:GPU 渲染工具
这个工具提供了关于每一帧渲染时发生情况的信息。垂直条被分为四个彩色段。从下到上,每一个都代表了完成不同子渲染操作所花费的时间:
-
蓝色条——绘制: 这表示绘制视图所花费的时间。当
View.onDraw()
方法需要做的工作太多时,这个时间会变长。 -
紫色条——准备: 这表示准备并将要在屏幕上显示的资源传输到渲染线程所花费的时间。
-
红色条——处理: 这是处理 OpenGL 操作所花费的时间。
-
橙色条——执行: 这是 CPU 等待 GPU 完成工作的时间。当 GPU 超负荷时,这个时间会变长。
adb shell dumpsys
方法有助于比较我们的优化结果,以证明我们做得是否好。当使用以下命令调用时,结果会打印在终端中:
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
这种渲染性能基准测试提供了比视觉测试更多的信息,如显示列表操作、内存使用情况、每个渲染操作的确切时间(这在视觉基准测试中会显示为一条条形图),以及关于视图层次结构的信息。
在 Android Marshmallow(API 级别 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
这更有效地解释了我们的应用程序帧渲染的实际性能。
在 Android Marshmallow 中增加了一个有用的先进特性,称为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
:这是起点。如果 UI 线程被占用,它可能与Vsync
值不同。 -
Vsync
:VSYNC 的时间值。 -
OldestInputEvent
:最老输入事件的时间戳。 -
NewestInputEvent
:最新输入事件的时间戳。 -
HandleInputStart
:将输入事件分派到应用程序的时间戳。 -
AnimationStart
:动画开始的时间戳。 -
PerformTrasversalsStart
:通过从DrawStart
减去来获得布局和测量时序的时间戳。 -
DrawStart
:开始绘图的时间戳。 -
SyncQueued
:发送到RenderThread
的同步请求的时间戳。 -
SyncStart
:绘图同步开始的时间戳。 -
IssueDrawCommandsStart
:GPU 开始绘图操作的时间戳。 -
SwapBuffers
:前后缓冲区交换的时间。 -
FrameCompleted
:帧完成的时间。
这些数据报告时间戳,因此需要通过减去两个时间戳来计算时序。结果可以向我们展示有关渲染性能的重要信息。例如,如果IntendedVsync
与Vsync
不同,则表示错过了一个帧,可能会出现卡顿。
可以通过在终端运行以下命令来执行这个新的dumbsys
命令:
adb shell dumbsys gfxinfo <PACKAGE_NAME> framestats
Systrace
Systrace 工具有助于分析渲染执行时序。它是 Android Device Monitor 的一部分,可以通过选择设备标签内的相关图标来访问。之后,将显示带有Systrace选项的对话框,如图 24所示:
图 24:Systrace 选项
这个工具从设备上所有要跟踪的进程收集信息,并将跟踪保存到一个 HTML 文件中,图形用户界面突出显示观察到的的问题,提供关于如何修复的重要信息。
结果类似于图 25中的内容。视角分为三个主要视图:上部包含追踪本身,下部包含另一部分高亮对象的详细信息,而右侧视图,称为警报区域,包含当前追踪中报告的警报摘要。上部主要描述了关于内核的详细信息,包含所有 CPU 信息;关于SurfaceFlinger,即 Android 合成器进程;然后是收集信息期间每个活动进程的详细信息,即使该进程是系统进程。每个进程都包含评估期间每个运行线程的详细信息:
图 25:Systrace 示例
让我们了解如何分析追踪:每个单一进程的每个绘制帧在Frames行中以带圆圈的F表示,如图 26 所示:
-
绿色边框表示它们没有问题
-
黄色和红色边框表示绘图时间超过了 16 ms 的目标,产生了滞后:
图 26:帧详情
每个错误的F都可以选择查看事件的详细描述。以下是 Systrace 针对红色帧报告的一个示例:
警报 | 调度延迟 |
---|---|
运行 | 6.401 ms |
未调度,但可运行 | 16.546 ms |
不可中断睡眠 | 唤醒 | 19.402 ms |
休眠 | 27.143 ms |
阻塞 I/O 延迟 | 1.165 ms |
帧 | |
描述 | 制作此帧的工作被暂停了几毫秒,导致出现卡顿。确保 UI 线程上的代码不会阻塞其他线程正在进行的工作,并且后台线程(例如,进行网络或位图加载)应以android.os.Process#THREAD_PRIORITY_BACKGROUND 或更低的优先级运行,这样它们就不太可能中断 UI 线程。这些后台线程在内核进程下的调度部分应显示优先级编号为 130 或更高。 |
如所述,此工具获取设备上每个进程和线程的信息,但如果我们想详细查看应用程序执行的部分以了解其在特定时间正在执行的工作,我们可以使用 API 告诉系统从哪里开始和结束追踪。这个 API 可以从 Android Jelly Bean(API 级别 18)开始使用,基于Trace
类。只需调用静态方法开始和结束追踪,如下所示:
Trace.beginSection("Section name");
try {
// your code
} finally {
Trace.endSection();
}
这样,新的追踪将包含一个带有你的部分名称及其详细信息的全新行。
记得在同一线程上调用Trace.beginSection()
和Trace.endSection()
方法。
摘要
在当代移动设备的理念中,应用程序是让用户访问我们远程服务的主要方式,因此它应该是获取这些服务的最主要手段。那么,用户对我们的应用程序的感知是成功的基础,它的用户体验和用户界面是衡量这一点的关键指标。因此,确保我们的应用程序渲染过程中没有延迟是非常重要的。
在本章中,我们所做的是了解设备如何渲染我们的应用程序,定义了每帧 16 毫秒的目标,并概述了硬件加速作为 Android 系统中主要的性能渲染改进。然后我们分析了开发者在构建应用程序 UI 时可能犯的主要错误,更详细地探讨了如何通过扁平化视图层次结构、在listview
中重用行视图以及定义开发自定义视图和布局的最佳实践来提高渲染速度。最后,我们介绍了平台提供的帮助我们发现改进优化和衡量应用渲染性能的有用工具。