摘录自: 《Android移动性能实战——腾讯SNG专项测试团队编著》,这是一本很棒的书,将Android的性能优化写的淋漓尽致。
前言
这本书是我接触过的第一本的Android性能优化的书。这本书用各种各样的性能检测的工具,将腾讯的作品的优化过程一点点记录下来,启发式的将性能优化这一门学问融入到了一个个单独的案例中,深入浅出的将这一门博大精深的学问展示在了读者的面前。对于一个读者而言,这本书是一本带有故事性的技术书、工具书。
磁盘
- 文件读写:每次打开、关闭或者读写文件,操作系统都需要经过从用户态转换为内核态的切换,这种状态的切换本身是很消耗性能的,所以为了提高文件的读写效率,就需要尽量减少用户态和内核态的切换。使用缓存可以避免重复读写,对于需要多次访问的数据,在第一次取出数据的时候,将数据放在缓存中,下次再访问的时候,就可以从缓存中取出来。
- Android手机里面的
libsqlite.so
调用系统的pread64
和pwrite64
函数进行I/O
操作。 -
序列化:
ObjectOutputStream
在序列化磁盘的时候,会把内存中的每个对象保存到磁盘,在保存对象的时候,每一个数据成员会带来一次I/O操作。因此,在使用ObjectOutputStream
的时候,在ObjectOutputStream
上面再封装一个输出流ByteArrayOuputStream
先将对象序列化后的信息写到缓存区中,然后再一次性地写到磁盘上。序列化磁盘读写方式 优化前 优化后 序列化写磁盘 ObjectOutputStream BufferedOutputStream + ObjectOutputStream ByteArrayOutputStream + ObjectOutputStream 序列化读磁盘 ObjectInputStream BufferedInputStream + ObjectInputStream ByteArrayInputStream + ObjectInputStream 序列化磁盘读写方式 优化前后 I/O次数 耗时(ms) 序列化写磁盘 优化前 499 162.8 优化后 1 87.3 序列化读磁盘 优化前 719 171.8 优化后 1 109.9 - Buffer缓冲区大小:在读/写时使用缓冲区可以减少读写次数,从而减少了切换内核态的次数,提高读/写效率,根据实际经验,这里推荐使用的Buffer大小为8KB,这和Java默认的Buffer大小一致,Buffer大小至少应为4KB。当然,Buffer也不是越大越好,Buffer如果太大,会导致申请Buffer的时间边长,反而整体效率不高。可以采用文件大小除以读写次数得到Buffer的大小。这个方法由两个影响因子决定,一是Buffer size不能大于文件大小;二是Buffer size根据文件保存所挂载的目录的Block size来确定Buffer大小,而数据库的pagesize,就是这样确定的。
- SQLite:数据库在打开后,先不要关闭,在应用程序退出时再关闭。
-
SQLite:在数据库中,应减少使用AUTOINCREMENT。AUTOINCREMENT可以保证主键的严格递增,但是使用
AUTOINCREMENT
会增加INSERT
耗时1倍以上,所以使用AUTOINCREMENT
不可以任性,用在该用的敌方效果才佳。
SQLite官方:这个AUTOINCREMENT关键字会增加CPU,内存,磁盘空间和磁盘I/O的负担,所以尽量不要用,除非必须。其实通常情况下都不是必需的。
-
Bitmap解码:
(1)解码Bitmap
不要使用decodeFile
,因为在Android4.4以上系统效率不高。
(2)解码Bitmap
使用decodeStream
,同时传入的文件流BufferedInputStream
。
(3)decodeResource
同样存在性能问题,请用decodeResourceStream
。
解决方案:- 解码Bitmap要使用
decodeStream
,不要说使用decodeFile
,同时传给decodeStream
的文件流是BufferedInputStream
。 -
decodeResource
同样存在这个问题,建议使用decodeResourceStream
。
- 解码Bitmap要使用
专项标准:磁盘
遵循标准 | 标准 | 优先级 | 规则起源 |
---|---|---|---|
避免主线程I/O | 避免主线程操作文件和数据库。 | P0 | 50%以上的卡顿问题都是由主线程I/O引起的。 |
使用apply代替Sharepreference.commit | P1 | apply是异步操作,commit是同步操作。 | |
提前初始化Sharepreference | P1 | 在多进程和旧版本的Android中,初始化过程的I/O读/写都是在主线程的。 | |
减少I/O读写量 | 减少使用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 | 无效索引的问题通常是严重的。除了触发全表扫描,产生大量的冗余的读/写之外,还降低了写入性能。 | |
减少I/O操作次数 | 使用8KB Buffer读/写 | P0 | 可以减少2~3倍的耗时。 |
批量更新数据库使用事务 | P0 | 启用事务,根据业务规模,会大量减少I/O读写量和操作的次数,从而提升效率。 | |
ZIP压缩大量小文件的时候建议使用zipInputStream | P2 |
内存
内存检测工具集
工具 | 问题 | 能力 |
---|---|---|
top/procrank | 内存占用过大,内存泄露 | 发现 |
meminfo | Native内存泄露,是否存在Activity、ApplicationContext泄露、数据库缓存命中率低 | 发现+初步定位 |
MAT、Finder、JHAT | Java层的重复内存、不合理图片解码、内存泄露等等 | 发现 + 定位 |
libc_malloc_debug_leak.so | Native内存泄露(JNI层) | 发现 + 定位 |
LeakCanary | Activity内存泄露 | 自动发现 + 定位 |
StrictMode | Activity内存泄露 | 自动发现 + 初步定位 |
APT | 内存占用过大,内存泄露 | 发现 |
GC Log from Logcat、GC Log生成图表 | 人工触发GC for Explicit而导致的卡顿,Heap内存不足触发GC for Alloc而导致的卡顿 | 发现 + 初步定位 |
Systrace | GC导致的卡顿 | 发现 |
Allocation Tracer | 申请内存次数过多和过大、辅助定位GC Log发现的问题 | 发现 + 定位 |
chrome devtool | HS的内存问题 | 发现 + 定位 |
Android使用的是一个去掉了swap的linux内核(至少在Android4.4以前的版本都是这样的),这样就阻碍了Android上的应用程序使用Page out(应用程序使用的内存,对操作系统而言都是一张张的page,而对于老化的page,操作系统可以将它们从内存中置换到硬盘上,这种操作叫做page out),这一常规的内存操作。
Android框架对于进程内存的第二个管控特征是,每个进程都有一个内存最高阀值(纯净的Native内存申请不算在内),一旦进程申请内存突破了这个阀值,将会产生异常,并退出运行时的内存空间。简单的说,也就是Android为每个进程已经分好了一块蛋糕,至于你吃或者不吃,是你的自己的事情了。但这是否意味着Android应用程序为了效率考虑,应该玩命的申请内存,使自己的内存沿着天花板滑行,这样是否最健康呢?答案也不一定。
Android的第三个管控特征是,进程都有可能被杀。在物理内存吃紧的时候(通常在使用meminfo查看内存概况的PSS总值达到设备物理内存80%的时候),Android框架就开始根据一套*的LRU进程Cache列表来杀死进程,被杀死的进程在死前将会得到通知,用以保存现场。而这部分被杀死的进程所腾出来的物理内存,就可以用于某些应用程序的内存申请需求。
Android内存框架下的各种管控特征:
- 没有Page out, 所以物理内存更加金贵。
- 每个进程都有一个内存上限,所以蛋糕是已经被分配好的。
- 所有的进程都有被杀的可能,所以要做好被杀准备。
专项标准:内存
遵循原则 | 标准 | 优先级 | 规则起源 |
---|---|---|---|
避免内存泄露 | 避免Activity泄露 | P0 | 大部分严重的内存泄露都是Activity泄露,因为这意味着被引用的View、图片等全部泄露。 |
减少常驻内存 | 尽量使用RGB565 | P1 | 手机QQ使用RGB565将节省部分图片的内存,高达50%。 |
避免内存重复 | P1 | 手机QQ去掉头像30%的重复缓存,提升缓存的命中率和流畅度。 | |
res/drawable里的图片,建议使用Drawable.createFromStream来加载 | P1 | 使用错误的文件夹,导致图片被放大,最终APP使用的内存增加。 | |
将图片放置到合适的资源文件夹(hdpi、xxhdpi等) | P1 | 使用错误的文件夹,导致图片被放大,最终APP使用的内存增加。 | |
减少GC | Bitmap尽量使用inBitmap | P1 | 可以减少GC,提升流畅度。 |
建议使用SpraseMap或者ArrayMap | P2 | ||
建议StringBuilder重用(如果由线程使用可配合ThreadLocal) | P1 | 手机QQ与QQ空间日志改造,利用delete来替代new,给予合理的初始化长度,写日志性能提升多倍。 |
网络
工具集
工具 | 问题 | 能力 |
---|---|---|
Wireshark | 最专业的网络分析工具,全部网络性能问题的分析定位都可以查看它。 | 发现+定位 |
Har+Pagespeed | 吧pcap转为har,上传到http://stevesouders.com/flint/ | 发现+定位 |
fiddler | 主要针对HTTP,帮助发现HTTP众多性能问题,还能模拟错误和演示的HTTP返回 | 发现+定位 |
tcpdump | 抓包工具,需要ROOT权限 | 发现+定位 |
traceroute | 定位网络路由问题,包括就近接入、跨运营商问题 | 发现+定位 |
ARO | 无压缩、重复下载、缓存失效等,还有雅虎军规中的其他问题 | 自动发现+定位 |
WebP/BPG | 图片压缩方案,前者基于webm的帧内压缩,后者基于H.264的帧内压缩 | 解决 |
SPDY/HTTP2.0/QUIC | 网络协议,利用FastTcpOpen减少握手次数,利用UDP更好的适应网络抖动 | 解决 |
WebPageTest.org | 如果要做Web应用的数据上报,建议参考之。它提供了LoadTime、StartRender、SpeedIndex,DOM Elements等耗时 | 发现+定位 |
tPackageCapture | 无ROOT转包 | 定位 |
ATC | 最专业的弱网络模拟工具,除能模拟窄带、延时、丢包、损坏包外,最关键的还有包乱序的情况 | 发现 |
工具库
工具 | 说明 |
---|---|
AndroidGodEye | AndroidGodEye是一个可以在PC浏览器中实时监控Android数据指标(比如性能指标,但是不局限于性能)的工具,你可以通过wifi/usb连接手机和pc,通过pc浏览器实时监控手机性能。 系统分为三部分: 1. Core 核心部分,提供所有模块; 2. Debug Monitor部分,提供Debug阶段开发者面板; 3. Toolbox 快速接入工具集,给开发者提供各种便捷接入的工具。 AndroidGodEye提供了多种监控模块,比如cpu、内存、卡顿、内存泄漏等等,并且提供了Debug阶段的Monitor看板实时展示这 些数据。而且提供了api供开发者在release阶段进行数据上报。 |
附录
- 安卓开发中必备的那些神器APP
- 《Android移动性能实战——腾讯SNG专项测试团队编著》