移动开发
Android应用性能优化最佳实践
罗彧成 著
图书在版编目(CIP)数据
Android应用性能优化最佳实践 / 罗彧成著. —北京:机械工业出版社,2017.1
(移动开发)
ISBN 978-7-111-55616-9
I. A… II. 罗… III. 移动终端-应用程序-程序设计 IV. TN929.53
中国版本图书馆CIP数据核字(2016)第315986号
Android应用性能优化最佳实践
出版发行:机械工业出版社(北京市西城区百万庄大街22号 邮政编码:100037)
责任编辑:李 艺 责任校对:董纪丽
印 刷:中国电影出版社印刷厂 版 次:2017年2月第1版第1次印刷
开 本:186mm×240mm 1/16 印 张:13.75
书 号:ISBN 978-7-111-55616-9 定 价:59.00元
凡购本书,如有缺页、倒页、脱页,由本社发行部调换
客服热线:(010)88379426 88361066 投稿热线:(010)88379604
购书热线:(010)68326294 88379649 68995259 读者信箱:hzit@hzbook.com
版权所有 ? 侵权必究
封底无防伪标均为盗版
本书法律顾问:北京大成律师事务所 韩光/邹晓东
Preface?前 言
为什么写这本书
一个好的应用,除了要有吸引人的功能和交互之外,在性能上也应该有高的要求,即使应用非常具有特色,或者功能和业务具有唯一性,在产品前期可能吸引了部分用户,但用户体验不好的话,也会给产品带来很差的口碑,如果有在体验上更好的竞品,用户也会很快转移。那么一个好的应用应该如何定义呢?主要有三方面:
业务/功能
符合逻辑的交互
优秀的性能
众所周知,Android系统作为以移动设备为主的一款操作系统,硬件配置有一定的限制,虽然配置现在越来越高级,但仍然无法和PC相比,在CPU和内存上的使用不合理或者耗费资源多时,就会碰到内存不足导致的稳定性问题、CPU消耗太多导致的卡顿问题等。例如,我们发布一款产品后会收到很多的反馈,这些反馈来自很多渠道,有用户反馈,有应用发布平台的反馈通道等。
面对这些问题时,大家想到的都是联系用户,然后看日志,特别是有关性能类问题的反馈,原因也非常难找,日志大多用处不大,为什么呢?因为性能问题大部分是非必现的问题,定位时很难复现,而又没有关键的日志,当然就无法找到原因了。这些问题非常影响用户的体验和功能的使用,所以解决这些问题是非常重要的。当前市场上讲解性能优化的书太少,即使有些书讲到,很多也是一笔带过,没有深入分析和寻找解决方案,所以有必要用一本书来从多个维度讲解在性能上我们面临了什么问题,如何解决这些问题,并在实际的项目中来优化我们的应用,以提高用户体验。
本书面向的读者
本书适合所有Android应用开发从业人员及在校学生,特别是有一定Android应用开发经验的开发人员,高级开发人员也可以通过本书了解更多的性能调优知识。
本书特色
本书为进阶类图书,对于一些基础技术和基础理论知识不会做过多的阐述,特别是入门类的知识点,大家可以从其他书籍获取相关的知识。书中以性能优化为核心,深入剖析性能优化具体涉及的技术背景与优化方案,同时提供典型案例,帮助读者更深入地掌握Android应用开发技术,理解Android的运行机制和原理,掌握Android性能优化的思想,让开发者快速成长,打造高质量的Android应用。
本书的主要内容
可以把用户能体验到的性能问题主要总结为4个类别:
流畅
稳定
省电
省流量
性能问题的主要原因是什么,原因有相同的,也有不同的,但归根结底,不外乎内存使用、代码效率、合适的策略逻辑、代码质量这一类问题。本书讲解内容的目标和方向如下图所示。
从上图可以看到,打造一个高质量的应用应该以4个方向为目标:快、稳、省、小。
快:使用时避免出现卡顿,响应速度快,减少用户的等待时间,满足用户预期。
稳:降低crash率和ANR率,不要在用户使用过程中崩溃和无响应。
省:节省流量和耗电,减小用户使用成本,避免使用时导致手机发烫。
小:安装包小可以降低用户的安装成本。
这4类问题需要从根源上解决,也就是要解决图中第二个框里的问题:卡顿、内存使用不合理、代码质量差、代码逻辑不优秀、安装包过大。这些问题也是在开发过程中碰到最多的问题,在实现业务需求的同时,也需要考虑到这些点,多花时间去思考,避免功能完成后再来做优化和修复Bug,这个时候带来的成本会增加。如果是维护之前的代码,就需要使用一系列工具来发现问题点。
性能优化不是更新一两个版本可以解决的,是持续性的需求,结合到实际中,在一个新产品/项目开始时,由于人力和上线时间的限制,可以把优先级放低,但有些点是在写代码时就要考虑的,这就体现出程序员的技术功底。
本书强调性能调优的核心思想和方向如下:
发现问题→分析问题原因及背景→寻找最优解决方案→解决问题。
本书一共7章,在简单介绍了Android Studio的使用指南后,分别从绘制(UI)、内存、存储、稳定性、耗电以及安装包6个方面进行优化,从系统上深入分析绘制和内存的原理,一步步深入了解导致性能问题的本质原因,同时讲述了多种性能优化工具的使用,通过分析典型案例,得到有效的优化方案,从而实现更高质量的应用。书中所讲述的内容均基于Android 6.0系统。
勘误和资源下载
由于写作时间实在有限,在书稿交付时仍有些许不安,为此先为此书可能存在的错误或者描述不清楚的地方致以真诚的歉意,如果你发现此书存在瑕疵或者有任何建议,请发邮件到5482586@qq.com,我会尽快回复,非常期待大家的反馈。
本书代码的下载地址:https://github.com/lyc7898/AndroidTech。
致谢
由于时间的问题,本书写作时间非常长,非常感谢杨福川编辑对我的鼓励和宽容,并且分享了非常有用的碎片化写作方法,使我一直坚持把本书写完。同时感谢李艺编辑的校对和勘误,才完成了这本图文并茂、格式清晰的技术书籍。
感谢我的妻子李萍女士对我的理解和支持,在我几乎将所有的时间投入工作中时一直给予最大的宽容和鼓励,使我每天即使再忙再累时仍然可以回到温馨的家。同时感谢我的父母和岳父母,感谢他们对我无私的帮助,他们都是伟大的父母。
特别感谢我的爷爷罗志华老先生,在我的学习生涯中给予的无私帮助,在工作和生活上的谆谆教诲。还要感谢刘景瑜老师,在求学阶段的鼓励和教诲,告诉我有很多需要去做的事情。
最后感谢我现在工作的公司,在这里我得到了最快的成长,学习到非常多的东西,感谢公司领导及所有同事,在这里工作,能感受到大家每天都在成长。
Contents?目 录
前 言
第1章 Android Studio使用指南 1
1.1 Android Studio的优势 1
1.2 Android Studio使用入门 2
1.2.1 Android Studio安装 2
1.2.2 创建一个Android Studio工程 3
1.2.3 从Eclipse项目迁移到Android Studio 5
1.3 Android Studio实用技巧 7
1.3.1 代码管理 7
1.3.2 代码编辑技巧 8
1.3.3 调试技巧 10
1.4 本章小结 11
第2章 绘制优化 12
2.1 Android系统显示原理 13
2.1.1 绘制原理 13
2.1.2 刷新机制 16
2.1.3 卡顿的根本原因 20
2.2 性能分析工具 21
2.2.1 卡顿检测工具 22
2.2.2 TraceView 23
2.2.3 Systrace UI性能分析 26
2.3 布局优化 29
2.3.1 常用布局优化工具 30
2.3.2 布局优化方法 34
2.4 避免过度绘制 43
2.4.1 过度绘制检测工具 44
2.4.2 如何避免过度绘制 44
2.4.3 案例:无过度绘制View的实现 45
2.5 启动优化 49
2.5.1 应用启动流程 49
2.5.2 启动耗时监测 52
2.5.3 启动优化方案 56
2.6 合理的刷新机制 58
2.6.1 减少刷新次数 58
2.6.2 避免后台线程影响 59
2.6.3 缩小刷新区域 59
2.7 提升动画性能 60
2.7.1 帧动画 60
2.7.2 补间动画 60
2.7.3 属性动画 62
2.7.4 硬件加速 63
2.8 卡顿监控方案与实现 66
2.8.1 监控原理 67
2.8.2 代码实现 68
2.9 本章小结 79
第3章 内存优化 80
3.1 Android内存管理机制 81
3.1.1 Java对象生命周期 81
3.1.2 内存分配 82
3.1.3 内存回收机制 84
3.1.4 GC类型 86
3.2 优化内存的意义 87
3.3 内存分析工具 90
3.3.1 Memory Monitor 90
3.3.2 Heap Viewer 91
3.3.3 Allocation Tracker 94
3.4 避免内存泄漏 96
3.4.1 内存泄漏定义 97
3.4.2 使用MAT查找内存泄漏 97
3.4.3 常见内存泄漏场景 103
3.4.4 内存泄漏监控 106
3.5 优化内存空间 109
3.5.1 对象引用 109
3.5.2 减少不必要的内存开销 110
3.5.3 使用最优的数据类型 112
3.5.4 图片内存优化 117
3.6 图片管理模块设计与实现 120
3.6.1 实现异步加载功能 121
3.6.2 实现三重缓存 130
3.6.3 开源图片组件 140
3.7 本章小结 142
第4章 存储优化 144
4.1 存储方式 144
4.1.1 SharedPreferences 145
4.1.2 文件存储 145
4.1.3 SQLite(需要扩展) 146
4.1.4 ContentProvider 147
4.2 序列化 147
4.2.1 Serializable与Parcelable 148
4.2.2 Gson实现JSON的序列化和反序列化 148
4.2.3 Nano Proto Buffers 149
4.2.4 FlatBuffers 149
4.2.5 小结 150
4.3 SharedPreferences优化 150
4.4 数据库使用及优化 151
4.4.1 数据库实现 152
4.4.2 数据库优化 157
4.5 本章小结 161
第5章 稳定性优化 162
5.1 提高代码质量 162
5.1.1 代码审查 163
5.1.2 代码静态扫描工具 166
5.2 Crash监控 168
5.2.1 Java层Crash监控 168
5.2.2 Native层Crash监控 171
5.2.3 Crash上报机制 173
5.3 ANR剖析 173
5.3.1 ANR介绍 173
5.3.2 ANR分析 174
5.3.3 ANR监控 176
5.4 提高后台进程存活率 178
5.4.1 应用进程优先级 178
5.4.2 利用SyncAdapter提高进程优先级 180
5.5 本章小结 183
第6章 耗电优化 184
6.1 耗电检测工具 184
6.2 三大模块省电优化 187
6.2.1 显示 187
6.2.2 网络 188
6.2.3 CPU 189
6.3 应用常用优化方案 191
6.3.1 计算优化 191
6.3.2 避免WakeLock使用不当 192
6.3.3 使用Job Scheduler 193
6.4 Doze模式 197
6.4.1 Doze模式介绍 197
6.4.2 Doze模式应用策略 198
6.4.3 测试Doze模式应用工作状态 199
6.5 本章小结 199
第7章 安装包大小优化 200
7.1 应用装包的构成 200
7.2 减少安装包大小的常用方案 203
7.2.1 代码混淆 203
7.2.2 资源优化 204
7.2.3 其他优化 206
7.3 本章小结 207
结束语 208
第1章
Android Studio使用指南
假设我们要选择一个IDE来开发应用,目前主流的IDE有Eclipse、Android Studio、Idea,在2015年前,这三个IDE各有优缺点,但现在,Android Studio是首选,因为随着Google对Android Studio的大力完善和支持,优势已经越来越明显,但目前仍有不少开发人员在使用Eclipse。为什么要首选Android Studio,它有什么优势,具体要如何使用,本章将逐一揭示。
本书的例子都是使用Android Studio开发的。
1.1 Android Studio的优势
为什么本书要推荐使用Android Studio呢,Android Studio的核心是一个智能代码编辑器,可进行高级代码完成、重构和调试。这款功能强大的代码编辑器可以帮助你成为更高产的Android应用开发人员。虽然Android发布初期有很多bug,功能也不完善,但随着版本的更新,已经在各方面领先其他IDE,下面列出了Android Studio的几点优势。
稳定速度快:使用Eclipse的开发人员都会碰到突然假死、卡顿、内存占用高等一系列影响开发效率的老问题,Android Studio在这块性能上得到了明显的提升,并且Android Studio使用了单项目管理模式,在启动速度上明显比Eclipse快。
功能强大的UI编辑器:集合了Eclipse+ADT的优点,并且能更实时地展示界面布局效果。
完善的插件管理:Android Studio支持了多种插件,可直接在插件管理中下载所需的插件。
完善地支持多种代码管理工具:不需要任何操作,直接支持SVN、Git等主流的代码管理工具。
整合了Gradle构建工具:Gradle继承了Ant的灵活性和Maven的生命周期管理,不使用XML作为配置文件格式,采用了DSL格式,使得脚本更加灵活简洁。
智能:智能保存,智能补齐,在实际的编辑代码中熟练使用后,可极大提高代码编写效率。
内置终端:不需要自己打开一个终端来使用ADB等工具。
谷歌官方支持:是Google官方专门为Android应用开发打造的利器,也是目前Google官方唯一推荐,并且不再支持其他IDE。
Android Studio的更多优势会在开发工作的细节中体现出来,可以参考一些Android Studio的使用书籍和文档,以便了解它的强大之处。
1.2 Android Studio使用入门
1.2.1 Android Studio安装
这里我们以在Windows系统上安装Android Studio为例,具体的安装步骤如下:
1)安装JDK,且为JDK 1.6及以上版本。
2)下载Android Studio安装包:developer.android.com/sdk/installing/studio.html。
3)单击安装包开始安装,首先进入选择组件界面,如图1-1所示。
图1-1 选择安装组件
一般已经安装Eclipse或其他Android开发环境的,只需要安装默认的选项(Android Studio)即可。
4)单击Next,如果已经下载过SDK,并且在前面的组件没有选择安装SDK,会弹出一个设置本地SDK的界面,选择本地SDK目录,如图1-2所示。
图1-2 选择SDK目录
5)一直单击Next,直到安装完成,单击Finish,首次启动Android Studio会有一个配置的过程,需要等待一下。
因为本书主要是讲性能优化,所以这里只是简单地介绍下基本的安装。需要了解更多安装细节,可以参考Android开发官网的详细介绍文档:
http://developer.android.com/intl/zh-cn/sdk/installing/index.html?pkg=studio。
如果首次启动出现错误导致启动失败,一般来说是因为联网下载一些配置文件失败,可以使用记事本打开studio的安装目录下/bin中的idea.properties文件,在最后一行添加disable.android.f?irst.run=true。
1.2.2 创建一个Android Studio工程
又到熟悉的HelloWold环节了,安装完成后,如果没有新建过项目,会出现一个欢迎对话框,选择新建项目。如果已经有项目,则可以通过File→New→New Project来新建一个项目,填写相关名称,包括应用名(Application name)、公司名(Company Domain)、包名(Package name)、项目本地路径(Project Location),如图1-3所示。
下一步选择开发平台,选择Android Phone,SDK使用Android 6.0,如图1-4所示。
图1-3 新建项目
图1-4 选择应用运行平台
后面两步都使用默认设置,最后单击Finish进入我们的项目。进入后可以看到,左侧的项目文件区域显示的文件树结果和Eclipse并不相同,而且本地目录的文件层级也不相同。因为Android Studio使用了Gradle项目构建工具,而Eclipse使用Ant构建项目。
如果不习惯这种结构,可以通过Gradle设置与Eclipse相同的目录结构。
进入后发现不能编译,这是因为还未设置编译工具。需要先配置Gradle,由于Android Studio没有自带Gradle插件,所以会自动下载Gradle,但需要设置代理,并且设置代理下载仍然会比较慢,这里介绍离线配置Gradle的方法,步骤如下:
1)从官网下载Gradle:http://gradle.org/gradle-download/,这里我们选择1.3版本。
2)进入C:\Users\.gradle\wrapper\dists\gradle-2.2-all\1vevkra640w3rb9hkuw50q5we,最后这个文件夹是随机生成的,所以直接进入,把下载好的gradle-1.3-all.zip放到这个文件夹内即可。
3)重新启动Android Studio,就可以正常编译了。
Gradle是一种依赖管理工具,基于Groovy语言,抛弃了基于XML的各种繁琐配置,取而代之的是一种基于Groovy的内部领域特定语言(DSL),掌握Gradle脚本的编译和打包是应用开发非常必要的。
1.2.3 从Eclipse项目迁移到Android Studio
Eclipse项目和Android Studio相比,在项目结构、构建系统以及IDE的风格上都有了较大的变化。从Eclipse ADT项目迁移到Android Studio有两种方法,一是直接把Eclipse项目导入Android Studio中,二是从Eclipse导出Gradle项目,然后在Android Studio中可以直接打开导出的项目。
Android Studio是基于intelliJ IDEA开发的一款IDE,与Eclipse有很多不同的地方,特别是工程的目录结构,区别非常大。在Eclipse Android中的Project在Android Studio中是一个Module,并且Android Studio是单项目管理,每个窗口只能打开一个Project,这也是Android Studio打开更快的原因之一。在Android Studio中,有多种视图结构类型:Packages、Project Files、Scratches、Problems、Production、Tests,可以根据自己需求来使用不同的视图类型。
Eclipse和Android Studio中项目结构的对应关系如表1-1所示。
表1-1 Eclipse与Android Studio项目结构对应的关系
Eclipse ADT Android Studio
Workspace Project
Project Module
Project-specif?ic JRE Module JDK
Classpath variable Path variable
Project dependency Module dependency
Library Module Library
AndroidManifest.xml app/src/main/AndroidManifest.xml
assets/ app/src/main/assets
res/ app/src/main/res/
src/ app/src/main/java/
tests/src/ app/src/androidTest/java/
从表1-1可以看出,结构发生了非常大的变化,如果直接按对应关系来迁移项目中的文件对应到Android Studio的目录结果,工作量不小并且容易出错。因此,为了简化迁移过程,Android的Studio提供了一个快速导入Eclipse项目的方法,可以将Eclipse和Ant构建脚本快速转换到Android Studio上。导入步骤如下:
1)在Android Studio菜单栏选择File→New→Import Project。如果是首次使用,在欢迎页可直接选择Import Project。
2)选择需要导入的Eclipse工程目录,AndroidManifest.xml必须是在根目录下。
3)在导入过程中会提示是否重新配置Eclipse中的所有第三方库和项目依赖关系,如果勾选,就会重新生成新的第三方管理库和依赖关系,依赖关系和第三方库也可以在build.gradle文件上修改。如图1-5所示,有三个选项可以选择,前两项目是jar包的引用规则,建议勾选,这样就不用管理这些jar包了。第三项是指是否需要把Module名创建为camelCase风格(首字母小写的命名规则)。
4)单击Finish按钮,完成后会生成一个import-summary.txt文件,它是导入过程中产生的日志,这个文件比较重要,如果项目比较大,导入过程不一定顺利,可以通过这个文件来发现导入过程中出现的问题。
这样就把Eclipse Android项目导入Android Studio了,注意前面讲过,导入完成后生成了一个描述导入过程的日志文件:import-summary.txt,文件中包含以下几个内容:
Ignored Files:描述在导入过程中忽略了哪些文件,这些文件是没有拷贝过来的,如果有需要,就要手动添加过来,一般会忽略Eclipse ADT相关的配置文件和一些自定义的文件/文件夹。
Replaced Jars with Dependencies和Replaced Libraries with Dependencies:列出那些替换的JAR包,在Android Studio中,如果在仓库中有这个库或者SDK,就会被替换。
Moved Files:文件移动路径列表。
Next Steps:接下来需要做些什么,如果没有问题,一般告诉你可以编译了。
Bugs:如果不能编译,就会列出当前项目存在的问题。
上面介绍的这种导入项目的方式也是Android官网推荐的,理由是简单高效,有问题也容易发现,最主要是不会改变原来的文件组织架构。
更多有关从Eclipse ADT迁移到Android Studio的内容可以从Android开发官网上查询:http://developer.android.com/intl/zh-cn/sdk/installing/migrate.html#prerequisites。
这样导入的项目还是会保留Eclipse的构建方式,比如在Eclipse上使用Ant构建,迁移后还是会使用Ant构建,如果先从Eclipse导出成Gradle项目就使用Gradle构建,当然也可以手动改构建方式。
1.3 Android Studio实用技巧
1.3.1 代码管理
Android Studio支持Git、SVN等主流的源码管理工具,让开发者可以不用离开Android Studio就可以提交和管理代码。我们熟悉的开源社区Github上的项目就是使用Git来管理的,下面是使用Android Studio把本地代码托管到Github上的流程。
1)在本地安装Git,可以从官网下载安装包:https://git-scm.com/downloads。
2)配置File→Setting→Version Control,分别配置Git目录(安装路径下Bin目录的GIt.exe文件)和GitHub账号(没有GitHub账号则需要先申请,开源项目都是免费使用的),如图1-6所示。配置完后可以单击测试按钮,配置没有问题就弹出测试成功提示框。
图1-6 配置代码管理工具Git
3)初始化Git项目:选择菜单栏→CVS→Enable Control Integration,在弹出的配置对话框中选择Git,完成后工具栏会新增如图1-7所示的快捷工具。
图1-7 VCS快捷工具
4)如果需要过滤掉不需要上传的文件或者目录,可以在File→Setting→VersionControl→Ignored Files中选择不需要同步的文件或文件夹,如图1-8所示,也可以通过修改项目目录下的.gitigonre文件来实现。建议使用前者,因为根据笔者实际的使用经验,修改.gitigonre文件有时会失效,而且管理也没有前者方便。
图1-8 过滤不需要同步的文件/文件夹
可以通过选择VCS→Git→Compare With Branch,指定Branch(分支)和本地代码做比较。提交前可以双击需要提交的文件来对比改动代码行。
5)同步代码到Github:选择VCS→Import into Version Control→Share Project on GitHub,如图1-9所示。
6)在图1-9中,单击Share按钮,弹出提交文件列表(可以看到前面过滤的文件不在列表内),同步完成后就可以在Github上看到我们同步的项目了。
7)后面需要提交代码,首先提交,然后选择VCS→Git→Push,即可同步代码到GitHub。
1.3.2 代码编辑技巧
如果您已经习惯了Eclipse的快捷键,可以通过File→Setting→Keymap来设置成Eclipse的快捷键,如图1-10所示。
图1-10 设置成Eclipse快捷键
Android Studio是一款非常智能的编辑器,在编写代码过程中,如果能熟练掌握一些小技巧,就可以提高写代码的速度,这里介绍一些常用的技巧。
内容补全:在写完一个方法名,或者一个判断语句(if、while、for)后,自动生成{}块并使光标停在{}中间,在写完方法名或需要用{}前,使用Ctrl + Shift + Enter组合键。
列选择:可以选择列块,按住Alt键选择代码块。
代码补全:使用<Enter>键从光标处插入补全的代码,对原来的代码不做任何操作。而使用<Tab>键时从光标处插入补全的代码,但是会删除后面的代码,直到遇到点号、圆括号、分号或空格为止。
查看方法调用路径:按Ctrl + Alt + H组合键。
布局文件编辑器:Android Studio提供了更加高级的布局文件编辑器,除了像Eclipse一样可以实时查看布局文件外,Android Studio的布局查看包括了很多种分辨率和手机模拟器,并且还可以自定义设备分辨率。
预览某个方法或者类的实现:在不离开当前文件或者当前类的情况下,快速预览某个方法或者类的实现,减少频繁切换文件/类,使用Ctrl + Shift + I组合键。
快速使用命令:对于没有设置快捷键或者忘记快捷键的菜单功能或者命令,可以通过输入其名字或者模糊匹配以快速调用。使用Ctrl + Shift + A组合键,比如输入‘commi’,会列出所有和commit相关的操作。
自动导包:进入settings→Editor→General→Auto Import设置页,选中Show import popup、Show import pupup、Optimize imports on the f?ly和Add unambiguous imports on the f?ly,就可以自动导包,并且会自动去掉无用的包。
Android Studio快捷键的更多使用技巧,可以通过Help-->Tip of the day,每日学习一个,在开发过程中逐渐掌握。
1.3.3 调试技巧
Android Studio另一个强大的地方,就是调试更加方便和高效,以下是调试过程中的常用场景。
调试程序:Android Studio可以随时进入调试模式,即使未以调试方式启动应用。我们可以通过菜单→Build→Attach to Android Process,也可以直接通过工具栏快捷工具Attach to Android Process进入调试模式。
条件断点:调试中非常有用的方式,可以自己直接赋值,减小调试时间,通过右键断点(也可以在Debug视图上的BreakPoint列表上)对一个断点加入条件,即填写Condition中的条件。只有当满足条件时,才会进入断点中。
分析传入/传出数据流:Menu→Analyze→Analyze Data Flow to Here这个操作将会根据当前选中的变量、参数或者字段,分析出其传递到此处的路径。如果你想知道某个参数是怎么传递到一段陌生的代码时,这是一个非常有用的操作。传出数据流(Analyze data f?low from here)则会分析当前选中的变量往下传递的路径,直到结束。
日志断点(Logging Breakpoints):一种不是暂停的断点,只是打印日志,当想打印一些日志信息,但是不想添加log代码后重新部署时,则可以在断点上单击鼠标右键,取消选中Suspend,然后勾选Log evaluated Expression,并在输入框中输入要打印的日志信息。
修改变量值:在调试过程中,修改变量值可以快速调试各个Case,提高异常处理的调试效率。在调试时进入断点后,如图1-11所示,如果需要修改level变量的值,右键单击level,在弹出的菜单中选择Set Value或者使用快捷键F2,就可以修改成需要的值,并且该值只对当次有效。
图1-11 调试时修改变量值
1.4 本章小结
Android Studio的安装并不复杂,首次使用可能会不适应并觉得效率甚至低于以前使用的其他IDE,但相信使用一段时间后,一定能体验到它的灵活和强大之处,同时Android Studio的技巧非常多,大家可以多从网上找到各种技巧。
古人云,“工欲善其事,必先利其器”,Android应用开发者的“器”则是指Android Studio,熟练运用开发工具,能极大程度提高开发效率。在掌握Android Studio这个开发神器后,再通过接下来的章节的学习,从UI性能、内存、稳定性等多个维度的优化,使Android应用程序更加高效、流畅地运行,从而打造出一款高质量的Android应用。
第2章
绘?制?优?化
Android应用启动慢,使用时经常卡顿,是非常影响用户体验的,应该尽量避免出现。卡顿的场景有很多,按场景可以分成4类:UI绘制、应用启动、页面跳转、事件响应,如
图2-1所示。在这四种场景下又有多个小分类,基本上覆盖了卡顿的各个场景。
图2-1 卡顿主要场景
这4种卡顿场景的根本原因又可以分成两大类。
界面绘制:主要原因是绘制的层级深、页面复杂、刷新不合理,由于这些原因导致卡顿的场景更多出现在UI和启动后的初始界面以及跳转到页面的绘制上。
数据处理:导致这种卡顿场景的原因是数据处理量太大,一般分为三种情况,一是数据处理在UI线程(这种应该避免),二是数据处理占用CPU高,导致主线程拿不到时间片,三是内存增加导致GC频繁,从而引起卡顿。
本章主要通过优化UI界面编程来减少卡顿,以场景为纬度,通过工具深入分析症结所在,找到导致问题的根本原因,利用涉及的相关技术背景,以及了解当前业内主流解决方案,然后结合实例来找到最终的优化方案,使应用流畅。
引起卡顿的原因有很多,但不管怎么样的原因和场景,最终都是通过设备屏幕上的显示来到达用户,归根到底就是显示有问题,所以,要解决卡顿,就要先了解Android系统的显示原理。
2.1 Android系统显示原理
说到显示原理,相信大家从网上或其他书籍上看过相关的知识,但大部分人看得云里雾里,是因为整个显示系统很复杂吗?确实很复杂,但我们只需要了解整体流程,抓住关键知识,从应用角度上来讲,需要掌握的不多,如果自己有兴趣,可以阅读专门介绍系统框架的书籍,结合源码来分析,这里就不过多地介绍系统层的知识了。下面我们首先介绍在应用开发上需要涉及的知识点和整体流程。
Android的显示过程可以简单概括为:Android应用程序把经过测量、布局、绘制后的surface缓存数据,通过SurfaceFlinger把数据渲染到显示屏幕上,通过Android的刷新机制来刷新数据。也就是说应用层负责绘制,系统层负责渲染,通过进程间通信把应用层需要绘制的数据传递到系统层服务,系统层服务通过刷新机制把数据更新到屏幕。
通过阅读Android系统的源码可以了解显示的流程,Android的图形显示系统采用的是Client/Server架构。SurfaceFlinger(Server)由C++代码编写。Client端代码分为两部分,一部分是由Java提供给应用使用的API,另一部分则是由C++写成的底层具体实现。下面通过介绍绘制原理和刷新机制来学习整个显示过程。
2.1.1 绘制原理
绘制任务是由应用发起的,最终通过系统层绘制到硬件屏幕上,也就是说,应用进程绘制好后,通过跨进程通信机制把需要显示的数据传到系统层,由系统层中的SurfaceFlinger服务绘制到屏幕上。那么应用层和系统层中的流程是什么样的呢?接下来将进行具体介绍。
1.?应用层
先来看一个UI界面的典型构成框架,也可以是一个Activity的构成。如图2-2所示,有很多不同层次的基本元素——View,整体是一个树型结构,有不同的嵌套,存在着父子关系,子View在父View中,这些View都经过一个相同的流程最终显示到屏幕上,这也意味着要完整地显示所有数据,就要对其中的View都进行一次绘制工作,并且针对每个View的操作都是一个递归过程。
在Android的每个View绘制中有三个核心步骤(见图2-3),通过Measure和Layout来确定当前需要绘制的View所在的大小和位置,通过绘制(Draw)到surface,在Android系统中整体的绘图源码是在ViewRootImp类的performTraversals()方法,通过这个方法可以看出Measure和Layout都是递归来获取View的大小和位置,并且以深度作为优先级。可以看出,层级越深,元素越多,耗时也就越长。
图2-2 页面构成框架 图2-3 View绘制流程
(1)Measure
用深度优先原则递归得到所有视图(View)的宽、高;获取当前View的正确宽度childWidthMeasureSpec和高度childHeightMeasureSpec之后,可以调用它的成员函数Measure来设置它的大小。如果当前正在测量的子视图child是一个视图容器,那么它又会重复执行操作,直到它的所有子孙视图的大小都测量完成为止。
(2)Layout
用深度优先原则递归得到所有视图(View)的位置;当一个子View在应用程序窗口左上角的位置确定之后,再结合它在前面测量过程中确定的宽度和高度,就可以完全确定它在应用程序窗口中的布局。
(3)Draw
目前Android支持了两种绘制方式:软件绘制(CPU)和硬件加速(GPU),其中硬件加速在Android 3.0开始已经全面支持,很明显,硬件加速在UI的显示和绘制的效率远远高于CPU绘制,但硬件加速并非如大家所想的那么完善,它也存在明显的缺点:
耗电:GPU的功耗比CPU高。
兼容问题:某些接口和函数不支持硬件加速。
内存大:使用OpenGL的接口至少需要8MB内存。
所以是否使用硬件加速,需要考虑一些接口是否支持硬件加速,同时结合产品的形态和平台,比如TV版本就不需要考虑功耗的问题,而且TV屏幕大,使用硬件加速容易实现更好的显示效果。
2.?系统层
真正把需要显示的数据渲染到屏幕上,是通过系统级进程中的SurfaceFlinger服务来实现的,SurfaceFlinger的具体实现和工作原理因为和应用层关系不大,所以这里不做过多介绍,只需要了解它主要是做些什么工作。
响应客户端事件,创建Layer与客户端的Surface建立连接。
接收客户端数据及属性,修改Layer属性,如尺寸、颜色、透明度等。
将创建的Layer内容刷新到屏幕上。
维持Layer的序列,并对Layer最终输出做出裁剪计算。
既然是两个不同进程,那么肯定需要一个跨进程的通信机制来实现数据传输,在Android的显示系统,使用了Android的匿名共享内存:SharedClient,每一个应用和SurfaceFlinger之间都会创建一个SharedClient,如图2-4所示。从图2-4中可以看出,在每个SharedClient中,最多可以创建31个SharedBufferStack,每个Surface都对应一个SharedBufferStack,也就是一个window。
一个SharedClient对应一个Android应用程序,而一个Android应用程序可能包含多个窗口,即Surface。也就是说SharedClient包含的是SharedBufferStack的集合。因为最多可以创建31个SharedBufferStack,这也意味着一个Android应用程序最多可以包含31个窗口,同时每个SharedBufferStack中又包含了两个(低于4.1版本)或者三个(4.1及以上版本)缓冲区,即在后面的显示刷新机制中会提到的双缓冲和三重缓冲技术。
图2-4 Android显示框架
最后总结起来显示整体流程分为三个模块:应用层绘制到缓存区,SurfaceFlinger把缓存区数据渲染到屏幕,由于是两个不同的进程,所以使用Android的匿名共享内存SharedClient缓存需要显示的数据来达到目的。
SurfaceFlinger把缓存区数据渲染到屏幕(流程如图2-5所示),主要是驱动层的事情,这里不做太多解释。
从图2-5中可以看出,绘制过程首先是CPU准备数据,通过Driver层把数据交给CPU渲染,其中CPU主要负责Measure、Layout、Record、Execute的数据计算工作,GPU负责Rasterization(栅格化)、渲染。由于图形API不允许CPU直接与GPU通信,而是通过中间的一个图形驱动层(Graphics Driver)来连接这两部分。图形驱动维护了一个队列,CPU把display list添加到队列中,GPU从这个队列取出数据进行绘制,最终才在显示屏上显示出来。
图2-5 渲染数据流程图
知道了绘制的原理后,那么到底绘制一个单元多长时间才是合理的,首先需要了解一个名词:FPS。FPS(Frames Per Second)表示每秒传递的帧数。在理想情况下,60 FPS就感觉不到卡,这意味着每个绘制时长应该在16ms以内,如图2-6所示。
但是Android系统很有可能无法及时完成那些复杂的界面渲染操作。Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需的60FPS。即为了实现60FPS,就意味着程序的大多数绘制操作都必须在16ms内完成。
如果某个操作花费的时间是24ms,系统在得到VSYNC信号时就无法进行正常渲染,这样就发生了丢帧现象。那么用户在32ms内看到的会是同一帧画面。主要场景在执行动画或者滑动ListView时更容易感知到卡顿不流畅,是因为这里的操作相对复杂,容易发生丢帧的现象,从而感觉卡顿。有很多原因可以导致CPU或者GPU负载过重从而出现丢帧现象:可能是你的Layout太过复杂,无法在16ms内完成渲染;可能是UI上有层叠太多的绘制单元;还有可能是动画执行的次数过多。
最终的数据是刷新机制通过系统去刷新数据,刷新不及时也是引起卡顿的一个主要原因。接下来将详细介绍系统是怎么刷新的以及在什么情况下会导致卡顿发生。
2.1.2 刷新机制
Google发布Android操作系统后,Android OS系统一直在不断优化、更新。但直到Android 4.0版本发布,有关UI显示不流畅的问题仍未得到根本解决。在整个Android版本升级过程中,Android在显示系统方面做了不少优化和改进,比如支持硬件加速等技术,但本质原因似乎和硬件关系并不大,也没有得到太多改善。而与高端硬件配置的Android机器价格相近的iPhone,其UI的流畅性强却是有目共睹的。
从Android 4.1(Jelly Bean)开始,Android OS开发团队便力图在每个版本中解决一个重要问题。作为严重影响Android口碑问题之一的UI流畅性差的问题,首先在Android 4.1版本中得到了有效处理。其解决方法即在4.1版本推出的Project Butter。Project Butter对Android Display系统进行了重构,引入三个核心元素:VSYNC、Triple Buffer和Choreographer。其中,VSYNC是理解Project Buffer的核心。VSYNC是Vertical Synchronization(垂直同步)的缩写,是一种在PC上已经很早就广泛使用的技术,读者可简单地把它认为是一种定时中断。Choreographer起调度的作用,将绘制工作统一到VSYNC的某个时间点上,使应用的绘制工作有序。接下来,本文将围绕VSYNC来介绍Android Display系统的工作方式。
在讲解刷新机制之前,先介绍几个名词以及VSYNC和Choreographer主要功能及工作方式。
双缓冲:显示内容的数据内存,为什么要用双缓冲,我们知道在Linux上通常使用Framebuffer来做显示输出,当用户进程更新Framebuffer中的数据后,显示驱动会把Framebuffer中每个像素点的值更新到屏幕,但这样会带来一个问题,如果上一帧的数据还没有显示完,Framebuffer中的数据又更新了,就会带来残影的问题,给用户直观的感觉就会有闪烁感,所以普遍采用了双缓冲技术。双缓冲意味着要使用两个缓冲区(在SharedBufferStack中),其中一个称为Front Buffer,另外一个称为Back Buffer。UI总是先在Back Buffer中绘制,然后再和Front Buffer交换,渲染到显示设备中。即只有当另一个buffer的数据准备好后,通过io_ctrl来通知显示设备切换Buffer。
VSYNC:从前面的双缓冲介绍中可以了解到,只有当另一个buffer准备好后,才能通知刷新,这就需要CPU以主动查询的方式来保证数据是否准备好,因为这种机制效率很低,所以引入了VSYNC。VSYNC是Vertical Synchronization(垂直同步)的缩写,可以简单地把它认为是一种定时中断,一旦收到VSYNC中断,CPU就开始处理各帧数据。
Choreographer:收到VSYNC信号时,调用用户设置的回调函数。一共有以下三种类型的回调:
CALLBACK_INPUT:优先级最高,与输入事件有关。
CALLBACK_ANIMATION:第二优先级,与动画有关。
CALLBACK_TRAVERSAL:最低优先级,与UI控件绘制有关。
接下来通过时序图来分析刷新的过程,这些时序图是Google在2012 Google I/O讲解新的显示系统提供的,图2-7所示的时序图有三个元素:Display(显示设备),CPU-CPU准备数据,GPU-GPU准备数据。最下面的时间为显示时间,根据理想的60FPS,以16ms为一个显示周期。
图2-7 没有VSync信息的刷新
(1)没有VSync信号同步
我们以16ms为单位来进行分析:
1)从第一个16ms开始看,Display显示第0帧,CPU处理完第一帧后,GPU紧接其后处理继续第一帧。三者都在正常工作。
2)时间进入第二个16ms:因为在上一个16ms时间内,第1帧已经由CPU、GPU处理完毕。所以Display可以正常显示第1帧。显示没有问题,但在本16ms期间,CPU和GPU并未及时绘制第2帧数据(前面的空白区在忙别事情去了),而是在本周期快结束时,CPU/GPU才去处理第2帧数据。
3)时间进入第3个16ms,此时Display应该显示第2帧数据,但由于CPU和GPU还没有处理完第2帧数据,故Display只能继续显示第一帧的数据,结果使得第1帧多画了一次(对应时间段上标注了一个Jank),这就导致错过了显示第二帧。
通过上述分析可知,在第二个16ms时,发生Jank的关键问题在于,为何在第1个16ms段内,CPU/GPU没有及时处理第2帧数据?从第二个16ms开始有一段空白的时间,可以说明原因所在,那就是CPU可能是在忙别的事情,不知道该到处理UI绘制的时间了。可CPU一旦想起来要去处理第2帧数据,时间又错过了。为解决这个问题,4.1版本推出了Project Butter,核心目的就是解决刷新不同步的问题。
(2)有VSync信号同步
加入VSync后,从图2-8可以看到,一旦收到VSync中断,CPU就开始处理各帧的数据。大部分的Android显示设备刷新率是60Hz(图2-7的时间轴也是60ms),这也就意味着每一帧最多只能有1/60=16ms左右的准备时间。假如CPU/GPU的FPS高于这个值,显示效果将更好。但是,这时又出现了一个新问题:CPU和GPU处理数据的速度都能在16ms内完成,而且还有时间空余,但必须等到VSYNC信号到来后,才处理下一帧数据,因此CPU/GPU的FPS被拉低到与Display的FPS相同。
从图2-9采用双缓冲区的显示效果来看:在双缓冲下,CPU/GPU FPS大于刷新频率同时采用了双缓冲技术以及VSync,可以看到整个过程还是相当不错的,虽然CPU/GPU处理所用的时间时短时长,但总体来说都在16ms以内,因而不影响显示效果。A和B分别代表两个缓冲区,它们不断交换来正确显示画面。但如果CPU/GPU的FPS小于Display的FPS,情况又不同了,如图2-10所示。
图2-8 有VSync的绘制
图2-9 双缓冲下的时序图
图2-10 双缓冲下CPU/GPU FPS小于刷新频率时序图
从图2-10可以看到,当CPU/GPU的处理时间超过16ms时,第一个VSync就已经到来,但缓冲区B中的数据却还没有准备好,这样就只能继续显示之前A缓冲区中的内容。而后面B完成后,又因为还没有VSync信号,CPU/GPU这个时候只能等待下一个VSync的来临才开始处理下一帧数据。因此在整个过程中,有一大段时间被浪费。总结这段话
就是:
1)在第二个16ms时间段内,Display本应显示B帧,但因为GPU还在处理B帧,导致A帧被重复显示。
2)同理,在第二个16ms时间段内,CPU无所事事,因为A Buffer由Display在使用。B Buffer由GPU使用。注意,一旦过了VSYNC时间点,CPU就不能被触发以及处理绘制工作了。
为什么CPU不能在第二个16ms处即VSync到来就开始工作呢?很明显,原因就是只有两个Buffer。如果有第三个Buffer存在,CPU就可以开始工作,而不至于空闲。于是在Andoird 4.1以后,引出了第三个缓冲区:Triple Buffer。Triple Buffer利用CPU/GPU的空闲等待时间提前准备好数据,并不一定会使用。
在大部分情况下,只使用到双缓存,只有在需要时,才会用三缓冲来增强,这时可以把输入的延迟降到最少,保持画面的流畅。
引入Triple Buffer后的刷新时序如图2-11所示。
图2-11 使用Triple Buffer时序图
在第二个16ms时间段,CPU使用C Buffer绘图。虽然还是会多显示一次A帧,但后续显示就比较顺畅了。是不是Buffer越多越好呢?回答是否定的。由图2-11可知,在第二个时间段内,CPU绘制的第C帧数据要到第四个16ms才能显示,这比双缓存情况多了16ms延迟。所以缓冲区不是越多越好,要做到平衡到最佳效果。
从以上的分析来看,Android系统在显示机制上解决了Android UI不流畅的问题,并且从Google I/O2012给出的视频来看,其效果也达到预期。但实际在应用开发过程中仍然存在卡顿的现象。因为VSync中断处理的线程优先级一定要最高,否则即使接收到VSync中断,不能及时处理,也是徒劳无功。
2.1.3 卡顿的根本原因
那卡顿的根本原因是什么呢,从Android系统的显示原理中可以看到,影响绘制的根本原因有以下两方面:
绘制任务太重,绘制一帧内容耗时太长。
主线程太忙了,导致VSync信号来时还没有准备好数据导致丢帧。
耗时太长,需要从UI布局和绘制上来具体分析,详见后面内容。这里我们主要讨论下第二个方面。我们知道所有的绘制工作都是由主线程,也就是UI线程来负责,主线程的关键职责是处理用户交互,在屏幕上绘制像素,并进行加载显示相关的数据。在Android应用开发中,特别需要避免任何阻碍主线程的事情,这样应用程序才能保持对用户操作的即时响应。
在实际的开发过程中,我们需要知道主线程应该做什么,总结起来主线程主要做以下几个方面的工作:
UI生命周期控制
系统事件处理
消息处理
界面布局
界面绘制
界面刷新
除了这些以外,尽量避免将其他处理放到主线程中,特别是复杂的数据计算和网络请求。
2.2 性能分析工具
从前一节可以看到,Android系统在4.1以后从框架上解决了由于系统问题导致的卡顿现象,但在实际的使用过程中,在用户的感受上,卡顿仍然是应用开发中主要面临的问题,而原因从上一节的分析中也知道本质是VSync信号到来时,不能及时处理绘制事件导致,本节先抛出以下两个问题:
1)应用层做了什么会导致VSync事件不能及时处理?
2)卡顿能监控吗?
性能问题并不容易复现,也不好定位,光从几个场景不能完全覆盖所有的问题,因此在做性能优化时,最直接有效的方法,就是尽量复现存在性能问题的场景,并监控此过程中程序的执行流程,如果能够方便地分析程序中函数的调用关系和执行时间,自然也就很容易找出性能瓶颈。
分析问题和确认问题是否解决,都借助了相应的调试工具,比如查看Layout层次的Hierarchy View、Android系统上带的GPU Prof?ile工具和静态代码检查工具Lint等。这些工具对性能优化都起到非常重要的作用。本节将介绍这些工具和另外两个性能优化非常重要的工具:TraceView和Systrace。这两个工具除了在UI上,对于在后面将要讲到的启动优化、动画优化等上都是很重要的工具,可以说大部分的性能分析都离不开这几个工具,接下来学习几个常用的与流畅度优化相关的工具的使用方法,在后面实际的优化方案中也会介绍其他辅助工具。
2.2.1 卡顿检测工具
要做性能优化,就非常有必要借助于一系列辅助工具,Android提供了多个开发辅助工具,在性能调优过程中非常重要,下面介绍几个常用的工具,在后面的具体优化过程中会多次使用到。
我们已经知道,从应用层绘制一个页面(View),主要有三个过程:CPU准备数据→GPU从数据缓存列表获取数据→Display设备绘制,这三个过程的耗时可以通过一个手机开发辅助工具查看:Prof?ile GPU Rendering。Prof?ile GPU Rendering是Android 4.1系统开始提供的一个开发辅助功能,在设置中打开开发者选项,如图2-12所示。
Prof?ile GPU Rendering功能特点如下:
它是一个图形监测工具,能实时反应当前绘制的耗时。
横轴表示时间,纵轴表示每一帧的耗时(单位为ms)。
随着时间推移,从左到右的刷新呈现。
提供了一个标准的耗时,如果高于标准耗时,表示当前这一帧丢失。
如果设置中没有开发者选项,可以通过设置页面中的“关于”选项,单击版本号七次即可打开开发者选项,在后面的章节中还会使用到开发者选项中的其他辅助工具。
打开Prof?ile GPU Rendering后可以看到实时刷新的彩色图,如图2-13所示。每一根竖线表示一帧,由多个颜色组成,不同颜色的解释如下:
图2-12 打开Prof?ile GPU rendering 图2-13 Prof?ile GPU rendering
每一条柱状图都由4种颜色组成:红、黄、蓝、紫,这些线对应每一帧在不同阶段的实际耗时。
蓝色代表测量绘制的时间,它代表需要多长时间去创建和更新DisplayList。在Android中,一个视图在进行渲染之前,它必须被转换成GPU熟悉的格式,简单来说就是几条绘图命令,蓝色就是记录了在屏幕上更新视图需要花费的时间,也可以理解为执行每一个View的onDraw方法,创建或者更新每一个View的Display List对象。在蓝色的线很高时,有可能是因为需要重新绘制,或者自定义视图的onDraw函数处理事情太多。
红色代表执行的时间,这部分是Android进行2D渲染Display List的时间,为了绘制到屏幕上,Android需要使用OpenGl ES的API接口来绘制Display List,这些API有效地将数据发送到GPU,最终在屏幕上显示出来。当红色的线非常高时,可能是由重新提交了视图而导致的。
橙色部分表示处理时间,或者是CPU告诉GPU渲染一帧的地方,这是一个阻塞调用,因为CPU会一直等待GPU发出接到命令的回复,如果柱状图很高,就意味着GPU太繁忙了。
紫色段表示将资源转移到渲染线程的时间,只有Android 4.0及以上版本才会提供。
在实际开发中,从图上虽然可以看到绘制的时间,但对不便于进行数据分析,比如进入某一个页面,柱形图虽然实时绘制出来,但不能更好地分析,这里可以通过:adb shell dumpsys gfxinfo com.**.**(包名)把具体的耗时输出到日志中来分析。
任何时候超过绿线(警戒线,对应时长16ms),就有可能丢失一帧的内容,虽然对于大部分应用来说,丢失几帧确实感觉不出卡顿,但保持UI流畅的关键就在于让这些垂直的柱状条尽可能地保持在绿线下面。
GPU Prof?ile工具能够很好地帮助你找到渲染相关的问题,但是要修复这些问题就不是那么简单了。需要结合另一个耗时工具和代码来具体分析,找到性能的瓶颈,并进行优化。在GPU Prof?ile Render发现有问题的页面后,可以通过另外一个工具Hierarchy Viewer来查看页面的布局层次和每个View所花的时间,在后面的布局优化章节,将通过实例来讲解和学习使用方法。
2.2.2 TraceView
TraceView是AndroidSDK自带的工具,用来分析函数调用过程,可以对Android的应用程序以及Framework层的代码进行性能分析。它是一个图形化的工具,最终会产生一个图表,用于对性能分析进行说明,可以分析到应用具体每一个方法的执行时间,使用可以非常直观简单,分析性能问题很方便。
1.?使用方法
在使用TraceVeiw分析问题之前需要得到一个*.trace的文件,然后通过TraceView来分析trace文件的信息,trace文件的获取有两种方式:
(1)在DDMS中使用
1)连接设备。
2)打开应用。
3)打开DDMS(若在Android Studio中则先打开Android Device Monitor)。
4)单击Strart Method Prof?iling按钮,如图2-14所示。
图2-14 在DDMS中打开TraceView
5)在应用中操作需要监控的点,比如进入一个Activity或者滑动一个列表,完成后单击Stop Method Prof?iling按钮,如图2-15所示。
图2-15 结束一次TraceView
6)结束会自动跳转到TraceView视图。
这种方法使用方便,但监控范围不够精确,如果需要精确监控某一个路径,就需要使用下一个方法:在代码中加入调试语句保存Trace文件。
(2)代码中加入调试语句保存trace文件
有时在开发过程中不好复现的问题,需要在关键的路径上获取TraceView数据,在测试时复现此问题后直接拿到Trace文件查看对应的数据。这时可以在代码中使用TraceView工具并生成对应的trace文件。在android.os.Debug类中提供了相应的方法,过程很简单步骤如下:
1)在需要开始监控的地方调用startMethodTracing()。
2)在需要结束监控的地方调用stopMethodTracing()。
3)系统会在SD卡中创建<trace-name>.trace文件。
4)使用traceveiw打开该文件进行分析。
调用代码如下:
// start tracing to "/sdcard/ui_performance.trace"
Debug.startMethodTracing("ui_performance");
// ...
// stop tracing
Debug.stopMethodTracing();
在应用程序中调用startMethodTracing()时,系统会在指定的路径上创建一个名为<trace_f?ilename>.trace文件。这个文件包含了方法名跟踪数据,以及与线程和方法名的映射表。然后系统开始缓存应用产生的跟踪数据,直到应用程序调用stopMethodTracing()结束,此时将其缓冲的数据写入输出文件中。如果系统达到最大缓存大小时,还没有调用stopMethodTracing(),系统会停止跟踪并发送一个通知。
在Android 4.4及更高版本中,可以通过基于采样的方法分析耗时情况,因为减少了分析Trace文件的次数,降低了IO读写,所以可以减少Trace工具在运行时对性能的影响,同时对分析结果也不会有很大的偏差。通过调用startMethodTracing()方法,就可以指定具体的采样间隔,定期采集样本数据分析。
在代码中使用此方法保存TraceView数据,不要忘记在应用中打开write to external storage权限(WRITE_EXTERNAL_STORAGE)。
2.?TraceView视图说明
Traceview视图分两部分,上半部分为时间片面板(Timeline Panel),下半部分为分析面板(Prof?ile Panel)。
时间片面板如图2-16所示。
图2-16 时间片面板
X轴表示时间消耗,单位为毫秒(ms),Y轴表示各个线程,每个线程中的不同方法使用了不同的颜色来表示,颜色占用面积越宽,表示该方法占用CPU时间越长。
时间片面板可以放大/缩小,也可以指定区域放到最大,方便查看具体的过程,一般优先选择放大耗时严重的区域。
分析面板(Prof?ile Panel)如图2-17所示。
图2-17 分析面板
分析面板看起来并不复杂,但需要理解各列数据的意义,每一列表示的意义如表2-1所示。
表2-1 分析面板参数意义
列 名 意 义
Name 所有的调用项,展开可以看到有的有Parent和Children子项,指被调用和调用
Inclusive 统计函数本身运行的时间+调用子函数运行的时间
incl inclusive时间占总时间的白分比
Exclusive 同级函数本身运行的时间
Excl 执行占总时间的白分比
Calls + Recur Calls / Total 该方法调用次数+递归次数
Cpu Time / Call 该方法耗时
Real Time / Call 实际时长
使用TraceView查看耗时,主要关注Calls + Recur Calls / Total和Cpu Time / Call这两个值,也就是关注调用次数多和耗时久的方法,然后优化这些方法的逻辑和调用次数,减少
耗时。
RealTime与cputime区别为:因为RealTime包括了CPU的上下文切换、阻塞、GC等,所以RealTime方法的实际执行时间要比CPU Time稍微长一点。
2.2.3 Systrace UI性能分析
在应用程序开发过程中,UI(用户界面)的流畅度是体验的核心,特别是在动画、跳转或者列表的滑动过程中,出现卡顿和无响应是非常影响用户体验的,要解决这些问题,首先要找到问题的原因,前面介绍的TraceView是分析性能的一款利器,下面再介绍一个分析应用程序UI性能的工具:Systrace。
Systrace是Android 4.1及以上版本提供的性能数据采样和分析工具。它可以帮助开发者收集Android关键子系统(如surfacef?linger、WindowManagerService等Framework部分关键模块、服务,View系统等)的运行信息,从而帮助开发者更直观地分析系统瓶颈,改进性能。Systrace的功能包括跟踪系统的I/O操作、内核工作队列、CPU负载等,在UI显示性能分析上提供很好的数据,特别是在动画播放不流畅、渲染卡等问题上。Systrace工具可以跟踪、收集、检查定时信息,可以很直观地查看CPU周期消耗的具体时间,显示每个线程和进程的跟踪信息,使用不同颜色来突出问题的严重性,并提供如何解决这些问题的建议。
由于Systrace是以系统的角度返回一些信息,并不能定位到具体耗时的方法,要进一步获取CPU满负荷运行的原因,就需要使用前面介绍过的工具Traceview。
1.?Systrace使用方法
Systrace的使用不复杂。但跟踪的设备必须是Android 4.1(API16)或更高版本。在4.3版本和4.3以前版本的使用上有些区别,后面会讲到。
4.3以前系统版本的设备需要打开Settings > Developer options > Monitoring > Enable traces。
(1)在DDMS上使用
在Eclipse和Android Studio中都可以在DDMS直接使用Systrace,其他IDE也能支持,且流程都相同,下面以Android Studio为例说明其使用流程。
1)打开Android Device Monitor,连接手机并准备需要抓取的界面。
2)单击Systrace按钮进入抓取前的设置,选择需要跟踪的内容(见图2-18):
3)手机上开始操作需要跟踪的过程(如滑动列表)。
4)到了设定好的时间后,生成Trace文件。
5)使用Chrome打开文件即可分析。
(2)使用命令行
使用命令行方式更灵活,速度更快,并且配置好后再使用能快速得到结果,在Android 4.3及更高版本的设备上使用Systrace时,可以省略设置跟踪类别标签来获取默认值,或者可以手动列入指定标签。命令如下:
$ cd android-sdk/platform-tools/systrace
$ python systrace.py --time=10 -o mynewtrace.html sched gfx view wm
其中参数设置对应的功能如表2-2所示。
表2-2 System参数命令
参数名 意 义
-h, --help 帮助信息
-o <FILE> 保存的文件名
-t N, --time=N 多少秒内的数据,默认为5秒,以当前时间点往后倒N秒时间
-b N, --buf-size=N 单位为千字节,限制数据大小
-k <KFUNCS> --ktrace=<KFUNCS> 追踪特殊的方法
-l, --list-categories 设置需要追踪的标签
-a <APP_NAME>, --app=<APP_NAME> 包名
--from-f?ile=<FROM_FILE> 创建报告的来源trace文件
-e <DEVICE_SERIAL>, --serial=<DEVICE_SERIAL> 设备号
其中categories中的标签比较多,可以从官方的文档上查询:http://developer.android.com/intl/zh-cn/tools/help/systrace.html
(3)应用中获取
Systrace不会追踪应用的所有工作,所以在有需求的情况下,需要添加要追踪的代码部分。在Android 4.3及以上版本的代码中,可以通过Trace类来实现这个功能。它能够让你在任何时候跟踪应用的一举一动。在获取Trace的过程中,即Trace.beginSection()与Trace.endSection()之间的代码工作会一直被追踪。
在代码中加入Trace跟踪需要注意以下两点:
在Trace被嵌套在另一个Trace中时,endSection()方法只会结束离它最近的一个beginSection(String),即在一个Trace的过程中是无法中断其他Trace的。所以要保证endSection()与beginSection(String)调用次数匹配。
Trace的begin与end必须在同一线程中执行。
下面这部分代码为使用Trace的例子,在整个方法中含有两个Trace块,可以根据需求定义更多的块,但都要成对出现,如果有开始块但没有结束块,会严重影响应用的性能。
public void ProcessPeople() {
Trace.beginSection("ProcessPeople");
try {
Trace.beginSection("Processing Jane");
try {
// code for Jane task...
} finally {
Trace.endSection(); // ends "Processing Jane"
}
Trace.beginSection("Processing John");
try {
// code for John task...
} finally {
Trace.endSection(); // ends "Processing John"
}
} finally {
Trace.endSection(); // ends "ProcessPeople"
}
}
2.?分析Systrace报告
通过前面方法获取到的trace.html文件,需要使用Chrome打开,有一些常用的快捷键,定义如表2-3所示。
目前Systrace产生的trace文件只能使用Chrome打开,使用Chrome打开文件后如图2-19所示。
从图2-19中可以看到完整的数据,其中和UI绘制关系最密切的是Alerts和Frame两个数据,接下来重点介绍Alerts和Frame。
图2-19 Systrace Viewer
(1)Alerts
从图2-19可以看到,Alerts一栏标记了性能有问题的点,单击该点可以查看详细信息,在右边侧边栏还有一个Alerts框,单击可以查看每个类型的Alerts的数量,单击某一个Alert可以看到问题的详细描述。
(2)Frame
每个应用都有一行专门显示frame,每一帧就显示为一个绿色的圆圈。当显示为黄色或者红色时,它的渲染时间超过了16.6ms(即达不到60fps的水准)。使用W键放大,看看这一帧的渲染过程中系统到底做了什么,同时它会将任何它认为性能有问题的东西都高亮警告,并提示要怎么优化。如图2-19所示,在Frame栏有一个F帧(第二帧)黄色告警,从下面的问题详细描述可以看出,警告的主要原因是ListView的回收和重新绑定花费太多时间。在Systrace中也会提供一些对应链接,提供更多解释。
如果想知道UI线程怎么会花费这么多时间的话,就需要使用2.2.2节讲到的TraceView,来分析具体是哪些函数在消耗时间。
2.3 布局优化
布局是否合理主要影响的是页面测量时间的多少,我们知道一个页面的显示测量和绘制过程都是通过递归来完成的,多叉树遍历的时间与树的高度h相关,其时间复杂度为O(h),如果层级太深,每增加一层则会增加更多的页面显示时间。
任何时候View中的绘制内容发生变化时,都需要重新创建DisplayList、渲染DisplayList,更新到屏幕上等一系列操作。这个流程的表现性能取决于View的复杂程度、View的状态变化以及渲染管道的执行性能。例如,假设某个Button的大小需要增大到目前的两倍,在增大Button大小之前,需要通过父View重新计算并摆放其他子View的位置。修改View的大小会触发整个HierarcyView的重新计算大小的操作。如果是修改View的位置,则会触发HierarchView重新计算其他View的位置。如果布局很复杂,就很容易导致严重的性能问题。
在优化前首先讲解两个布局优化的常用工具。
2.3.1 常用布局优化工具
1.?Hierarchy Viewer
Hierarchy Viewer是Android SDK自带的一款可视化调试工具,用来检查Layout嵌套及绘制时间,以可视化的布局角度直观获取Layout布局设计和各种属性信息,开发者在调试和布局UI界面时可以很方便地使用,提高用户的开发效率。
出于安全考虑,Hierarchy Viewer只能连接Android开发版手机或模拟器。
在应用程序DEBUG模式中,无法启动Hierarchy Viewer。
接下来一步步介绍如何使用Hierarchy Viewer。
Step1:构造页面
先构造一个简单的页面LayoutPerActivity,该页面如图2-20所示,然后启动应用,进入这个页面。
图2-20 页面显示
Step2:打开Hierarchy View
Eclipse和Android Studio都带有Hierarchy View工具,下面介绍在这两款IDE上打开Hierarchy View的方法。
Android Studio
在Android Studio上可以直接在快捷工具栏打开Android Device Monitor,如图2-21所示。Android Device Monitor上就有Hierarchy View视图,可直接查看。
或者选择Tools->Android->Android Device Monitor菜单,直接打开Hierarchy View。
Eclipse
在Eclipse的ADT Android插件中,不能直接启动Hierachy Viewer,可以从Android SDK工具包中,通过命令行的方式启动,在Android SDK下的tools目录下,在命令行方式下运行hierachyviewer。
使用AS打开后的整体窗口如图2-22所示。
图2-22 Hierarchy View整体窗口
可以看到4个窗口,各个窗口的功能如下:
Windows:显示当前设备信息,以及当前设备的所有页面列表。
View Properties:当前选中View的属性。
TreeView:把Activity中所有控件(View)的层次结构从左到右显示出来,其中最右边部分是最底层的控件(View)。
Tree Overview:全局概览,以缩略图的方式显示整个应用中各控件的层次关系,并且框出TreeView窗口中显示部分在全局中的位置,如果一个界面中的控件和层级比较多,可以通过鼠标移动这个显示区域移动。
Layout View:整体Layout布局图,以手机屏幕上真实位置呈现出来,在TreeView中选中某一个控件时,会在Layout View用红色的框标注。
Step3:使用Hierarchy Viewer查看层级和耗时
查看层级图:在Windows窗口页,选择需要查看的组件,双击或单击Load View Hierarchy按钮即可打开。双击后在Tree View界面中,从左到右把所有控件层级图显示出来,这样就可以看到整体界面的层级深度。
查看某个View的耗时:在快捷键工具栏中单击Obtain layout times for tree rooted at selected node按钮,如图2-23所示。
图2-23 查看单个View的耗时
这时可以看到Tree View中的页面增加了属性,单击一个控件,可以看到这个View的耗时情况。
根据图2-24从上往下看,1 view表示这个控件是这个树下的最后一个控件,即表示是它本身,下面的时间表示Measure、Layout以及Draw三个阶段的耗时。最后一个框有不同色的三个指示灯,分别对应当前控件在测量、布局以及画视图三个阶段,颜色表示这个控件占用的时间百分比,如果是绿色的,表示该控件在该阶段比其他50%的控件的速度要快,黄色表示比其他50%的控件的速度要慢,红色表示该控件在该阶段的处理速度是最慢的,就需要注意了。
到这里我们就知道如何使用Hierarchy View来分析一个页面的层级和耗时,并且使用这个工具,用户可以很方便地查看和调试应用中的UI界面,分析其性能,建议开发者在开发阶段也使用这款工具。但一个应用的界面非常多,如果一个个这样分析的话效率非常低,所以再介绍另外一个工具Lint,用于检查所有页面的层级,并把深度高于N(自定义)的界面输出,然后通过Hierarchy View工具来仔细分析。
2.?布局层级检查
Android Lint是Android SDK Tools 16(ADT 16)之后引入的代码检查工具,通过代码静态检查,可以发现潜在的代码问题,并给出优化建议。Android-Lint检查工具使用的方式有以下两种:
命令行使用脚本执行。
在IDE中使用视图化工具。
Lint的检查结果分为6类,如图2-25所示。
图2-25 Android Lint示意图
Correctness(正确性)
Security(安全性)
Performance(性能)
Usability(可用性)
Accessibility(可达性)
国际化
问题的严重程度(severity)从高到低依次是:
Fatal
Error
Warning
Information
Ignore
扫描规则和缺陷级别可以在File→Settings→Inspections→Android Lint中配置,Lint的功能非常强大,强烈建议开发者深入学习使用方法,这里只讲解如何使用Android Lint来发现XML布局检查。使用Lint扫描前,先配置需要检查的项目,只需要检查Layout层级深度。先进入File→Settings→Inspections→Android Lint。
如图2-26所示,这里的配置只扫描Layout的层级和View的个数,如下所示:
TooDeepLayout:表示布局太深,默认层级超过10层会提示该问题,可以自定义环境变量ANDROID_LINT_MAX_DEPTH来修改。布局深度增加会导致内存消耗也随之增加,因此布局尽可能浅而宽。
TooManyViews:表示控件太多,默认超过80个控件会提示该问题。
在Android Studio中启动Lint,从菜单栏选择Analyze→Inspect Code,进去后可以指定扫描的范围,可以是整个工程,也可以是一个Module或单独的文件。启动扫描,扫描结果如图2-27所示。
图2-26 设置Lint规则
可以很清楚地显示哪个layout有问题,进而打开对应文件并修改。介绍完两个工具,接下来我们从多个方面来优化UI,让应用使用更流畅。
2.3.2 布局优化方法
在Android应用开发中,常用的布局方式主要有LinearLayout、RelativeLayout、FrameLayout等,通过这些布局可以实现各种各样的界面。我们需要知道如何高效地使用这些布局方式来组织UI控件,布局的好坏影响到绘制的时间,本节将通过减少Layout层级,减少测量、绘制时间,提高复用性三个方面来优化布局,优化的目的就是减少层级,让布局扁平化,以提高绘制的时间,提高布局的复用性节省开发和维护成本。
1.?减少层级
层级越少,测试和绘制的时间就越短,通常减少层级有以下两个常用方案:
合理使用RelativeLayout和LinearLayout。
合理使用Merge。
(1)RelativeLayout与LinearLayout
使用LinearLayout布局的具体方法,见代码清单2-1。
代码清单2-1 使用LinearLayout布局
<LinearLayout xmlns:android="http:// schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/layout_per_txt_1"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:text="TITLE"
android:textSize="20sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="100dp"
android:orientation="vertical">
<TextView
android:id="@+id/layout_per_txt_2"
android:layout_width="fill_parent"
android:layout_height="50dp"
android:text="Des"/>
<ImageView
android:id="@+id/imageView_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@mipmap/ic_launcher" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
这是一个简单的布局页面,在根视图上嵌套了两个LinearLayout,并且有两层显示界面,如图2-28所示。
使用2.3.1节介绍的方法通过Hierarchy View来查看下层级情况,如图2-29所示。
图2-29 Hierarchy View检查结果
由图2-29可以看到一共有7级,使用RelativeLayout进行优化,达到相同的布局效果,并且RelativeLayout允许子元素指定它们相对于其他元素或父元素的位置,有最大*度的布局属性,而且布局层次最浅,占用内存最少。修改见代码清单2-2。
代码清单2-2 使用RelativeLayou布局
<RelativeLayout xmlns:android="http:// schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/layout_per_txt_1"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@color/yellow"
android:text="TITLE"
android:textColor="#FFFF0000"
android:textSize="20sp" />
<TextView
android:id="@+id/layout_per_txt_2"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_toEndOf="@id/layout_per_txt_1"
android:layout_toRightOf="@id/layout_per_txt_1"
android:background="@color/blue"
android:text="Des"/>
<ImageView
android:id="@+id/imageView_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/layout_per_txt_2"
android:layout_toEndOf="@id/layout_per_txt_1"
android:layout_toRightOf="@id/layout_per_txt_1"
android:background="@mipmap/ic_launcher" />
</RelativeLayout>
这样就可以减少两个层级,用一个RelativeLayout就可以达到显示的效果,再使用Hierarchy View来查看层级,可以看到减少到5层,如图2-30所示。
图2-30 优化后的Hierarchy View检查结果
但ReativeLayout也存在性能低的问题,原因是RelativeLayout会对子View做两次测量,在RelativeLayout中子View的排列方式是基于彼此的依赖关系,因为这个依赖关系可能和布局中View的顺序并不相同,在确定每个子View的位置时,需要先给所有子View做一次排序。如果在RelativeLayout中允许子View横向和纵向互相依赖,就需要横向、纵向分别进行一次排序测量。但如果在LinearLayout中有weight属性,也需要进行两次测量,因为没有更多的依赖关系,所以仍然会比RelativeLayout的效率高,在布局上RelativeLayout不如LinearLayout快。
但是如果布局本身层次太深,还是推荐用RelativeLayout减少布局本身层次,相较于测量两次,虽然会增加一些计算时间,但在体验上影响不会特别大,如果优化掉两层仅仅是增加一次测量,还是非常值得的,布局层次深会增加内存消耗,甚至引起栈溢出等问题,即使耗点时间,也不能让应用不可用。
根据以上分析,可以总结出以下几点布局原则:
尽量使用RelativeLayout和LinearLayout。
在布局层级相同的情况下,使用LinearLayout。
用LinearLayout有时会使嵌套层级变多,应该使用RelativeLayout,使界面尽量扁平化。
由于Android的碎片化程度很高,市面上的屏幕尺寸也是各式各样,使用RelativeLayout能使构建的布局适应性更强,构建出来的UI布局对多屏幕的适配效果更好,通过指定UI控件间的相对位置,使不同屏幕上布局的表现基本保持一致。当然,也不是所有情况下都得使用相对布局,根据具体情况选择和搭配使用其他布局方式来实现最优布局。
(2)Merge的使用
从名字上就可以看出,Merge就是合并的意思。使用它可以有效优化某些符合条件的多余的层级。使用Merge的场合主要有以下两处:
在自定义View中使用,父元素尽量是FrameLayout或者LinearLayout。
在Activity中整体布局,根元素需要是FrameLayout。
我们仍以前面的布局为例,在页面增加一个自定义控件TopBar,故在代码清单2-2布局的基础上增加如下代码:
<RelativeLayout xmlns:android="http:// schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.ycl.androidtech.ui.TopBar
android:id="@+id/lay_out_topbar"
android:layout_width="fill_parent"
android:layout_height="50dp"/>
……………
</RelativeLayout >
其中TopBar的XML布局如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http:// schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="@dimen/topbar_height">
<ImageView
android:layout_gravity="center"
android:id="@+id/backImg"
android:layout_width="60dp"
android:layout_height="44dp"
android:background="@drawable/img_top_back"
android:focusable="true" />
<TextView
android:id="@+id/titleTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:text="标题"/>
</LinearLayout>
显示结果如图2-31所示。这种布局在一些列表的Item中非常常见,而且列表中Item本身的层级比较深,因此优化显得更有意义。
我们使用HierarchyView查看增加TopBar后的布局层级,如图2-32所示。可以看到,就是这么简单的一个布局,却把层级增加了两级,从图2-32中很明显地看出TopBar后一层的LinearLayout是多余的,这时可以使用Merge把这一层消除。
图2-32 增加TopBar后的布局层级
使用Merge来优化布局,使用Merge标签替换LinearLayout后,原来的LinearLayout属性也没有用了,修改后的代码如代码清单2-3。
代码清单 2-3
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http:// schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="@dimen/topbar_height">
<ImageView
android:layout_gravity="center"
android:id="@+id/backImg"
android:layout_width="60dp"
android:layout_height="44dp"
android:background="@drawable/img_top_back" />
<TextView
android:layout_gravity="center"
android:id="@+id/titleTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:singleLine="true"
android:text="标题"
android:textSize="18sp" />
</merge>
运行后再使用Hierarchy View查看当前层级,如图2-33所示。
图2-33 合并后的布局层级
这样就把多余的LinearLayout消除了,原理是在Android布局的源码中,如果是Merge标签,那么直接将其中的子元素添加到Merge标签Parent中,这样就保证了不会引入额外的层级。
如果Merge代替的布局元素为LinearLayout,在自定义布局代码中将LinearLayout的属性添加到引用上,如垂直或水平布局、背景色等。
但Merge不是所有地方都可以任意使用,有以下几点要求:
Merge只能用在布局XML文件的根元素。
使用merge来加载一个布局时,必须指定一个ViewGroup作为其父元素,并且要设置加载的attachToRoot参数为true(参照inf?late(int, ViewGroup, boolean))。
不能在ViewStub中使用Merge标签。原因就是ViewStub的inf?late方法中根本没有attachToRoot的设置。
这一节讲了如何减少层级,那么在Android系统中,多少层才是合理的呢?当然是越少越好,但从Lint检查的配置上看,超过10层才会报警,实际上在开发时,随着产品设计的丰富和多样性,很容易超过10层,根据实际开发过程中超过15层就要重视并准备做优化,20层就必须修改了。在实在没有办法优化的情况下,需要把复杂的层级用自绘控件来实现,自绘控件中的图层层级再多,在布局上也只是一层,但这样也会带来过度绘制的问题,在后一章节中会重点介绍这个问题的优化方案。
在Activiy的总布局中使用Merge,但又想设置整体的属性(布局方式或背景色),可以不使用setContentView方法加载Layout,而使用(id/content)将FrameLayout取出来,在代码中手动加载布局,但如果层级压力不大(小于10级),则没有必要,因为这样代码的维护性较差。
2.?提高显示速度
我们在开发的过程中会碰到这样的场景或者显示逻辑:某个布局当中的子布局非常多,但并不是所有元素都同时显示出来,而是二选一或者N选一,打开这个界面根据不同的场景和属性显示不同的Layout。例如:一个页面对不同的用户(未登录、普通用户、会员)来说,显示的布局不同。或者,有些用户喜欢对不同的元素使用INVISIBLE或者GONE隐藏,通过设计元素的visable属性来控制,这样虽然达到了隐藏的目的,但效率非常低,原因是即使将元素隐藏,它们仍在布局中,仍会测试和解析这些布局。Android提供了ViewStub控件来解决这个场景。
ViewStub是一个轻量级的View,它是一个看不见的,并且不占布局位置,占用资源非常小的视图对象。可以为ViewStub指定一个布局,加载布局时,只有ViewStub会被初始化,然后当ViewStub被设置为可见时,或是调用了ViewStub.inf?late()时,ViewStub所指向的布局会被加载和实例化,然后ViewStub的布局属性都会传给它指向的布局。这样,就可以使用ViewStub来设置是否显示某个布局。
代码清单2-4是两个ViewStub通过不同的初始化来加载两个不同的布局,以满足用户的需求。
代码清单2-4 使用ViewStub
<?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
android:id="@+id/topBar"
layout="@layout/common_top_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/topbar_height" />
<ViewStub
android:id="@+id/viewstub_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout="@layout/viewstub_text_layout1"/>
<ViewStub
android:id="@+id/viewstub_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout="@layout/layout_bitmap_show"/>
</LinearLayout>
在调用时,根据需求切换不同的Layout,这样可以提高页面初始化的速度,使用代码如下:
View view = inflater.inflate(R.layout.fm_xml_show, container, false);
if (changeView) {
ViewStub stub = (ViewStub) view.findViewById(R.id.viewstub_text);
stub.inflate();
changeView = false;
} else {
ViewStub stub = (ViewStub) view.findViewById(R.id.viewstub_image);
stub.inflate();
changeView = true;
}
ViewStub显示有两种方式,上面代码使用的是inf?late方法,也可以直接使用ViewStub.setVisibiltity(View.Visible)方法。
使用ViewStub时需要注意以下几点:
ViewStub只能加载一次,之后ViewStub对象会被置为空。换句话说,某个被ViewStub指定的布局被加载后,就不能再通过ViewStub来控制它了。所以它不适用于需要按需显示隐藏的情况。
ViewStub只能用来加载一个布局文件,而不是某个具体的View,当然也可以把View写在某个布局文件中。如果想操作一个具体的View,还是使用visibility属性。
VIewStub中不能嵌套Merge标签。
不过这些限制都无伤大雅,我们还是能够用ViewStub来做很多事情,ViewStub的主要使用场景如下:
在程序运行期间,某个布局在加载后,就不会有变化,除非销毁该页面再重新加载。
想要控制显示与隐藏的是一个布局文件,而非某个View。
因为ViewStub只能Inf?late一次,之后会被置空,无法继续使用ViewStub来控制布局。所以当需要在运行时不止一次显示和隐藏某个布局时,使用ViewStub是无法实现的。这时只能使用View的可见性来控制。
3.?布局复用
我们在开发应用时还会碰到另一个常见的场景,就是一个相同的布局在很多页面(Activity或Fragment)会用到,如果给这些页面的布局文件都统一加上相同的布局代码,维护起来就很麻烦,可读性也差,一旦需要修改,很容易有漏掉的地方,Android的布局复用可以通过<include>标签来实现,就像提取代码公用部分一样,在编写Android布局文件时,也可以将相同的部分提取出来,在使用时,用<include>添加进去。例如:
<LinearLayout xmlns:android="http:// schemas.android.com/apk/res/android"
……
android:orientation="vertical">
<include
android:id="@+id/topBar"
layout="@layout/common_top_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/topbar_height" />
</ LinearLayout >
例如,在大部分应用中,基本上所有的应用都会带有头部栏(TopBar),主要是显示标题和返回键功能,这样只需要维护一份代码,就可以修改所有的显示效果。这个示例的TopBar布局XML如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http:// schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="match_parent"
android:background="@color/black"
android:layout_height="@dimen/topbar_height">
<ImageView
android:layout_gravity="center"
android:id="@+id/backImg"
android:layout_width="60dp"
android:layout_height="44dp"
android:background="@drawable/img_top_back"
android:focusable="true" />
<TextView
android:layout_gravity="center"
android:id="@+id/titleTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:layout_centerVertical="true"
android:gravity="center"
android:singleLine="true"
android:textColor="#FFFFFFFF"
android:text="标题"
android:textSize="18sp" />
</LinearLayout>
类似于TopBar的这类常用控件,包括菜单,可以把具体实现抽象到页面的基类(BaseActivity)中,这样布局和具体的实现都收归到一个地方,方便维护。
提高布局效率的方法总体来说就是减少层级,提高绘制速度和布局复用。影响布局效率主要有以下几点:
布局的层级越少,加载速度越快。
减少同一层级控件的数量,加载速度会变快。
一个控件的属性越少,解析越快。
根据本节的分析,对优化的总结如下:
尽量多使用RelativeLayout或LinearLayout,不要使用绝对布局AbsoluteLayout。
将可复用的组件抽取出来并通过< include />标签使用。
使用< ViewStub />标签加载一些不常用的布局。
使用< merge />标签减少布局的嵌套层次。
尽可能少用wrap_content,wrap_content会增加布局measure时的计算成本,已知宽高为固定值时,不用wrap_content。
删除控件中的无用属性。
2.4 避免过度绘制
过度绘制(Overdraw)是指在屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的UI结构(如带背景的TextView)中,如果不可见的UI也在做绘制的操作,就会导致某些像素区域被绘制了多次,从而浪费多余的CPU以及GPU资源。
当设计上追求更华丽的视觉效果时,我们很容易陷入采用复杂的多层次重叠视图来实现这种视觉效果的怪圈。这很容易导致大量的性能问题,为了获得最佳性能,必须尽量减少Overdraw情况发生。
我们一般在XML布局和自定义控件中绘制,因此可以看出导致过度绘制的主要原因是:
XML布局->控件有重叠且都有设置背景
View自绘-> View.OnDraw里面同一个区域被绘制多次
2.4.1 过度绘制检测工具
要知道是否有过度绘制的情况,可以通过手机设置中的开发者选项,打开Show GPU Overdraw选项,打开后会有不同的颜色区域表示不同的过度绘制次数,如图2-34所示。
具体步骤如下:
1)系统版本要求:需要Android 4.1以上版本。
2)在手机的“设置”→“开发者选项”中打开“显示GPU过度重绘”开关(注:对未默认开启硬件加速的界面需要同时打开“强制进行GPU渲染”开关)。
3)在设置时,如果有App已经打开,需要终止App进程,重新启动。
4)然后即可通过界面的颜色判断界面重绘的严重程度。
打开后可以根据不同的颜色观察UI上的Overdraw情况,蓝色、淡绿、淡红、深红代表4种不同程度的Overdraw情况,不同颜色的含义如下:
无色:没有过度绘制,每个像素绘制了1次。
蓝色:每个像素多绘制了1次。大片的蓝色还是可以接受的。如果整个窗口是蓝色的,可以尝试优化减少一次绘制。
绿色:每个像素多绘制了2次。
淡红:每个像素多绘制了3次。一般来说,这个区域不超过屏幕的1/4是可以接受的。
深红:每个像素多绘制了4次或者更多。严重影响性能,需要优化,避免深红色区域。
我们的目标是尽量减少红色Overdraw,看到更多的蓝色区域。
2.4.2 如何避免过度绘制
1.?布局上的优化
在XML布局上,如果出现了过度绘制的情况,可以使用Hierarchy View来查看具体的层级情况,可以通过XML布局优化来减少层级。需要注意的是,在使用XML文件布局时,会设置很多背景,如果不是必需的,尽量移除。布局优化总结为以下几点:
移除XML中非必需的背景,或根据条件设置。
移除Window默认的背景。
按需显示占位背景图片。
使用Android自带的一些主题时,activity往往会被设置一个默认的背景,这个背景由DecorView持有。当自定义布局有一个全屏的背景时,比如设置了这个界面的全屏黑色背景,DecorView的背景此时对我们来说是无用的,但是它会产生一次Overdraw。因此没有必要的话,也可以移除,代码如下:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.getWindow().setBackgroundDrawable(null);
}
针对ListView中的Avatar ImageView的设置,在getView的代码中,判断是否获取对应的Bitmap,获取Avatar的图像之后,把ImageView的Background设置为Transparent,只有当图像没有获取到时,才设置对应的Background占位图片,这样可以避免因为给Avatar设置背景图而导致的过度渲染。
2.?自定义View优化
事实上,由于我们的产品设计总是追求更华丽的视觉效果,仅仅通过布局优化很难做到最好,这时可以对复杂的控件使用自定义View来实现,虽然自定义View减少了Layout的层级,但在实际绘制时也是会过度绘制的。原因是有些过于复杂的自定义View(通常重写了onDraw方法),Android系统无法检测在onDraw中具体会执行什么操作,无法监控并自动优化,也就无法避免Overdraw了。但是在自定义View中可以通过canvas.clipRect()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。canvas.clipRect()可以很好地帮助那些有多组重叠组件的自定义View来控制显示的区域。clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制,并且可以使用canvas.quickreject()来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。接下来介绍使用一个自定义View避免OverDraw的案例。
2.4.3 案例:无过度绘制View的实现
我们来实现一个四张图片叠加的自定义View。为了方便看到效果,这四张图片都有一定的重合区域,如果直接绘制,由于系统是不知道有重合区域,就会导致过度绘制,打开Show GPU Overdraw后看到如图2-35所示的效果图,可看出叠加层次越多,过度绘制就越严重。
从图2-35中可以看出,重叠部分都有过度绘制的情况,接下来通过一个例子来避免这种情况,以下代码是描述其中一张图片的类。
public class SingleCard {
public RectF area;
private Bitmap bitmap;
private Paint paint = new Paint();
public SingleCard(RectF area) {
this.area = area;
}
public void setBitmap(Bitmap bitmap) {
this.bitmap = bitmap;
}
public void draw(Canvas canvas) {
canvas.drawBitmap(bitmap, null, area, paint);
}
}
实现布局的Fragment及控制代码OverDrawFragement如代码清单2-5所示。
代码清单2-5 OverDrawFragement
protected View createView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fm_overdraw, container, false);
multicardsView = (MultiCardsView) view.findViewById(R.id.cardview);
multicardsView.enableOverdrawOpt(true);
int width = getResources().getDisplayMetrics().widthPixels;
int height = getResources().getDisplayMetrics().heightPixels;
int cardWidth = width /3;
int cardHeight = height /3;
int yOffset = 40;
int xOffset = 40;
for (int i = 0; i < cardResId.length; i++) {
SingleCard cd = new SingleCard(new RectF(xOffset, yOffset, xOffset + card
Width, yOffset + cardHeight));
Bitmap bitmap = loadImageResource(cardResId[i], cardWidth, cardHeight);
cd.setBitmap(bitmap);
multicardsView.addCards(cd);
xOffset += cardWidth / 3;
}
Button overdraw = (Button) view.findViewById(R.id.btn_overdraw);
overdraw.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
multicardsView.enableOverdrawOpt(false);
}
}
);
Button perfectdraw = (Button)
view.findViewById(R.id.btn_perfectdraw);
perfectdraw.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
multicardsView.enableOverdrawOpt(true);
}
}
);
return view;
}
实现图片叠加的自定义View:MultiCardsView,如代码清单2-6所示。
代码清单2-6 MultiCardsView
public class MultiCardsView extends View{
private ArrayList<SingleCard> cardsList = new
ArrayList<SingleCard>(5);
private boolean enableOverdrawOpt = true;
public MultiCardsView(Context context) {
this(context, null, 0);
}
public MultiCardsView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MultiCardsView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void addCards(SingleCard card) {
cardsList.add(card);
}
// 设置是否消除过度绘制
public void enableOverdrawOpt(boolean enableOrNot) {
this.enableOverdrawOpt = enableOrNot;
invalidate();
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (cardsList == null || canvas == null)
return;
Rect clip = canvas.getClipBounds();
GLog.d("draw", String.format("clip bounds %d %d %d %d", clip.left,
clip.top, clip.right, clip.bottom));
// 根据enableOverdrawOpt值来调用不同的绘制方法,对比效果
if (enableOverdrawOpt) {
drawCardsWithotOverDraw(canvas, cardsList.size() - 1);
} else {
drawCardsNormal(canvas, cardsList.size() - 1);
}
}
// 实现没有过度绘制的方法
protected void drawCardsWithotOverDraw(Canvas canvas, int index) {
if (canvas == null || index < 0 || index >= cardsList.size())
return;
SingleCard card = cardsList.get(index);
// 判断是否没和某个卡片相交,从而跳过那些非矩形区域内的绘制操作
if (card != null && !canvas.quickReject(card.area, Canvas.EdgeType.BW)) {
int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
// 只绘制可见区域
if (canvas.clipRect(card.area, Region.Op.DIFFERENCE)) {
drawCardsWithotOverDraw(canvas, index - 1);
}
canvas.restoreToCount(saveCount);
saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
// 只绘制可见区域
if (canvas.clipRect(card.area)) {
Rect clip = canvas.getClipBounds();
card.draw(canvas);
}
canvas.restoreToCount(saveCount);
}else{
drawCardsWithotOverDraw(canvas, index - 1);
}
}
// 普通绘制
protected void drawCardsNormal(Canvas canvas, int index) {
if (canvas == null || index < 0 || index >= cardsList.size())
return;
SingleCard card = cardsList.get(index);
if (card != null) {
drawCardsNormal(canvas, index - 1);
card.draw(canvas);
}
}
}
代码清单2-6在OnDraw时调用了两个不同的方法,常用绘制方法drawCards-Normal()以及无过度绘制方法drawCards-WithotOverDraw(),效果如图2-36所示。
可以看出,使用drawCardsWithotOver-Draw()避免了过度绘制,从代码清单2-6中可以看到,调用了两个关键的方法:
快速判断Canvas是否需要绘制:Canvas. QuickReject。
在绘制一个单元之前,首先判断该单元的区域是否在Canvas的剪切域内。若不在,直接返回,避免CPU和GPU的计算和渲染工作。
避免绘制越界:Canvas.ClipRect。
每个绘制单元都有自己的绘制区域,绘制前,Canvas.ClipRect(Region.Op. INTERSECT)帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内,才会被绘制,其他的区域被忽视。这个API可以很好地帮助那些有多组重叠组件的自定义View来控制显示的区域。clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。
这个案例可以避免层次很多的自定义View导致过度绘制的问题。
在listview或其他容器控件中,itemview如果比较复杂,建议实现成一个自绘View,使用此案例来绘制可以使listview滑动更流畅。
2.5 启动优化
随着应用的功能越来越丰富、启动时需要初始化的工作多、界面的元素复杂等,启动速度不可避免地受到影响,比如一开始单击时出现黑屏或者白屏,甚至在低端机型上出现假死的现象,本节通过学习应用的启动流程、启动速度的监控,发现影响启动速度的问题所在,并优化启动的逻辑,提高应用的启动速度。
2.5.1 应用启动流程
Android应用程序的载体是APK文件,其中包含了组件和资源,APK文件可能运行在一个独立的进程中,也有可能产生多个进程,还可以多个APK运行在同一个进程中,可以通过不同的方式来实现。但有两点需要注意,第一,每个应用只对应一个Application对象,并且启动应用一定会产生一个Application对象;第二,应用程序可视化组件Activity是应用的基本组成之一,因此要分析启动的性能,就有必要了解这两个对象的工作流程和生命周期。
1.?Application
Application是Android系统框架中的一个系统组件,Android程序启动时,系统会创建一个Application对象,用来存储系统的一些信息。Android系统会自动在每个程序运行时创建一个Application类的对象,并且只创建一个,可以理解为Application是一个单例类。
应用可以不指定一个具体的Application,系统会自动创建,但一般在开发中都会创建一个继承于系统Application的类实现一些功能,比如一些数据库的创建、模块的初始化等。但这个派生类必须在AndroidManifest.xml中定义好,在application标签增加name属性,并添加自己的Application的类名,代码如下:
<application
android:name=".GmfApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
AndroidManifest.xml中的application标签很多,这些标签的说明在官网有详细的介绍,这里就不做讲解。
启动Application时,系统会创建一个PID,即进程ID,所有的Activity都会在此进程上运行。在Application创建时初始化全局变量,同一个应用的所有Activity都可以取到这些全局变量的值,Application对象的生命周期是整个程序中最长的,它的生命周期就等于这个应用程序的生命周期,因为它是全局的单例的,所以在不同的Activity或者Service中获得的对象都是同一个对象。因此在安卓中要避免使用静态变量来存储长久保存的值,可以用Application,但并不建议使用太多的全局变量。
AndroidManifest.xml文件上的application标签指定了重写的Application类后,看看该类可以重载的几个抽象接口,代码清单2-7是一个自定义的Application。
代码清单2-7 自定义的Application
public class GmfApplication extends Application {
private static Context mContext = null;
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
mContext = this;
}
@Override
public void onCreate() {
super.onCreate();
}
@Override
public void onTerminate() {
super.onTerminate();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
@Override
public void onLowMemory() {
super.onLowMemory();
}
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
}
public static Context getContext(){
return mContext;
}
}
从代码清单2-7中可以看到几个重要的抽象接口,这些接口的调用时机如下:
attachBaseContext(Context base):得到应用上下文的Context,在应用创建时首先调用。
onCreate():应用创建时调用,晚于attachBaseContext()方法。
onTerminate():应用结束时调用。
onConf?igurationChanged():系统配置发生变化时调用。
onLowMemory():系统低内存时调用。
onTrimMemory(int level):系统要求应用释放内存时调用,level为级别。
从上面的抽象方法可以看出,这些方法都在这个应用生命周期之中,attachBaseContext和onCreate在应用创建时必须调用,而其他需要满足一定的触发时机。
在开发过程中,尽量使用Application中的Context实例,因为使用Activity中的Context可能会导致内存泄漏。也可以使用Activity的getApplicationContext方法。
2.?Activity
Activity大家都非常熟悉了,这里也不做太多解释,只需要理解它的生命周期,因为这在启动优化的过程中非常重要。
在Activity的生命周期中,系统会按类似于阶梯金字塔的顺序调用一组核心的生命周期方法,如图2-37所示。也就是说,Activity生命周期的每个阶段就是金字塔上的一阶。当系统创建一个新Activity实例时,每个回调方法会将Activity状态向顶端移动一阶。金字塔顶端是Activity在前台运行并且用户可以与其交互的时间点。当用户开始离开Activity时,系统调用其他方法在金字塔中将Activity状态下移,从而销毁Activity。在有些情况下,Activity将只在金字塔中部分下移并等待(如当用户切换到其他应用时),Activity可从该点开始移回顶端(如果用户返回到该Activity),并在用户停止的位置继续。
图2-37 Activty生命周期金字塔模型
大多数应用包含若干不同的Activity,用户可通过这些Activity执行不同的操作。无论Activity是用户单击应用图标时创建的主Activity,还是应用在响应用户操作时开始的其他Activity,系统都会调用其onCreate()方法创建Activity的每个新实例。因此必须实现onCreate()方法,执行后,在Activity整个生命周期中只需要出现一次基本应用启动逻辑。例如,onCreate()的实现应定义用户界面并且可能实例化某些类范围变量、声明用户界面(在XML布局文件中定义)、定义成员变量,以及配置某些UI。
onCreate()方法包括一个savedInstanceState参数,在有关重新创建Activity中非常有用。
从Application和Activity的介绍中,可以总结出应用启动的流程,如图2-38所示。
其中,启动分为两种类型:冷启动和热启动。
冷启动:因为系统会重新创建一个新的进程分配给它,所以会先创建和初始化Application类,再创建和初始化Main-Activity类(包括一系列的测量、布局、绘制),最后显示在界面上,如图2-38所示。
热启动:因为会从已有的进程中启动,所以热启动不会再创建和初始化Application,而是直接创建和初始化MainActivity(包括一系列的测量、布局、绘制),即Application只会初始化一次,只包含Activity中的生命周期流程。
2.5.2 启动耗时监测
因为一个应用在启动或者跳入某个页面时是否流畅,时间是否太长,仅仅通过肉眼来观察是非常不准确的,并且在不同设备和环境会有完全不同的表现,所以要准确知道耗时,就需要有效准确的数据,首先通过shell来获取启动耗时。
1.?adb shell am
应用启动的时间会受到很多因素的影响,比如首次安装后需要解压apk文件,绘制时GPU的耗时等,所以在应用层很难获取到启动耗时,但借助ADB可以得到准确的启动时间。
使用adb shell获得应用真实的启动时间,代码如下:
adb shell am start -W [packageName]/[packageName.AppstartActivity]
执行后可以得到三个时间:
ThisTime:一般和TotalTime时间一样,如果在应用启动时开了一个过度的全透明的页面(Activity)预先处理一些事,再显示出主页面(Activity),这样将比TotalTime小。
TotalTime:应用的启动时间,包括创建进程+Application初始化+Activity初始化到界面显示。
WaitTime:一般比TotalTime大些,包括系统影响的耗时。
但这个方法只能得到固定的某一个阶段的耗时,不能得到具体哪个方法的耗时,下面介绍第二个方案:代码打点输出耗时。
2.?代码打点
通过代码打点来准确获取记录每个方法的执行时间,知道哪些地方耗时,然后再有针对性地优化,下面通过一个简单的例子来讲解打点的方案。
以下代码是一个统计耗时的数据结构,通过这个数据结构记录整个过程的耗时情况。
public class TimeMonitor {
private final String TAG = "TimeMonitor";
private int monitorId = -1;
// 保存一个耗时统计模块的各种耗时,tag对应某一个阶段的时间
private HashMap<String, Long> mTimeTag = new HashMap<String, Long>();
private long mStartTime = 0;
public TimeMonitor(int id) {
GLog.d(TAG,"init TimeMonitor id:" + id);
monitorId = id;
}
public int getMonitorId() {
return monitorId;
}
public void startMoniter() {
// 每次重新启动,都需要把前面的数据清除,避免统计到错误的数据
if (mTimeTag.size() > 0) {
mTimeTag.clear();
}
mStartTime = System.currentTimeMillis();
}
// 打一次点,tag交线需要统计的上层自定义
public void recodingTimeTag(String tag) {
// 检查是否保存过相同的tag
if (mTimeTag.get(tag) != null) {
mTimeTag.remove(tag);
}
long time = System.currentTimeMillis() - mStartTime;
GLog.d(TAG, tag + ":" + time);
mTimeTag.put(tag, time);
}
public void end(String tag,boolean writeLog){
recodingTimeTag(tag);
end(writeLog);
}
public void end(boolean writeLog) {
if (writeLog) {
// 写入到本地文件
}
testShowData();
}
public void testShowData(){
if(mTimeTag.size() <= 0){
GLog.e(TAG,"mTimeTag is empty!");
return;
}
Iterator iterator = mTimeTag.keySet().iterator();
while (iterator != null && iterator.hasNext()){
String tag = (String)iterator.next();
GLog.d(TAG,tag + ":" + mTimeTag.get(tag));
}
}
public HashMap<String, Long> getTimeTags() {
return mTimeTag;
}
}
这个对象可以用在很多需要统计的地方,不仅可以统计应用启动的耗时,还可以统计其他模块,如统计一个Activity的启动耗时和一个Fragment的启动耗时。流程为:在创建这个对象时,需要传入一个ID,这个ID是需要统计的模块或者一个生命周期流程的ID,ID自定义并且是唯一的,一个TimeMonitor对应一个ID。其中end(Boolean writeLog)方法表示这个监控的流程结束,其中writeLog表示是否需要写入本地,建议实现这个方法,可以统计一系列的数据,最好上传到服务器,用来监控这个应用在外网的实际启动状况。
上传到服务器时建议抽样上报,比如根据用户ID的尾号来抽样上报,虽然不影响性能,但还是尽量不要全部上报,用后台下发抽样比较好。
比如现在要统计启动应用在各阶段的耗时,就自定义一个ID,为了使代码更好管理,编写一个专门定义所有ID的类,方便以后的维护,代码如下:
public class TimeMonitorConfig {
// 应用启动耗时
public static final int TIME_MONITOR_ID_APPLICATION_START = 1;
}
因为耗时统计可能会在多个模块和类中需要打点,所以需要一个单例类来管理各个耗时统计的数据,这里使用了一个单例类来实现:TimeMonitorManager,代码如下:
public class TimeMonitorManager {
private static TimeMonitorManager mTimeMonitorManager = null;
private static Context mContext = null;
private HashMap<Integer,TimeMonitor> timeMonitorList = null;
public synchronized static TimeMonitorManager getInstance(){
if(mTimeMonitorManager == null){
mTimeMonitorManager = new TimeMonitorManager();
}
return mTimeMonitorManager;
}
public TimeMonitorManager(){
timeMonitorList = new HashMap<Integer,TimeMonitor>();
}
// 初始化某个打点模块
public void resetTimeMonitor(int id){
if(timeMonitorList.get(id) != null){
timeMonitorList.remove(id);
}
getTimeMonitor(id);
}
// 获取打点器
public TimeMonitor getTimeMonitor(int id){
TimeMonitor monitor = timeMonitorList.get(id);
if(monitor == null){
monitor = new TimeMonitor(id);
timeMonitorList.put(id,monitor);
}
return monitor;
}
}
在有需要的地方通过这个方法进行打点,为了得到有效的数据,总结起来主要在两个方面需要打点:
应用程序的生命周期节点,如Application的onCreate、Activity或Fragment的回调函(onCreate、onResume等)。
启动时需要初始化的重要方法,如数据库初始化、读取本地的一些数据等。
其他耗时的一些算法。
例如,在启动时加入统计,在Application和第一个Activity加入打点统计,结合前面讲过的启动生命周期,首先进入的是Application的attachBaseContext()方法,然后在Oncreate结束时打第一个点,在AppstartActivity结束打第二个点,在AppstartActivity中的onStart()打最后一个点,代码如下:
Application:
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base); TimeMonitorManager.getInstance().resetTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START);
}
@Override
public void onCreate() {
super.onCreate();
InitModule(); TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recodingTimeTag("ApplicationCreate");
}
第一个Activity:
@Override
protected void onCreate(Bundle savedInstanceState) {
TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recodingTimeTag("AppStartActivity_create");
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_app_start);
mLogo = (ImageView) this.findViewById(R.id.logo);
// mStartHandler.sendEmptyMessageDelayed(0,1000);
// useAnimation();
useAnimator();
TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recodingTimeTag("AppStartActivity_createOver");
}
@Override
protected void onStart() {
super.onStart();
TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).end("AppStartActivity_start",false);
}
结果如下所示:
可以在项目中核心基类的关键回调函数和核心方法加入打点,另外插桩也是一种不错的方式。
2.5.3 启动优化方案
在Android应用开发中,应用启动速度对用户体验非常重要,也是一个应用给用户的第一个性能方面的体验,因此应用启动优化是非常有必要的。应用启动优化的核心思想就是要快,在启动过程中做尽量少的事。但是应用功能越丰富,模块越多,需要初始化的地方也越多,导致了应用启动变慢。
为了优化启动的速度,首先要了解启动时做了什么,来看一个例子,启动源码中性能优化的启动应用(应用源码在http://github.com/lyc7898/AndroidTech),通过打点,统计从单击打开应用到首页显示完成的时间,后面章节会讲到打点的注意事项和具体实现。表2-4是通过打点获取到这个应用启动时,各个模块占用的时间。
因为这个应用比较简单,没有什么模块和数据,所以大头是在绘制工作上,也就是闪屏页(显示启动LOGO的页面)和进入后首页的布局上,其次是一些初始化工作,但实际上一个稍大型的应用,模块初始化和数据准备工作的占比会高很多,因为这块的优化也是非常有必要。
从总体上看,启动主要完成三件事:UI布局、绘制和数据准备,因此启动速度的优化就是需要优化这三个过程,我们也可以通过一些启动界面策略进行优化。接下来从启动耗时最高的UI布局和启动加载逻辑两个方向优化,达到降低启动耗时的目的。因为这个应用非常简单,所以不具有代码性,但优化的流程和方案是通用的。
1.?UI布局
这个应用启动过程为:启动应用→Application初始化→AppstartActivity→HomePageActivity。AppstartActivity是应用的闪屏页(也叫启动页),我们看到大部分应用都有这么一个页面,为什么要有闪屏页呢?闪屏页的存在主要有两个好处:一是可以作为品牌宣传展示,如节日运营或热点事件运营,也可以做广告展示(不要太低端);其二,因为闪屏一般需要停留一段时间,在这段时间可以做很多事情,比如底层模块的初始化、数据的预拉取等。
首先需要优化AppstartActivity的布局,从前面的章节可以知道,要提高显示的效率,一是减少布局层级,二是避免过度绘制,因为前面已经有很详细的例子了,这里不做过多介绍,优化的步骤如下:
使用Prof?ile GPU Rendering检查启动时是否有严重的掉帧,见2.2.1节。
使用Hierarchy View检查布局文件(XML)分析布局并优化,见2.3.1节。
2.?启动加载逻辑优化
一个应用越大,涉及的模块越多,包含的服务甚至进程就会越多,如网络模块的初始化、底层数据初始化等,这些加载都需要提前准备好,有些不必要的就不要放到应用中。可以用以下四个维度分整理启动的各个点:
必要且耗时:启动初始化,考虑用线程来初始化。
必要不耗时:首页绘制。
非必要耗时:数据上报、插件初始化。
非必要不耗时:不用想,这块直接去掉,在需要用的时再加载。
把数据整理出来后,按需实现加载逻辑,采取分步加载、异步加载、延期加载策略,如图2-39所示。
图2-39 启动优化方向
要提高应用的启动速度,核心思想是在启动过程中少做事情,越少越好。
在应用中,增加启动默认图或者自定义一个Theme,在Activity首先使用一个默认的界面可以解决部分启动短暂黑屏问题,如android:theme="@style/Theme.AppStartLoad"。
2.6 合理的刷新机制
在应用开发的过程中,因为数据的变化,需要刷新页面来展示新的数据,但频繁刷新会增加资源开销,并且可能导致卡顿发生,所以,需要一个合理的刷新机制来提高整体的UI流畅度。合理的刷新需要注意以下几点:
尽量减少刷新次数。
尽量避免后台有高CPU线程运行。
缩小刷新区域。
2.6.1 减少刷新次数
毫无疑问,减少刷新次数可以减少系统的开销,在功耗和页面的性能上可以表现得更优秀,但不刷新就不能及时让用户看到最新的数据,可以从以下几个方面减少刷新次数:
1.?控制刷新频率
在有些功能上需要频繁刷新某个控件(View),比如下载进度条或者播放进度条,没有必要在数据每次变化时都更新对应的控件,需要注意的是一定不能出现过度刷新(进度刷新频率大于系统显示的刷新频率)的情况。可以通过定时控制刷新频率,相同的刷新只做一次,比如播放进度条的刻度是100,如果数据变化没有1%,完全没有必要刷新,这样可以减少UI的刷新负担。
2.?避免没有必要的刷新
首先需要判断是否需要刷新,比如数据没有变化、需要刷新的控件(View)不在可见区域,就没有必要刷新。但是需要注意,如果一个View从不可见到可见,一定要刷新一次。
一般来说,在第一片数据(从无到有)和最后一片数据(结束了一个刷新周期)时,一定要刷新一次,以保证完整性。
2.6.2 避免后台线程影响
后台线程虽然不会直接影响到主线程的工作,但如果后台线程开销很大,占用CPU过高,导致系统GC频繁和CPU时间片资源紧张,还是有可能会导致页面的卡顿。因此在需要迅速刷新的情况下避免这类线程在高峰工作。比如ListView的滚动,如果ListView中的Item需要下载图片显示,在ListView滚动时可以暂停其他UI的操作,示例代码如下:
listView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView absListView, int scrollState) {
if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) {
mImageWorker.setPauseWork(true);
} else {
mImageWorker.setPauseWork(false);
}
}
});
通过监听ListView的onScrollStateChanged事件,在滚动时暂停图片下载线程工作,结束后再开始,可以提高ListView的滚动平滑度。
2.6.3 缩小刷新区域
在以下两个场景下可以采用局部刷新的方法来节省更多的资源。
一是在自定义View中。自定义View一般采用invalidata方法刷新。如果需要更新的数据只是在某一个区域内改变,在调用invalidata方法更新这个区域时,也会更新整个视图,这就浪费了不需要更新区域资源。Android提供系统了两个局部更新数据的方法:
invalidate(Rect dirty);
invalidate(int left, int top, int right,int bottom);
使用这两个方法可以只更新需要更新的区域,其他不需要数据更新的区域不会更新,这样能节省部分资源。
第二种是容器中的某个Item发生了变化,只需要更新这一个Item即可。比如在ListView中,如果是单条操作,就必须调用Adapter的notifyDataSetChanged()刷新。
2.7 提升动画性能
在打造优秀体验的应用和实现酷炫效果的过程中,动画是不可或缺的重要组成部分。Android平台提供了三个动画框架:帧动画(Frame Animation)、补间动画(Tween Animation)和属性动画(Property Animation)。属性动画在Android 3.0开始支持,开发者使用这些动画框架来实现各种动画效果,这三个框架都有其优势和局限性,要深入了解就需要明白它们的实现原理。
属性动画只有Android API 11(Android 3.0)以上才支持,如果使用11以下的SDK,请导入NineOldAndroids动画库,用法完全一致。
虽然通过动画可以实现很多酷炫的动画,但带来的性能开销也有不同程度的影响。在实现动画的过程中,主要从以下三个纬度来对比性能:
流畅度:流畅度是动画的核心,控制每一帧动画在16ms以内完成。
内存:避免内存泄漏,减小内存开销。
耗电:减小运算量,优化算法,减小CPU占用。
因篇幅所限,本文不从原理上讲解所有动画的具体实现,只简单了解下目前Android系统支持的三种动画实现方式,并且以应用启动动画为例来对比不同动画的实现效果和性能上的开销,这个启动动画由两部分组成:旋转360度和从0到原始尺寸的放大。
2.7.1 帧动画
帧动画很简单,就是将一组图片按顺序显示出来,可以使用AnimationDrawable来定义使用帧动画。但由于帧动画要求图片过多,如果图片较大,内存占用就非常大,效果也比较差,除非图片足够多。所以帧动画是消耗资源最多,效果最差的一种,这里就不再介绍使用方法,能不用就不用。