当前位置: 首页 > 新闻 > 信息荟萃
编号:1964
Android移动性能实战.pdf
http://www.100md.com 2020年1月19日
第1页
第4页
第20页
第21页
第35页
第914页

    参见附件(21930KB,989页)。

     Android移动性能实战是由腾讯出版的一半安卓移动开发的书籍,书籍主要面对的是移动性能方向的实战,全书一共分为3大部分,帮助开发者能够更加深入的进行性能维护开发。

    Android移动性能实战简介

    本书从资源类性能中的内存、CPU、磁盘、网络、电量和交互类性能中的流畅度、响应时延,多个性能测评和优化的方向出发。每个方向,都会帮助读者深入浅出地学习必须要懂得的原理和概念,区分众多专项工具使用的场景和对应的使用方法;同时提炼总结不同类型的性能缺陷和对应的排查手段、定位方法和解决方案,透过真实的案例,让大家身临其境地快速学习;提供建立专项性能标准的武器与武器的来源,让读者能快速落地项目并产生成效。本书的最后,还会帮助读者从全新的角度学习如何应对专项测评要面对的两个基础问题:UI 自动化测试和竞品测试。

    Android移动性能实战作者

    本书的作者是来自腾讯SNG专项测试团队的工程师们,他们负责手机QQ、QQ空间、QQ音乐等的性能评测与优化工作,在App的资源类性能、交互类性能的分析与优化上挖掘很深,累积了不少案例和经验。

    Android移动性能实战目录

    第1部分 资源类性能

    第1章 磁盘:最容易被忽略的性能洼地

    1.1 原理

    1.2 工具集

    1.3 案例 A:手机 QQ 启动有 10 次重复读写 /proc/cpuinfo

    1.4 案例 B:对于系统 API,只知其一造成重复写入

    1.5 案例 C:手机 QQ 启动场景下主线程写文件

    1.6 案例 D:Object Output Stream 4000 多次的写操作

    1.7 案例 E:手机 QQ“健康中心”使用的 Buffer 太小

    1.8 案例 F:手机 QQ 解压文件使用的 Buffer 太小

    1.9 案例 G:刚创建好表,就做大量的查询操作

    1.10 案例 H:重复打开数据库

    1.11 案例 I:AUTOINCREMENT 可没有你想的那么简单

    1.12 案例 J:Bitmap 解码,Google 没有告诉你的方面

    1.13 专项标准:磁盘

    第2章 内存:性能优化的终结者

    2.1 原理

    2.2 工具集

    2.3 案例 A:内类是有危险的编码方式

    2.4 案例 B:使用统一界面绘制服务的内存问题

    2.5 案例 C:结构化消息点击通知产生的内存问题

    2.6 案例 D:为了不卡,所以可能泄漏

    2.7 案例 E:登录界面有内存问题吗

    2.8 案例 F:使用 WifiManager 的内存问题

    2.9 案例 G:把 WebView 类型泄漏装进垃圾桶进程

    2.10 案例 H:定时器的内存问题

    2.11 案例 I:FrameLayout.POSTDELAY 触发的内存问题

    2.12 案例 J:关于图片解码配色设置的建议

    2.13 案例 K:图片放错资源目录也会有内存问题

    2.14 案例 L:寻找多余的内存——重复的头像

    2.15 案例 M:大家伙要怎么才能进入小车库

    2.16 Android 要纠正内存世界观了

    2.17 专项标准:内存

    第3章 网络:性能优化中的不可控因素

    3.1 原理

    3.2 工具集

    3.3 案例 A:WebView 缓存使用中的坑

    3.4 案例 B:离线包失败导致重复

    3.5 案例 C:使用压缩策略优化资源流量

    3.6 案例 D:手机 QQ 发图速度优化

    3.7 案例 E:手机 QQ 在弱网下 PTT 重复发送

    3.8 专项标准:网络

    第4章 CPU:速度与负载的博弈

    4.1 原理

    4.2 工具集

    4.3 案例 A:音乐播放后台的卡顿问题

    4.4 案例 B:要注意 Android Java 中提供的低效 API

    4.5 案例 C:用神器 renderscript 来减少你图像处理的 CPU 消耗

    4.6 专项标准:CPU

    第5章 电池:它只是结果不是原因

    5.1 原理

    5.2 工具集

    5.3 案例 A:QQWi-Fi 耗电

    5.4 案例 B:QQ 数据上报逻辑优化

    5.5 案例 C:动画没有及时释放

    5.6 案例 D:间接调用 WakeLock 没有及时释放

    5.7 案例 E:带兼容性属性的 WakeLock 释放的巨坑

    5.8 专项标准:电池

    第2部分 交互类性能

    第6章 原理与工具集

    6.1 原理

    6.2 工具集

    6.2.1 Perfbox 自研工具:Scrolltest

    6.2.2 Systrace(分析)

    6.2.3 Trace View(分析)

    6.2.4 gfxinfo(分析)

    6.2.5 Intel 的性能测试工具:UxTune(测评 + 分析)

    6.2.6 Hierarchy Viewer(分析)

    6.2.7 Slickr(测评 + 分析)

    6.2.8 图形引擎分析神器——Adreno Profiler 工具使用说明

    6.2.9 Chrome DevTool

    第7章 流畅度:没有最流畅,只有更流畅

    7.1 案例 A:红米手机 QQ 上的手机消息列表卡顿问题

    7.2 案例 B:硬件加速中文字体渲染的坑

    7.3 案例 C:圆角的前世今生

    7.4 案例 D:让企鹅更优雅地传递火炬

    7.5 案例 E:H5 页面卡顿,到底是谁闯的祸

    7.6 专项标准:流畅度

    第8章 响应时延:别让用户等待

    8.1 案例 A:Android 应用发生黑屏的场景分析

    8.2 案例 B:“首次打开聊天窗口”之痛

    8.3 专项标准:响应时延

    第3部分 其他事项

    第9章 还应该知道的一些事儿

    9.1 UI 自动化测试

    9.2 专项竞品测试攻略

    9.3 未来的未来

    Android移动性能实战截图

    内容简介

    本书从资源类性能中的内存、CPU、磁盘、网络、电量和交互类性能中的流畅度、响应时

    延,多个性能测评和优化的方向出发。每个方

    向,都会帮助读者深入浅出地学习必须要懂得的

    原理和概念,区分众多专项工具使用的场景和对

    应的使用方法;同时提炼总结不同类型的性能缺

    陷和对应的排查手段、定位方法和解决方案,透

    过真实的案例,让大家身临其境地快速学习;提

    供建立专项性能标准的武器与武器的来源,让读

    者能快速落地项目并产生成效。本书的最后,还

    会帮助读者从全新的角度学习如何应对专项测评

    要面对的两个基础问题:UI自动化测试和竞品测

    试。

    本书适合从事移动App性能测评和优化的工

    程师阅读,内容有一定的技术深度和广度,建议

    读者在阅读本书的同时扩展阅读其他经典的技术

    类书籍。未经许可,不得以任何方式复制或抄袭本书之部

    分或全部内容。

    版权所有,侵权必究。

    图书在版编目(CIP)数据

    Android移动性能实战腾讯SNG专项测试团队编

    著.-北京:电子工业出版社,2017.4

    ISBN 978-7-121-31064-5

    Ⅰ.①A… Ⅱ.①腾… Ⅲ.①移动终端-应

    用程序-程序设计 Ⅳ.①TN929.53

    中国版本图书馆CIP数据核字(2017)第047562

    号

    策划编辑:付 睿

    责任编辑:徐津平

    特约编辑:顾慧芳

    印 刷:三河市双峰印刷装订有限公司装 订:三河市双峰印刷装订有限公司

    出版发行:电子工业出版社

    北京市海淀区万寿路173信箱

    邮编:100036

    开 本:787×980 116 印张:22.5 字数:

    504 千字

    版 次:2017年4月第1版

    印 次:2017年4月第1次印刷

    定 价:79.00元

    凡所购买电子工业出版社图书有缺损问题,请向

    购买书店调换。若书店售缺,请与本社发行部联

    系,联系及邮购电话:(010)88254888,88258888。质量投诉请发邮件至

    zlts@phei.com.cn,盗版侵权举报请发邮件至

    dbqq@phei.com.cn。

    本书咨询联系方式:010-51260888-819

    faq@phei.com.cn。推荐序一

    写在开头,送贾岛《剑客》诗一首:“十年

    磨一剑,霜刃未曾试。今日把似君,谁为不平

    事!”我们团队工作重心转到移动互联网领域已

    经好几年了,团队在移动领域测试技术积累可以

    说是从零基础开始的,几年来,配套的各类技术

    攻坚、工具平台建设都具备了很好的沉淀和规

    模,同时团队在这期间的自我实践提升和转变速

    度也非常快,如果自我吹捧一下,那么这就是一

    支优秀团队所具备的核心竞争力。这几年来,看

    着大家能不断探索攻克一个个难题并填坑,其实

    是一件很幸福开心的事情!这期间的学习、探索

    和实践,借用一句典故就是“工欲善其事,必先

    利其器”,我们都在说“磨刀不误砍柴工”,道理都

    是一致的,腾讯的专项技术测试团队从2010年开

    始组建,近7年来已经不断体现出其强大影响力

    和价值,成为了研发团队最坚实的战斗伙伴之

    一,而我们专项技术测试团队这几年也不断夯实

    了移动测试领域的重点攻坚领域、填补了几乎所有短板,并且也是努力从基础提升做起后到现在

    带来的结果体现!这好比在练武术时,早期教练

    会让学员练习扎马步,大家在健身房请私教时,会发现教练要求学员一定练深蹲,这些日久才能

    发挥威力体现基本功的基础动作,对武术提升和

    健身起到举足轻重的作用。我们团队过去持续保

    持平和心态,聚焦在短板上不断学习、钻研和沉

    淀,也在今天不断体现出了价值和给业务提供着

    重大支持。这是一支务实、踏实但又保持持续创

    新的团队,这也是我们团队的宝贵财富和一贯传

    承的管理思路。

    每次看到行业里有新书出来时,我基本都会

    第一时间来了解获取,首先希望拜读理解作者的

    思路,然后看书内容里的技术实践深度,我这个

    人很懒又很挑剔,宁愿花很多时间来提炼内容写

    个PPT给大家做分享,也不愿花很多时间坐在电

    脑旁边码字、写出一摞看起来厚厚的很有“成就

    感”的文档来给人读,因此我是真心佩服那些能

    写出大部头书籍的同仁,过去几年里承蒙同仁抬

    爱,我给多本书写过序,虽然让我有些“愤愤

    然”,我是“作序君”嘛,但也总是很欣慰,佩服同

    仁和我的朋友同事们熬夜码字的毅力,也佩服他

    们能抽丝剥茧,把自己的经验实践用一本书完整

    地呈现给读者的魄力。但其实我想说,写书本身

    是一件严肃的事情,也是把自己扒光了晾给大家看的一个过程,一本书如果东拼西凑,大部分内

    容要么是截图、纯图片,要么是“腾挪”了很多他

    人的内容,这种书其实出版出来也是体现了作者

    典型的“囊中羞涩”,此类书不出也罢,因为会食

    之无味,让读者读完基本没啥收获,反而浪费时

    间,误人子弟。

    我们SNG专项技术测试团队这次要出版的书

    籍,我不想给予太多的赞美,不然就是在自我吹

    捧了,毕竟这本书算是集我们团队之力的实践分

    享,同时也是团队工作日常点滴积累所得,希望

    对大家有用。本书所有内容产生的背景是日常工

    作开展过程中各个维度攻坚实践的过程,本书以

    看到问题—定位问题—解决问题—找共性—抽象

    化平台工具化—提炼原理的方式积累呈现出来,每个维度每个领域的案例都是真实的,容不得取

    巧,更没有很多花哨的架子,放出来的是点滴积

    累出来的真实工作经验。初期来阅读时,很多行

    业同仁可能会觉着有点乱甚至晕,我建议先把书

    籍的目录章节仔细研究,梳理清楚这本书希望传

    递给大家的思想和体系,然后再有针对性地阅读

    和学习,这样才能事半功倍。

    两年多前,整体回顾我们团队专项测试开展

    情况时,我勾画了一个所谓的“专项测试战略地

    图”,不过我们的团队属于有些“不按套路出牌的团队”,并没严格按我规划的在推进,或即使在

    推进,也并没老实地回头看这个“地图”,但让我

    们更开心的是团队自身的从下往上创新、创造的

    意识,使这期间诞生了很多创新型项目工具,这

    其实是团队自己的“道”,非常值得鼓励,欣喜看

    到左冲右突的人在团队中大有人在,幸事!其

    实,让一个人的思想和行为不得自由的,有两个

    牢笼:一个是对过去的贪恋和自满,定死了自己

    的思维和进取;另一个是对未来的恐惧,以及对

    它的贪婪,定死了自己的勇敢和视野。打破这两

    个牢笼,会顿悟得“道”。我们在人际交往中,对

    一个人的评价经常因为生活中的小事情决定,这

    是因为一个人的秉性很难改变,不管是淳朴务实

    还是爱慕虚荣的表现,回归到工作上时,不外乎

    是持之以恒、不断聚焦和专业化,或是昙花一

    现、只做一时耀眼的流星,而很多人并没悟透这

    个最基本的道理。“逻辑思维”里说过一个小典

    故,僧侣得道前的日常作业是挑水、劈柴、做

    饭,得道后还是挑水、劈柴、做饭,做一行能做

    到如此才是“大道”。

    今天我们走的这条路很不幸,不再有看板可

    以让我们比对,时代变化太快,过去的经验、经

    历未必还管用,未来的道路如何也无法预测,但

    这好比待在一个黑暗的道路上摸索前行,可能有

    很多弯路,可能不断跌倒,但只要有信念,坚定前行,无论荆棘坎坷,彼岸总会泛出微光指引我

    们不断前行,相信那也是我们心底最灿烂的光明

    之火!

    腾讯社交网络质量部 吴凯华推荐序二

    现在的移动互联网是一个用户体验为王的时

    代,你的用户群会决定你的产品的成败。而移动

    无线测试中的专项测试就变得非常重要,功能和

    业务测试保证了一个产品的生命,而专项测试则

    能够延续一个产品的生命。

    移动互联网到底是什么?带给老百姓的是生

    活的便捷,带给程序员的是新鲜的技术和更快的

    工作节奏。在早期大家都在谈论Android、iOS和

    WP(Windows Phone),然后则开始谈论物联

    网。而如今神秘选手横扫全球围棋界,所有人都

    在讨论这个“选手”,结果AlphaGo的出现让2016

    年成为了人工智能元年,也掀起了人工智能在人

    类历史上的第三次浪潮。

    当我们还在用诺基亚砸核桃的时候,Android

    和iPhone来了;

    当我们以为移动支付只有支付宝的时候,微信支付来了;

    当我们觉得二维码这项发明没有意义的时

    候,微信和支付宝等App狠狠地给了我们响

    亮的耳光;

    当我们开始玩朋友圈的时候,公众号出现

    了;

    当我们开始熟练使用公众号的时候,小程序

    来了;

    当我们觉得AR没有什么实际的有黏度的用户

    场景的时候,Pokémon Go让所有人都拿着手

    机扫全世界,甚至在美国的高架上还有专门

    的路标提示不要玩Pokémon Go;

    当我们觉得手机只能用来打电话、玩游戏、支付、上网的时候,Google Cardboard让我

    们知道原来我们可以进入手机的世界;

    当我们还沉浸在抨击VR还不成熟的时候,Vive、PSVR、Oculus等让我们欲罢不能;

    当我们以为Siri已经能够打败人类的时候,AlphaGo让我们明白其实人工智能才刚刚向

    人类发起挑战;

    当我们在各个演唱会上面看到全息投影,觉

    得离我们还很遥远的时候,Gatebox出现了

    (日本全息投影女管家)。

    这一切的一切说明了移动互联网并没有具体

    的形态,它仅仅代表着一个高速发展的时代已经来了。我们很幸运,能够活着看到时代的发展和

    变迁,我们也会很累需要不停地去接受和面对挑

    战。

    测试这个行业就如同移动互联网一样发展迅

    速,我们完全可以去用“当我们还在学习,使用

    xxx的时候,yyy已经成为了新的宠儿”这样的句

    式,相信所有的互联网从业人员都会有这样的感

    受。综合这些年所有人问我的问题,我总结两点

    在这里给大家分享:

    在这样一个社会中,不要浪费时间在思考,实践才能够抓住“红利期”。

    不要纠结于先有鸡,还是先有蛋。很多人纠

    结于自己没有这个,没有那个,所以不够级

    别去做一些事情。想做了就去做,我们不应

    该等到自己达到了一个等级才去做事情,而

    是要在做事情的过程中让自己达到对应的级

    别。

    专项测试这个概念出现时间其实并不长,但

    其重要性和普及率都是非常高的。我自己也是最

    早做专项测试的人员之一,深知其中需要填坑无

    数。从2015年开始很多公司起步做专项,但对于

    具体的方法和策略以及专项测试基线往往都不是很清楚,导致专项的测试投入产出比不高,大家

    都期望能够有一种统一的标准和方法出现。

    移动专项测试是不是只有大公司才需要做

    呢?答案肯定是“当然不是”,任何一个关心用户

    体验的企业都应该关心、重视专项测试。纵览全

    书,这可以说是至今为止我看到过最详细的专项

    测试宝典。从书中的内容我能感受到的不仅仅是

    腾讯SNG专项测试团队做专项测试的认真专业的

    态度,更多的是一种孜孜不倦的探索精神。书中

    涉及的内存、磁盘IO、电量、流量等方面的专项

    测试都会涵盖有案例、总结标准以及原理讲解。

    再次感谢腾讯SNG专项测试团队能够为国内

    移动互联网行业产出这样一本专项测试宝典,我

    相信看到这本书的测试朋友都会像我一样欣喜若

    狂。在我看来,这本宝典不仅能够帮助更多企业

    的测试团队变得越来越专业,也对测试行业进步

    做出了不小的贡献。

    书中最后提到,未来是什么?我们不是预言

    家,我们也不知道未来究竟是什么。但我们知道

    未来已经到来,你准备好了吗?在这样一个有的

    人每天在抱怨这个抱怨那个,有的人踏踏实实地

    在钻研技术,有的人有能力让影响力变现的时

    代,你是否明白自己要做什么?你想成为什么样的人?最后奉上我一直很喜欢的一句话,与大家

    共勉。

    “It's not who I am underneath,but it's what I do

    that defines me”

    ——黑暗骑士

    《大话移动App测试》系列作者 陈晔前言

    为什么会有这本书

    记得笔者从微博和MAC QQ项目中解放出来

    后,就开始接手手机QQ,组建专项测试团队。

    那时有几个小伙伴,我们一起做手机QQ的专项

    测试,发现推动专项问题解决非常困难。产品的

    需求压力巨大,性能越来越差,我们开始用更严

    厉的标准像守护者一样守护手机QQ,例如安装

    包的大小。接手后的第一个手机QQ版本,涨了

    10MB,这使我们看到了风险,顶着各部门的KPI

    需求,我们制定了一系列严厉的指标,超过的需

    求都不允许通过,从此安装包大小刹住了车。但

    KPI的压力巨大,像是洪水,不排解,堤坝只能

    越建越高,我们的压力也越来越大。产品经理开

    始不断地问,为什么安装包不能变大呢?为什么

    不能占用更多的内存?我提供更多服务,为什么

    不能消耗更多的流量?Why?Why not?在这些质疑中,我们经历了许多,除了工

    具、流程之外,更多带给我们的是真实的经验。

    例如,安装包不能再变大了。这里需要证据,运

    营同事找到了应用宝的数据,发现有不少用户是

    通过3G网络下载安装包的,另外安装包大小对下

    载失败率也有影响。在跟老板汇报过数据后,我

    们拍定了更严厉的标准:0增长。慢慢地随着我

    们团队人数的增加,类似这样的故事也越来越

    多。跟大家想象的一样,其中有跟开发人员的

    PK、有不服输自己去解决专项Bug的、有跟产品

    经理PK需求、与专项性能平衡的,等等。但是知

    道故事的人并不多,知道“为什么”的人就更少

    了。我们觉得这些故事应该被记录下来和分享出

    去,然后就有了本书。本书中会介绍工具、原

    理,但更重要的是提供了一个个真实的案例、Bug解决方案。谁适合阅读本书

    以下职位的小伙伴们适合阅读本书。

    终端专项测试:这个职位的测试人员,负责

    产品的性能、安全、稳定性、兼容性等各个

    方面。我们希望你通过阅读本书,可以有效

    地归纳总结知识、拓展思路,也可以作为你

    在专项测试领域的一本“字典”,随时翻查。

    终端系统测试:这个职位的测试人员,需要

    全面负责功能测试、专项测试等各个方面,利用合适的测试策略发现和预防风险。而专

    项测试是测试本身一个空间最广阔、蕴含知

    识最丰富的分支,学习和了解专项测试,对

    系统测试人员本身职业生涯的发展有着不可

    或缺的重要作用,也有利于制定出最合适的

    测试策略。

    高级终端开发:终端开发人员必然需要面对

    许多性能上的难题,本书希望成为你的一部

    指南书。还有,必须要说,越是高级的终端

    开发人员,越是需要啃硬骨头,而专项恰巧

    就是这个硬骨头。另外,产品经理不能看这本书吗?答案是

    能。因为不懂测试的开发不是好的产品经理。如何利用本书

    本书力求做到以下三点。

    第一,通过结构化的知识体系,让读者在心

    中建立起性能专项的知识体系。希望做到“授之

    以渔”,所以我们会从资源类性能和交互类性能

    入手。

    第二,案例均来自手机QQ、QQ空间、QQ音

    乐等的真实项目案例,结合工具集和原理,希望

    让读者对其中的技巧和知识使用更加得心应手。

    第三,提炼专项标准。在测试行业中,很多

    测试人员都需要有标准在背后支撑,特别是对于

    性能这些不黑不白的事情。虽然制定让人信服的

    标准很难,但我们愿意踏出这一步。

    因此大家阅读的时候会发现,为了上面的三

    点,本书的大部分章节会分为原理、工具集、案

    例、专项标准四部分来介绍。

    原理 主要是为了说明一些不脱离实际的实用的基础知识。因为有好的基础知识,才能PK得

    过开发人员,说服得了产品经理,用“专业知

    识”武装自己。

    工具集工欲善其事,必先利其器。但工具那

    么多,该选哪个呢?根据我们的经验,本书中对

    工具做了不同纬度的分类,助你消灭选择恐惧

    症。

    案例 按照分析专项问题的思路来划分我们

    的案例,我们力求做到让读者可以举一反三。

    专项标准 会从原则、标准、优先级、来源

    等来描述。原则像是宪法,在没有对应的具体标

    准的时候,可遵循原则。标准更多是直接从案例

    中提炼的规则,可直接操作落地。优先级和来源

    都是为了让大家推动标准的时候更有把握。在开始性能专项之旅之前

    为了坚定你把这本稍微晦涩难懂的书读完的

    信心,笔者必须让你弄清楚性能的重要性和这本

    书将会告诉你些什么。下面,先从几个不同的角

    度来谈谈性能的重要性。

    首先,性能是基础功能。这句话不是我说

    的,是Pony Ma在一次大会上说的,即使不算终

    端性能,也都能印证这句话的正确性。其中最经

    典的例子就是PC的传文件功能。对于这个功能来

    说,在不同的网络环境下,尽可能地利用好带

    宽,保证成功率和提升传输速度就是这个功能的

    描述。而对性能的不断打磨,也让这个功能成为

    用户使用QQ的重要原因之一。所以产品经理要

    升级,要打怪通关,怎么能忽略性能呢?

    其次,性能可以给予更多丰富用户体验的空

    间,也可以彻底破坏用户体验。这里举两个例

    子。第一个例子,过年时候,上了一个有强迫症

    的功能,口令红包。这个功能就相当于一次对于

    客户端的消息压测,会带来前所未有的性能压

    力,幸好聊天窗口的性能还不错,才能承载起来。第二个例子,内存中OOM会带来crash,卡顿

    到了极端会ANR,这些都会严重破坏用户体验。

    最后,性能可以直接跟钱产生关系,可以省

    钱也可以费钱。关于省钱,例如手机QQ的部分

    业务功能切换为使用WebP来压缩图片,这不仅节

    省了用户流量,更重要的是从带宽费用上为公司

    节省了不少支出。关于费钱,例如http content

    length设置错误带来的重复下载,就会浪费用户流

    量,甚至可能导致一次让公司损失大量金钱的事

    故。

    移动专项性能是一个完整的体系,如图1所

    示的Android性能专项地图,它涉及很多方面知

    识,作为移动专项的一个重要分支,包括资源类

    性能、交互类性能两个方面,所以本书将从这两

    个方面,依据图1中的脉络,讲述这些重要的案

    例、经验和工具,让你快速成长。图1

    博和其余专项测试团队成员的鼎力支持。

    张金旭、闫石、潘在亮、刘海锋、周文乐、李昶

    感谢吴凯华、肖衡、邱俊、汪斐、石延龙、樊林。

    力、付越、付云雷、黄天琳、欧阳霞、唐志彬、主要编著成员有:黄闻欣、杨阳、丁铎、谭

    了不少案例和经验。

    能、交互类性能的分析与优化上挖掘很深,积累

    等的性能评测与优化工作,在App的资源类性

    工程师们,他们负责手机QQ、QQ空间、QQ音乐

    本书的作者是来自腾讯SNG专项测试团队的

    致谢目录

    推荐序一

    推荐序二

    前言

    第1部分 资源类性能

    第1章 磁盘:最容易被忽略的性能洼地

    1.1 原理

    1.2 工具集

    1.3 案例A:手机QQ启动有10次重复读

    写proccpuinfo

    1.4 案例B:对于系统API,只知其一造成重复

    写入

    1.5 案例C:手机QQ启动场景下主线程写文件

    1.6 案例D:Object Output Stream 4000多次的写

    操作

    1.7 案例E:手机QQ“健康中心”使用的Buffer太

    小

    1.8 案例F:手机QQ解压文件使用的Buffer太小

    1.9 案例G:刚创建好表,就做大量的查询操作

    1.10 案例H:重复打开数据库

    1.11 案例I:AUTOINCREMENT可没有你想的那

    么简单

    1.12 案例J:Bitmap解码,Google没有告诉你的方面

    1.13 专项标准:磁盘

    第2章 内存:性能优化的终结者

    2.1 原理

    2.2 工具集

    2.3 案例A:内类是有危险的编码方式

    2.4 案例B:使用统一界面绘制服务的内存问题

    2.5 案例C:结构化消息点击通知产生的内存问

    题

    2.6 案例D:为了不卡,所以可能泄漏

    2.7 案例E:登录界面有内存问题吗

    2.8 案例F:使用WifiManager的内存问题

    2.9 案例G:把WebView类型泄漏装进垃圾桶进

    程

    2.10 案例H:定时器的内存问题

    2.11 案例I:FrameLayout.POSTDELAY触发的内

    存问题

    2.12 案例J:关于图片解码配色设置的建议

    2.13 案例K:图片放错资源目录也会有内存问

    题

    2.14 案例L:寻找多余的内存——重复的头像

    2.15 案例M:大家伙要怎么才能进入小车库

    2.16 Android要纠正内存世界观了

    2.17 专项标准:内存

    第3章 网络:性能优化中的不可控因素3.1 原理

    3.2 工具集

    3.3 案例A:WebView缓存使用中的坑

    3.4 案例B:离线包下载失败导致重复下载

    3.5 案例C:使用压缩策略优化资源流量

    3.6 案例D:手机QQ发图速度优化

    3.7 案例E:手机QQ在弱网下PTT重复发送

    3.8 专项标准:网络

    第4章 CPU:速度与负载的博弈

    4.1 原理

    4.2 工具集

    4.3 案例A:音乐播放后台的卡顿问题

    4.4 案例B:要注意Android Java中提供的低效

    API

    4.5 案例C:用神器renderscript来减少你图像处

    理的CPU消耗

    4.6 专项标准:CPU

    第5章 电池:它只是结果不是原因

    5.1 原理

    5.2 工具集

    5.3 案例A:QQWi-Fi耗电

    5.4 案例B:QQ数据上报逻辑优化

    5.5 案例C:动画没有及时释放

    5.6 案例D:间接调用WakeLock没有及时释放

    5.7 案例E:带兼容性属性的WakeLock释放的巨坑

    5.8 专项标准:电池

    第2部分 交互类性能

    第6章 原理与工具集

    6.1 原理

    6.2 工具集

    6.2.1 Perfbox自研工具:Scrolltest

    6.2.2 Systrace(分析)

    6.2.3 Trace View(分析)

    6.2.4 gfxinfo(分析)

    6.2.5 Intel的性能测试工具:UxTune(测评+分

    析)

    6.2.6 Hierarchy Viewer(分析)

    6.2.7 Slickr(测评+分析)

    6.2.8 图形引擎分析神器——Adreno Profiler工具

    使用说明

    6.2.9 Chrome DevTool

    第7章 流畅度:没有最流畅,只有更流畅

    7.1 案例A:红米手机QQ上的手机消息列表卡顿

    问题

    7.2 案例B:硬件加速中文字体渲染的坑

    7.3 案例C:圆角的前世今生

    7.4 案例D:让企鹅更优雅地传递火炬

    7.5 案例E:H5页面卡顿,到底是谁闯的祸

    7.6 专项标准:流畅度第8章 响应时延:别让用户等待

    8.1 案例A:Android应用发生黑屏的场景分析

    8.2 案例B:“首次打开聊天窗口”之痛

    8.3 专项标准:响应时延

    第3部分 其他事项

    第9章 还应该知道的一些事儿

    9.1 UI自动化测试

    9.2 专项竞品测试攻略

    9.3 未来的未来第1部分 资源类性能

    从整个软件的性能来说,资源类性能就像是

    撑起冰山一角的下面的冰层,如图1所示。构成

    这部分的,我称之为3+1+1。3+1是传统部分——

    磁盘、CPU和内存,加1是与环境密切相关的网

    络;最后的一个加1则是因为移动网络而显得特

    别重要的电池(耗电)。但为什么它们能撑起那

    冰山一角呢?图1

    很简单,因为冰山一角体现出来的交互类性

    能,包括流畅度、时延等,实际上都是资源问

    题,例如流畅度问题,可以是内存的垃圾回收太

    频繁导致的,因为有些GC会STOP THE

    WORLD;又可以是CPU问题,decode图片开了过

    多的子线程,导致主线程的CPU资源被争抢;更

    可以是在主线程中读写磁盘,磁盘读写耗时抖

    一抖、界面也跟着卡一卡,等等。所以关注资源

    类性能,其实是关注问题的本质去解决问题的方

    式。第1章 磁盘:最容易被忽略的性

    能洼地

    1.1 原理

    在没有SSD硬盘之前,大家都会觉得我们的

    HDD硬盘很好用,什么5400转、7200转,广告都

    是棒棒的。直到有一天,SSD出现了,发现启动

    Windows的时候,居然可以秒开,这才幡然醒

    悟。因此,对于外行来说,磁盘IO性能总是最容

    易被忽略的,精力会更集中在CPU上。但是对于

    内行人来说,大家都懂得,性能无非是CPU密集

    型和IO密集型。磁盘IO就是其中之一。那么到

    了移动时代,我们的存储芯片性能究竟怎样呢?

    在讨论这个问题之前,我们来看一个测试数据。

    如图1-1所示,我们的顺序读写的性能进步

    得非常快,很多新的机型,顺序读写比起以前的

    性能,那是大幅度提升,跟SSD的差距已经缩小

    了很多。但是这里有个坏消息,随机读写的性能

    依旧很差,见MOTO X、S7、iPhone 6S Plus。到

    这里,必须给大家介绍第一个概念:随机读写。图1-1

    随机读写

    随机写无处不在,举两个简单例子吧。第一

    个例子最简单,数据库的journal文件会导致随机

    写。当写操作在数据库的db文件和journal文件中

    来回发生时,则会引发随机写。如表1-1所示,将

    一条数据简单地插入到test.db,监控pwrite64的接

    口,可以看到表中有底纹的地方都是随机写。第

    二个例子,如果向设置了AUTOINCREMENT(自

    动创建主键字段的值)的数据库表中插入多条数

    据,那么每插入一条数据,都需要操作两张数据

    库表,这就意味着存在随机写。

    表1-1

    底层调用 文件 字节数 偏移

    pwrite64 test.db 4096 0

    pwrite64 test.db 4096 12288

    pwrite64 test.db-journal 4616 0

    pwrite64 test.db-journal 4 4616

    pwrite64 test.db-journal 4096 4620

    pwrite64 test.db-journal 4 8716

    pwrite64 test.db-journal 4 8720 pwrite64 test.db-journal 4096 8724

    pwrite64 test.db-journal 4 12820

    pwrite64 test.db 4096 0

    pwrite64 test.db 4096 12288

    pwrite64 test.db 4096 16384

    pwrite64 test.db-journal 28 0

    从上面的例子可知,随机读写是相对顺序

    读写而言的,在读取或者写入的时候随机地产生

    offset。但为什么随机读写会如此之慢呢?

    1.随机读会失去预读(read-ahead)的

    优化效果。

    2.随机写相对于顺序写除了产生大量

    的失效页面之外,更重要的是增加了触

    发“写入放大”效应的概率。

    那么“写入放大”又是什么呢?下面我们来介

    绍第二个概念:“写入放大”效应。

    “写入放大”效应

    当数据第一次写入时,由于所有的颗粒都为

    已擦除状态,所以数据能够以页为最小单位直接写入进去。当有新的数据写入需要替换旧的数据

    时,主控制器将把新的数据写入到另外的空白闪

    存空间上(已擦除状态),然后更新逻辑LBA地

    址来指向到新的物理FTL地址。此时,旧的地址

    内容就变成了无效的数据,但主控制器并没执行

    擦除操作而是会标记对应的“页”为无效。当磁盘

    需要在上述无效区域进行再次写入的话,为了得

    到空闲空间,闪存必须先复制该“块”中所有的有

    效“页”到新的“块”里,并擦除旧“块”后,才能写

    入。(进一步学习,可参见:

    http:bbs.pceva.com.cnforum.php?

    mod=viewthread&action=printable&tid=8277。)

    比如,现在写入一个4KB的数据,最坏的情

    况就是,一个块里已经没有干净空间了,但是恰

    好有一个“页”的无效数据可以擦除,所以主控就

    把所有的数据读出来,擦除块,再加上这个4KB

    新数据写回去。回顾整个过程,其实只想写4KB

    的数据,结果造成了整个块(512KB)的写入操

    作。同时带来了原本只需要简单地写4KB的操作

    变成了“闪存读取(512KB)->缓存改(4KB)-

    >闪存擦除(512KB)->闪存写入

    (512KB)”,这造成了延迟大大增加,速度慢是

    自然的。这就是所谓的“写入放大”(Write

    Amplification)问题,如图1-2所示。图1-2下面我们通过构造场景来验证写入放大效应

    的存在。

    场景1:正常向SD卡写入1MB文件,统计文

    件写入的耗时。

    场景2:先用6KB的小文件将SD卡写满,然

    后将写入的文件删除。这样就可以保证SD卡没有

    干净的数据块。这时再向SD卡写入1MB的文件,统计文件写入的耗时。

    图1-3是分别在三星9100、三星9006以及三星

    9300上进行的测试数据,从测试数据看,在SD卡

    没有干净数据块的情况下,文件的写入耗时是正

    常写入耗时的1.9~6.5倍,因此测试结果可以很好

    地说明“写入放大”效应的存在。图1-3

    那么写入放大效应最容易是在什么时候出现

    呢?外因:手机长期使用,磁盘空间不足。内

    因:应用触发大量随机写。这时,磁盘IO的耗时

    会产生剧烈的波动,App能做的只有一件事,即

    减少磁盘IO的操作量,特别是主线程的操作量。

    那么如何发现、定位、解决这些磁盘IO的性能问

    题呢?当然就要利用我们的工具了。1.2 工具集

    工具集如表1-2所示,后文分别进行介绍。

    表1-2

    工具 问题 能力

    SystraceStrace

    主线程IO、IO操作耗

    时过长

    发现

    STRICTMODE 主线程IO

    发现

    +定位

    IO

    Monitor【自

    研】

    主线程IO、多余IO、Buffer过小等

    发现

    +定位

    SQL IO

    Monitor【自

    研】

    主线程IO、全表扫

    描、不合理事务等

    发现

    +定位

    ·STRICTMODE

    STRICTMODE应该是入门级必备工具了,可

    以发现并定位磁盘IO问题中影响最大的主线程

    IO。由如图1-4所示的代码可见,启用方法非常简单。

    图1-4原理也非常简单,主要是文件操作

    (BlockGuardOs.java)、数据库操作

    (SQLiteConnection.java)和SharePreferences操作

    (SharedPreferencesImpl.java)的接口中插入检查

    的代码。我们截取了一段Android源码中文件操作

    的监控实现代码,如图1-5所示,最后实际调用

    StrictMode中的onWriteToDisk方法,通过创建

    BlockGuardPolicyException来打印IO调用的堆

    栈,帮助定位问题。图1-5

    详细代码:

    http:androidxref.com4.4.4_r1xreflibcorelunisrcmainjavalibcoreioBlockGuardOs.java91

    ·Perfbox:IOMonitor

    原理:IOMonitor的功能可以归结为通过

    Hook Java层系统IO的方法,收集区分进程和场

    景的IO信息。

    1.Hook java方法

    IO Monitor Hook java方法借鉴了开源项目

    xposed,网上介绍xposed的文章很多,这里就用

    如图1-6所示的流程图来简要说明获取此次IO操

    作信息的方法。图1-6

    2.区分进程和场景的IO信息收集

    区分进程和场景的IO信息收集有以下4个步

    骤。

    (1)app_process替换

    app_process是Android中Java程序的入口,通

    过替换app_process就可以控制入口,在任何一个

    应用中运行我们的代码。替换后的app_process工

    作流程如图1-7所示。图1-7

    (2)将libfork.so添加到环境变量

    LD_PRELOAD中

    在UNIX中,LD_PRELOAD是一个可以影响

    程序的运行时链接的环境变量,让你可以定义在

    程序运行前优先加载的动态链接库。而这个功能

    就可以用来有选择性地载入不同动态链接库中的

    相同函数。而在zygote进程启动前设置

    LD_PRELOAD环境变量,这样zygote的所有子进

    程都会继承这个环境变量。libfork.so实现了一个

    fork函数,当app_process通过fork函数来启动

    zygote进程时,会优先使用libfork.so中实现的fork

    函数,fork函数的流程如图1-8所示。图1-8

    (3)将XPlatform.jar添加到环境变量

    CLASSPATH中

    将XPlatform.jar加入到CLASSPATH中,是为

    了可以让像common.jar这种插件型jar使用

    XPlatform.jar中的类。手机QQ中也存在类似事

    情,开发的同事把整个工程编译成了两个dex文

    件,在手机QQ启动后,把第二个dex文件放入

    CLASSPATH中(与XPlatform实现方法不同,但

    效果相同),这样主dex可以直接import并使用第

    二个dex中的类。如果不加入CLASSPATH,需要

    借助DexClassLoader类来使用另一个jar包中的

    类,这样使用起来很麻烦,并且会有很大的限

    制。

    在系统启动过程中,app_process进程实际上

    是zygote进程的前身,所以XPlatform.jar是在

    zygote进程中运行的。

    在XPlatform中主要Hook了两个java方法,来

    监控system_server进程和应用进程的启动,并在

    这些进程中做一些初始化的操作。这里面用了一

    个fork的特性,父进程使用fork创建子进程,子进

    程会继承父进程的所有变量,由于zygote使用fork创建子进程,所以在zygote进程中进行Hook,在

    它创建的任何一个应用进程和system_server进程

    也是生效的。

    XPlatform工作流程图如图1-9所示。图1-9

    这样就实现了在应用进程启动时,控制在指

    定进程中运行IO Monitor的功能。

    (4)区分场景的IO信息收集

    为了实现分场景的IO信息收集,我们给IO

    Monitor添加了一个开关,对应的就是Python控制

    脚本,这样便可以实现指定场景的IO信息收集,使测试结果做到更精准,如图1-10所示。图1-10

    这样我们就实现了区分进程和场景的IO信息

    收集。

    在介绍了我们的工具原理之后,来看一下采

    集的IO日志信息,包括文件路径、进程、线程、读写文件的次数、大小和耗时以及调用的堆栈,如图1-11所示。图1-11

    图1-9中的数据说明:某个文件的一次

    对应CSV文件中的一行,每次调用

    系统的API(read或者write方法),读写次数

    (readcount,writecount)就加1。读写耗时(readtime,writetime)是计算open到close的时

    间。

    ·SQLite性能分析监控工具SQL IO

    Monitor

    我们知道,数据库操作最终操作的是磁盘上

    的DB文件,DB文件和普通的文件本质上并无差

    异,而IO系统的性能一直是计算机的瓶颈,所以

    优化数据库最终落脚点往往在如何减少磁盘IO

    上。

    无论是优化表结构、使用索引、增加缓存、调整page size等,最终的目的都是减少磁盘IO,而这些都是我们常规的优化数据库的手段。习惯

    从分析业务特性、尝试优化策略到验证测试结果

    的正向思维,那么我们为何不能逆向一次?既然

    数据库优化的目的都是减少磁盘IO,那我们能不

    能直接从磁盘IO数据出发,看会不会有意想不到

    的收获。

    1.采集数据库IO数据

    要想实现我们的想法,第一步当然要采集数

    据库操作过程中对应的磁盘IO数据。由于之前通

    过Java Hook技术,获取到了Java层的IO操作数据,虽然SQLite的IO操作在libsqlite.so进行,属

    于Native层,但我们会很自然地想到通过Native

    Hook采集SQLite的IO数据。

    Native Hook主要有以下实现方式。

    (1)修改环境变量LD_PRELOAD。

    (2)修改sys_call_table。

    (3)修改寄存器。

    (4)修改GOT表。

    (5)Inline Hook。

    下面主要介绍(1)、(4)、(5)三种实

    现方式。

    (1)修改环境变量LD_PRELOAD

    这种方式实现最简单,重写系统函数open、read、write和close,将so库放进环境变量

    LD_PRELOAD中,这样程序在调用系统函数时,会先去环境变量里面找,这样就会调用重写的系

    统函数。可以参考看雪论坛的文章“Android使用

    LD_PRELOAD进行Hook”(http:bbs.pediy.comshowthread.php?

    t=185693)。

    但是这种Hook针对整个系统生效,即系统所

    有IO操作都被Hook,造成Hook的数据量巨大,系统动不动就卡死。

    (4)修改GOT表

    引用外部函数的时候,在编译时会将外部函

    数的地址以Stub的形式存放在.GOT表中,加载时

    linker再进行重定位,即将真实的外部函数写到此

    stub中。Hook的思路就是替换.GOT表中的外部函

    数地址。而libsqlite.so中的IO操作是调用libc.so

    中的系统函数进行,所以修改GOT表的Hook方案

    是可行的。

    然而现实总不是一帆风顺的,当我们的方案

    实现后,发现只能记录到libsqlite.so中的open和

    close函数调用,而由于sqlite的内部机制而导致的

    readwrite调用我们无法记录到。

    (5)Inline Hook

    在前两种方案无果后,只能尝试Inline

    Hook。Inline Hook可以Hook so库的内部函数,我们首先想到的是Hook libsqlite.so内部IO接口

    posixOpen、seekandread、seekandwrite以及

    robust_close。但是在成功的路上总是充满波折,sqlite内部竟然将大部分的关键函数定义为static函

    数,如posixOpen。在C语言中,static函数是不导

    出符号的,而Inline Hook就是要在符号表中找到

    对应的函数位置。这样一来,通过Hook sqlite内

    部函数的路子又行不通了。

    static int posixOpen(const char zFile,int flags,int mode){

    return open(zFile,flags,mode);

    }

    既然这样不行,那我们只能更暴力地Hook

    libc.so中的open、read、write和close方法。因为

    不管sqlite里面怎么改,最终还是会调用系统函

    数,唯一不好的是这样录到了该进程所有的IO数

    据。这种方法在自己编译的libsqlite.so里面证实

    是可行的。

    正当我满怀欣喜地去调用手机自带的

    libsqlite.so库时,读写数据再一次没有被记录

    到,我当时的内心几乎是崩溃的。为什么我自己

    编译的libsqlite.so库可以,用手机上的就不行

    呢?没办法,只能再去看如图1-12所示的源码,最后在seekAndRead里面发现,sqlite定义了很多

    宏开关,可以决定调用系统函数pread、pread64

    以及read来进行读文件。莫非我自己编的so和手

    机里面的so的编译方式不一样?图1-12

    笔者又Hook 了pread和pread64,这一次终于

    记录到了完整的IO数据,原来手机里面的

    libsqlite.so调用系统的pread64和pwrite64函数来进

    行IO操作,同时通过Inline Hook获取到了数据库

    读写磁盘时page的类型,sqlite的page类型有表叶

    子页、表内部页、索引叶子页、索引内部页以及

    溢出页,采集的数据库日志信息如图1-13所示。图1-13

    费尽了千辛万苦,终于拿到了数据库读写磁

    盘的信息,但是这些信息有什么用呢?我们能想

    到可以有以下用途。

    通过IO数据的量直观地验证数据库优化效

    果。

    通过偏移量找出随机读写进行优化。

    但是我们又面临另外一个问题,因为获取的

    磁盘信息是基于DB文件的,而应用层操作数据库

    是基于表的,同时又缺乏堆栈,很难定位问题。

    基于此,我们又想到了另外一个解决方法,就是

    Hook应用代码的数据库操作,通过堆栈把两者对

    应起来,这样就可以把应用代码联系起来,更方

    便分析问题。

    2.Hook应用层SQL操作

    Hook应用代码其实就是Hook SQLiteDatabase

    里面的数据库增删改查操作,应用代码SQL语句

    如图1-14所示,Java层Hook基于Xposed的方案实

    现。最终可以通过堆栈和磁盘信息对应起来,如

    图1-15所示。图1-14

    图1-15

    获取到了这么多数据,我们在后面数据库相

    关的案例中可以看一下如何应用。1.3 案例A:手机QQ启动有10次

    重复读写proccpuinfo

    问题类型:冗余读写

    解决策略:缓存

    案例分析:通过IO信息可以发

    现proccpuinfo被读了10次,且前9次的堆栈完全

    一样,说明前9次是同一个方法调用的,所以可

    以在获取cpuinfo的方法中,将读取的信息保存起

    来,下次再调用时,就不用再去文件中读取了,如图1-16所示。图1-16

    解决方案:

    从代码中可以看出,开发的同事用静态数据

    成员将第一次读取的信息保存起来,后面就不需

    要读这些信息了,优化后,该文件的读操作由10

    次降为2次,如图1-17所示图1-17

    我们知道每次打开、关闭或者读写文件,操

    作系统都需要从用户态到内核态的切换,这种状

    态的切换本身是很消耗性能的,所以为了提高文

    件的读写效率,就需要尽量减少用户态和内核态

    的切换。使用缓存可以避免重复读写,对于需要

    多次访问的数据,在第一次取出数据时,将数据

    放到缓存中,下次再访问这些数据时,就可以从

    缓存中取出来。1.4 案例B:对于系统API,只知

    其一造成重复写入

    问题类型:冗余读写

    解决策略:延迟写入

    案例分析:Android系统中使用

    SharedPreferences文件来保存数据非常方便,在

    需要保存数据的地方调用commit就可以,但是很

    多开发同学可能并不知道每调用一次

    commit,就会对应一次文件的打开和关闭,从而造成因commit方法的随意调用而导致文

    件的重复打开和关闭,Android源码如图1-18所

    示。图1-18

    手机QQ就出现过这样的案例,从IO Monitor

    获取的数据可以看出,safe_mode_com.qzone.xml文件被写入了两次,如图1-19所示。

    图1-19

    通过堆栈找到源代码,可以看出在同一个方

    法中连续使用commit方法,从而造成

    safe_mode_com.qzone.xml被打开了两次,如图1-

    20所示。图1-20

    解决方案:只需要保留最好的一个commit方

    法即可。跟上面的道理差不多,也可以使用缓存

    来保存多次写入的数据,延迟写入,从而减少写

    入次数。1.5 案例C:手机QQ启动场景下

    主线程写文件

    问题类型:主线程读写

    解决策略:移到子线程

    案例分析:从IO信息中,可以看出该文件

    是在主线程进行写操作的。我们需要避免在主线

    程进行IO操作,尤其是写操作。因为写入放大效

    应有时会让平时十多毫秒的操作放大几十倍,因

    此需要把该IO操作放到如下的子线程中操作。

    解决方案:将主线程的IO操作移到非主线

    程,问题得到解决,如图1-21所示。图1-21

    众所周知,Android的UI操作在主线程进行操

    作,主线程耗时越少,因此UI界面的生成可以更

    快,所以尽量减少在主线程的操作,上文

    StrictMode中主线程IO的规则也从另外一个方面

    印证了这点。然而事情并非那么简单,大家要有

    更深层次的思考。如果IO本身跟要展示的关键内

    容非常相关,那么改子线程即改善了交互类性能中的流畅度,俗称不“卡”了。但是默认的子线程

    的线程优先级并不高,IO操作会变得更慢,而

    IO的内容又是界面的核心内容,那么就彻底变成

    了“慢”的问题,例如后面第2部分中响应时延相

    关章节提到的白屏、黑屏。所以将IO放到子线程

    是第一步,更重要的是如后面的案例一样,怎么

    真正地减少IO,甚至避免IO。1.6 案例D:Object Output

    Stream 4000多次的写操作

    问题类型:IO效率低

    解决策略:合理使用ByteArrayOutputStream

    案例分析:手机QQ“附近的人”的功能中,大小为16KB的文件在序列化磁盘时(如图1-22所

    示),因为使用了ObjectOutputStream,导致

    写次数达到了4000+次(如图1-23所示)。有人

    会有疑问,ObjectOutputStream到底是怎么工

    作的?这需要从源码里来寻找答案。图1-22

    图1-23

    由图1-24的源码可以看出,ObjectOutputStream在序列化磁盘时,会把内存中

    的每个对象保存到磁盘,在保存对象的时候,每

    个数据成员会带来一次IO操作,也就是为什么

    16KB的文件会有4000次IO的缘故。图1-24

    解决方案:在ObjectOutputStream上面再封装

    一个输出流ByteArrayOutputStream,先将对象序

    列化后的信息写到缓存区中,然后再一次性地写

    到磁盘上,如图1-25所示。图1-25

    实验室:寻找序列化最佳的实践

    问题:Android QQ在序列化读写磁盘时,存在直接使用ObjectInputStream和

    ObjectOutputStream来读写磁盘而导致磁盘IO次

    数过多的情况,对于一个几十KB的文件,写次数

    达1000多次,频繁地写入势必严重影响App性

    能。

    解决方案:可以通过使用缓冲区,有效减少

    磁盘IO的次数,推荐如表1-3所示的方式来序列

    化磁盘。

    表1-3

    读写方式

    序

    列

    化

    写

    磁

    盘

    优

    化

    前

    ObjectOutputStream

    优

    化

    后

    BufferedOutputStream+ObjectOutputStream

    ByteArrayOutputStream+ObjectOutputStream

    序

    列

    化

    读

    磁

    盘

    优

    化

    前

    ObjectInputStream

    优

    化

    后

    BufferedInputStream+ObjectInputStream

    ByteArrayInputStream+ObjectIntputStream 对相同的内容,通过不同的方式序列化到磁

    盘,磁盘的IO次数和耗时对比如表1-4所示,由

    表可以看出,使用推荐的方式写耗时减少46%,读耗时减少36%,对于IO任务频繁的App来说,这个效果会更明显。

    表1-4

    IO次数 耗时(ms)

    序列化写

    优化前 499 162.8

    优化后 1 87.3

    序列化读

    优化前 719 171.8

    优化后 1 109.9 1.7 案例E:手机QQ“健康中

    心”使用的Buffer太小

    问题类型:IO效率低

    解决策略:合理地设置Buffer的大小

    案例分析:在“健康中心”通过计算文件的

    md5值来验证文件安全性的业务时,从 IO信息可

    以得到,OfflineSecurity方法读取了100多个

    文件,如图1-26所示。拿第一个文件来分析,大

    概17KB的文件被读了18次,可以得出该方法在读

    取文件时使用了1KB的Buffer,从如图1-27所示的

    代码中看也确实如此。图1-26图1-27

    解决方案:从如图1-28所示的代码中看,开

    发的同事最终使用了4KB的Buffer来提高读写效

    率。

    图1-28在读写时使用缓冲区可以减少读写次数,从

    而减少了切换内核态的次数,提高读写效率,根

    据实际经验,这里推荐使用的Buffer大小为8KB,这和Java默认的Buffer大小一致,Buffer大小至少

    应为4KB。当然,Buffer也不是越大越好,Buffer

    如果太大,会导致申请Buffer的时间变长,反而

    整体效率不高。从上文看出,IO Monitor可以获

    取到读写的大小和次数,其中读写次数就是调

    用系统API的次数,所以读写大小除以读写次数

    可以得到Buffer的大小,如果Buffer太小就会存在

    问题,这样一目了然。这里其实还有一种更智能

    地确定Buffer大小的方法。这个方法由两个影响

    因子决定,一是Buffer size不能大于文件大小;二

    是Buffer size根据文件保存所挂载的目录的block

    size来确认Buffer大小,而数据库的pagesize,就

    是这样确定的,具体可见Android源码中

    SQLiteGlobal.java的getDefaultPageSize。1.8 案例F:手机QQ解压文件使

    用的Buffer太小

    问题类型:IO效率低

    解决策略:使用BufferedOutputStream

    案例分析:在手机里面,发现一处IO效率

    不高的Bug,10MB的文件要写磁盘22000次,如

    图1-29所示,计算下来每次写磁盘只有496个字

    节,这里是不是有和上一案例同属于Buffer设置

    太小的问题呢?

    图1-29

    我们找到对应的代码,如图1-30所示,看到开发人员这里竟然用的是20KB的Buffer,为什么

    最终写磁盘时Bufter只有496个字节呢?要想知道

    答案,还得通过我们的“实验室”去看看系统的源

    码。图1-30实验室:寻找压缩文件的最佳实践

    从Android的源码看到,Android压缩文件提

    供了两个API,分别是ZipFile和ZipOutpurStream,要想弄清楚这两个API的区别,我们还是从ZIP的

    文件结构说起。

    Zip文件结构

    ZIP文件结构如图1-31所示,File Entry表示一

    个文件实体,一个压缩文件中有多个文件实体。

    文件实体由一个头部和文件数据组组成,Central

    Directory由多个File header组成,每个File header

    都保存了一个文件实体的偏移。图1-31

    (1)Local File Header(本地文件夹)

    本地文件头偏移的具体描述如表1-5所示。文

    件的最后到End of central directory结束。

    表1-5

    偏移 字节数 描述

    0 4 固定值0x04034b50

    4 2 解压缩版本

    6 2 标志

    8 2 压缩方式

    10 2 文件最后修改时间

    12 2 文件最后修改日期

    14 4 CRC-32校验

    18 4 压缩后大小

    22 4 压缩前大小

    26 2 文件名称长度(n)

    28 2 扩展字段长度(m)

    30 n 文件名称

    30+n m 扩展字段

    (2)Data descriptor(数据描述符)当头部标志第3位(掩码0×08)置位时,表

    示CRC-32校验位和压缩后大小在File Entry结构的

    尾部增加一个Data descriptor来记录。数据描述符

    偏移的具体描述如表1-6所示。

    表1-6

    偏移 字节数 描述

    0 04 固定值0x08074b50

    04 4 CRC-32校验

    48 4 压缩后大小

    812 4 压缩前大小

    (3)Central Directory是什么

    中央目录文件夹(Central Directory File

    Header)偏移的具体描述如表1-7所示。

    表1-7

    偏移

    字节

    数

    描述

    0 4 固定值0x02014b50

    4 2 压缩版本

    6 2 解压缩版本 8 2 标志

    10 2 压缩方式

    12 2 文件最后修改时间

    14 2 文件最后修改日期

    16 4 CRC-32校验

    20 4 压缩后大小

    24 4 压缩前大小

    28 2 文件名称长度(n)

    30 2 扩展字段长度(m)

    32 2 文件注释长度(k)

    34 2 文件开始的分卷号

    36 2 文件内部属性

    38 4 文件外部属性

    42 4

    对应文件实体在文件中的偏

    移

    46 n 文件名称

    46+n m 扩展字段

    46+n+m k 文件注释

    (4)End of Central Directory record(年底中

    央目录记录)所有的File Header结束后面是该数

    据结构,其偏移的描述如表1-18所示。

    表1-18

    偏

    移

    字节

    数

    描述

    0 4 固定值0x06054b50

    4 2 当前分卷号

    6 2 Central Directory的开始分卷号

    8 2

    当前分卷Central Directory的记录

    数量

    10 2 Central Directory的总记录数量

    12 4

    Central Directory的大小

    (byte)

    16 4

    Central Directory的开始位置偏

    移

    20 2 ZIP文件注释长度(n)

    22 n ZIP文件注释

    问题1:Central Directory的作用

    通过Central Directory可以快速获取ZIP包含的

    文件列表,而不用逐个扫描文件,虽然Central

    Directory的内容和文件原来的头文件有冗余,但

    是当ZIP文件被追加到其他文件时,就只能通过

    Central Directory获取ZIP信息,而不能通过扫描文

    件的方式,因为Central Directory可能声明一些文

    件被删除或者已经更新。Central Directory中Entry的顺序可以和文件的实际顺序不一样。

    问题2:ZIP如何更新文件?

    举例说明:一个ZIP包含A、B和C三个文

    件,现在准备删除文件B,并且对C进行了更新,可以将新的文件C添加到原来ZIP的后面,同时添

    加一个新的Central Directory,仅仅包含文件A和

    新文件C,这样就实现了删除文件B和更新文件

    C。

    在ZIP设计之初,通过软盘来移动文件很常

    见,但是读写磁盘是很消耗性能的,对于一个很

    大的ZIP文件,只想更新几个小文件,如果采用

    这种方式效率非常低。

    ZIP文件解压

    Android提供两种解压ZIP文件的方法:

    ZipInputStream和ZipFile。

    (1)ZipInputStream

    ZipInputStream通过流式来顺序访问ZIP,当

    读到某个文件结尾时(Entry)返回-1,通过

    getNextEntry来判断是否要继续往下读,ZipInputStream read方法的流程图如图1-32所示。图1-32

    问题3:为什么要判断是否是压缩文件?

    因为文件在添加到ZIP时,可以通过设置

    Entry.setMethod(ZipEntry.STORED)以非压缩的

    形式添加到文件中,所以在解压时,对于这种情

    况,可以直接读文件返回,不需要解压。

    这里重点介绍一下

    InflaterInputStream.read方法,其流程图如1-

    33所示。图1-33

    从图1-33的流程图可以看出,Java层将待解

    压的数据通过我们定义的Buffer传入Native层。每

    次传入的数据大小是固定值,为512字节,在

    InflaterInputStream.java中定义如下:

    static final int BUF_SIZE = 512;

    对于压缩文件来说,最终会调用zlib中的

    inflate.c来解压文件,inflate.c通过状态机来对文

    件进行解压,将解压后的数据再通过Buffer返

    回。对inflate解压算法感兴趣的读者可以看源

    码,传送门

    http:androidxref.com4.4.4_r1xrefexternalzlibsrcinflate.c,返回count字节并不等于Buffer的大小,取决于

    inflate解压返回的数据。

    (2)ZipFile

    ZipFile通过RandomAccessFile随机访问ZIP文

    件,通过Central Directory得到ZIP中所有的

    Entry,Entry中包含文件的开始位置和size,前期

    读Central Directory可能会耗费一些时间,但是后

    面就可以利用RandomAccessFile的特性,每次读

    入更多的数据来提高解压效率。ZipFile中定义了两个类,分别是RAFStream

    和ZipInflaterInputStream,这两个类分别继承自

    RandomAccessFile和InflateInputStream,通过

    getInputStream返回,ZipFile的解压流程和

    ZipInputStream类似。

    ZipFile和ZipInputStream真正不同的地方在于

    InflaterInputStream.fill,fill源码如图1-34所

    示。图1-34InflaterInputStream.read的流程图如1-35

    所示,读者就能明白两者的区别之处。

    从流程图可以看出,ZipFile的读文件是在

    Native层进行的,每次读文件的大小是由Java层

    传入的,定义如下。

    Math.max(1024,(int)

    Math.min(entry.getSize,65535L));

    即ZipFile每次处理的数据大小在1KB和64KB

    之间,如果文件大小介于两者之间,则可以一次

    将文件处理完。而对于ZipInputStream来说,每次

    能处理的数据只能是512个字节,所以ZipFile的

    解压效率更高。图1-35

    3.ZipFile vs ZipInputStream效率对比

    解压文件可以分为如下三步:

    (1)从磁盘读出ZIP文件;

    (2)调用inflate解压出数据;

    (3)存储解压后的数据。

    因此两者的效率对比可以细化到这三个步骤

    来进行对比。

    (1)读磁盘

    ZipFile在Native层读文件,并且每次读的数

    据在1~64KB之间,ZipInputStream只有采用更大

    的Buffer才可能达到ZipFile的性能。

    (2)infalte解压效率

    从上文可知,inflate每次解压的数据是不定

    的,一方面和inflate的解压算法有关,另一方面

    取决于Native层infalte.c每次处理的数据,从以上

    分析可知,ZipInputStream每次只传递512字节数据到Native层,而ZipFile每次传递的数据可以在

    1KB~64KB,所以ZipFile的解压效率更高。从

    java_util_zip_Inflater.cpp源码看,这是Android做

    的特别优化。

    demo验证(关键代码)

    ZipInputStream关键代码,如图1-36所示。图1-36

    ZipFile关键代码,如图1-37所示。图1-37

    我们用两个不同压缩率的文件对demo进行测

    试,文件说明如表1-9所示。

    表1-9

    测试数据,如表1-10所示。

    表1-10结论:①ZipFile的read调用的次数减少

    39%~93%,可以看出ZipFile的解压效率更高。

    ②ZipFile解压文件耗时,相比ZipInputStream

    有22%~73%的减少。

    (3)存储解压后的数据

    从上文可以知道,inflate解压后返回的数据

    可能会小于Buffer的长度,如果每次在读返回后就直接写文件,此时Buffer可能并没有充满,造

    成Buffer的利用效率不高,此处可以考虑将解压

    出的数据输出到BufferedOutputStream,等Buffer满

    后再写入文件,这样做的弊端是,因为要凑满

    Buffer,会导致read的调用次数增加,下面就对

    ZipFile和ZipInputstream做一个对比。

    demo(关键代码)

    ZipInputStream的关键代码如图1-38所示。图1-38

    ZipFile的关键代码如图1-39所示。图1-39

    同样对上面的两个压缩文件进行解压,测试

    数据如表1-11所示。

    表1-11

    低压缩率

    (ms)

    高压缩率

    (ms)

    ZipInputStream 930.2 1347.2

    ZipFile 794.5 1056.8

    ZipFile耗时减

    少

    15% 22%

    结论:①ZipFile与ZipInputStream相比,耗时

    仍减少15%~22%。

    ②与不使用Buffer相比,ZipInputStream的耗

    时减少14%~62%,ZipFile解压低压缩率文件耗

    时有6%的减少,但是对于高压缩率,耗时将有

    9%的增加(虽然减少了写磁盘的次数,但是为

    了凑满Buffer,增加了read的调用次数,导致整体

    耗时增加)。

    问题4:那么问题来了,既然ZipFile效率这

    么好,那么ZipInputStream还有存在的价值吗?千万别被数据迷惑了双眼,上面的测试仅仅

    覆盖了一种场景,即文件已经在磁盘中存在,且

    须全部解压出ZIP中的文件,如果你的场景符合

    以上两点,使用ZipFile无疑是正确的。同时,也

    可以利用ZipFile的随机访问能力,实现解压ZIP中

    间的某几个文件。

    但是在以下场景,ZipFile则会略显无力,这

    时ZipInputStream的价值就体现出来了。

    ①当文件不在磁盘上,比如从网络接收的数

    据,想边接收边解压,因ZipInputStream是顺序按

    流的方式读取文件,这种场景实现起来毫无压

    力。

    ②如果顺序解压ZIP前面的一小部分文件,ZipFile也不是最佳选择,因为ZipFile读Central

    Directory会带来额外的耗时。

    ③如果ZIP中的Central Directory遭到损坏,只

    能通过ZipInputStream来按顺序解压。

    (4)结论

    如果ZIP文件已保存在磁盘中,且解压ZIP中

    的所有文件,建议用ZipFile,效率较ZipInputStream提升15%~27%。

    仅解压ZIP中间的某些文件,建议用ZipFile。

    如果ZIP没有在磁盘上或者顺序解压一小部

    分文件,又或ZIP文件目录遭到损坏,建议

    用ZipInputStream。

    从以上分析和验证可以看出,同一种解压方

    法使用的方式不同,效率也会相差甚远,最后再

    回顾一下ZipInputStream和ZipFile最高效的用法

    (有底纹的代码为关键部分),如图1-40所示。图1-401.9 案例G:刚创建好表,就做

    大量的查询操作

    问题类型:冗余读写

    解决策略:利用INSERT OR REPLACE

    问题:

    通过获取手机QQ首次启动的IO数据,看到

    大量对848688603.db的读操作,且每次读的大小

    是16字节,如图1-41所示。找到对应的SQL语

    句,其对应的是大量的SELECTFROM

    ExtensionInfo WHERE uin=?和SELECT FROM

    Friends WHERE uin=?语句,select语句耗时超过

    6秒,如图1-42所示。图1-41图1-42

    分析:

    每次读的字节数只有16字节,且偏移都一

    样,说明Friends和ExtensionInfo里面并没有内

    容。原来在Friends和ExtensionInfo创建完之后,在插入好友信息前,需要先去表里查询一下是否

    存在该记录。此时表是空的,所以才有大量的16字节的读取,如图1-43所示。

    图1-43

    解决方案:

    (1)首次安装的表为空时,不要去做无谓

    的查询操作。(2)对于覆盖安装,在表已经存在的情况

    下,可以使用INSERT OR REPLACE语句来完成

    插入。1.10 案例H:重复打开数据库

    问题类型:重复打开数据库

    解决策略:缓存数据库连接

    问题:

    在使用数据库测试工具在统计手机QQ启动

    过程中各个DB打开次数时,发现多个业务打开数

    据库的次数不止一次,而最多的竟然打开数据库

    424次(如图1-44所示),简直骇人听闻。图1-44

    分析:

    多次打开数据库有什么影响?

    先看一下SQLiteDatabase的源码,getWriteableDatabase方法的注释说明:一旦

    打开数据库,该连接就会被缓存,以供下次使

    用,只有当真正不需要时,调用close关闭即可,如图1-45所示。图1-45

    为什么要这样呢?

    因为打开数据库比较耗时,如

    app_plugin_download.db的两次耗时分别为80ms和

    120ms。每次打开数据库,同时会有一些IO操

    作。getWriteableDatabase的注释也明确说明该方

    法比较耗时,不能在主线程进行。

    解决方案:

    数据库在打开后,先不要关闭,在应用程序

    退出时再关闭。1.11 案例I:AUTOINCREMENT

    可没有你想的那么简单

    问题类型:冗余读写

    解决策略:减少使用AUTOINCREMENT

    背景:

    最近在分析手空(Android)的数据库读写

    时,发现有一条插入语句耗时平均在60ms+,SQL语句为:INSERT INTO events(timestamp,content,status,send_count) V ALUES

    (1445916309639,test,1,100),可以看到这条插入语句非常简单,仅仅是插

    入3个整形和一个简单的字符串。而一般的插入

    操作最多也就十几ms,所以这个问题值得我们好

    好研究一下。

    索引惹的祸

    首先我们拿到创建这个表的SQL语句,见如下的代码,以及对应的events表结构,如图1-46所

    示,这个表结构除了创建status为索引外,似乎并

    无特殊之处。

    create table if not exists events(event_id INTEGER PRIMARY KEY AUTOINCREMENT

    NOT NULL,content TEXT,status INTEGER,send_count INTEGER,timestamp LONG)

    CREATE INDEX if not exists status_idx ON events(status)图1-46

    问题1:难道是索引导致插入这么耗时吗?

    作为常识:索引是为了提高查询的速度,但

    在数据库插入操作时,因为要维护索引,会使插

    入效率有所降低。但是真的会降低这么多吗?还

    是要通过数据来说话。

    这个表是MTA用来存储上报记录的,我们找

    到了负责该表的同事,给了一个没有索引的

    SDK,再次编包验证。事实证明,索引对插入速

    度的影响是很有限的,一条语句简单的插入操作

    竟然要55ms,如表1-12所示。

    表1-12

    INSERT耗时(ms)

    MTA DB

    (含索引)

    MTA DB

    (不含索引)

    62.3 55.5

    这下就奇怪了,除了索引,这个表也没有特

    别的地方了。因此我们决定采用排除法,把该表

    的特性一点点去掉,看到底是谁在搞鬼。AUTOINCREMENT漏网之鱼

    接下来我们把AUTOINCREMENT关键字去

    掉,测试同样的表结构,测试结果让我们大吃一

    惊。有AUTOINCREMENT的INSERT耗时是不含

    该关键字耗时的3倍,如表1-13所示。

    表1-13

    INSERT耗时(ms)

    测试DB

    (无

    AUTOINCREMENT)

    测试DB

    (含

    AUTOINCREMENT)

    16.7 53.9

    为了保证数据严谨,又分别测试了使用事务

    对1条语句和50条语句进行插入操作的耗时,结

    果表明对于批量插入,两者的差距有所减少,但

    是仍差2倍之多,如表1-14所示。

    表1-14

    INSERT 1

    条(ms)

    INSERT 50

    条(ms)

    不用

    AUTOINCREMENT

    10 30 使用

    AUTOINCREMENT

    46 63

    倍率 4.6 2.1

    到这里,用过SQLite的读者,可能会对这个

    结果觉得难以置信,因为AUTOINCREMENT关键

    字在SQLite里面很常用,大家用的时候似乎也没

    有担心效率问题。接下来要弄明白仅多一个

    AUTOINCREMENT为什么会有这么大的差别。

    问题2:AUTOINCREMENT是什么?

    AUTOINCREMENT其实就是“自增长”,这个

    关键字只会出现在INTEGER PRIMARY KEY后

    面,而INTEGER PRIMARY KEY就是“主键”,下

    面先来了解一下主键。

    SQLite表的每行都有一个行号,行号用64位

    带有符号的整型数据表示。SQLite支持使用默认

    的列名ROWID、_ROWID_和OID来访问行号。同

    时,如果表里某一列指定为INTEGER PRIMARY

    KEY类型,那么这一列和ROWID是等价的。也就

    是说,如果你指定某一列为主键,访问该列其实

    就是访问行号。

    问题3:行号是如何生成的?对于刚创建的表来说,行号默认是从1开始

    的,如果在插入数据时明确指定行号,则会将数

    据插入对应的行,如果没有指定行号,则SQLite

    会选择比当前已用行号大1的行来进行插入。如

    果当前已用行号已达到最大值,数据库引擎会尝

    试寻找当前表里面没有使用的行号,如果没有找

    到可用的行号,就会出现SQLITE_FULL错误。

    小结:如果你没有删除过数据,并且没有指

    定最大值的行号,行号选择算法可以保证行号是

    递增且唯一的。但是如果你有删除数据或者使用

    了最大行号,之前删除的行号可能被复用,并不

    能保证插入数据的行号是严格递增的。

    问题4:主键加了AUTOINCREMENT,会有

    什么变化?

    上面提到,AUTOINCREMENT只能用来修饰

    主键,主键在被“自增长”修饰之后,会略微有些

    区别。

    (1)数据库引擎选择的行号会比所有

    之前用过的行号都大,即使数据被删

    除,行号也不会被复用,可以保证行号

    严格单调递增。(2)如果行号的最大值被用过,那么

    在插入新数据时,会报SQLITE_FULL错

    误。

    小结:AUTOINCREMENT的作用是保证主

    键是严格单调递增的。

    AUTOINCREMENT实现原理

    SQLite创建一个叫sqlite_sequence的内部表来

    记录该表使用的最大的行号。如果指定使用

    AUTOINCREMENT来创建表,则sqlite_sequence

    也随之创建。UPDATE、INSERT和DELETE语句

    可能会修改sqlite_sequence的内容。因为维护

    sqlite_sequence表带来的额外开销将会导致

    INSERT的效率降低。

    使用数据库测试工具,可以获取到两种情况

    下磁盘的读写数据,如图1-47所示。从中可以看

    出,AUTOINCREMENT会使写磁盘次数由2次增

    加到11次。这也能很好地说明,由于要维护

    sqlite_sequence而增加额外的IO开销。图1-47

    AUTOINCREMENT的坑

    在主键加上AUTOINCREMENT后,可以保证

    主键是严格递增的,但是并不能保证每次都加

    1,因为在插入失败后,失败的行号不会被复

    用,这就造成主键会有间隔。以手机QQ为例

    子,有80%的数据库表使用了AUTOINCREMENT

    关键字。coco尝试去掉创建表时的

    AUTOINCREMENT,对比相同场景相同时间序列

    的事务耗时,可以看到优化后,事务耗时比之前

    有所减少,如图1-48所示。图1-48

    同时对比手机QQ的启动耗时,发现LoginA和

    首次启动的耗时都略微有一些下降,同时CPU和

    内存也有一些降低,优化前后手机QQ时延性能

    对比,如表1-15所示。

    表1-15总结:

    AUTOINCREMENT可以保证主键的严格递

    增,但使用AUTOINCREMENT会增加INSERT耗

    时1倍以上,所以使用AUTOINCREMENT时不可

    以任性,用在该用的地方效果才佳。比如,客户

    端需要拿该主键和服务器校对数据,需要保证主

    键唯一性。

    最后以SQLite官网的一句话作为结尾:

    这个AUTOINCREMENT关键词会增加CPU,内存,磁盘空间和磁盘IO的负担,所以尽量不要

    用,除非必需。其实通常情况下都不是必需的。1.12 案例J:Bitmap解码,Google没有告诉你的方面

    问题类型:IO效率低

    解决策略:使用decodeStream代替decodeFile

    随着Android SDK的升级,Google修改了

    Bitmap解码API的实现,从而埋下了一个性能的

    坑。先把这个坑说出来,后面再详细介绍发现和

    解决过程。

    解码Bitmap不要使用decodeFile,因为在

    Android 4.4以上系统效率不高。

    解码Bitmap使用decodeStream,同时传入的

    文件流为BufferedInputStream。

    decodeResource同样存在性能问题,请用

    decodeResourceStream。

    背景:

    最近用IO监控工具检测Android QQ的性能时,提了41个读写磁盘Buffer太小的Bug单(我们

    认为读写磁盘如果Buffer小于8KB,会导致IO效

    率不高),如图1-49所示。

    图1-49

    图1-49中红框出部分的含义:

    aio_static_50.png大小1702字节,需要读取磁盘

    215次。查看堆栈,发现都是已经存在很久的代

    码,令人不解的是,之前的版本也一直都有IO性

    能检测,为什么现在才提单呢?

    分析:

    (1)验证数据准确性看到这个数据,首先想到SDK是否因为最近

    修改引入了Bug,不过通过demo验证,数据是可

    信的。

    (2)为什么之前没有提单

    我们已经用IO工具检测手机多个版本,之前

    为什么没有提单?对比之下,发现之前用的手机

    一直都是三星9300(Android 4.3系统),这次换

    成Nexus 5(Android 4.4系统),难道这和系统版

    本有关系吗?

    我们用一个demo在两台手机上验证,如表1-

    16所示的结果令人出乎意料。

    表1-16

    图片大小 读磁盘次数

    三星9300 12608 1

    Nexus5 12608 108

    在两个手机上decode同一张图片,读磁盘次

    数相差巨大,这时我们可以确定这个问题是和系

    统版本有关的。

    追根溯源——decodeFiIe你用过吗?为了能够一探究竟,最好的方法是对比两个

    版本API的实现有何不同。

    通过源码看到,BitmapFactory.java提供多个

    decode Bitmap的API,有decodeFile、decodeResource、decodeByteArray、decodeFileDescriptor、decodeStream、decodeResourceStream。而大家最常用的是

    decodeFile,前面提的Bug单也都是用的这个

    API。我们来对比一下这个API在Android 4.3和

    Android 4.4的实现差异。

    图1-50是Android 4.3 decodeFile的实现流

    程图,看到最终读磁盘用的是

    BufferedInputStream,并且Buffer大小为

    DECODE_BUFFER_SIZE = 16×1024,这也就是为

    什么在4.3系统decode大小为12KB的图片,只需

    要读一次磁盘就可以的原因。图1-50

    我们再看一下如图1-51所示的Android 4.4的

    实现流程图。图1-51

    从图1-51看到,4.4系统去掉了

    isMarkSupported的判断,而是直接调用

    nativeDecodeStream,这就导致Native在decode图

    片时,每次都要实际去读磁盘,故导致读次数增

    加很多。

    解决方案:

    通过上面的分析,我们知道决定写磁盘次数

    的是:传给nativeDecodeStream的文件流是否使用

    了Buffer,而在4.4系统上,如果使用decodeFile,生成的文件流只能是FileInputStream,这是无法修

    改的。但是如果我们直接调用decodeStream,是

    可以传递带Buffer的文件流进来的,所以解决方

    法是:不要使用decodeFile,而改用

    decodeStream,并且传入BufferedInputStream类型

    的文件流,如1-52所示。

    图1-52对修改方案进行demo验证的数据如表1-17所

    示,在三星9300(Android 4.3)上,decodeFile和

    decodeStream的耗时几乎一致,而在

    MI2S(Android 5.0)上,decodeStream的速度是

    decodeFile的3倍之多。

    表1-17

    机型(系统版

    本)

    系统API

    耗

    时

    读磁盘

    次数

    I9300(Android

    4.3)

    decodeStream 25 2

    decodeFile 26 1

    MI 2S(Android

    5.0)

    decodeStream 18 2

    decodeFile 49 108

    结论

    (1)解码Bitmap要使用decodeStream,不要使用decodeFile,同时传给

    decodeStream的文件流是

    BufferedInputStream。

    (2)decodeResource同样存在这个问

    题,建议使用decodeResourceStream。1.13 专项标准:磁盘

    专项标准:磁盘,如表1-18所示。

    表1-18

    遵

    循

    原

    则

    标准

    优

    先

    级

    规则起源

    避

    免

    主

    线

    程

    IO

    避免主线程操作文件

    和数据库

    P0

    50%以上的

    卡顿问题都是

    由主线程IO引

    起的

    用apply代替

    Sharepreference.commit

    P1

    apply是异步

    操作,commit

    是同步操作

    提前初始化

    Sharepreference

    P1

    在多进程和

    旧版本的

    Android中,初

    始化过程的IO

    读写是在主线

    程的 减

    少

    IO

    读

    写

    量

    减少使用select P1

    减少从数据

    库读取的数据

    量,减少耗

    时

    利用缓存减少重复读

    写

    P2

    内存缓存命

    中率极高,投

    入产出高

    数据库减少使用

    AUTOINCREMENT

    P1

    因为要多操

    作一个表,所

    以Insert耗时减

    少2~4倍

    使用合适的数据库分

    页

    P0

    sqlite读写磁

    盘是以page为

    单位的,在

    3.12.0版本之

    前,sqlite默认

    page size是

    1KB,从3.12.0

    开始,page

    size调整为

    4KB

    频繁查询的表使用索

    引

    P0

    索引可以极

    大地减少读磁

    盘的数据量,极大地提升效

    率 避免无效索引 P0

    无效索引的

    问题通常是严

    重的。除了触

    发全表扫描,产生大量冗余

    的读写之外,还降低了写入

    性能

    减

    少

    IO

    操

    作

    次

    数

    使用8KB Buffer读写 P0

    可以减少2~3

    倍的耗时(案

    例D、J、F、E

    也与之相

    关)

    批量更新数据库使用

    事务

    P0

    启用事务,根据业务规

    模,会大量减

    少IO读写量

    和操作次数,从而提升效

    率

    ZIP压缩大量小文件时

    建议使用

    ZipInputStream

    P2 第2章 内存:性能优化的终结者

    2.1 原理

    那天几个小伙伴在讨论重复下载的流量问

    题,一个负责内存的小伙伴云雷,默默地走过来

    强行插入说,一切最后都会变成内存问题,如图

    2-1所示。然后大家相视一笑,为什么呢?想想,质疑重复下载问题,可以缓存到存储中;缓存到

    存储要读出来,就变成磁盘IO问题;为了避免磁

    盘IO问题怎么办,用内存缓存起来。什么都用内

    存缓存起来,App的常驻内存就会很大,变成内

    存问题,甚至最后成为OOM的导火索。图2-1

    这里我们就以OOM为起点,介绍Android内

    存的原理。Out of Memory,OOM,通常会在

    decode图片的时候触发,但不一定是decode图片

    的问题,因为也许它只是压垮骆驼的稍微大一点

    的稻草而已。那什么时候会压垮骆驼?在虚拟机

    的Heap内存使用超过堆内存最大值(Max

    Memory Heap)的时候,那么在这里大家需要理

    解的第一个概念就是Dalvik(ART)虚拟机的堆

    内存最大值。

    1.虚拟机的堆内存最大值

    在虚拟机中,Android系统给堆(Heap)内

    存设置了一个最大值,可以通过

    runtime.getruntime.maxmemory获取,而据我们2016年2月的统计,大部分用户使用的手机

    的最大堆内存应该都设置在64MB以上,而

    128MB的手机份额也在飞速增加,估计是因为屏

    幕分辨率变大了,解码图片的内存消耗相应变

    大,所以给予每个App的最大堆内存也与日俱

    增。而游戏作为消耗内存的特殊存在,Android开

    通了一个绿色通道,可以在manifest里面设置

    LargeHeap为true。

    从这里可以发现,你的App真的不可能完全

    使用1GB、2GB的内存,系统只分给你一小部

    分。分这么小内存有一个重要的原因,是Android

    默认没有虚拟内存。在内存资源稀缺的大背景

    下,为了保证在极端情况下,前台App和系统还

    能稳定运行,就只有靠low memory killer机制。

    2.Low Memory Killer

    下面引出另一个重要概念Low Memory

    Killer,也是App消耗内存过大导致的另外一个结

    果。在手机剩余内存低于内存警戒线的时候,就

    会召唤Low Memory Killer这个劫富济贫的“杀

    手”在后台默默干活。这里只要记住一句话,App

    占用内存越多,被Low Memory Killer处理掉的机

    会就越大。如果OOM和Low Memory Killer都没有干掉你

    的App,那也不代表App就没有内存问题。因为

    还有一类问题,会直接导致App卡顿,即GC。

    3.GC(Garbage Collection)

    最简单的理解就是没有被GC ROOT间接或直

    接引用的对象的内存会被回收。在具体执行中,ART和Dalvik(>2.3)会有很多不同,如图2-2所

    示,并发GC的时候ART比Dalvik少了一个stop-

    the-world的阶段,因此Dalvik比ART更容易产生

    Jank(卡顿),当然,无论ART还是Dalvik并发

    GC的stop-the-world的时间并不长。然而,糟糕的

    情况是GC for Alloc,这个情况在内存不足以分配

    给新的对象时触发,它stop-the-world的时间因为

    GC无法并发而变得更长(虽然在Android 3.0中增

    加了局部回收(Partial),在Android5.0中增加了

    新增回收(Sticky)优化了不少,但时间依然很

    长),如图2-3所示。图2-2图2-3

    那么说到底,我们还是要避免GC FOR

    ALLOC,跟要避免OOM一样,关键是要管理好

    内存。什么是管理好内存?除了减少内存的申请

    回收外,更重要的是减少常驻内存和避免内存泄

    漏。说起内存泄漏,就必须要提Activity内存泄

    漏。

    4.Activity内存泄漏

    因为Activity对象会间接或者直接引用View、Bitmap等,所以一旦无法释放,会占用大量内

    存。案例部分将会介绍有不少关于Activity内存泄

    漏的案例。但是无论是什么案例,都离不开不同

    的GC ROOT对Activity的直接引用、this0间接引

    用、mContext间接引用,如表2-1所示。

    表2-1

    引用的

    方式\GC

    ROOT

    CIass-(静态变

    量)

    活着的线程

    mContext

    间接引

    用

    静态View,InputMethodManager

    SensorManager、WifiManager(其他Service

    进程都可以) this0间

    接引用

    内类引用 匿名

    类TimerTimerTaskHandler

    直接引

    用

    静态Actvity

    提示:大家学习完后面的案例之后,也不妨

    回到这里看一下。

    那么另外一个情况就是内存常驻了,而通常

    在常驻内存中最大的就是图片。俗话说,互联网

    产品最讲究的体验精神,即“有图有真相”。但是

    这些图片在内存中的存储不合理会导致什么呢?

    首当其冲的是Crash堆栈,如图2-4所示。图2-4

    然后是疯狂GC,触发我们前面说到的GC for

    Alloc,导致Stop-the-world的“卡”,如图2-5所

    示。图2-5

    最后是功能异常,有损体验:内存没了,图

    还要加载,如图2-6所示。图2-6 内存没了,图还要加载

    当然,以上的损害都说明,我们将“大卡

    车”停进的“内存”造成危害。既然有这么多的损

    害,为什么不能把图片下载来都放到磁盘(SD

    Card)上呢?其实答案不难猜,放在内存中,展

    示起来会“快”那么一些。快的原因有如下两点。

    硬件快(内存本身读取、存入速度快)。

    复用快(解码成果有效保存,复用时,直接

    使用解码后对象,而不是再做一次图片解

    码)。

    很多同学不知道所谓“解码”的概念,可以简

    单地理解,Android系统要在屏幕上展示图片的时

    候只认“像素缓冲”,而这也是大多数操作系统的

    特征。而我们常见的jpg、png等图片格式,都是

    把“像素缓冲”使用不同的手段压缩后的结果,所

    以相对而言,这些格式的图片,要在设备上展

    示,就必须经过一次“解码”,它的执行速度会受

    图片压缩比、尺寸等因素影响,是影响图片展示

    速度的一个重要因素。

    5.图片缓存两害相权取其轻,官方建议使用LRU算法来

    做图片缓存,而不是之前推荐的WeekReference,因为WeekReference会导致大量GC。原理示意图

    如图2-7所示。图2-7

    官方建议使用一个进程所能申请的最大内存

    的四分之一作为图片缓存(Android进程都有最大

    内存上限,这依据手机ROM而定,在手机出厂的

    那一刻就被固定下来,一般情况下无法更改)。

    图片缓存达到容积上限时,内部使用LRU算法做

    淘汰,淘汰那些“又老又少被用到”的图片,这就

    是内存图片缓存的大体设计思维。但是对于许多

    图片类App,内存对于它们实在是捉襟见肘,因

    此官方有两个非常著名的硬盘缓存方案。

    1.

    DiskLruCache(https:android.googlesource.complatformlibcore+jb-

    mr2-

    releaselunisrcmainjavalibcoreioDiskLruCache.java,https:github.comJakeWhartonDiskLruCache),简单理解就是LruCache的硬盘版本,容错性强,但是对比BlobCache,IO性能很一般。

    2.

    BlobCache(http:androidxref.com6.0.1_r10xrefpackagesAppsGallery2gallerycommonsrccomandroidgallery3dcommonBlobCache.java),这个方案源自Android原生的相册,仅仅利用三个

    文件,包括索引(Index)文件、活动(Active)

    文件和非活动(Unactive)文件,通过

    FileInputStream.getChannel.map把索引文件直接映射到内存,通过索引(其实就是偏

    移)来读取活动文件中的图片缓存。清除旧图片

    的方法简单粗暴,直接seek到文件头部,覆盖写

    入就可以。这一切都是为了用最小的磁盘IO代价

    完成磁盘缓存。

    另外官方也建议,把从内存淘汰的图片,降

    低压缩比存储到本地,以备后用。这样就可以最

    大限度地降低以后复用时的解码开销。

    现在我们来归纳一下,内存问题主要包括常

    驻问题(主要是图片缓存)、泄漏问题(主要是

    Activity泄漏)、GC问题(关键是GC For

    Alloc),后果会导致App Crash、闪退、后台被

    杀、卡顿,而且这是各种资源类性能问题积压的

    最后一环。因此可见其重要性,下面,我们来看

    看有什么好工具和实用的案例可以帮助我们理解

    和解决这些终极性能问题。2.2 工具集

    这里要特别强调,Android关于内存的工具不

    少,如表2-2所示,灵活地选择工具就显得特别重

    要。我们特别推荐涵盖一定初步定位和定位能力

    的工具,可以让我们一步到位地剖析问题、提升

    效率。实在有些场景无法覆盖,这时可以使用仅

    有“发现”能力的工具。

    表2-2

    工具 问题

    能

    力

    topprocrank

    内存占用过大,内存泄漏

    发

    现

    STRICTMODE Activity泄漏

    发

    现

    meminfo

    Native内存泄

    漏、是否存在

    Activity、ApplicationContext

    泄漏、数据库缓

    存命中率低

    发

    现

    +初

    步

    定

    位 MAT、Finder、JHAT

    Java层的重复内

    存、不合理图片

    解码、内存泄漏

    等

    发

    现

    +定

    位

    libc_malloc_deBug_leak.so

    Native内存泄漏

    (JNI层)

    发

    现

    +定

    位

    LeakCanary

    Activity内存泄

    漏

    自

    动

    发

    现

    +定

    位

    StrictMode

    Activity内存泄

    漏

    自

    动

    发

    现

    +初

    步

    定

    位

    APT

    内存占用过大,内存泄漏

    发

    现

    人工触发GC for

    Explicit而导致的

    发

    现 GC Log from Logcat、GC

    Log生成图表

    卡顿,Heap内存

    不足触发GC for

    Alloc而导致的卡

    顿

    +初

    步

    定

    位

    Systrace GC导致的卡顿

    发

    现

    Allocation Tracer

    申请内存次数过

    多和过大、辅助

    定位GC Log发现

    的问题

    发

    现

    +定

    位

    chrome devtool HS的内存问题

    发

    现

    +定

    位

    1.topprocrank

    得到内存曲线的方法很多,top就是其中一

    种,但是很遗憾,它的输出列信息(如图2-8所

    示)中只包含RSS与VSS,所以Android中Top的使

    用更多地集中在某个进程的CPU负载方面。下面

    借着介绍top的契机,说明VSS、RSS、PSS、USS

    的含义。

    VSS:Virtual Set Size虚集合大小。RSS:Resident Set Size常驻集合大小。

    PSS:Proportional Set Size比例集合大小。

    USS:Unique Set Size独占集合大小。

    图2-8

    RSS与PSS相似,也包含进程共享内存,但有一个麻烦,RSS并没有把共享内存大小平分到

    使用共享的进程头上,以至于所有进程的RSS相

    加会超过物理内存很多。而VSS是虚拟地址,它

    的上限与进程的可访问地址空间有关,和当前进

    程的内存使用关系并不大,就比如在A地址有一

    块内存,在B地址也有一块内存,那么VSS就等

    于A Size加B Size,而至于内存是什么属性,它并

    不关心。所以很多file的map内存也被算在其中,我们都知道,file的map内存对应的可能是一个文

    件或硬盘,或者某个奇怪的设备,它与进程使用

    内存并没有多少关系。

    而PSS、USS最大的不同在于“共享内存”(比

    如两个App使用MMAP方式打开同一个文件,那

    么打开文件而使用的这部分内存就是共享的),USS不包含进程间共享的内存,而PSS包含。这

    也造成了USS因为缺少共享内存,所有进程的

    USS相加要小于物理内存大小的原因。

    最早的时候官方推荐使用PSS曲线图来衡量

    App的物理内存占用,同时,用户在原生的

    Android 操作系统上唯一能看到的内存指标

    (在“设置-应用程序-正在运行的某程序”)就是

    PSS,而Android 4.4之后加入USS(如图2-8所

    示)。但是PSS,有个很大的麻烦,就是“共享内

    存”,这种情况发生在A进程与B进程都会使用一

    个共享SO库,那么SO库中初始化所用的那部分

    内存就会被平分到A与B的头上。但是A是在B之

    后启动的,那么对于B的PSS曲线图而言,在A启

    动的那一刻,即使B没有做任何事情,也会出现

    一个比较大的阶梯状下滑,这会给用曲线图分析

    软件内存的行为造成致命的麻烦。

    USS虽然没有这个麻烦,但是由于Dalvik虚

    拟机申请内存牵扯到GC时延和多种GC策略,这

    些都会影响曲线的异常波动,比如异步GC是

    Android 4.0以上系统很重要的新特性,但是GC什

    么时候结束?曲线什么时候“降”?就变得很诡异

    了,而测试人员通常希望退出某个界面后可以明

    显地看到曲线有大的降落。还有GC策略,什么时

    候开始增加Dalvik虚拟机的预申请内存大小

    (Dalvik启动时是有一个标称的start内存大小

    的,为Java代码运行时预留,避免Java运行时再

    申请而造成卡顿),但是这个预申请大小是动态

    变化的,这也会造成USS忽大忽小。

    另外:

    (1)Android 4.4以后增加了一个新的名词,被称为ProcessStats(PS),用于反映内存负载,其最终计算也用到了PSS。

    (2)PROCRANK在模拟器里面会存在,而

    大多数真机则没有。不过下面介绍的Meminfo才

    是正道,所以也不多介绍了。

    2.meminfo

    介绍完topprocrank,我们知道了它们的适用

    范围以及局限性。接下来介绍一个Android官方非

    常推荐的工具dumpsys meminfo。

    meminfo的使用如下:

    meminfo dump options:[-a] [--oom]

    [process]

    其中,-h 帮助信息。

    -a 打印所有进程的内存信息,以及当前设备

    的内存概况。--oom 按照OOM Adj值进行排序。

    [process] 可以是进程名称,也可以是进程

    id,用于打印某个进程的内存信息。如果不输入参数,meminfo只会打印当前设

    备的内存概况,如图2-9和图2-10所示,这两张图

    由三个部分构成。图2-9

    图2-10

    (1)按照PSS排队,用于查看进程的内存占

    用,一般用它来做初步的竞品分析,同样功能的应用程序应该具有不相上下的PSS。

    (2)OOM Adj排队,展示当前系统内部运

    行的所有Android进程的内存状态和被杀顺序,越

    靠下方的进程越容易被杀,排序按照一套复杂的

    算法,算法涵盖了前后台、服务或界面、可见与

    否、老化等,但其查看的意义大于测试,可以做

    竞品对比,比如先后退回后台的被测产品,没过

    多久出现竞品的排名在被测产品上方的情况(即

    被测产品退回后台更容易被系统杀掉,不过

    Android 4.4以后出现了“内存负载”概念,这也不

    一定是坏事了)。

    (3)整机PSS分布,按照降序排列各类PSS

    占用,此部分仅用于粗略查看设备内存概况,也

    可以查看物理内存的使用是否已接近物理内存的

    最大值。

    输入进程标识参数后,meminfo会打出一份

    有关进程的详细内存概况,如图2-11和图2-12所

    示。图2-11图2-12

    另外,配合命令行watch来观察meminfo也是

    不错的选择,例如,每隔5秒刷新一次,watch-n

    5 dumpsys meminfo com.tencent.mobileqq。

    3.Procstats

    从基础的曲线图PSS引出了meminfo,那么怎

    样才能把meminfo这样一个“点”上的值变成一

    个“统计”的值呢?绘制二维曲线是一种方法(纵

    轴PSS、横轴时间),但这并不能为用户选择App

    提出好的建议(太难于理解,不好简易量化),所以Android想出了更好的办法,即“内存负载”。

    这个概念在Android 4.4被提了出来,那么负载是

    怎么计算的呢?见如下公式:

    内存负载=PSS×运行时长

    运行时长被分为前台、后台和缓冲(与App

    在设备上的运行状态一致)时长,与之对应内存

    负载就出现了前台内存负载、后台内存负载和缓

    冲内存负载这三个概念。为了更直观展示它们,就有了Procstats工具(如图2-13所示),它位于

    Android L的开发者选项中,虽然官方也没有说什

    么时候要把它移出来给用户看,但相信很快就会被公布于众。图2-13

    Procstats的查看方式很简单,有如下两种。

    (1)每个进程后面都有一个百分比数值,它用于统计“此状态下的”运行时间,所谓的此状

    态即上文所述的——前台、后台、缓冲。默认展

    示的是“后台负载”,所以按照图2-13所展示的,QQ MSF进程在被统计的2小时6分钟内,位于后

    台的运行时长也是2小时16分钟。

    (2)每个进程都有一条绿色的进度条,越

    长表示负载越高,没有一个统一的刻度值,只是

    一种展示而已,它的长短由App的PSS和此状态下

    的运行时间的积来决定,由图2-13可见,微信的

    内存负载高过QQ空间。

    引申一下,三个状态中对于Android系统来

    说,有如下的潜规则。

    (1)对于前台而言,是用户正在使用的,所以这部分内存一定会保证,是用户不关注的,所以在内存负载中不应该“默认展示”。

    (2)对于缓冲而言,Android认为可以回

    收,此类软件有良好的被杀恢复能力,所以没有将它杀死完全是系统的责任,在内存负载中也不

    应该“默认展示”。

    (3)对于后台而言,Android认为这完全是

    App行为,而且系统因为种种原因也无权杀死它

    并回收内存,而且并非用户当前所使用(正在使

    用的App是运行于“前台”状态下的),有可能是

    用户不想支付的代价,所以这部分内存负载应该

    被“默认展示”。

    作为测试人员,可以通过竞品对比查看哪个

    App在后台的内存负载更高,用以说明被测软件

    在完成自我特性的同时,内存指标是否被Android

    系统认可(即软件的全局观),认可度高的App

    相比认可度低的App而言肯定更容易被用户所青

    睐。

    4.DDMS(Monitor)

    DDMS的全称是:Dalvik DeBug Monitor

    Server,即Dalvik虚拟机调试监控服务,其界面如

    图2-14所示。图2-14

    DDMS是一个调试信息合集,里面包含时

    延、内存、线程、CPU、文件系统、流量等一系

    列信息的获取和展示,其中和内存相关的要提到

    两个功能:Update Heap、Allocation Tracker,以

    及内存快照Dump Hprof file。

    Update Heap:会获取GC的信息,包括当前

    已分配内存、当前存活的对象个数、所剩内存、动态虚拟机heapSize,还有一个分配大小的分布

    柱状图,主要用于查看,问题定位能力有限。

    Allocation Tracker:会展示最近的500条内存

    分配,以及分配发生时刻的线程堆栈信息(如图

    2-15所示),官方推荐用它来提升流畅度。因为

    申请内存多,GC就多,而正如原理所介绍的,GC会挂起全部线程,引入卡顿问题。图2-15

    Dump Hprof file:用于对选中的进程进行内

    存快照,至于内存快照的使用将会在MAT部分中

    介绍。

    另外,在Android Studio也有类似的功能,而

    且更加好用,其截图如2-16所示。图2-16

    5.MAT

    MAT的全称为Memory Analyzer(内存分析器),是IBM Eclipse顶级开源项目,但MAT的设

    计初衷并非专门用于分析Android应用程序内存,它最初的作用是分析运行在J2SE或J2ME下的Java

    类型应用程序的内存问题。由于Dalvik在一定程

    度上也可以理解为Java虚拟机,Google就没有重

    复开发内存分析工具,而沿用了MAT。

    使用MAT需要抓取Hprof文件(内存快

    照),抓取Android应用程序快照的方法有很多,前文说的DDMS的Dump Hprof file功能就是其中最

    简单、通用性最强的一种。以下还有两种不常用

    的方法。

    (1)在adb shell模式下使用kill -10 pid。

    (2)在adb shell模式下使用am dumpheap pid

    outfilePath。

    通过(1)抓取的Hprof文件位于datamisc文

    件夹下,但是Android 4.x以上系统已经不支持这

    条命令了,am命令虽然可以指定手机端的输出目

    录,但是分析过程仍需要把它从手机端复制到电

    脑端。

    通过DDMS抓取的Hprof文件不能直接用于

    MAT,需要通过Android SDK Tools中的Hprof-conv工具转换一下,才能用于MAT。不过在安装

    DDMS的Eclipse上继续安装MAT插件,这样使用

    DDMS抓取Hprof文件就不会出现“另存为”窗口,而会直接自动转化后在MAT插件中展示了。

    使用MAT打开Hprof文件后,通常会见到如2-

    17所示的界面。图2-17

    Insepctor:左侧框出来的部分,用于展示对

    象的一些信息,比如继承关系、成员、内部静态

    变量等,非常重要。

    内存详情:右侧上方框出来的部分,用于展

    示一个快照的内部数据,这是分析工作主要的操

    作台。

    状态以及扩展窗口:在右侧下方框出来部

    分,可以看到操作记录、工作日志、命令行信息

    等,可以辅助书写工作摘要。

    在使用MAT前有几个重要的知识点需要掌

    握。

    (1)GC的原理,即对象之间的引用关系的

    理解。

    (2)被测产品的一般内存特征,比如在手

    机QQ聊天窗口场景下,内存中有多少个Map对象

    和多少个List对象等特征。

    (3)有一定的Java代码阅读能力。

    MAT的使用技巧,主要包含如图2-18所示的几个方面。

    图2-18

    MAT非常灵活且强大,但使用门槛颇高,不

    易于上手,因为是开源软件,所以有些信息没有

    做得足够详细、易用,比如统治者视图,虽然它

    是MAT的重要组件,但是里面的信息做过大量过

    滤,如果过度依赖它来发现问题,很可能出现遗

    漏测试的悲剧。更多有关MAT的使用方法,因为

    其功能过多就不在这里一一详述了,会在后续案

    例中有更加生动、详尽的描述。

    6.FinderMAT足够强大,但是对于初级或者需要大量

    内存覆盖测试的测试人员来说,其强大的功能、复杂的操作、适配Android内存测试时的小误差无

    疑是一场噩梦。Finder是我们基于MAT进行的二

    次开发,主要为Android内存测试人员提供一个更

    系统化、简单化、流程化的内存测试体验,降低

    测试门槛、提高测试效率、稳定测试成果,其界

    面如图2-19所示。图2-19

    右边Finder菜单栏下扩展区菜单是主要操作

    区,有如下功能。

    Activity:获取dump中的所有Activity对象。

    Top Classes:以对象数量或对象大小为维度

    来获取对象降序列表。

    Compare:对比两个Hprof文件内容的差别。

    Bitmap:获取dump中所有的Bitmap对象。

    Same Bytes:查询dump中以byte[]类型出

    现并且内容重复的对象。

    TwoExecutionThreeDump:“两遍三dump”用于

    分析三个dump中持续增长的对象。

    Singleton:查询dump中的单例。

    About Finder:作者和感谢人。

    右边FinderRule菜单栏下扩展区菜单是Run

    Memory rules(执行内存规则),主要配合

    Memory Rule使用。

    左边功能区展示的是Finder中一个很好用的

    功能BitmapView:通过Eclipse的Windows→Show

    View→Other,调出FinderView(Bitmaps

    View),用于查看某个Bitmap对象中的图像,如图2-20所示。图2-20

    图2-20左边功能区展示的是Finder中另一个

    很好用的功能Memory Rule:通过Eclipse的

    Windows→Show View→Other,调出

    FinderView(Memory rules View),它用来积累

    用例和类对象规则,比如登录成功后内存中应该

    存在多少map对象。

    使用效果

    (1)能够准确定位泄漏问题(通过Finder定

    位的泄漏缺陷修改率达80%以上)。

    (2)更够发现细微的内存问题(通过曲线

    工具发现问题后,不太敏感,常常出现小泄漏测

    试不出来,但是在用户那里频繁爆发的情况)。

    (3)更加节省测试成本(以前内存测试,一个测试人员需要经过数月的培训,大量地了解

    产品架构,才能开始内存测试工作,且测试效率

    基本上为两个工作日一个需求,使用Finder后可

    以达到经过3~5小时培训,在不了解产品架构情

    况下,就开始内存测试工作,且半天就可以测试

    完成一个需求)。7.推荐LeakCanary

    LeakCanary是Square出品的一款非常优秀的

    Activity内存泄漏检测工具。值得注意的是,在

    leakcanaryleakcanary-

    androidsrcmainjavacomsquareupleakcanaryAndroidExcludedRefs.java

    这个文件中,定义了不少Android系统导致的内存

    泄漏坑和解决的黑科技,其中就包括

    InputMethodManager泄漏等。

    8.LeakInspector

    天网是Android手机经过长期积累和提炼,集

    内存泄漏检测、自动修复系统Bug、自动回收已

    泄漏Activity内资源、自动分析GC链、白名单过

    滤等功能于一体,并深度对接研发流程、自动分

    析责任人并提缺陷单的一站式内存泄漏解决方

    案。前面推荐了Square开源的内存泄漏检测组件

    LeakCanary,与之相比有什么不同呢?

    LeakInspector与LeakCanary两个工具的功能对比如

    表2-3所示。

    表2-3

    对比项 LeakInspector LeakCanary

    Activity泄 ? ? 基

    础功

    能

    漏检测

    自定义对

    象泄漏检

    测

    · ?

    位图检

    测

    · ?

    显示泄漏

    对象

    ·只显示类名 ?

    泄漏提

    醒

    · ?

    自动

    dump

    · ?

    Hprof分

    析

    ·云端分析 ?本地分析

    提单跟

    进

    · ?

    白名单配

    置

    ·动态配置 ?源码写死

    配合自动

    化

    · ?

    兜

    底功

    能

    修复系统

    泄漏

    · ?

    回收资

    源

    · ? 第一,检测能力与原理方面不同

    (1)检测能力

    两个工具都支持对Activity、Fragment以及其

    他自定义类(比如QQAppInterface)泄漏的检

    测,但LeakInspector还有针对Bitmap的检测能

    力:

    ①检测有没有在View上decode超过该View尺

    寸的图片,若有则上报出现问题的Activity及与其

    对应的View id,并记录它的个数与平均占用内存

    的大小。

    ②检测图片尺寸是否超过所有手机屏幕大

    小,违规则提单。

    而LeakCanary没有这个能力。

    (2)检测原理

    检测原理图如图2-21所示,两个工具的泄漏

    检测原理都是在onDestroy时检查弱引用,不同的

    地方在于LeakInspector使用WeakReference来检测

    对象是否已经释放,而LeakCanary使用

    ReferenceQueue,两者效果没什么区别。图2-21

    针对Activity,如何实现在onDestroy时启动监

    控呢?在Android 4.0以上系统中,两者都通过注

    册Activity的生命周期,重写onActivityDestroyed

    方法实现。然而在Android 4.0以下的系统上,LeakCanary需要手动在每一个Activity.onDestroy中

    添加启动检测的代码,而LeakInspector反射了

    Instrumentation来截获onDestroy,接入时修改成本

    更低。

    下面我们用自己的MonitorInstrumentation替换

    系统原来的Instrumentation对象代码,如图2-22所

    示。图2-22

    第二,泄漏现场处理方面不同

    (1)dump采集

    两者都能采集dump,但LeakInspector提供了

    回调方法,能让用户增加自定义信息,比如运行

    时LOG、TRACE、DUMPSYS等信息,辅助分析

    定位问题,如图2-23所示。图2-23

    (2)白名单定义

    所谓的白名单,主要是为了处理一些系统引

    起的泄漏问题,以及一些因为业务逻辑需要开后

    门的情形而设置的。分析时如果碰到白名单上标

    识的类,则不对这个泄漏做后续处理。

    二者的配置有以下两个差异。

    ①LeakInspector的白名单以XML配置的形式

    存放在服务器上。

    优点:跟产品(甚至用例)绑定,测试、开

    发同学可以很方便地修改相应配置。

    缺点:白名单里的类不区分系统版本一刀

    切。

    LeakCanary的白名单写死在其源码的

    AndroidExcludedRefs.java类里,如图2-24所示。

    优点:定义非常详细,区分系统版本。

    缺点:每次修改必定得重新编译。图2-24

    ②LeakCanary的系统白名单里定义的类比

    LeakInspector方案中定义的多很多,因为它没有

    下面所述的自动修复系统泄漏功能。

    (3)修复系统泄漏

    针对系统泄漏,LeakInspector进行了预处

    理,通过反射自动修复目前碰到的一些系统泄

    漏,只要在onDestroy里调用一个修复系统泄漏的

    方法即可。LeakCanary也能识别系统泄漏,但它

    仅仅对该类问题给出了分析,没有提供实际可用

    的解决方案。

    (4)回收资源

    如果已经发生了泄漏,LeakInspector会对整

    个Activity的View进行遍历,把图片资源等一些占

    内存的数据释放掉,保证此次泄漏只会泄漏一个

    Activity的空壳,尽量减少对内存的影响。而

    LeakCanary没有类似逻辑,只能依赖人工修改来

    解决内存问题。

    LeakInspector回收资源的大体方法如图2-25

    所示。图2-25

    以recycleTextView为例,我们回收资源的方

    式如图2-26所示。图2-26第三,后期处理(如图2-27所示)方面不同图2-27

    (1)分析与展示

    采集dump之后,LeakInspector会自动通过

    Magnifier上传dump文件,并调用MAT命令行来进

    行分析,得到这次泄漏的GC链;LeakCanary则用

    开源组件HAHA来分析,同样返回一条GC链。

    从分析过程来看,两者都不需要用户介入。

    但是整个分析流程比较耗时,LeakInspector将分

    析放在服务器上,极大地减轻了手机的负担,而

    且马上能开始下一次测试;LeakCanary连分析也

    放在手机上做,此时基本无法执行其他测试,只

    能等分析完毕。

    从获取的GC链展示来看,LeakCanary得到的

    GC链包含被hold住的类对象,用户很可能不需要

    用MAT打开Hprof即可解决问题;而LeakInspector

    方案得到的GC链里只有类名,用户还得用MAT

    打开Hprof看才能定位问题,有点不方便,如图2-

    28所示。图2-28

    (2)后续跟进闭环

    LeakInspector在dump分析结束之后,会提交

    缺陷单,并且把缺陷单分配给对应类的负责人或

    SVN最后修改人;发现重复的问题则更新旧单,同时具备重新打开单等状态扭转逻辑。

    LeakCanary会在通知栏提醒用户,需要用户自己

    记录该问题并做后续处理。

    第四,配合自动化测试方面不同

    LeakInspector方案跟自动化测试可以无缝结

    合,当自动化脚本执行过程中发现内存泄漏,即

    由它采集dump,然后发送到服务器进行分析,生

    成JSON格式的结果,最后提单,整个流程一气呵

    成无须人力。而LeakCanary把分析结果通过通知

    栏告知用户,该结果无法传到自动化下一个流

    程,必须有人工介入。

    9.JHat节点

    JHat是Oracle推出的一款Hprof分析软件,它

    和MAT并称为Java内存静态分析利器,但是两者

    最初认知不同,MAT更注重单人界面式分析,而JHat起初就认为Java的内存分析是联合多人协作

    的过程,所以使用多人界面式分析(BS结构)。

    因为这款软件是Oracle推出的,也就是传统意义

    上的“正统”软件,所以jhat被置于JDK中,安装了

    JDK的读者,设置好Java_Home与Path后,在命令

    行中输入jhat命令可查看有没有相应的命令。

    JHat的使用如图2-29所示。图2-29

    正常的使用非常简单:

    jhat xxx.hprof

    JHat的执行过程是解析Hprof文件,然后启动

    httpsrv服务,默认是在7000端口监听Web客户端

    链接,维护Hprof解析后数据,以持续供给Web客

    户端的查询操作。执行结果如图2-30所示。图2-30

    JHat还有两个比较重要的功能分别如下。

    (1)统计表,如图2-31所示。图2-31

    (2)OQL查询(OQL是一种模仿SQL语句的

    查询语句,通常用来查询某个类的实例数量,或

    者同源类数量),如图2-32所示。图2-32

    对于中小型团队,建议就不要考虑JHat工具

    了,JHat比MAT更加灵活,且符合大型团队安装

    简易、团队协作的需求,并不非常适合中小型高

    效沟通型团队使用。

    10.libc_malloc_deBug_leak.so

    Android构建在一个被精简的Linux上,Dalvik

    虚拟机是一个Java运行时环境,而Dalvik本身其

    实是在Linux上运行的,和所有别的Java运行时环

    境一样Dalvik为了能够使更多的CC++人员融

    入,提供了NDK以便C类开发者开发Android

    App。官方虽然提供了相应的工具,但是在工具

    介绍的一开始,就严厉地说:并非所有的App都

    需要使用NDK技术,而且NDK技术并不会带来通

    常猜想的那些性能优势,它仅仅应该在两种情况

    下被使用:第一,你有大量的C++库要被复用;

    第二,你编写的程序是高CPU负载的,比如游戏

    引擎、物理模仿等。

    虽然官方明确限制了NDK的作用,但是依然

    有不少的产品使用了混合架构,即有一部分功能

    由Java编写,另外的使用NDK。NDK使用的内存

    是透传出Dalvik的,因此在Hprof分析过程中,是见不到这部分内存分配的。前面介绍的分析级别

    工具在NDK面前都没作用。为了检测NDK所编写

    的C代码在运行时耗费的内存,我们就必须使用

    一个特殊的工具,或者可以称它为库——

    libc_malloc_debug_leak.so。

    Android底层Linux申请内存所用到的库是

    libc.so,而libc_malloc_debug_leak就是专门来监

    视libc.so内部接口(malloc、calloc等)被调用的

    调试库。把libc_malloc_debug_leak.so放到libc.so

    的旁边,并且设置Android框架的

    libc.debug.malloc属性为“1”,然后重启Android框

    架,就打开了Android C类内存申请监控的开关。

    界面展示如图2-33所示。图2-33

    在独立版DDMS中是可以展示Native Heap

    的,这部分就是C类内存申请,通常用NDK编出

    的程序需要以so形式出现,并在安装后放入系统

    的data目录中。这和系统本身的so是有区别的,所以我们只用看这部分内存的大小,就是size字

    段,点击每个申请,都拥有一个申请过程的调用

    栈,根据栈后的method字段,就可以知道方法所

    在的内存偏移,最后使用NDK自带的

    addr2line.exe,就可以将地址转化为方法名称。

    NDK在Android下并非是一种推荐的编程方

    式,但是由于其内存自管理与CPU高负载支撑的

    特性,却俘获了很多开发者的心灵,Android对于

    这块的内存测试方案并没有多少新意,仅仅能够

    做到的是看看分配大小、看看申请此大小的方法

    是谁而已,如此直白就没有必要过多叙述了。不

    过需要注意的是,在使用NDK的时候,一定要在

    Application.mk文件中加入编译选项“-Wl,-

    Map=xxx.map -g”,否则会引起无法使用addr2line

    的麻烦。

    11.APT

    APT是腾讯另外一个团队出品的一款Android应用测试工具,并且是开源的。与Finder同样作

    为IDE的插件形式出现,不同的是Finder是MAT的

    插件,而APT是DDMS的插件(DDMS有独占问

    题,所以使用APT的机器上不能正常使用

    DDMS),APT实现的是实时监控能力,它可以

    监控多个App的CPU、内存指标,并且把它们画

    成图标形式,很直观,CPU曲线展示如图2-34所

    示。图2-34

    内存曲线展示,如图2-35所示。图2-35

    SMap展示如图2-36所示。图2-36

    另外,因为APT实现的内存监控是调用

    meminfo分析结果来实现的,而meminfo每次调用

    会增加Dalvik的HeapAlloc值(即使App什么事情

    也没有做),但是这并不影响App本身功能,到

    一定值HeapAlloc会释放这部分meminfo调用造成

    增加值,所以用APT监控HeapAlloc很有可能会出

    现上面展示图的特征。

    SMap在一定程度上可以反映Native的内存分

    配量,但是毕竟没有监控malloc、calloc、delete

    来得直接、准确,得到的数据也混入了大量代码

    段占据内存空间,而这部分内存空间是App编码

    所必备的或者说很难减少、很难精简定位的,所

    以也不建议使用SMap来做Native内存测试。

    12.GC Log

    跟GC相关的有两个工具,一个是在Logcat中

    输出的GC日志,另外一个就是Allocation

    Tracer。这里先介绍Logcat输出的日志。而日志分

    成Dalvik的GC日志与ART的GC日志两种。

    (1)Dalvik GC日志解析(如表2-4所示)

    表2-4

    GC_CONCURRENT

    freed 2049K,65% free 3571K9991K,GC产生原因回收的

    内存大小

    currAllocatedcurrFootprint

    GC产生原因如下。

    GC_EXPLICIT:通过Runtime.gc与

    VMRuntime.gc,SIGUSR1触发产生的GC,虽

    不支持局部GC,但稍微幸运一点的是支持并发

    GC。然而在列表滑动和动画播放的时候,最好还

    是不要出现这类日志。因为在这种高CPU低响应

    时延的场景,人工触发GC来消耗CPU的行为应该

    尽可能避免。

    GC_FOR_[M]ALLOC:没有足够的内存空

    间给予即将分配的内存,这时会触发GC。这种

    GC因为不是并发GC,所以对卡顿的影响更大,应该尽量避免。QQ以内存触顶率

    (MaxMemoryHeap的80%作为阈值)作为内存对

    用户影响的外网上报指标,其实归根结底就是根

    据GC for Alloc的概念提出的。

    GC_FOR_CONCURRENT:当超过堆占用阈

    值时会自动触发。应该是最常见的GC了,也是最健康的GC,因为它支持局部GC、并发GC。所以

    暂停时间也因为是并发GC,所以会分成mark和被

    修改对象的remark两部分的耗时。对于详情,读

    者可以再次阅读前面的“原理:GC(Garbage

    Collection)”一节。

    GC_BEFORE_OOM:在触发OOM之前触发

    的GC。这种GC不能并发,不能局部GC,所以耗

    时长,也容易卡住界面。

    GC_HPROF_DUMP_HEAP:在dump内存之

    前触发的GC。这种GC不能并发,不能局部GC,所以耗时长,也容易卡住界面。

    currAllocatedcurrFootprint:就是在App实际

    使用的堆中对象的数量堆大小。

    extAllocatedextLimit:这部分主要包括系统

    Bitmap、user-defined和view-inflated的

    Bitmap。这部分日志,在内存在3.1版本之

    后,回归到Java Heap之后就没有了。

    暂停时间:一个耗时和两个耗时的区别在于

    是否是并发GC。正如原理中所说,并

    发GC需要第二次挂起线程的机会来处理那些

    被标记又因并发过程中被修改的对象。另外有一个工具

    (https:github.comoba2cat3logcat2memorygraph),可以轻易地把Dalvik的GC日志绘制成如图2-37所

    示的图表,可谓功能强大。图2-37

    (2)ART日志解析(如表2-5所示)

    ART的日志与Dalvik的日志差距非常大,除

    了格式不同之外,打印的时机也不同,非要在慢

    GC时才打印出来。

    表2-5GC产生原因如下。

    Concurrent、Alloc、Explicit:跟Dalvik的基本上一样,这里就不重复介绍了。

    NativeAlloc:Native内存不足以分配内存时

    触发,跟Alloc类似。

    Background:后台GC,触发是为了给后面的

    内存申请预留更多空间。

    CollectorTransition、HomogeneousSpaceCompact、DisableMovingGc、HeapTrim:产生的主要原

    因是ART的GC算法更加复杂。

    GC类型如下。

    Full:简单可以理解为跟Dalvik的FULL GC差

    不多。

    Partial:简单可以理解为跟Dalvik的局部GC

    差不多,策略是不包括Zygote Heap。

    Sticky:可以理解为另外一种局部中的局部

    GC,选择局部的策略是上次垃圾回收后新分

    配的对象。

    GC方式如下。

    mark sweep:先记录全部对象,然后从GC

    ROOT开始找出间接和直接的对象并标注。利用之前记录的全部对象和标注的对象对

    比,其余的对象就应该需要垃圾回收了。

    concurrent mark sweep:使用mark sweep采集

    器的并发GC。

    mark compact:在标记存活对象的时候,所

    有的存活对象压缩到内存的一端,而另外一

    端可以更加高效地回收。

    semispace:在做垃圾扫描的时候,把所有引

    用的对象从一个空间放到另外一个空间,剩

    余在旧的空间中的对象,只要直接GC整个空

    间就可以。

    通过GC日志,我们可以知道GC的量和它对

    卡顿的影响,也可以初步定位一些如主动调用

    GC、可分配的内存不足、过多使用Weak

    Reference等问题。但是还是不知道代码行,这时

    就必须使用Allocation Tracer或者前面的MAT了。

    13.Allocation Tracer

    在Android Studio里面打开Android Monitor,选择进程,这里选择了比较火的名为FaceU的App

    来做例子。点击Allocation Tracer,然后经过几秒

    或者同步观察GC日志发现问题的时候,再次点击

    Allocation Tracer暂停录制,如图2-38和图2-39所

    示。图2-38图2-39

    这时候,观察Android Studio的代码区,会出

    现录制结果。一般来说按照Size排序,然后从最

    大的一层层展开,如图2-40和图2-41所示。图2-40图2-41

    然后通过dump to source跳转到对应的代码

    行,结合申请了大量的ArrayListIterator结果,就

    可以思考一下有没有优化的方法,例如用

    ArrayList.toArray来优化。

    14.自带防泄漏功能的线程池组件

    开发人员做子线程操作的时候,喜欢用匿名

    内部类Runnable来操作,敲起代码来简单、快

    捷、方便。然而,如果某个Activity放在线程池里

    的任务不能及时执行完毕,在Activity销毁时很容

    易导致内存泄漏。

    原因为何?下面来看一段很简单的代码,如

    图2-42所示。图2-42

    用javac编译之后,结果如图2-43所示。图2-43

    可见这个匿名内部Runnable类持有一个指向

    Outer类的引用,这样一来如果某Activity里面的

    Runnable不能及时执行,就会使它外围的Activity

    无法释放,产生内存泄漏。那么,我们要想自动

    避免这个问题,有没有办法呢?从上面分析可

    见,只要在Activity退出时没有这个引用就可以,那我们就通过反射,在Runnable入线程池前先干

    掉它,如图2-44所示。

    图2-44

    这个任务是我们的Runnable对象,而“this0”就是上面所述指向外部类的引用了。

    当然,等到执行它的时候,没了这个引用可

    能会出问题。因此干掉它之后得“留个全尸”,找

    个WeakReference“墓地”放起来,要执行了先get一下,如果是null说明Activity已经回收,任务就放

    弃执行。

    15.Chrome Devtool

    什么表现可能是内存泄漏问题引起的?

    先看一个例子:

    《疯狂打怪

    兽》

    进入游戏黑屏

    vivo y11

    华为t8954

    华为c8813dq

    索尼爱立信

    《疯狂打怪

    兽》

    进入游戏

    ANR

    lt18i

    “玩吧”这个功能的卡帕莱自动化测试结果显

    示,《疯狂打怪兽》这个游戏在某些机型上有黑

    屏、ANR。

    我们用已有的测试机无法重现Bug,不过发

    现游戏进行一段时间后确实有一些卡顿。用

    Chrome Devtool Timeline工具检查了一下,游戏过

    程中内存一直在涨,进一步抓内存快照分析发现

    有泄漏,如图2-45所示。图2-45

    跟Java层内存泄漏类似,页面出现黑屏、ANR、卡顿,这些都很有可能是内存问题。

    启动远程调试

    首先,我们要有一个工具,可以实时抓到手

    机上的内存消耗。Java层使用的是ADT工具,但是对于HTML5页面而言,抓取JavaScript的内存

    需要用到Chrome Devtools来进行远程调试。方式

    有两种,第一种可以先把URL抓取出来放到

    Chrome里访问,第二种用Android H5远程调试。

    下面具体介绍稍微复杂一点的Android H5远程调

    试,如表2-6所示。

    表2-6

    场景 是否支持调试

    纯H5(手

    机浏览器

    里打开)

    Chrome支持远程调试

    默认

    Hybrid H5

    (App内

    嵌H5页)

    系统自带的浏览器内核,Android

    4.4以前是WebKit内核,不支持;

    Android 4.4系统开始为Chromium内

    核,支持远程调试

    TBS

    hybrid H5

    (QQ空

    间、手机

    QQ、微信

    内嵌H5)

    使用QQ浏览器提供的TBS内核,支

    持远程调试

    不同的场景需要不同的启用远程调试的方

    法前面提到,Chrome和TBS都支持远程调试,接下来介绍三种场景下的调试方法。

    (1)纯H5

    这个最方便,适用于Android 4.0+系统。下面

    具体说明需要准备什么。

    一台PC(安装最新版Chrome)

    一根USB线

    一部手机(安装手机Chrome)

    第1步:手机安装Chrome,打开USB调试模

    式,通过USB连上电脑,在Chrome里打开一个页

    面,比如百度页面。然后在PC Chrome地址栏里

    访问Chrome:inspect ......

您现在查看是摘要介绍页, 详见PDF附件(21930KB,989页)