实战
移动App性能评测与优化
TMQ专项测试团队 编著
图书在版编目(CIP)数据
移动App性能评测与优化/ TMQ专项测试团队编著. —北京:机械工业出版社,2016.9
(实战)
ISBN 978-7-111-54826-3
I. 移… II. T… III. 移动终端-应用程序–程序测试–研究 IV. TN929.53
中国版本图书馆CIP数据核字(2016)第213174号
本书通过六个专题方向介绍腾讯公司移动互联网事业群在移动应用性能评测优化方面的实战经验,涉及内存、电量、流畅度、导航、网络优化和应用安装包瘦身。每个专题都有案例说明,重点在讲述问题解决的思路,以及研究过程中碰到的问题。读者可以通过本书快速了解提升应用核心性能的思路与方法,打造更加优秀的移动应用。主要内容有:第1章是内存篇,介绍了各种内存使用情况分析的方法和一些优化技巧。第2章是电量篇,从app层面到rom层面,从硬件测试方法到软件测试方法,结合多个案例从多方面介绍电量测试的切入点和测试方法。第3章是流畅度篇,介绍了Android流畅度的测试和优化方法。第4章是导航篇,介绍了路线规划、语音播报这两个导航中最重要模块的测试方法和经验。第5章是网络篇,重点介绍提升上传速度和成功率、产品流量优化。第6章是应用安装包瘦身篇,结合一个瘦身实际案例介绍了当前常用的瘦身方法、瘦身工具以及瘦身过程中的技巧。第7章是工具篇,介绍腾讯公司开发并开源的测试工具GT,专门针对移动应用的性能评测与优化,可帮助读者将优化技术真正应用到实际工作中。
移动App性能评测与优化
出版发行:机械工业出版社(北京市西城区百万庄大街22号 邮政编码:100037)
责任编辑:吴 怡 责任校对:董纪丽
印 刷: 版 次:2016年9月第1版第1次印刷
开 本:186mm×240mm 1/16 印 张:14
书 号:ISBN 978-7-111-54826-3 定 价:59.00元
凡购本书,如有缺页、倒页、脱页,由本社发行部调换
客服热线:(010)88379426 88361066 投稿热线:(010)88379604
购书热线:(010)68326294 88379649 68995259 读者信箱:hzit@hzbook.com
版权所有·侵权必究
封底无防伪标均为盗版
本书法律顾问:北京大成律师事务所 韩光/邹晓东
The 对本书的赞誉
移动互联网正从爆发增长期进入精耕细作期,用户的使用体验无疑是能够给产品带来更强生命力的最关键因素之一。移动APP的用户体验,可以归结为内存、电量、流畅度、网络流量等几个具体的关键指标,针对这些关键指标的专项质量保证和评测优化,无疑具有重要的价值。
2014年夏天,通过公司间质量保证团队的技术交流互访活动,我初次了解到腾讯公司各个事业群质量团队开展移动APP专项测试的相关工作,当时即留下了较为深刻的印象。两年后,很高兴看到其中一个优秀团队—腾讯移动互联网事业群的品质中心(TMQ)专项测试团队将他们的实践经验总结出来,付梓成册。
这首先是一本实战派的工程师攻略。不拘泥于教科书般的面面俱到,提供的是针对关键领域的优秀实践经验沉淀;围绕典型案例,提供了可操作性很强的流程和工具解决方案。本书不仅帮助大家解决What to do的问题,同时提供When和How的实践指南,可以成为移动APP开发和质量保证工程师的实用手册。
同时,通过本书也能窥见一个优秀质量团队的良好工作方式和习惯。例如,不仅满足于发现问题,而是进一步构建“定位问题→优化产品→持续监控”的质量闭环;再例如,在充分调研并使用第三方工具的基础上,有针对性地设计开发自研工具来提升效率,并通过开源回馈社区。相信关心工程质量体系和质量保证团队建设的leader们也会从本书获得启发。
—胡星,百度公司主任测试架构师
在腾讯的体系下,廖叔和他的团队从来都是能够给大家带来惊喜的;惊喜不仅仅来自于这个团队的卓越产出,更来自于很多原创性的突破,《移动APP性能评测与优化》一书就是这类惊喜之一。这本书除了较为体系化地介绍了移动应用性能评测与优化的方方面面,在一些单点上也有很多原创性的突破,如电量的硬件测试方案、GT评测插件等。相信无论是刚入移动测试领域的新手,还是浸淫于此领域多年的老兵,都能给你带来不同的收获。
—李俊,蚂蚁金服技术风险部负责人
在业内参与过诸多移动测试技术相关的分享,TMQ专项测试团队编写的《移动App性能评测与优化》这本书属于其中最精工细作的内容之一。初看并没有惊艳的感觉,中规中矩地覆盖了领域常见的一些技术体系。真正细读,会发现其中的闪光点和良苦用心:不仅仅浮于表面给出方法论或工具使用细节,而是大量解读深入的技术原理与机制,并期望给业务带去质量体验的变化。在日益浮躁的行业中,这是一个能坚持耕耘、钻研技术、抱有更高理想的团队,应当给予掌声!
—钱承君,百度测试架构师
Preface 序
廖叔突然来找我,说要出版一本书,这本书已经整理完成,想让我给作序一下,一看书名《移动App性能评测与优化》,好家伙!真有毅力把他们这几年的实践竟然总结出书了。对我来说,这肯定是盛情难却也乐意之极的事情!看着这书的内容时,让我也很感慨,因为整本书的结晶都来自我们腾讯内部一个很“特殊”、很“奇怪”的测试岗位,我们叫“专项技术测试”。初看名称可能不明就里,这个岗位成立于2010年初, 当时还是PC/Web一统互联网时代,初衷是能在测试开展中深钻安全、性能、协议等领域的难题,为研发和质量团队及时输送炮弹。我们内部要求这个岗位要能深入底层,系统全面地理解和掌握操作系统、网络、安全等底层的技术原理,要具备足够扎实和丰富的开发背景及技能,同时还要能自主调研和开发各类测试工具以便更加高效地开展测试工作。进入移动互联网时代后,我们看到当初的“先见之明”为今天内部的测试领域积累了一大批优秀的攻坚性人才,极大地丰富了测试能力和支撑范畴,成为了研发团队极其亲密和信任的战友,甚至于研发团队在版本发布前,没有看到这个团队的测试数据和报告输出,内心会非常忐忑不安。
回顾这5、6年的发展历程,特别是近几年移动互联网浪潮席卷之下,专项技术测试已经从当初PC/Web的三个定点测试领域扩展到围绕iOS/Android下的流畅度/卡顿、耗电/CPU、强弱网络、内存泄露(OOM)、稳定性(Crash),数据库(SQLite)、I/O、兼容性等多个维度上,涉及的技术要求更深、知识面也要更广,针对性的测试开展难度同时也更高。在这个不断摸索和研究的过程中,专项测试的同仁也许是第一次有机会和研发一起针对更多未知领域组织学习和彼此探讨,掌握产品技术架构,理解各种问题的根因,逼着自己不断加深对操作系统、网络等底层实现的理解和学习,逼着自己熟练使用各种调试工具分析定位原因。这个过程是痛苦且非常快乐的,而团队也是得以在这样的经历中摸索总结出了各方面的测试经验。我们众多脍炙人口的产品,都是内部有严格的前后版本评测,以及和竞品的评测,指标更优后才允许发布,这其中的成果应该当之无愧的有专项测试的功劳。
针对专项测试的组织开展,我们内部在谈论一个专项战略地图建设,概要来说,专项测试的组织开展和未来方向目标,应该从四个层面来梳理和规划:1)第一层(最底层),涉及移动操作系统iOS/Android、网络协议、安全、数据库,以及相关的开发技术。专项测试的同仁必须得在一个或多个领域具备丰富的理解和掌握,看到一个表象的问题,可以很容易联想到底层实施上可能的困难或问题点,这才能为具体问题定位带来价值和高效。2)第二层,涉及稳定性(Crash)、内存泄露(OOM)、流畅度/卡顿、耗电/CPU、强弱网络、兼容性等多个领域的原理理解,清楚不同领域的起因/导因,知道技术实现时的接口调用各种潜在问题,并能借助调试定位工具轻松地排查和问题定位。3)第三层,涉及不同领域的工具开发或改造封装,能针对专项维度的各类问题,设计出自动化工具,更加容易地发现和跟踪到问题。把第二层的理解体系的封装在这些开发出的各种工具里,让工具可以灵活地替代人的眼睛和大脑自动测试和发现各类问题。4)第四层,进一步封装,把各类测试工具能纳入持续集成和自动化测试平台中,实现时刻在自动执行、自动统计分析和问题定位的能力。从纷杂的可能没有任何头绪的问题表象中,借助这个分层的Map设计和执行,我相信专项的攻坚将变得非常有针对性和目的性,同时我们也更容易衡量自己当前的进展。
上面谈论了很多专项的建设,这些不同维度的测试开展和性能提升,归根结底还是要落地到实践以及具体的经验总结提炼。这本书我想应该是一本研发和测试都特别需要认真研读的宝贵教材,我用了接近2天时间快速通读了一遍,虽然对很多的技术原理和问题定位步骤都是比较熟悉了解的,对很多工具的介绍也看着很亲切,但能结合各种问题/案例,抽丝剥茧,不仅清楚透彻地讲出原理,告知跟因,同时还把不同类型的问题提炼出了实施执行的步骤,一步步清晰展示在我们面前,为这个思路和行动必须要唱一曲! 这本书从内存、电量、流畅度、网络、安装包瘦身以及相关领域的一些工具给予了仔细讲解,思路清晰,有足够的技术深度和实践案例讲解,是测试领域里难得的一本基于优秀实践总结出来的好书!
作为腾讯内部同样从事测试领域的一员,为我们给同行贡献出来的本书鼓掌和致敬!提升自己最好的途径就是积极学习,善于总结,让自己少走弯路,我想同样作为同仁的你们,应该来阅读这本书,也要认真地来学习这本书!
吴凯华
腾讯社交网络质量部副总经理,
腾讯质量管理通道分会会长
2016年6月29日
前 言 Preface
写作背景
当前移动设备越来越多地涌现在我们日常生活中,像网络购物、充值缴费、新闻资讯、理财、团购、车辆保养等都可以通过移动设备来搞定。通过移动设备可以帮助人们更便捷高效地完成很多事,同时越来越多的需求也希望能通过移动设备来完成,这样也催生了很多工作机会,让IT技术人员能开发更多的App来满足不同用户的不同需求。相对于传统PC,移动设备有其自身的特点,如屏幕小、移动网络复杂且需要收费、电量有限等。因此,在完成用户一系列需求的背后,我们也面临一系列的问题。比如说,如何能保证开发的App内存开销低?如何保证App在功能不变的情况下足够省电?如何做到页面滑动流畅顺滑?如何保证网络开销尽可能的低?等等。
上面一系列的问题,我们都曾经遇到过,写这本书的目的也是希望能将我们团队在“如何开发高性能质量App”上探索的经验和成果分享给读者,将我们在团队中碰到的真实案例总结出来,给做移动互联网应用的研发团队,包括测试团队,提供参考。
在我们团队的工作过程中,经常会碰到上面的问题,刚开始是和有研究探索过的前辈交流,再自己不断地实践、升华、提炼。后面也出现其他团队的人来咨询交流,再者,团队内部人员也会不断地流动,新人的加入,也需要资深的同事将积累的经验提供给新人不断学习。这样,出于以上各种原因,越来越觉得很有必要将团队近几年在移动互联网应用开发中,如何进行评测调优的实践经验总结出来。先是内部收集大家手上案例并沉淀,发现大家负责的专题都不一样,虽然零散但都很有价值,因此萌生了写书的念头。正好可以借写书将我们做过的优秀案例梳理总结出来,包括其中走过的弯路,踩过的坑都展现给大家。
本书内容
我们将日常工作中优化的案例按不同的纬度划分总结,总计有六个专题方向和最后一个自研的随身调测工具GT。六个专题研究方向分别是:内存、电量、流畅度、导航、网络优化和应用安装包瘦身。每个专题对应一章的内容,通篇都有案例说明,重点在讲述问题解决的思路,以及过程中碰到的问题,同时也介绍了移动应用测试的方法等。下面针对每个章节做一下基本的介绍,读者可以通过介绍了解该章讲述的基本内容。
第1章是内存篇,介绍了各种内存使用情况分析的方法和一些优化技巧。使读者能够准确地了解应用内存的消耗情况,找出存在的内存问题,并在开发过程中尽量节约使用内存。
第2章是电量篇,本章从app层面到rom层面,从硬件测试方法到软件测试方法,结合多个案例从多方面介绍电量测试的切入点和测试方法以及测试原理。介绍了基本的硬件测试方法;介绍了GT、PowerStat、Battery Historian等软件测试方法;以及一种通过大数据去分析用户异常耗电场进而景制定优化策略的测试思路;总结了一些在功耗测试中的优化经验。
第3章是流畅度篇,介绍了android流畅度的测试和优化方法。一开始先介绍评测APP流畅度的方法,结合我们实际的测试经验,阐述FPS在流畅度测试中的不足之处,然后针对FPS的不足,讨论我们如何对测试方法进行改进,从而使得我们的测试方法能够准确地反映出当前APP的流畅度情况。接着结合具体的案例,阐述我们如何对Android APP的流畅度进行测试以及优化。最后总结我们在实践中的流畅度优化方法,这些方法针对Android大部分的APP都具有通用性。
第4章是导航篇,介绍了路线规划,语音播报这两个导航中最重要模块的测试方法和经验。导航类评测的难点在于,case无穷尽;单看自家产品的结果很难给出优劣的评价;人工评测费时费力,达不到足够的量。我们通过后台日志筛选了用户访问量大的case,作为评测的case,以有限的量尽可能覆盖更多的用户。利用多个产品进行对比,更容易发现产品的好坏。我们还提出了几种自动化评测的方案,提高了评测效率,也提升了评测的量。
第5章是网络篇,重点介绍了我们团队网络优化的两个案例。一个是提升上传速度和成功率的“鱼翅项目”,重点讲解了在移动网络环境下如何根据一次次的实验结果,来一步步改进优化算法,最终提炼出了能应对网络质量瞬息万变的鱼翅算法;另一个是某产品流量优化项目,重点讲解了流量测试方法、自动化测试的经验以及提炼出的流量优化的通用方法。在两个案例中都详细分享了我们解决问题的思路,相信这些思考问题的方法能给大家在网络优化以及其他方面深入开展工作带来一些启发。
第6章是应用安装包瘦身篇,结合一个瘦身实际案例介绍了当前常用的瘦身方法、瘦身工具以及瘦身过程中的技巧。
第7章是工具篇,通过前面章节介绍的测试探索与实践,我们已经积累了比较丰富的测试经验,但在实践时经常发现,市面上很难找到能够满足特点测试需求或提高测试效率的工具来辅助测试活动,所以我们就需要自己动手来实现这样的工具。像我们团队开发的可以公开的工具目前有APT、GT、PowerStats,不同的工具适用于不同的测试场景。各有不同的使用限制,其中以GT的适用性最广。本章将以GT为例,先讨论开发测试工具的初心:即“什么时候是开发一个工具的恰当时机?”“我们需要解决什么样的问题?”“我们如何决定工具的形态?”这三个问题,然后对GT的基础能力在实际调测活动中起到的作用进行简要的论证。
谁适合阅读本书
本书介绍了在移动应用体验中用户关心的几类痛点,如内存、流量、电量、流畅度等,从现象到本质,利用什么工具,发现什么问题,抽丝剥茧,直追代码,找出问题的根因。每章通过一系列的案例描述移动应用的测试及优化的方法,并提供相应问题的解决方案。本书最后一章讲解了测试利器GT,通过GT工具能够让测试更灵活,让开发更透明。
本书可能适合下面这些人:
希望通过代码从本质上解决性能问题的开发人员。通过本书规范开发设计工作,减少性能开销,保证开发高质量的App。
希望提高质量、发现性能问题的测试人员。利用本书提供的方法以及思路,查找负责测试App的性能问题,并提供开发人员相应问题如何解决的参考案例。
希望针对新领域进行专题研究的团队负责人。可以参考我们在成立专题研究时,如何进行问题的剖析、探索和实践。
希望从事测试相关行业的新手。通过本书了解目前在移动App上的专项测试维度有哪些,测试工作是如何开展的。
对腾讯移动品质中心(TMQ)专项测试团队感兴趣的同行。可以通过本书了解我们团队在测试方面的一些思考和尝试。
本书阅读建议
本书中第1章到第6章从移动App各个不同维度进行专题研究、深入分析,因此,没有很明显的顺序关系,读者可以根据需要参考的维度或感兴趣的维度,查找相应的章节,进行阅读。
书中最后一章介绍我们团队自研的一款脱机测试工具GT。对于GT使用的各种问题或内部原理感兴趣的,可以直接阅读第7章。
关于作者
本书的作者是来自腾讯移动品质中心(TMQ)专项测试团队的资深测试工程师们,他们长期负责腾讯公司部分重要的手机应用(手机浏览器、手机管家、应用宝、腾讯地图等)的性能评测与优化工作。在App的内存、电量、流量、流畅度、网络、安装包大小等核心性能维度,积累了相当丰富的评测、优化经验。
主要编著成员有:蒋翠翠、李金涛、廖志、廖海珍、罗家润、马蕾、秦守强、文娟、阳文彬、叶方正、翟翌华、张媛、张志伟(按拼音顺序排列)。
TMQ(腾讯移动品质中心)是腾讯最早专注在移动APP测试的团队。TMQ微信公众号专注于移动测试技术精华,饱含腾讯多款亿级APP的品质秘密,文章皆独家原创,我们不谈虚的,只谈干货!欢迎扫描二维码关注我们。
TMQ深圳
特别感谢
李金涛致谢:
感谢腾讯MIG应用宝项目组的支持和帮助,让我们的想法得以在应用宝上落地和实现!感谢廖志和叶方正两位Leader的指导,在我们遇到难点时及时帮助和协调!感谢跟我一起做应用宝安装包瘦身的小伙伴王洋、曹荣丽、周茜以及开发和设计同学,大家一起通力合作才达成我们的目标!
廖志致谢:
这辈子最想感谢的人是我的外公陈光煜老先生,他不仅尽力给予了我童年最好的教育,而且用他的言传身教教会了我为人正直。
其次,我要感谢我的外婆赵碧君老太太,她不仅含辛茹苦地帮助我母亲把我抚养成人,也用她自身的言行教会了我善良。当然,我也要感谢生我养我无私奉献她近乎所有给我的母亲陈正伟,希望她能一直健康、快乐的安享晚年。
再次,我自然要感谢全力支持我工作的妻子王维,若不是她在家撑起半边天、用心教育我们的儿子廖紫安,我就没有足够的时间去思考、探索和沉淀我的各项测试技术经验。
最后,我想对五年来一路相伴、并肩战斗的专项测试团队的兄弟姐妹们真心说一句:我爱你们,你们是最棒的!
廖海珍致谢:
作为主编,首先感谢各位章节书写的负责人和领导麦克,廖叔。写书的工作历时很长,近半年了,大家都是靠工作之外的时间来做,而且期间经过几轮思考,又不断的要求调整修改文章布局和细节,感谢各位的支持。通过大家克服各种工作忙,时间不够,文笔如何表达优化思路等困难,才能将整本书完成。致以:写内存篇的张志伟、蒋翠翠,写电量篇的阳文彬、张媛,写流畅度篇的罗家润、叶方正,写导航篇的马蕾、文娟,写网络优化篇的廖志、翟翌华,写应用安装包瘦身篇的李金涛,写工具GT篇的秦守强,在此表示深深的感激。
其次,也要感谢当时周末非工作时间和我一起讨论书的脉络的罗小松,两个小时的碰撞,书的架构原型产生了。众人拾柴火焰高,就是和小松,还有后面麦克,廖叔,不断的讨论交流,才慢慢形成了这本书的主要架构。其实,早期设计中他也有一部分的写作工作,后面为了突出各案例技术点,内容做了调整,将他负责的章节砍掉了。还有张媛,因为我的时间安排不开,让她帮忙修改写了网络优化部分内容,后来因为有更适合的表达方式,将她修改的都作废了,一并感谢。
最后,感谢我的老公王士伟和孩子开开。刚开始启动写书的时候,作为主编,觉得需要这么多人一起协助,还有安排各种组织讨论工作,而且又不是擅长的领域,第一次做,个人表示亚历山大。最初自己也尝试写了网络优化部分的初稿,写了30页快写崩溃了,当时很迷茫,不确定是否能把书写完,感谢老公不断的鼓励和孩子给我带来的快乐,让我更有动力和自信完成写书大业。
罗家润致谢:
首先感谢专项组对我的培养和帮助。其次要感谢麦克,流畅度这块的测试一开始都是麦克带我着做的,教会了我很多东西。最后感谢耿大师和曹老师,流畅度里的案例大部分工作都是和他们一起完成的。
马蕾致谢:
感谢专项组和地图组所有小伙伴们一直以来对我工作的支持和帮助。感谢廖叔和超姐对专项测试方向的指导和建议。感谢家里的两只鹦鹉乖乖和萌萌一直以来的陪伴。
秦守强致谢:
感谢GT用户交流群里的各位业界朋友,正是他们旺盛的好奇心推动了GT这个工具产品的不断演进,这才有了本书中GT的相关内容。
文娟致谢:
首先,感谢专项组和地图组的小伙伴们给予的支持和鼓励,感谢廖叔提供诱导专项优化的机会以及对我的指导,感谢项目建立初期参与调研的小洪同学,还有在整个过程中给我提供各种意见和建议的allison,以及提供工具支持的金涛和明明。
再次,感谢家人的陪伴。
叶方正致谢:
感谢所有有目标、有理想的人。
翟翌华致谢:
首先感谢我的Leader廖叔,在工作中他开阔的思路,追求极致的工作方式,给了我不少帮助,得益于他的指导,我的网络流量优化项目开展得井井有条,做到了极致,得到了项目组的大力认可,个人也得到了快速的成长。
其次也要感谢项目参与者罗家润、王洋、朱明,我们都投入了很多精力在网络流量优化项目上,所有的成果都是属于大家的,也感谢组内所有给与我帮助的同事们,项目组氛围非常融洽,大家和睦得像一家人。
最后感谢我的老婆和孩子,家庭方面老婆付出了很多,才使得我能更专心的投入工作,感谢你们对我工作的理解,最后把这本书送给你们,我最爱的老婆周洋和儿子帅帅。
张媛致谢:
感谢我的同事袁建发。文中提到的PowerStat2.0工具,是在他之前开发的电量分析工具基础之上再次开发并优化完善的。工具对实际项目的电量测试分析起到了很大的作用。
张志伟致谢:
感谢我的家人,有他们的陪伴我才能完成书里的内容。献给乐乐!
Contents 目 录
序
前言
第1章 越用越卡为哪般—降低待机内存 1
1.1 新手入门 1
1.2 规范测试流程及常见等问题 6
1.2.1 测试流程 7
1.2.2 Dalvik Heap的常见问题 8
1.2.3 示例 9
1.2.4 新的问题 10
1.3 新问题的进一步挖掘 12
1.3.1 Dalvik Heap内部机制 13
1.3.2 问题所在 15
1.3.3 优化Dalvik内存碎片 17
1.4 进阶:内存原理 19
1.4.1 从物理内存到应用 20
1.4.2 smaps 22
1.4.3 zygote共享内存机制 25
1.4.4 多进程应用 25
1.5 案例:优化dex相关内存 26
1.5.1 从class对象说起 27
1.5.2 一个类的内存消耗 28
1.5.3 dex mmap 29
1.5.4 dex文件优化 30
1.6 本章小结 33
第2章 手机发烫是为何—降低耗电量 35
2.1 电量测试方法 36
2.1.1 硬件测试 37
2.1.2 软件测试 42
2.2 电量优化方法 55
2.2.1 优化方法一:CPU时间片 55
2.2.2 优化方法二:wake lock 55
2.2.3 优化方法三:传感器 56
2.2.4 优化方法四:云省电策略 56
2.3 本章小结 56
第3章 怎样才能如丝般顺滑—流畅度评测 57
3.1 流畅度评测方法介绍 57
3.2 流畅度 59
3.3 真的?用SM就够了吗 65
3.4 流畅度优化案例 69
3.4.1 通过SM评估App的流畅度 70
3.4.2 从最简单的UI层优化入手 71
3.4.3 Lint扫描,发现代码中的流畅度性能问题 79
3.4.4 优化App的逻辑层 84
3.4.5 优化App的IO层 88
3.4.6 流畅度优化经验 89
3.5 本章小结 91
第4章 坑爹的路线如何破—导航评测 93
4.1 路线规划评测 93
4.1.1 路测,人工评测,还是自动化 94
4.1.2 选择测试用例 95
4.1.3 寻找bad case的判断准则 96
4.1.4 判断路线是否相似 97
4.1.5 自动化评测方案—无路况版 97
4.1.6 自动化评测方案—路况版 101
4.1.7 特殊情况 104
4.1.8 展望 106
4.2 播报诱导评测 106
4.2.1 播报诱导常用测试方法:路测 106
4.2.2 室内评测是否能代替路测 107
4.2.3 耳听为虚,眼见为实 109
4.2.4 找到更多不好的用例 110
4.2.5 评测平台的建成 113
4.2.6 评测用例的优化 113
4.2.7 让评测更快,更好,更准 116
4.3 本章小结 117
第5章 修一条时刻畅通的高速路—网络优化 119
5.1 上传速度和成功率的优化 119
5.1.1 任务背景及方案雏形 120
5.1.2 鱼翅的要点 124
5.1.3 探索过程中的经验与思考 131
5.2 流量优化 135
5.2.1 摸清现状 136
5.2.2 优化精简 148
5.2.3 持续监控 153
5.2.4 优化过程中的经验与思考 153
5.3 本章小结 154
第6章 苗条才是美—应用安装包瘦身 155
6.1 瘦身的方向选择 155
6.2 案例:瘦成一道闪电 156
6.2.1 代码部分 157
6.2.2 资源部分 168
6.2.3 极限压缩zip 175
6.3 本章小结 181
第7章 工欲善其事必先利其器—打造趁手的测试工具GT 183
7.1 初心 184
7.1.1 选择恰当的时机 184
7.1.2 需要解决的问题 185
7.1.3 决定工具的形态 186
7.2 在实践中发挥作用 186
7.2.1 CPU 186
7.2.2 Jiffies 188
7.2.3 电量 189
7.2.4 流畅度和FPS 191
7.2.5 内存 191
7.2.6 流量 192
7.3 工具的获取 193
7.4 GT使用 193
7.4.1 GT在场测中 194
7.4.2 GT在自动化测试中 203
7.5 本章小结 205
第1章
越用越卡为哪般——降低待机内存
在智能手机兴起的这几年中,我们见证了手机内存从256MB到4GB的巨大变化,进程可用的内存也从仅有16/32MB到现在可以使用2GB以上的内存。与此同时,应用的功能也日益复杂,也有更多的进程在同时运行,需要协作和互相切换的应用越来越多。
因此,在硬件资源增长后,应用开发者们依然会感觉到内存是稀缺资源。我们仍然需要每个应用开发者了解内存的消耗情况,并尽量节约使用内存。否则,应用会越用越卡。本章将从内存分析入手,讲解如何降低App的待机内存。
1.1 新手入门
当软件实现了新功能后,准备发布版本前,往往需要进行一轮性能测试以确定没有性能问题,这类测试通常包括功能的流畅度、电量消耗和内存使用情况等。
由于内存组成具有复杂性,实际上并没有简单通用的方法就能够发现所有的内存问题。下面,我们会围绕一组案例展开,通过对案例的分析讲解各种内存测试的工具和方法。这些例子都是从真实的测试案例中提取的,经过加工后使得问题表现得更加明显。
接下来我们从一个最常见的内存泄漏开始,作为最典型的内存问题,类似的情况可能在无数应用的无数版本中出现过,而且还会不断地在新版本里出现。对于这样的问题,我们必须要准确识别出来。
在大部分应用中,经常会有一类功能是需要加载附加资源的,比如显示从网络下载的文本或图片。这类功能往往需要在内存中存放要使用的资源对象,退出该功能后,就需要将这些资源对象清空。如果忘了清理,或者是代码原因造成的清理无效,就会形成内存泄漏(GC)。我们的测试任务就是保证功能的正常,并且不会有遗留的内存对象造成泄漏。
要开始进行性能测试,测试工具是必不可少的。我们一般都会优先使用SDK/IDE自带的工具,因此首先会想到的工具就是和IDE集成在一起的Android Device Monitor/Android Studio了。
大多数情况下,功能代码都是由Dalvik虚拟机里执行的Java代码实现的,因此主要的内存消耗也是由Java代码使用new分配的内存。Android Device Monitor和Android Studio能够方便地观察Heap Alloc部分的大小,进行初步的统计,还能够观察到GC发生时的内存变化情况,如图1-1和图1-2所示。
图1-1 使用Android Device Monitor观察应用的内存消耗
图1-2 使用Android Studio观察应用的内存消耗
在图1-1中,我们能够看到应用当前消耗了多少内存,以及各种不同类型对象的初步统计。在图1-2中,Android Studio进一步将内存数据进行了图形化,这样就能方便地看出GC(垃圾回收)情况和明显的内存趋势。如果存在明显的内存泄漏,那么在图中就会表现为随着功能的反复使用,内存值不断升高,即使出现GC也没法降下来,如图1-3所示。
图1-3 典型的内存泄漏
发现了内存泄漏,通常就可以交给开发去处理了。但我们并不只是给开发人员丢一个问题描述和复现路径过去,而是利用手头的工具,获得一些更详细的数据,能够使大家更快地定位和解决问题,并对内存进行分析。这样分析内存获得详细数据的首选工具就是Eclipse Memory Analyzer Tool(MAT)。
MAT是使用非常广泛的Java内存分析工具,功能强大。已经有很多关于它的详细教程,在本书中就不再细述用法。本节主要介绍使用MAT在分析Android应用时的一些常用技巧。
通常我们用MAT打开hprof文件后,能够在首页看到Top Consumers和Component Report等功能,使用这些功能能够快速定位一些大块的内存消耗。但对于Android应用的hprof文件,我们在使用了Top Consumers统计使用情况后,往往只能看到如图1-4所示的情况。
图1-4 使用MAT分析内存构成
系统的资源类占据了很大一部分的内存,而其余的前几名也往往是系统类。这是由于从虚拟机角度不会区分系统框架和应用自身的对象,后面的1.4.3节会详细说明出现这种现象的原因。
为了去除这部分对分析的干扰,我们在用Android SDK提供的hprof-conv转换时需要增加一个参数:
hprof-conv [-z] <infile><outfile>
-z: exclude non-app heaps, such as Zygote
另一种可替代的方法是使用OQL。如果hprof文件是已经转换过的,可以在数据中寻找应用的Application类对象,将对象地址转换为十进制后输入以下查询语句:
select * from instanceof java.lang.Object s where s.@objectAddress > 1107296256
使用-z参数转换或OQL查询后得到的对象集合就只包含应用代码分配的部分了。在此基础上使用MAT提供的Top Consumers和Component Report等功能就能够得到比较准确的结果,如图1-5所示,没有了系统类所占内存的干扰,只有应用自身代码创建的对象,对于发现内存问题比较有帮助。
图1-5 分离之后再次分析内存构成
对于一般的内存泄漏类问题,使用以上方法后通过MAT提供的分析报告就很容易识别出来。在我们以往的测试经历中,用这种方法发现了上百次的内存问题。这些内存往往是加载后忘了释放的Bitmap,临时生成的byte数组和文件缓冲区,包含Handler的Activity,等等。
接下来我们看一个真实的应用测试案例。在这个案例里,有些位图在使用完之后由于种种原因,一直没有销毁而存在于ImageLoader里,使用一段时间后ImageLoader会变得越来越庞大。使用上面介绍的方法去除了系统的影响后,MAT的泄漏报告给出了结果,如图1-6所示,ImageLoader消耗了接近1/3的内存。
有了这样的数据,接下来就可以结合图片追踪代码,看引用到ImageLoader的代码部分哪里有问题,从而快速修复问题。
图1-6 MAT识别出来的问题
1.2 规范测试流程及常见等问题
最开始进行内存测试时,我们可能还有些摸不着头脑,试着找了些工具,看了看教程就开始动手了。有时候因为问题比较明显,就真的发现了问题。再之后遇到类似的测试需求,我们就会按上次的经验去做。有时候可能发现问题,也可能发现不了,还有些时候甚至是在白费工夫。因为随着明显的问题逐渐被找出来,剩下的都是更加复杂而不太明显的问题了,甚至有些问题更是可以归属到优化范畴或者产品策略之内,而不再是简单的内存问题。
随着经验的逐渐增加,我们逐渐意识到,以前的很多测试方法都属于随机乱测。对于较为成熟的软件,这类方法的测试有效性往往比较低,运气好了才会遇到问题。如果是较深层次的问题,要么遇不到,要么遇到了也找不出原因。因此,有必要总结出一套成熟的流程方法,能够考虑到各个方面,才能提高测试的有效性。
1.2.1 测试流程
由于内存测试属于性能测试,Android系统又和Linux有很多相通之处,因此我们可以参考常见的Linux性能测试方法和指标,来制定客户端性能测试方案。常见的测试方法包括Monkey/UIAutomator类的常规压力测试、大数据/操作的峰值压力测试、长时间运行的稳定性测试等。这些方法都可以叠加在内存测试的方案中,观察这类场景下的应用内存情况,经常能够发现类似内存泄漏或OOM的问题。
参考了常见性能测试的方案,以及总结了以往对内存性能测试的经验后,我们总结出了一套进行内存测试的经验性流程,下面介绍这个流程中的要点。
1. 代码
通常用来进行内存测试的版本是纯净版本,不应该附加多余的Log和调试用组件。例如有些情况下,为了测试界面延迟/函数执行时间等性能,会加入一些桩点代码。在内存测试中这些代码是不必要的,它们可能会分配临时内存,引起更多的GC,导致应用出现运行缓慢、卡顿等现象。
2. 测试场景
测试场景通常有两类。一类是当前有新开发或改动的某项功能,需要对该功能进行性能测试。因此测试场景主要针对该功能组织,包括功能的开启前、运行、结束后等测试点。另一类是整体性能,考察应用的常见场景,在综合使用情况下的性能指标。测试场景应当包括启动后待机,切换到后台,执行主要功能,以及反复执行各功能后。
在各类场景中,经常作为测试重点的有:
包含了图片显示的界面。
网络传输大量数据。
需要缓存数据的场景。
3. 场景转换成用例
选取了测试场景后,用例设计也要考虑内存测试的特点。一些常见的方法是:
结合场景比较操作前后或不同版本的内存变化。
显示多张图片的前台进程。
多个场景来回切换。
长时间运行进程的内存增长。
4. 执行
由于GC和广播机制的存在,应用内存通常都在不停地波动,幅度可能会达到几百KB,因此执行时需要考虑这种情况。在采集数据时,需要多次采集并计算平均值。
执行完成,我们就可以根据数据进行比较初步的分析以确定方向。一方面是我们熟悉的Dalvik Heap部分,即由Java代码直接分配的内存,可以通过IDE直接观察到使用情况,也可以使用MAT进行细致的分析。
另一方面,假如我们发现Dalvik Heap没怎么增长,而其他部分增长了许多,这种情况下的分析就要复杂一些,我们留待后面的章节再说。
1.2.2 Dalvik Heap的常见问题
随着测试的执行,随之而来的就是一大堆产生的数据。对产生的数据进行分析,找出可能存在的问题,以及问题可能的原因是接下来的重点。
由于大部分Android应用是以Java代码开发的,所以Dalvik Heap内存出现问题也是最常见的情况。常见的现象有以下几种:
随着功能的反复执行,Heap内存一直在持续增长。这种情况通常是出现了内存泄漏,这种情况最适合用LeakCanary等泄漏检查工具进行白盒测试分析。
代码执行时出现了频繁的GC,Heap Alloc内存大幅度波动。这种情况通常是分配了许多临时变量或数组,随后又被迅速回收,这种情况在确定具体场景后适合使用Heap Viewer / Allocation Tracker等工具来查看具体分配的对象。
每次启动应用后,Heap内存相比以前版本稳定增长。这种情况通常出现在启动后待机或使用某功能后,可能是由新功能及代码改动引入的固定内存增长。这种情况适合获取Heap Dump后进行多版本或功能使用前后的对此,能够迅速找到增长原因。
Heap Alloc变化不大,但进程的Dalvik Heap Pss(Proportional Set Size)内存明显增加。这种情况比较少见,是由于分配了大量小对象造成的内存碎片,在后面的章节里会详细讲解,具体内容请见下一节。
1.2.3 示例
1.1节已经介绍了出现内存泄漏时的问题及分析方法,在这里我们再以一个真实的例子介绍常见的几种内存问题和分析方法。
这是发生在手机管家4.x的某个版本上的案例,新版中加入了一些功能,开发人员估计新功能可能会分配几万字节到几十万字节的内存,因此我们来进行内存方面的测试验证。当新功能的代码合入后,我们发现应用启动后的内存增长超过了2MB,这可大大超出了所有人的预期,一定是有什么地方出了严重的问题。
由于新加入了好几个功能,因此要逐个去排查。如果某个新功能的代码都在同一个package下,那么就可以使用MAT的过滤功能来验证这部分代码是否使用了内存,如图1-7所示。
图1-7 使用MAT的过滤功能
经过一番筛选排查,发现内存中多出了一些新对象,多消耗了约300KB内存,目前这并不能解释内存增长了2MB的原因。但仔细检查多出来的对象并清理掉不用的部分也是有帮助的。
经过检查,这部分内存是其他新功能使用的。对此我们需要进一步确认,这些对象是否是有用的,还是临时创建的。对于临时创建不再使用的对象可以主动销毁,而对于保存着信息将要用到的对象也可以进行进行压缩裁剪,以进一步减少占用的内存。
在以上排查中,我们确实发现了一些问题,但将一些不用的对象清理后再执行测试,总体内存并没有明显减少。现在看来,Dalvik Heap里分配的内存并没有增加许多,说明问题是不能只在Dalivk Heap里就能解决的,也许是别的部分出现了问题?接下面我们就继续深挖下去。
1.2.4 新的问题
经过上一轮的优化,在内存监视器里新版本的Heap内存表现已经比较好了,新功能只消耗了几万字节到几十万字节内存。但是要注意的是,Heap内存并不是应用的全部,我们在设置或其他管理工具里看到的应用内存大小是应用整个进程的内存使用量。也有可能出现Heap部分完全没有增长而其他部分增长的情况。
要观察进程的内存使用情况,就需要用到其他的观测工具,Android里最常用于观察进程内存的方法就是dumpsys meminfo <package name|pid>命令。
对我们的新版应用执行该命令,能够得到以下的输出结果:
** MEMINFO in pid 17481 [com.example] **
Shared Private Heap Heap Heap
Pss Dirty Dirty Size Alloc Free
------ ------ ------ ------ ------ ------
Native 28 8 28 5744 3739 1117
Dalvik Heap 10112 10224 9624 14076 10386 3690
Dalvik Other 3212 3076 0 0
Stack 270 270 0 0
Ashmem 2 0 0 0
Other dev 7 0 4 0
.so mmap 1867 1330 160 0
.jar mmap 4 0 4 0
.apk mmap 2944 0 2690 0
.dex mmap 4110 64 3420 0
Other mmap 16 4 4 0
Unknown 2351 2331 0 0
TOTAL 24895 12404 6212 0
在以上输出结果中,左边Pss列的数据标识进程各部分对真实物理内存的消耗,左下角的TOTAL值就是我们在各种管理工具里看到的应用内存消耗。
而Android Studio等工具里显示的内存值,在这里是Dalvik Heap Alloc部分。根据以上的数据,我们可以看到Dalvik Heap和Heap Alloc不是相等的,而且除了Dalvik Heap之外,还有其他很多部分也会消耗内存。
这时候我们再对比一下旧版,看看是否也如此:
** MEMINFO in pid 14233 [com.example] **
Shared Private Heap Heap Heap
Pss Dirty Dirty Size Alloc Free
------ ------ ------ ------ ------ ------
Native 28 8 28 5664 3767 1040
Dalvik Heap 8026 10372 7508 11784 10113 1671
Dalvik Other 3159 3076 0 0
Stack 260 260 0 0
Ashmem 2 0 0 0
Other dev 7 0 4 0
.so mmap 1887 1344 160 0
.jar mmap 4 0 4 0
.apk mmap 2941 0 2680 0
.dex mmap 4013 64 3360 0
Other mmap 16 4 4 0
Unknown 2256 2244 0 0
TOTAL 22599 17372 13716 0
这时候就会发现问题了,Heap Alloc没增加多少,但Dalvik Heap Pss增加了许多。而其他部分基本保持不变或有少量增长。可见问题还是出现在Dalvik Heap部分,但只靠检查分配的对象是看不出来问题的。
Java代码的内存分配和释放都是由虚拟机管理的,那么这个问题会是虚拟机的问题吗?我们接下来继续通过虚拟机部分机制来探索这些内存增长的原因。
1.3 新问题的进一步挖掘
上一节介绍了内存测试的基本流程,讲述了如何发现并处理简单的内存问题。对于Dalvik Heap部分总结出了一些常见的问题模式,以及如何使用工具识别和处理这些常见的内存问题。
当简单问题不再是问题的时候,我们就会开始遇上一些奇怪问题了,类似于下面这些:
“我们这个版本引入了一个挺简单的库,内存就涨了2MB。”
“这些代码只是初始化了几个对象,还没有开始用呢。”
“我只是改了一行代码,没有创建新对象。”
“我一行代码都没改,怎么会涨呢?”
这次出现的问题就是这样一类问题,新版本的Dalvik Heap Pss内存出现了2MB左右的增长,但Dalvik Heap Alloc只增长了273KB,而从Dalvik Heap Free也能看出大部分增长的内存是处于空闲状态的。
对问题经过一段时间的观察,我们有以下几点发现:
经过较长时间待机后也没有被释放回系统。
有几处代码会导致内存增长,只要将这些代码屏蔽掉,内存使用情况就下降到正常水平。
这些代码分配的内存并不多,甚至有些地方是不需要分配内存的。
有些代码并不是这个版本新加入的,已经存在较长时间了。
使用裁剪功能的方法编译并分析内存后,基本可以确定是新加入代码消耗了内存,但并没有内存泄漏,代码经过审查也没有发现问题。
这个结果让我们陷入了困惑,常用的方法找不出问题,说明有更深层次的原因。接下来要从更底层的DVM虚拟机寻找问题。
1.3.1 Dalvik Heap内部机制
为了弄清楚为什么DVM占着内存不释放,我们阅读了DVM分配内存部分的代码。位置在Android源码的dalvik/vm/alloc下,约255KB。分析的主要流程如下:
1)DVM使用mmap系统调用从系统分配大块内存作为Java Heap。根据系统机制,如果分类的内存尚未真正使用,就不计入PrivateDirty和Pss。例如图1-8所示,Heap Size/Alloc很多,但大部分是共享的,实际使用的较少。所以反映到PrivateDirty/Pss里的内存并不多。
图1-8 共享内存较多的进程
2)新建对象之后,由于要向对应的地址写入数据,内核开始真正分配该地址对应的4KB物理内存页面。
Alloc.cpp中,从第176行起的代码如图1-9所示。
图1-9 DVM虚拟机分配内存的代码
3)运行一段时间后,开始垃圾回收(GC),有些对象被回收了,有些会一直存在,如图1-10所示。
图1-10 黑点表示的内存会被回收
4)在GC时,有可能会进行trim,即将空闲的物理页面释放回系统,表现为PrivateDirty/Pss下降。HeapSource.cpp中,第431行代码如图1-11所示。
图1-11 释放内存回系统的代码(一)
HeapSource.cpp中,第1304行代码如图1-12所示。
图1-12 释放内存回系统的代码(二)
1.3.2 问题所在
在了解DVM分配释放内存的机制后,根据dumpsys观察到的现象,猜测可能出现了页利用率问题(页内碎片)。如图1-13所示,第一行:在开始阶段,内存分配较满。第二行:经过GC(垃圾回收)后,大部分对象被释放,少部分留下来。
图1-13 产生内存碎片
这种情况下可能会产生的问题是,整页的4KB内存中可能只有一个小对象,但统计PrivateDirty/Pss时还是按4KB计算。
在通常的JVM中,借助Compacting GC机制,整理内存对象,将散布的内存移动到一起。但根据DVM的代码,DVM的Mark-Sweep算法不能移动对象,即没有内存整理功能,这种情况下就会形成内存空洞。
在猜测了可能的问题后,需要验证是否如猜测原因所致,由于MAT的对象实例数据中有地址和大小信息,我们先从MAT中导出数据。
在MAT中列出所有对象实例:list_objects java.*,然后选中所有数据并导出为csv格式,如下所示:
Class Name,Shallow Heap,Retained Heap,
class java.lang.Class @ 0x41fdd1e8,16,56,
class test.bxi$3 @ 0x432501c8,0,0,
class test.aaw$c$1 @ 0x4324fef8,0,0,
class test.ds @ 0x4324fc88,8,48,
class test.bxh @ 0x4324f438,8,248,
class test.bxg @ 0x4324f248,0,0,
class test.bxd$1 @ 0x4324f028,0,0,
处理导出的csv文件,按页面进行统计,取每个对象的地址的高位(&0xfffff000),结果相同的对象处在同一页面中。最后再按每个页面所有对象的大小分类统计,绘出直方图如图1-14所示。
图1-14 对页面利用率进行分类统计
这张图就是被测应用的页面利用率分布图,左边是利用率低的页面,右边是利用率高的页面。如果发现利用率低的页面数目增加,说明小对象碎片的数量增加了。
1.3.3 优化Dalvik内存碎片
为了能够找出有问题的代码,我们将上一步得到的数据继续处理。取出所有使用不满2KB的页面的内存块地址,再使用OQL将地址导入到MAT中,分析地址对应的对象是什么。图1-15所示就是将地址重新导入到MAT中得到的对象列表。
图1-15 内存碎片页中的对象
在这里基本就能看出来是哪些对象造成了内存的碎片化,数量比较多的前几类自然嫌疑比较大,可以先对前几个类的相关代码进行分析。也可以对这些代码进行针对性的内存测试,观察内存情况。
通过对生成这些对象的代码分析和模拟实验,我们还原出问题的基本过程:
生成对象的过程需要较多的临时变量。
批量生成过程中,由于还有空闲内存,虚拟机没有做垃圾回收。
完成后才进行垃圾回收,清除了所有的临时变量,留下碎片化的内存。
下面是造成这个问题的类似代码,执行这段代码将会在内存中形成很多碎片,造成很高的Pss占用:
private Object result[] = new Object[100];
void foo() {
for(int i = 0; i < 100; ++i) {
byte[] tmp = new byte[2000];
result[i] = new byte[4];
}
}
图1-16显示了类似情况下数组的分配范围,可见数组中每个成员的内存地址都是不连续的,并且相隔很远。这种情况下就会消耗很多个物理内存页面,增加Heap Free,造成例子中的问题。
图1-16 内存碎片对象地址的例子
经验总结
根据上述的流程,我们搞清楚了造成问题的原因,并且找到了问题代码。那么应当总结一些经验,以供借鉴。对于测试人员来说,有以下三个经验:
MAT是探索Java堆并发现问题的好帮手,能够迅速发现常见的图片和大数组等问题。但MAT也不是万能的,比如这个问题的数据就隐藏在对象的地址中。
对Android测试经验来说,可能容易找到的是应用代码及框架的各种测试经验和指导,底层以及涉及性能的测试经验并不太多。这方面可以借鉴Linux系统的测试经验,了解内核及进程相关的知识,熟悉常用工具。
内存分配的最小单位是页面,通常为4KB。
对于开发人员,以下两个经验也许能有帮助:
尽量不要在循环中创建很多临时变量。
可以将大型的循环拆散、分段或者按需执行。
1.4 进阶:内存原理
在上一节里,我们通过深入调查Dalvik虚拟机的方式,解决了Dalvik Heap Pss消耗内存过高的问题。除了Dalvik Heap Pss部分之外,应用还有其他许多消耗内存的部分。本节主要介绍其他部分的内存是如何被分配和消耗的。
同样以我们的应用为例,在几个版本之后,新加入了一个缓存功能。缓存功能会预先取一些手机的信息,并放在内存中供其他功能使用,这样可以减少后续功能的消耗,加快运行速度。
有了之前的经验,我们自然会想到不能简单粗暴地将所有缓存一次生成,这样可能会产生大量的碎片,因此需要选择一种合适的策略来进行。在选择新功能的缓存策略时,内存测试也同样有用,通过对不同策略的测试,决定哪种策略比较有效,并且消耗内存比较少。
在测试过程中我们发现,随着使用不同的策略,Dalvik Heap部分会随之增减。与此同时,不同策略执行代码的时机也会使Dalvik Other和Dex Mmap的内存消耗变化。总结规律如下:
不生成缓存时,Dalvik Other和Mmap会随之下降。
按需生成缓存时,即使只生成一条记录,Dalvik Other和Mmap也会增加。
生成多条缓存记录时,Dalvik Other和Mmap会在开始增加,然后一直保持不变。
Dalvik Other不会下降,Mmap偶尔会下降。
通常我们只是大致了解到,Dalvik Other和Mmap和代码数量相关,对于越复杂的应用,这部分内存就越多,并没有进行过定量的分析。但现在随着对Dalvik Heap部分的优化,我们发现Dalvik Other和Mmap在内存中的比重越来越大。在这个版本里,占总内存的将近一半,不能再置之不理,而是要寻找办法对这部分内存进行优化。
对于这些不熟悉的部分,我们首先要先去了解背后的原理,才能够针对性地去研究这些内存是如何被消耗的。
1.4.1 从物理内存到应用
我们首先要了解系统的内存机制,搞清楚物理内存是如何被分配到各个进程的,以及共享内存的机制,等等,理解这些机制对测试及优化都会有很大帮助。
根据Google提供的Android整体架构图,如图1-17所示,可以看到Android系统是基于Linux内核的,因此底层的内存分配及共享机制与Linux基本相同。但由于Android是为移动设备设计的,所以整套架构为了符合移动设备的特性,需要有较低的内存及能耗需求。因此Android只使用了Linux内核,不使用传统Linux系统的组件。这些组件虽然功能强大,但是较为消耗系统资源。Google开发了若干较小的组件,例如将庞大的glibc换为bionic库,使用SQLite数据库等。Android还扩充了许多内核机制和实现,其中对内存影响较大的是Ashmem和Binder机制。
在Ashmem及COW(Copy-On-Write)机制的基础上,Android进程最明显的内存特征是与zygote共享内存。为了加快启动速度及节约内存,Android应用的进程都是由zygote fork出来的。由于zygote已经载入了完整的Dalvik虚拟机和Android 应用框架的代码,fork出的进程和zygote共享同一块内存,这样就节约了每个进程单独载入的时间和内存。应用进程只需载入自己的Dalvik字节码及资源就可以开始工作。
图1-17 Android架构图
综上所述,一个在运行的Android应用进程会包含以下几个部分:
Dalvik虚拟机代码(共享内存)
应用框架的代码(共享内存)
应用框架的资源(共享内存)
应用框架的so库(共享内存)
应用的代码(私有内存)
应用的资源(私有内存)
应用的so库(私有内存)
堆内存,其他部分(共享/私有)
有了整体视角后,我们再开始深入观察某一个应用的内存情况。在之前的测试中,我们使用系统提供的dumpsys meminfo工具来观察内存值。它能够将不同的内存消耗分类统计,输出成便于查看的格式。
但如果我们想细致地研究各部分内存的由来,只靠这个工具是不够的,我们有必要按照系统划分各部分的方式来理解和分析内存。
通过阅读和分析dumpsys meminfo的代码,我们能够了解到Android是如何划分各部分内存的。下面详细讲解dumpsys meminfo工具是如何统计各部分内存值的。
1.4.2 smaps
由于Android底层基于Linux内核,进程内存信息也和Linux一致,所以Dalvik Heap之外的信息都能够从/proc/<pid>/smaps中取得。
在smaps中,列出了进程的各个内存区域,并根据分配的不同用途做标识,以下是root用户使用cat /proc/<pid>/smaps的一个例子:
788c2000-789bf000 rw-p 00000000 00:00 0 [stack:5113]
Size: 1012 kB
Rss: 4 kB
Pss: 4 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 4 kB
Referenced: 4 kB
Anonymous: 4 kB
AnonHugePages: 0 kB
Swap: 0 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Locked: 0 kB
VmFlags: rd wr mr mw me nr
dumpsys统计各个内存块的Pss、Shared_Dirty、Private_Dirty等值,并按以下原则进行了归并:
/dev/ashmem/dalvik-heap和/dev/ashmem/dalvik-zygote归为Dalvik Heap。
其他以/dev/ashmem/dalvik-开头的内存区域归为Dalvik Other。
Ashmem对应/dev/ashmem/下所有不以dalvik-开头的内存区域。
Other dev对应的是以/dev下其他的内存区域。
文件的mmap按已知的几个扩展名分类,其余的归为Other Mmap。
其他部分,如[stack]、[malloc]、Unknown等。
了解了dumpsys的方法后,我们可以自己解析smaps,看看归并前各项的内存都是多少。这样能够得到比dumpsys更详细的信息,有助于分析一些问题。
首先将Pss分为以下几大类,计算各部分占比。在这个例子里,几大项是三分天下的节奏。Dalvik和Other dev内存都占了30%以上,剩下的是mmap和Unknown。进行内存优化时不能只看Dalvik部分,需要同时评估所有的部分。
1. Dalvik
Dalvik内存分为多个区域,meminfo统计的是所有区域累加的值:
Dalvik_Heap 5529
+ /dev/ashmem/dalvik-heap (deleted) 4680
+ /dev/ashmem/dalvik-zygote (deleted) 849
Dalvik_Other 3240
* LinearAlloc 1229
* Accounting 1579
* Code_Cache 432
+ /dev/ashmem/dalvik-LinearAlloc (deleted) 1229
+ /dev/ashmem/dalvik-aux-structure (deleted) 1291
+ /dev/ashmem/dalvik-bitmap-2 (deleted) 192
+ /dev/ashmem/dalvik-card-table (deleted) 96
+ /dev/ashmem/dalvik-jit-code-cache (deleted) 432
其中:
Dalvik_Heap—包括dalvik-heap和dalvik-zygote。堆内存,所有的Java对象实例都放在这里。
LinearAlloc—包括dalvik-LinearAlloc。线性分配器,虚拟机存放载入类的函数信息,随着dex里的函数数量而增加。著名的65535个函数的限制就是从这里来的。
Accounting—包括dalvik-aux-structure、dalvik-bitmap、dalvik-card-table。这部分内存主要做标记和指针表使用。dalvik-aux-structure随着类及方法数目而增大,dalvik-bitmap随着dalvik-heap的 增大而增大。
Code_Cache—包括dalvik-jit-code-cache。jit编译代码后的缓存,随着代码复杂度的增加变大。
由于堆内存部分往往是应用消耗内存最多的地方,在内存优化中,最常见的方法就是减少Dalvik Heap中创建的对象,能够直接减少Dalvik Heap,并间接减少Accounting部分。减少代码会直接减少运行辅助部分。
在进行不同版本的对比测试时,我们往往会发现Dalvik Other和Dex Mmap出现了稳定的增长,这是由新加入的代码引入的内存消耗。
根据Dalvik虚拟机的原理,在加载class时,会根据类的变量个数及函数个数申请相应大小的内存,作为运行时的内部指针。这部分内存就会体现在LinearAlloc及aux-structure的增长中。随着版本的开发,应用class的数目及复杂度也在不断地增长,因此Dalvik Other部分也在不断地增长。
由于这部分内存的增长取决于代码复杂度,因此通常情况下并没有简单直接的方法能够降低它们的消耗。但是通过仔细分析它们的组成及原理,还是能够找出一些间接的方法降低这部分内存的,详细方法请见2.6节。
2. mmap
系统会将一些文件mmap到内存中,对各个文件进行mmap的时机及大小比较复杂。dex_mmap是其中主要的内容:
apk_mmap 648
dex_mmap 1448
+ /data/dalvik-cache/data@app@com.example-2.apk@classes.dex 917
+ /system/app/Stk.odex 16
+ /system/app/TelephonyProvider.odex 140
+ /system/framework/android.policy.odex 8
+ /system/framework/bouncycastle.odex 2
+ /system/framework/conscrypt.odex 3
+ /system/framework/core.odex 50
+ /system/framework/ext.odex 19
+ /system/framework/framework.odex 249
+ /system/framework/framework2.odex 44
jar_mmap 4
ttf_mmap 47
so_mmap 3127
other_mmap 11
应用的dex会占据较大的空间,并且随着代码增加使得dex文件变大,占用的内存也会增加。减小dex的(相当于减少代码)尺寸能够降低这部分内存占用,同时也会减少dalvik部分的内存。
1.4.3 zygote共享内存机制
上一小节介绍了应用各部分内存的含义,读者对dumpsys meminfo输出的大部分数据都能够有所理解。但dumpsys meminfo工具还会输出Heap Size/Alloc/Free部分的数值。我们知道这些数值是Dalvik虚拟机统计的内存堆的使用量,但这些数值是如何对应到Pss内存上的?比如Heap Alloc和Heap Pss往往相差不远,那是不是可将其看做基本等同的呢?下面我们试图解释这几项数值之间的关系。
由于虚拟机运行时并不区分某个对象实例是Android框架共享的还是应用独有的,Heap Alloc统计的是由虚拟机分配的所有应用实例的内存,所以会将应用从zygote共享的部分也算进去,于是Heap Alloc值总是比实际物理内存使用值要大。
Heap Alloc虽然反映了Java代码分配的内存,但存在框架造成的失真。除此之外,进程还有许多其他部分也需要使用内存。为了准确了解应用消耗的内存,我们要从进程角度而不是虚拟机角度来进行观察。
Pss表示进程实际使用的物理内存,是由私有内存加上按比例分担计算的各进程共享内存得到的值。例如,如果有三个进程都使用了一个消耗30KB内存的so库,那么每个进程在计算这部分Pss值的时候,只会计算10KB。总的计算公式是:
Dalvik Pss内存 = 私有内存Private Dirty
+ (共享内存Shared Dirty / 共享的进程数)
从实际含义来讲,Private Dirty部分存放的是应用新建(new)出来的对象实例,是每个应用所独有的,不会再共享。Shared Dirty部分主要是zygote加载的Android框架部分,会被所有Android应用进程共享。通常进程数的值在10~50的范围内。
Pss是一个非常有用的数值,如果系统中所有进程的Pss相加,所得和即为系统占用内存的总和。但要注意的是,进程的Pss并不代表进程结束后系统能够回收的内存大小。
1.4.4 多进程应用
根据上一节中的描述,当一个进程结束后,它所占用的共享库内存将会被其他仍然使用该共享库的进程所分担,共享库消耗的物理内存并不会减少。实际上,对于所有共享使用了这个库的应用,Pss内存都会有所增加。对于一般的进程,只是共享着zygote进程的Android框架等基础部分,而通常手机使用时的应用进程数达到几十个至上百个,所以某个进程结束后,其他进程内存增加的情况并不明显。
但对于多进程的应用来说,由于多个进程之间会共享很多内容,包括代码、资源、so库等,因此单个进程结束造成的影响就会比较明显。以有两个进程的应用为例,进程共享着部分内存,因此当一个进程不再需要这些内存时,就会出现如图1-18所示的场景。表现为一个进程的内存下降了,另一个进程的内存就会明显上升。
图1-18 两个共享内存进程的内存变化
由此可见,我们在统计多进程的应用内存和进行优化时,需要综合考虑,以免出现努力优化了一个进程的内存,却造成其他进程内存增长的情况。
1.5 案例:优化dex相关内存
上一节提到,随着代码功能的增加,代码复杂度也在不断地变大,这时我们往往会发现Dalvik Other和Dex Mmap这两部分消耗的内存也在不断增加。在之前的例子里,我们知道这两部分的内存已经接近总内存的一半。在Dalvik Heap已经充分优化的情况下,我们有必要继续研究这部分内存如何优化。
我们已经知道Dalvik Other存放的是类的数据结构及关系,而Dex Mmap是类函数的代码和常量。通常情况下,要减少这部分内存,需要从代码出发,精简无用代码,或者将功能插件化。但如果我们深入理解了系统,也能够找到一些其他方法来降低这部分的内存消耗。
1.5.1 从class对象说起
在MAT的对象实例列表中,我们往往能有很多class条目,如图1-19所示。
图1-19 MAT中似乎不消耗内存的class条目
这些对象是各种类型的元数据。从MAT的信息看来,它们只是保存了各个类的静态成员,所以对于没有静态成员的类型,Shallow Heap的值为0,并不消耗内存。但实际上,这只是class消耗内存的冰山一角。我们从下面的例子开始:
这段代码是一个数学处理库提供的函数,代码十分简单,只是新建了两个对象,但将这段代码在一个空应用中执行后,我们能够观察到以下的内存增长:
Dalvik Heap增长约1.8MB。
Dalvik Other增长约60KB。
.dex mmap增长约300KB。
Dalvik Heap的增长是我们能预期的。通常来说,能够从代码的逻辑中分析出执行这段代码总共需要分配多少内存,也能够在MAT中看到新建对象消耗的内存。当应用使用完新建的对象后,就会将heap内存释放,但Dalvik Other和.dex mmap部分是不会释放的。接下来我们首先分析一下这两部分为什么消耗了这么多内存。
1.5.2 一个类的内存消耗
首先,如果我们在代码中要使用一个类,例如以下代码:
Foo f = new Foo();
虚拟机在执行到这步时会做什么呢?
第一步是loadClass操作,将类信息从dex文件加载进内存:
1)读取.dex mmap中class对应的数据。
2)分配native-heap和dalvik-heap内存创建class对象。
3)分配dalvik-LinearAlloc存放class数据。
4)分配dalvik-aux-structure存放class数据。
第二步是new instance操作,创建对象实例:
1)执行.dex mmap中<clinit>和<init>的代码。
2)分配dalvik-heap创建class对象实例。
在这个过程中,可能还会分配dalvik-bitmap和jit-code-cache内存。如果class Foo引用了其他类型,那就还需要先按照同样的逻辑创建被引用的class。由此可见,在创建一个类实例的每一步都需要消耗内存。我们接下来大概计算一下new操作需要消耗的内存。
根据Dalvik虚拟机的代码,能够得知class根据类成员和函数的数目分配LinearAlloc和aux-structure的多少,以及class本身及函数需要的字节数。我们再根据一个应用中所有class的总量进行平均计算,得到以下一组数据。
第一步是loadClass操作,加载类信息:
.dex mmap(class def + class data): 载入一个类需要先读取259字节的mmap。
dalvik-LinearAlloc: 在LinearAlloc区域分配437字节,存放类静态数据。
dalvik-aux-structure: 在aux区域分配88字节,存放各种指针。
第二步是new instance操作,创建对象实例:
.dex mmap(code):为了执行类构造函数,还需要读取252字节的mmap。
dalvik-heap: 根据类的具体内容而变化。
可见在创建对象实例的操作中,Dalvik Other和.Dex Mmap部分就各需要约500字节的内存空间。但是考虑到4KB页面的问题,由于这些内存并不是连续分布的,所以可能需要分配多个4KB页面。当然由于很多类会在一起使用,使得实际的页面值不会那么多。
以我们举例的应用为例,总共有7042个类,启动后载入了1522个类,这时侯应用的.dex mmap内存消耗大约是5MB,平均后约为3.4KB。Dalvik Other的部分会少一些,但依然是远远超出需要使用的大小。
1.5.3 dex mmap
dex mmap在Android应用中的作用是映射classes.dex文件。Dalvik虚拟机需要从dex文件中加载类信息、字符串常量等,还需要在调用函数的时候直接从mmap内存中读取函数代码(dvm bytecode)来执行;所以该部分内存是程序运行必不可少的。
以一个示例应用为例,我们能够在MAT中看到,应用加载了大约1500个class类型,而dex文件的class类型共有10635个。使用dex mmap动态统计功能统计后发现,虽然只加载了1500个类,但dex内存通常高达4~6MB,差不多是dex文件大小的一半,如表1-1所示。
表1-1 dex内存的利用率
启动后加载class数 总共class数 class数占比
约1500 10635 14%
启动后dex mmap内存 dex文件大小 dex文件大小占比
约4~6MB 10.9MB 37%~55%
从以上数据可以看到,很大一部分dex内存空间被浪费了,实际使用到的数据和代码并没有那么多,这是为什么呢?这是由于dex文件在生成时按字母顺序排列。由于4KB页面加载的原因,实际运行时会加载许多相邻但不会被用到的数据。例如在代码中使用了A1类,虚拟机就需要加载包含A1类数据的页面。但由于A1的数据只有1KB,那在加载的4KB页面中,还会有A2A3A4类,总共占用了4KB内存。
假设我们的代码里在用到A1类后,还会用到B1、C1、D1类,那么如果能在dex文件中将A1、B1、C1、D1类放在一起,虚拟机就只需要加载一个4KB页面,不仅减少了内存使用,还对程序的运行速度有好处。因此,优化的思路就是调整dex文件中数据的顺序,将能够用到的数据紧密排列在一起。
1.5.4 dex文件优化
为了达到优化的目的,我们需要先了解dex文件的结构。dex文件结构如表1-2所示。
表1-2 dex文件结构
区 域 描 述 内 容
Header
索引区 String Id list 指向Data的偏移量
Type Id list
Method Prototype Id list
Field Id list
Method Id list
Class Definition list
Data区 ClassData 类数据 常量及变量定义Id
接口Id
成员函数Prototype Id
类Annotation的偏移量
StringData 字符串数据 类名
Proto字符串
常量字符串
Code 函数代码 Dalvik字节码
函数Debuginfo的偏移量
函数Annotations的偏移量
StaticValues 静态变量初始值
Debuginfo Debug信息
Annotation Annotations
Map list
简而言之,为了节约空间,dex将原先在各个class文件中重复的信息集中放置在一起,并以索引和指针的形式支持快速访问。虚拟机能够通过索引表在Data区域中找到需要的信息。
下面我们看一个访问字符串的例子。在dex文件结构中,读取字符串需要先到StringIdList中查表,然后根据查到的地址到Data区读取内容。StringIdList的数据结构如下:
struct DexStringId {
u4 stringDataOff;
};
现在我们模拟虚拟机读取一个字符串,来观察内存的消耗。假设有一个字符串的id = 6728,对应的地址就会是112 + 6728 = 6990。因此虚拟机首先根据string ID读取0x006990 - 0x006994的内容,此时系统会加载0x006000~0x006fff的整页内存,从Pss角度来看,会增加4KB。
虚拟机读到的内容是stringDataOff = 0x531ed4,随后虚拟机会继续从0x531ed4读取字符串内容,假设字符串长度是45字节,则虚拟机会读取0x531ed4~0x531f04的内容,但此时系统也必须加载0x531000~0x531fff的整页内存,从Pss角度来看,会再次增加4KB。
由此可见,在有些情况下,虚拟机读取data区的一个数据,就至少要消耗8KB物理内存。如果多次读取的分散在文件各处的数据,就可能会以4KB的倍数快速消耗内存。
Android SDK提供了dexdump工具来观察dex文件内容,我们以此工具来看看dex的数据内容:
dexdump classes.dex
Processing 'classes.dex'...
Opened 'classes.dex', DEX version '035'
Class #0 header:
...
Class #0 -
Class descriptor : 'Laaa/aaa;'
...
Class #1 -
Class descriptor : 'Laaa/bbb;'
...
Class #2 -
Class descriptor : 'Lbbb/ccc;'
...
根据对dex数据的观察,我们发现dex文件中数据基本是按类名的字母顺序进行排列的,这样同样包名的类会排在一起。但在实际程序执行中,同一个package下的类并不会全部一起调用,而是和很多其他package下的类进行交互,但mmap加载了整个页面,可能会有很多无用数据。为了减少这样的情况,我们在生成文件时要尽量将使用到的数据内容排布在一起。在APK的编译流程中,Proguard混淆工具正好是能够对类名进行修改的,可以根据程序运行的逻辑,将那些会互相调用的类改为同一个package名,这样就可以使它们的数据排布在一起。
以上表数据为例,Class的排列顺序是aaa/aaa、aaa/bbb、bbb/ccc。假设我们的应用运行逻辑是aaa/aaa、bbb/ccc,而aaa/bbb在某些特殊时候才能用到。但在当前的排列情况下,加载了aaa/aaa和bbb/ccc就必然要加载aaa/bbb。我们可以用Proguard等工具来控制类名,将aaa/bbb等不常用的类放在后面,则aaa/bbb平时就不会加载。如下所示:
dexdump classes.dex
Processing 'classes.dex'...
Opened 'classes.dex', DEX version '035'
Class #0 header:
...
Class #0 -
Class descriptor : 'La0;' # 原aaa/aaa
...
Class #1 -
Class descriptor : 'La1;' # 原bbb/ccc
...
Class #2 -
Class descriptor : 'La2;' # ...
...
Class #100 -
Class descriptor : 'La100;' # 平时用不到的aaa/bbb
...
经验总结
根据上述的流程,我们探讨了Dalvik Other和.dex mmap部分的内存,大致搞清楚了它们被消耗的机制,以及一些能够减少消耗的方法。经验如下:
在优化内存时,不只有堆内存,还有其他许多类型的内存能够进行分析和优化。
dex文件有很多优化空间。在仔细统计并调整了dex文件的顺序后,往往能够节约1MB以上的mmap内存。
引入SDK库和调用新的系统API时需要考虑成本。有可能一些不常用的功能会导致大量的内存消耗。这时有可能需要多进程方案,将这些影响内存的操作放入临时进程执行。
1.6 本章小结
在这一章里,我们通过对几个案例的分析,基本了解了Android应用的各种内存组成,以及这些成分是如何被消耗的,也总结出了一些节约和优化内存的经验。在这一小节里我们把经验都列出来供读者参考。
内存的主要组成索引:
Native Heap:Native代码分配的内存,虚拟机和Android框架本身也会分配
Dalvik Heap:Java代码分配的对象
Dalvik Other:类的数据结构和索引
so mmap:Native代码和常量
dex mmap:Java代码和常量
内存工具:
Android Studio/Memory Monitor:观察Dalvik内存
dumpsys meminfo:观察整体内存
smaps:观察整体内存的详细组成
Eclipse Memory Analyzer:详细分析Dalvik内存
测试经验:
MAT是探索Java堆并发现问题的好帮手,能够迅速发现常见的图片和大数组等问题。
仅靠MAT提供的功能也不是万能的,比如内存碎片问题就隐藏在对象的地址中。
要测试非Dalvik部分,有必要了解Linux的进程和内存原理、内存共享机制,熟悉常用命令行工具。
内存分配的最小单位是页面,通常为4KB,这个限制往往会引发各种碎片问题。
碎片不仅仅是Dalvik内存,包括各种文件的mmap也有可能产生碎片。
性能优化:
尽量不要在循环中创建很多临时变量。
可以将大型的循环拆散、分段或者按需执行。
引入SDK库和调用新的系统API时需要考虑成本。有可能一些不常用的功能会导致大量的消耗。这时候有可能需要多进程方案,将这些影响内存的操作放入临时进程执行。
除了Dalvik堆内存,还有其他类型的内存在了解了原理后也能够进行分析和优化。
dex文件有很多优化空间。在仔细统计并调整了dex文件的顺序后,往往能够节约1MB以上的mmap内存。
第2章
手机发烫是为何——降低耗电量
智能手机兴起的时候,坊间流传着这样么一句话:“用智能手机的男人一定是个好男人,因为他每天必须回家充电!”,这句调侃的话说出多少手机用户的辛酸。随着智能手机的实用性、娱乐性越来越完善,我们对其依赖程度日益加深,甚至到了寸步不离的地步,衣食住行都依赖这个小小的移动终端。不管是在餐厅、地铁、商场甚至大街上,我们都能看到大片的低头族,且其数量呈崛起之势。我们每天将大部分珍贵的碎片时间献给了它。然而由于电池技术的局限性,智能手机这个全民好伴侣“偶尔”会在我们沉浸其中时戛然而止,让人生无可恋。
在我们日常使用智能手机过程中也会有体会,当我们的手机安装了市场top100的应用,即使不怎么使用手机也会很快没电,而如果将手机恢复出厂设置,三方应用都不安装,放置一周拿起来还是电量充足。真相只有一个:手机耗电的最终元凶是软件。
那么要怎么改善软件的耗电状况呢?我们可以从两个方向着手,一是从应用软件的运行载体手机系统入手,即操作系统厂商Google和ROM厂商,在系统层面做一些策略,在保证应用的用户体验的前提下尽量限制应用的不必要耗电;二是从应用软件本身入手,在保证用户的必要体验前提下,尽可能减少不必要的操作。
本章将分享我们在降低耗电方面做的一些工作。
2.1 电量测试方法
自腾讯移动互联网事业群(下文称“MIG”)开始着手手机ROM(tita)研发,为手机省电能做的也越来越多,例如控制系统本身的功耗;限制三方应用不正当的操作;统一众多三方应用的后台动作等。而笔者作为测试人员,要关注的问题有:什么样的操作是耗电的呢?参考标准是什么呢?怎样去量化呢?怎样衡量使用优化策略后的成效呢?这些问题都是需要解决的。其中至关需要解决的是怎样去量化整机的耗电问题。
耗电给大家最直观的印象就是了解手机使用时的电流、电压、电量等数据,初中的物理课本就告诉我们:
电能W(焦耳J)=电功率P(瓦特W)×时间t(秒s)
=电压U(福特V)×电量(库仑)
电功率P(瓦特W)=电压U(福特V)×电流I(安培A),表示电流做功快慢
电量Q(库仑C)=电流I(安培A)×时间t(秒s)
我们经常看到如图2-1所示的手机电池会标注3.7V 1730mAh(6.4Wh),其中mAh表示电量,Wh表示电能,手机的电池可以解读为在提供稳定电压3.7V的情况下,可以提供稳定电流1730mA一个小时。如果我们在测试的过程中给手机提供恒定的电压,那么只需要获取电流值就可以量化手机的功耗。
图2-1 手机电池信息
下面主要介绍如何来获取手机使用时候的电流值,分硬件、软件两个方面。
2.1.1 硬件测试
方法1:通过Android API获取,代码如下:
registerReceiver(receiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
这种方法的缺点:获取手机整机耗电,实时性差精度小(只能监控电池电量剩余量和跳变),测试工具本身的性能消耗,手机休眠后无法持续监测。
方法2:通过读取系统电池传感器设备节点。
/sys/class/power_supply/battery/uevent
这种方法的缺点是:测试工具本身的性能消耗,持续采集频率不得超过100Hz,只有部分机型支持此节点(Nexus 4, Nexus One)。
方法3:使用外置电流仪。
这种测试方案可以很好弥补上面两种方案的缺点,不受测试机型限制,不会造成测试方案本身带来的额外的性能消耗,可以达到很高的测试精度,可以达到很高的采集频率。唯一的缺点就是电流仪价格高得感人。
确定了测试方案,下面详细介绍测试环境的搭建,测试工具的使用,以及一些方案落地成果。
1. 硬件测试工具简介
测试工具包括如图2-2所示。图2-2a是直流恒压电源(Agilent 66319D)。图2-2b是GPIB(通用接口总线)。图2-2c是模拟电池。图2-2d是PC端电流软件。
使用方法如下:
1)把假电池装进待测试的手机,如图2-3a所示。
2)假电池出来的两根引线连接到稳压电源的输出接口OUTPUT上引线的接线正负位置,如图2-3b所示。
3)GPIB线连接稳压电源和电脑,如图2-3c所示。
4)整体连接图展示,如图2-3d所示。
完成以上步骤后,PC端装好电流软件,启动,设置相关的参数,然后开启稳压电源,开启手机,就可以进入手机的电流测试了。
图2-2 硬件测试工具示意图
图2-3 硬件测试工具接线图
采集数据流程如下:
1)Reset电源的初始状态。
2)直流恒压电源设置为4.2V电压值。
3)参数配置完成后按下DLOG按钮记录日志(参数配置见使用手册)。
4)按下Measure按钮开始电流测试,测试完成后保存LOG到自定义目录。
分析结果如下:单击Marker线,会产生2条垂直的标记线,可以任意移动,软件右下方自动产生成2条标记线之间的时间和耗电量,如图2-4所示(耗电量=时间×电流)。
图2-4 数据采集软件
2. 案例分享
以上介绍的硬件测试方法,主要适用于对整机场景功耗的量化。下面介绍下我们在使用这种测试方法在实际项目上的应用成果。
【例2-1】分析CPU频率与电量消耗的关系
CPU的功耗会随着频率提升而增加已经是大家的共识,仅仅只考虑CPU的频率而定论它的功耗是否正确呢,对此我们做了一系列的测试,同时控制CPU的频率和使用率,观察在各个场景下的电流值,如图2-5所示。
图2-5 CPU主频与耗电的关系
通过测试数据发现,在CPU空闲的情况下,CPU频率对耗电的影响几乎是忽略不计的,因此在系统空闲的时候对手机强制降频是不能节电的(单核的情况下,在多核的情况下场景会更加复杂);在CPU使用率30%的情况下,手机达到800MHz以后手机的功耗随CPU频率增加的幅度增加,这时候可以考虑适当降低CPU频率获得更好的功耗控制;在CPU使用达到50%,手机的功耗已经和频率成直线增加,这时候降低CPU频率增加CPU使用率,并不一定会给手机功耗带来很好的成效;所以在选择降频节能方法时,要充分考虑CPU频率和使用率带来的是真的省电还是徒劳无功。
【例2-2】分析手机屏幕背景色与功耗的关系
在手机的正常使用过程中,屏幕其实是最大的耗电元凶,那么显示屏幕材质,以及选择屏幕背景色对手机功耗的影响到底是怎么样的,我们可以通过硬件的测试方式来做一个详细的对比。表2-1是当年主流手机显示屏技术与功耗的对照表。
而不同的显示屏幕实现技术在不同颜色显示上的表现是怎么样的?在不同的屏幕上我们长时间使用的背景色应该怎么选择呢?以下是三星官方给出的OLED和LED的色彩功耗对比图,如图2-6所示。
表2-1 各种材质屏幕和功耗的关系
技术 材质类型 功耗
LCD LCD+LED背光
TFT-LCD
IPS
SLCD
OLED AMOLED
Super AMOLED
图2-6 不同屏幕不同颜色与功耗的关系
可以看出全白的时候OLED耗电是LCD的3倍,全黑的时候LCD耗电是OLED的10倍,30%白色的时候两者相当。
针对LCD屏幕,我们只需要关注屏幕亮度值(0~255)带来的耗电,值越大耗电越大;而针对OLED屏幕,我们不但要关注屏幕的亮度值,还需要关注每个像素的RGB值。对此我们也做了一些验证,帮助项目组在适配不同机型时,选择怎么样的色彩,如图2-7所示。
由以上的数据可知SLCD屏幕同图片耗电最亮是最暗的2~3倍,建议在亮度选择的时候使用适当的亮度;Super AMOLED最高亮度时全黑比全白节电60%,建议应用UI尽量采用深色调。
图2-7 SLCD对比Super AMOLED屏幕亮度与功耗
2.1.2 软件测试
上面一节讲述的都是从整机层面去量化并控制手机功耗,相比做App的公司来说做手机ROM的厂商较少。那么从应用的性能优化出发,上面讲述的硬件测试方法就不那么实用了,原因如下:
每个App本身的耗电是微量的,硬件测试方法中仪器本身波动可能无法体现App的功耗;
硬件测试方法只能通过测试整机功耗来体现App耗电,而这样很难避免其他App影响测试结果。
为了解决这个问题,MIG专项测试组从分析Android系统电量统计原理入手,并结合在实际项目中对耗电优化做的工作,来分享下对App功耗优化的认识。
【例2-3】2014年下半年,MIG重点业务手机管家电量测试数据显示,在无网络条件下,管家后台待机1小时,V1版本待机电量消耗1060.78mAs,而当时最新的V2版本的待机电量消耗为1891.06mAs,比V1版本增长78%。
分析CPU时间片消耗,并没有发现异常。所以,专项组开始从ROM层级的电量统计原理出发,根据可能引起电量异常的点,逐步查找问题所在。
根据经验及理论知识,手机待机进入休眠状态后,耗电的主要原因是系统被唤醒,CPU或传感器工作。因此我们针对可能引起电量异常的点,设计测试场景,使用不同的测试工具分别对CPU时间片消耗、唤醒次数进行测试。
通过多次测试后分析数据发现,在无网络情况下,V2版本CPU时间片消耗少于V1版本,但alarm触发频次明显增长。
根据获得的alarm信息查找代码,发现是维持网络连接心跳的alarm出现了异常。继续追查代码后,最终定位到是代码逻辑错误。V2版本发心跳包前没有进行判断,因而无网络时也会按照产品设计去持续发送心跳包。问题修改后,V2版本电量消耗减少30%。
在解决这个问题的过程中,发现通过一次测试获取到尽可能多的有效数据是当前电量测试迫切的需求。什么样的数据是有效的呢?Android用户通常是在系统设置的“电池”中查看各App的电量消耗情况。我们可以通过研究“电池”模块计算电量的原理,去看看组成App电量消耗的因素有哪些。
1. 电量统计原理
(1)Android电量统计接口
Android在4.1版本以后在系统增加了battery info模块,记录一定时间周期内整机的功耗状态以及每个应用的功耗详情。Android系统上App的电量消耗由cpu 、wake lock、数据传输(移动网络&WiFi)、WiFi运行、gps、other sensors组成。在ROM源码中,组成App电量消耗各部分定义如下:
CPU的电量消耗:cpuSpeedStepTimes[step]/totalTimeAtSpeeds×(userTime + systemTime)×powerCpuNormal[step]
wake lock的电量消耗:进程wakelock时间×power_profiler.xml中type= cpu_awake的数值
数据传输的电量消耗:进程数据传输量×getAverageDataCost()
WiFi运行的电量消耗:进程WiFi运行时间×power_profiler.xml中type= wifi_on的数值
GPS的电量消耗:传感器时间×power_profiler.xml中type=gps_on的数值
other sensors的电量消耗:传感器时间×默认传感器的电量消耗
手机其他部分的电量消耗定义如下:
蜂窝通信电量消耗:运行时间×power_profiler.xml中type=radio_active的数值
屏幕电量消耗:开屏能量消耗 + 亮度能量消耗
信号电量消耗:处于各信号强度下的能量消耗 +扫描信号时的能量消耗
Wifi电量消耗:WiFi开启能量消耗 + WiFi运行能量消耗
CPU空闲时电量消耗:空闲时间×power_profiler.xml中type=cpu_idle的数值
蓝牙模块电量消耗:打开蓝牙的能量消耗 +蓝牙在AT command下的能量消耗 + 进程蓝牙能量消耗
(2)通过系统文件获取电量记录
就像Linux系统对各个应用在CPU、内存上的消耗有详细的记录一样,Android系统在运行过程在帮助解决功耗问题上或许也会留下一些记录。Android在4.1版本以后加入了battery info模块,详细记录手机运行状态变化的时间点及对应的内容。在Android 5.0后Google使用工具Battery Historian在Web端更直观的展示手机状态随时间的变化。而且随着6.0更新了Battery Historian 2.0加入引起手机状态变化的应用,更好帮助开发者控制应用功耗。
在手机连接PC后,在终端使用命令adb shell dumpsys batterystats(4.1-4.3使用命令adb shell dumpsys batteryinfo),打印出详细的耗电相关信息,一类整机状态,一类应用状态,数据详细解释如图2-8所示。
例如: -16h26m01s882ms 090 64020241 +wakelock 表示在手机在使用adb shell dumpsys batterystats之前16h26m01s882ms的时候,手机电量剩余90%;系统应用在这个时刻申请了一个wakelock锁;系统的整体状态是64020241,这一串数字是对系统状态的记录,其解释如图2-9所示。
在低16位中,每4位表示一种状态:第0~3位表示系统屏幕的亮度,其值可以是0~5,依次表示dark、dim、medium、light、bright、other;第4~7位表示信号强度,其值可以是0~5,依次表示none、poor、moderate、good、great、other;第8~11位表示电话状态,其值可以是0~4,依次表示in、out、emergency、off、other;第12~15位表示移动网络传输的状态,其值可以是0~15,依次表示为none、gprs、edge、umts、cdma、evdo_0、evdo_A、1xrtt、hsdpa、hsupa、hspa、iden、evdo_b、lte、ehrpd、hspap、other。
图2-8 手机一段时间的运行状态
图2-9 手机运行状态解析
高16位每一位表示一种状态:16~30位依次表示的状态为bluetooth、WiFi、phone_in_call、plugged、screen、video、audio、wifi_mulicast、wifi_scan、wifi_full_lock、wifi_running、phone_scan、gps、sensor、wake_lock。
2. 软件测试工具
研究了Android电量统计及Setting应用电池部分的源码后,我们了解到App的电量消耗由CPU、wake lock、数据传输(移动网络&WiFi)、WiFi运行、GPS、other sensors的电量消耗组成。Setting应用简单地把这些数据加起来展示给用户,但对于测试人员来说,分开看每一部分的具体消耗更有意义。
“工欲善其事,必先利其器”。根据实际测试需求,MIG专项测试组开发了两款电量测试工具。一个是,在第三方电量统计应用PowerStat的基础上,做了完善优化及兼容性适配,命名为PowerStat2.0。另一个是,在性能测试平台类工具GT上独自开发的资源消耗分析插件。
下面简单介绍这两种工具及Google官方提供的App电量分析工具。
(1)PowerStat2.0
PowerStat工具是我们TMQ专项测试团队自研的一款软件电量测试工具,可到GT官网(http://gt.qq.com)下载。它基于安卓系统的耗电排行的隐藏接口获取的数据。在Android系统4.3及以下可正常使用,在4.4及以上需要作为系统应用使用。如图2-10和图2-11所示。
图2-10 PowerStat应用详情功耗
图2-11 PowerStat定时保存数据
目前工具涵盖以下功能:
细化耗电项。
详细显示各硬件功耗。
定时保存数据功能。
(2)GT资源消耗分析插件
GT工具是我们TMQ专项测试团队自研的一款随身调的测试工具。在测试过程中,能够脱机采集各纬度的性能数据,如图2-12和图2-13所示。
图2-12 GT插件采集功耗数据
图2-13 GT插件功耗数据展示
(3)Battery Historian 2.0
Battery Historian是随着Android 5.0面世的,Historian 2.0是Historian的升级版,兼容Android 5.0,在Android 6.0上面详细到应用对整机状态变更影响的信息。它归总了我们在Android 4.4上针对整机异常耗电定位方案,以及软件测试方法定位App耗电方案。详细的介绍以及工具环境搭建可上github https://github.com/google/battery-historian。下面主要介绍一下这个工具展现的内容。
System Stats是系统耗电的情况总览,包括各种耗电类型的总体数据和应用耗电排名情况,如图2-14所示。
图2-14 System Stats
Historian 2.0 是耗电的统计图,横坐标是时间,纵坐标是电量百分比,鼠标落到曲线上,变颜色的部分代表1%的耗电区间,如图2-15所示。
图2-15 Historian 2.0
App Stats选着单个应用,查看这个应用方的耗电情况,如图2-16所示。
图2-16 App Stats
3. 案例分享
【例2-4】工具有效帮助开发提高电量问题的定位效率
手机管家V3版本灰度发布后有个别内部用户反馈手机管家耗电异常,经常排行在耗电排行榜第一的位置,如图2-17所示。
图2-17 管家耗电异常
正好专项组刚刚把工具“耗电排行2.0”及原理分享到项目团队,此次的问题追查,开发人员使用工具高效率的定位到了问题方向,最终解决了问题。
优化效果:修改问题后,待机12个小时左右,管家电量消耗从354mAh减少到48mAh,减少了86%。
问题解决过程:
1)在用户反馈有问题的机型上安装“耗电排行2.0”工具,待机10小时,测试手机管家后台待机CPU时间片消耗是长版测试数据的277倍。
2)通过工具定位到是CPU时间片消耗异常后,后面的问题就比较好解决了。通过获取线程的CPU时间片消耗,抓到异常的线程。通过观察异常线程基本是每秒钟都在消耗CPU,推测是一直在循环做事情。通过代码搜索定位到导致死循环的代码问题。
3)修复问题后,管家功耗接近长版测试数据,如图2-18所示。
图2-18 管家功耗测试数据
【例2-5】明确系统接口使用方法,减少电量消耗
在手机管家V4的灰度版本测试中,发现网络切换广播事件比较多。在公司无线网络环境中进行1小时后台待机,管家由于监听到网络切换广播而消耗掉的CPU时间片为1581jiffies(时间片)。
对于这个广播,最初的理解是只有在发生网络中断、网络切换才会发出。通过测试发现,连接公司的无线网络,在客户端接收到CONNECTIVITY_ACTION事件广播时,读取到的网络信息并没有发生改变。但由于监听到了广播,上层代码还是会去执行相应操作,从而白白消耗掉CPU时间片,直接导致电量的消耗。
究竟是什么条件触发了这个广播?应用直接通过监听系统提供的CONNE-CTIVITY_ACTION事件去触发上层代码逻辑是否恰当?为了解决这些问题,专项测试组从Android源码追查根源,进行分析。
优化效果:1小时的后台待机测试,管家在网络切换部分CPU时间片消耗减少到3jiffies。与修改前的版本相比较,时间片消耗减少了99.8%,电量消耗从6.096mAs减少到0.01mAs。
分析解决过程:
1)广播是由Android系统监听到相应事件发出的,那么我们就从ROM源码入手,查找触发广播的原因。首先,我们在ROM源码中查找广播发送的方法:
2)找到方法后,继续跟踪到底是由哪些地方触发到广播的发出。通过在方法中增加打印函数调用栈信息的语句来追踪触发的源头:
3)在Ubuntu环境中重新编增加了调试信息的ROM,通过待机测试,收集到触发条件,包括:网络切换(开关移动网络、移动网络切换WiFi网络)和环境的改变(无线路由的添加、VPN连接成功、DHCP租续)。
4)此次测试发现的频繁接收到网络切换广播,到底是由那个条件引起的呢?继续通过抓包采集数据做进一步的分析。通过抓包数据分析,发现在固定间隔手机和路由器会进行一次DHCP续租,每次续租的同时,客户端会接收到一条CONNECTIVITY_ACTION事件广播。
5)通过读源码,清楚了CONNECTIVITY_ACTION事件除了网络切换之外还有其他的比较多的场景触发,而且根据触发条件不同每次可能会有多条广播发出。而业务层的实际需求暂时只是需要监听到网络断开或切换事件的广播。
6)最终,开发人员在框架的广播服务中新增一个事件,在满足业务需求的前提下过滤掉了大部分的系统广播。
【例2-6】预估功耗,协助把控产品需求的增加
在V4版本中,管家有计划增加“连接WiFi后自动上报GPS定位信息”的新功能。定位方法采用GPS定位、基站定位和网络定位。产品在设计功能逻辑前,需要明确该功能对管家整体耗电量的影响。
测试结论:
使用GPS定位,在不考虑有数据上报的情况下,按照1天定位10次计算,电量消耗大约为3250mAs,占管家1天电量消耗的4.79%。
使用基站定位,按照1天定位10次计算,电量消耗大约为62mAs,占管家1天电量消耗的0.1%。
由于使用GPS定位,电量消耗明显;使用基站定位,后续需要根据上报内容进行地址转换,这块功耗暂时无法确定;使用网络定位,在很多机型上不支持。由于三种方法均有明显缺陷,并且产品需求不强烈,所以最后确定这块功能不增加。
【例2-7】手机没有安装什么应用,耗电快
经常会有朋友说:“我的手机什么也没有安装,也使用了一键清理,手机放在兜里还会发烫,电池不经用。”
分析:这就是人的主观意识,觉得手机没有实际使用就不应该耗电,而忽略了“智能”手机在没有操作的时候也是可以自行运行的,它为了保证使用者的体验,会在后台调整最好的姿势。在这个案例中,我们在终端使用命令adb shell dumpsys batterystats > battery.txt 把这一段时间和电量相关的数据保存到文件battery.txt文件中,使用文本查看,发现有一段时间内手机的联网方式一直在变化,如图2-19所示。
这个手机在这三分钟之内切换了17次联网方式,手机在频繁切换联网,产生这种现象的可能原因有:1)这段时间所处的环境中网络信号不好;2)手机刷的基带不匹配。
【例2-8】手机应用没有动作,耗电快。
与案例2-4的现象差不多,手机运行一个小时,这个时间段内所应用的运行时间,前台运行,后台持锁,CPU运行状态如下:并没有异常,但是手机耗电很快。
图2-19 手机一段时间的状态图
图2-20 手机应用没有动作,耗电快
分析:通过图2-20上面一个表格,并不能看出哪个应用持有了锁或者持有传感器或者使用WiFi扫描导致了系统异常耗电。通过终端输入命令adb shell batterystats,能确定手机这个时间段是没有休眠的,把这个时间段内系统的所有内核锁使用情况汇总如图2-20下面一个表格,能发现GPSD内核锁一直存在,没有被释放,导致了系统不休眠,造成异常耗电。
2.2 电量优化方法
以上从硬件和软件两种测试方法介绍了我们在功耗优化上做的工作,所谓条条道路通罗马,我们所做的只是冰山一角,相信有其他更多更好用的测试方案。针对不同的测试对象,选取合适的测试方式及测试工具,才能够达到监控优化电量消耗的目的。
在这一章节,笔者根据多次在项目电量优化中的实际经验与电量统计的理论知识,提供几条优化方法,供大家参考,希望在这些方面避免跳入耗电的大坑。
2.2.1 优化方法一:CPU时间片
当应用退到后台运行时,尽量减少应用的主动运行。
当检测到CPU时间片消耗异常时,深入线程分析:通过获取运行过程中线程的CPU时间片消耗,去抓取消耗时间片异常的线程,通过线程去定位相应代码逻辑。
使用DDMS的traceview工具:获取进程运行过程的traceview,定位CPU占用率异常的方法。
2.2.2 优化方法二:wake lock
前台运行时,不要去注册wake lock。此时注册没有任何意义,却会被计算到应用电量消耗中。
后台运行时,在保证业务需要的前提下,应尽量减少注册wake lock。
降低对系统的唤醒频率。使用partial wake lock代替full wake lock,因为屏幕的亮起,也会消耗手机电量。
在注册后,也要注意及时释放,否则锁的持有时间会被一直计算到电量消耗中。
2.2.3 优化方法三:传感器
在用“耗电排行2.0”工具时,发现目前被应用使用最多的传感器就是GPS传感器。Google官方ROM对GPS模块定义的基础电量值是90mA。所以合理地设置GPS的使用时长和使用频率,也能降低手机电量的消耗。
2.2.4 优化方法四:云省电策略
因为手机使用场景的复杂性,用户习惯的多样性,环境的随机性,导致了很难定位用户异常耗电的根本原因,为了弄清用户在怎样的环境中,在怎样的使用场景中有异常耗电,可以考虑使用定期上报灰度用户手机电量数据的方式来分析问题。
最终是要在这茫茫数据中找出哪些用户的功耗是异常的;并对用户行为进行分析,找出异常耗电的根源;再从异常耗电的用户的耗电场景中总结出同一性的异常耗电场景加以适当的控制。
根据被测对象的特性,建立筛选标准。并借助自动化实现每日监控,对用户数据进行分析,以调整产品策略,最终实现产品功耗的优化。
2.3 本章小结
本章从应用层面到系统层面,从硬件测试方法到软件测试方法,结合多个案例多方面介绍电量测试的切入点和测试方法以及测试原理。
本章介绍的几个软件测试工具,GT、PowerStat以及BatteryHistorian都是基于Android系统本身就有的接口。可以看出基本的测试思路都是项目遇到性能瓶颈时,首先从系统方面入手,是否有合适的监控手段,然后在根据官方的意见去优化,总结。