Android最佳性能实践
Android最佳性能实践(一)——合理管理内存
Android最佳性能实践(二)——分析内存的使用情况
Android最佳性能实践(三)——高性能编码优化
Android最佳性能实践(四)——布局优化技巧
Android代码内存优化
Android代码内存优化建议-OnTrimMemory优化
Android性能优化指南
本文总结了一下常见的性能优化点,供开发人员参考。
1.1 如何对Android应用进行性能分析
一款App流畅与否安装在自己的真机里,玩几天就能有个大概的感性认识。不过通过专业的分析工具可以使我们更好的分析应用。而在实际开发中,我们解决完当前应用所有bug后,就会开始考虑到性能优化。
如果不考虑使用其他第三方性能分析工具的话,我们可以直接使用ddms中的工具,其实ddms工具已经非常的强大了。ddms中有traceview、heap、allocation tracker等工具都可以帮助我们分析应用的方法执行时间效率和内存使用情况。对于第三方分析工具,有LeakCanary可以用来检测内存泄露,详情可查询相关资料。
1.2 Android性能优化常用工具
1.2.1 TraceView
- TraceView简介
TraceView是一个数据分析工具,而数据的采集则需要使用Android SDK中的Debug类或者利用DDMS工具。
- 用法如下
开发者在一些关键代码段开始前调用Android SDK中Debug类的startMethodTracing(),并在关键代码段结束前调用stopMethodTracing()。这两个函数运行过程中将采集运行时间内该应用所有线程(注意,只能是Java线程)的执行情况,并将采集数据保存到/mnt/sdcard/下的一个文件中。开发者然后需要利用SDK中的Traceview工具来分析这些数据。
- 例图如下图所示
点击上图中所示按钮即可以采集目标进程的数据。当停止采集时,DDMS会自动触发Traceview工具来浏览采集数据。
下面,我们通过一个示例程序介绍Traceview的使用。
实例程序如下图所示:界面有4个按钮,对应四个方法。
点击不同的方法会进行不同的耗时操作。
我们分别点击按钮一次,要求找出最耗时的方法。点击前通过DDMS 启动 Start Method Profiling按钮。
然后依次点击4个按钮,都执行后再次点击上图中红框中按钮,停止收集数据。
接下来我们开始对数据进行分析。
当我们停止收集数据的时候会出现如下分析图表。该图表分为2大部分,上面分不同的行,每一行代表一个线程的执行耗时情况。main线程对应行的的内容非常丰富,而其他线程在这段时间内干得工作则要少得多。图表的下半部分是具体的每个方法执行的时间情况。显示方法执行情况的前提是先选中某个线程。
我们主要是分析main线程。
上面方法指标参数所代表的意思如下:
列名 |
描述 |
Name |
该线程运行过程中所调用的函数名 |
Incl Cpu Time |
某函数占用的CPU时间,包含内部调用其它函数的CPU时间 |
Excl Cpu Time |
某函数占用的CPU时间,但不含内部调用其它函数所占用的CPU时间 |
Incl Real Time |
某函数运行的真实时间(以毫秒为单位),内含调用其它函数所占用的真实时间 |
Excl Real Time |
某函数运行的真实时间(以毫秒为单位),不含调用其它函数所占用的真实时间 |
Call+Recur Calls/Total |
某函数被调用次数以及递归调用占总调用次数的百分比 |
Cpu Time/Call |
某函数调用CPU时间与调用次数的比。相当于该函数平均执行时间 |
Real Time/Call |
同CPU Time/Call类似,只不过统计单位换成了真实时间 |
我们为了找到最耗时的操作,那么可以通过点击Incl Cpu Time,让其按照时间的倒序排列。我点击后效果如下图:
通过分析发现:method1最耗时,耗时2338毫秒。
那么有了上面的信息我们可以进入我们的method1方法查看分析我们的代码了。
1.2.2 Heap
heap简介(检测内存泄露)
- 1. 使用步骤如下:
1) 切换到DDMS透视图,并确认Devices视图、Heap视图都是打开的;
2) 点击选中想要监测的进程,比如system_process进程;
3) 点击Devices视图界面中最上方一排图标中的“Update Heap”图标;
4) 点击Heap视图中的“Cause GC”按钮;
5) 此时在Heap视图中就会看到当前选中的进程的内存使用量的详细情况。
说明:
1) 点击“Cause GC”按钮相当于向虚拟机请求了一次gc操作;
2) 当内存使用信息第一次显示以后,无须再不断的点击“Cause GC”,Heap视图界面会定时刷新,在对应用的不断的操作过程中就可以看到内存使用的变化;
3) 内存使用信息的各项参数根据名称即可知道其意思,在此不再赘述。
2.如何才能知道我们的程序是否有内存泄漏的可能性呢?
1) 这里需要注意一个值:Heap视图中部有一个Type叫做data object,即数据对象,也就是我们的程序中大量存在的类类型的对象。在data object一行中有一列是“Total Size”,其值就是当前进程中所有Java数据对象的内存总量,一般情况下,这个值的大小决定了是否会有内存泄漏。可以这样判断:
2) 不断的操作当前应用,同时注意观察data object的Total Size值;
3) 正常情况下Total Size值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况,所以说虽然我们不断的操作会不断的生成很多对象,而在虚拟机不断的进行GC的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平;
4) 反之如果代码中存在没有释放对象引用的情况,则data object的Total Size值在每次GC后不会有明显的回落,随着操作次数的增多Total Size的值会越来越大,直到到达一个上限后导致进程被kill掉。
5) 此处以system_process进程为例,在我的测试环境中system_process进程所占用的内存的data object的Total Size正常情况下会稳定在2.2~2.8之间,而当其值超过3.55后进程就会被kill。
1.2.3 allocation tracker
详情参考: Android性能专项测试之Allocation Tracker(Device Monitor)
1. allocation tracker 简介
追踪内存分配信息,按顺序排列,这样我们就能清晰看出来某一个操作的内存是如何一步一步分配出来的。比如在有内存抖动的可疑点,我们可以通过查看其内存分配轨迹来看短时间内有多少相同或相似的对象被创建,进一步找出发生问题的代码。
2. 使用步骤:
1.运行DDMS,只需简单的选择应用进程并单击Allocation tracker标签,就会打开一个新的窗口,单击“Start Tracing”按钮;
2.让应用运行你想分析的代码。运行完毕后,单击“Get Allocations”按钮,一个已分配对象的列表就会出现第一个表格中。
3.单击第一个表格中的任何一项,在表格二中就会出现导致该内存分配的栈跟踪信息。通过allocation tracker,不仅知道分配了哪类对象,还可以知道在哪个线程、哪个类、哪个文件的哪一行。
1.2.4 第三方分析工具
- LeakCanary(内存泄漏工具)
https://github.com/square/leakcanary
- BlockCanary(检测卡顿)
https://github.com/markzhai/AndroidPerformanceMonitor
1.3 内存优化
1.3.1常见问题之OOM
OOM介绍
OOM指的是内存溢出,也就是超出内存占用,此时还需要介绍一下内存泄漏的概念,就是对象没有及时被回收,两者的关系就是内存泄漏可能造成内存溢出,这个就好比自己丢的垃圾要及时回收,如果垃圾堆多了,垃圾桶不够用了,就溢出来了。
OOM常见问题:
1.资源释放问题
代码的原因,导致某些资源得不到释放,如Context、Cursor、IO流的引用
2.对象内存过大问题
如Bitmap、XML文件,造成内存超出限制。
解决方法:
方法a: 等比例缩小图片(只是改变图片大小,并不能彻底解决内存溢出)
方法b:对图片采用软引用,及时地进行recyle()操作(是否回收视情况而定)
方法c:使用加载图片框架处理图片,如Picasso,imageloader等等(框架自带缓存)
3.static关键字的使用问题
用static修饰的变量,会导致该变量的生命周期延长,如果用它来引用一些资源耗费过多的实例(Context的情况最多),这时就要谨慎对待了。
错误示例:
引用链如下:
Activity->mContext
错误分析:
Activity赋值到mContext。那么及时Activity已经onDestroy,但由于仍有对象保存它的引用,所以该Activity依然不会被释放。
解决方案:
Context尽量使用Application的Context,因为Application的Context的生命周期比较长,引用它不会出现内存泄露的问题。或者使用WeakReference代替强引用。比如可以使用WeakReference<Context> mContex;
4.线程导致内存溢出
线程产生OOM的主要原因在于线程生命周期的不可控。
错误示例:
错误分析:
假设run()方法内处理的操作很费时,当我们开启该线程后,将设备的横屏变为了竖屏,一般情况下当屏幕转换时会重新创建Activity,按照我们的想法,老的Activity应该会被销毁才对,然而事实上并非如此。
由于我们的线程是Activity的内部类,所以MyThread中保存了Activity的一个引用,当MyThread的run函数没有结束时,MyThread是不会被销毁的,因此它所引用的老的Activity也不会被销毁,因此就出现了内存泄露的问题。
可能你会考虑到用AsyncTask,但事实上AsyncTask的问题更严重,Thread只有在run函数不结束时才出现这种内存泄露问题,然而AsyncTask内部的实现机制是运用了线程池,该类Thread对象的生命周期不确定且无法控制,因此如果AsyncTask作为Activity的内部类,就更容易出现内存泄露的问题。
解决方案:
将线程的内部类,改为静态内部类(非静态内部类隐式持有外部类对象的强引用,而静态类则不拥有),然后在线程内部采用弱引用保存Context引用。
5.横竖屏切换导致OOM
原因分析:
来回切换,导致activity的生命周期不断销毁重建,次数多了就OOM
解决方案:
没有固定的解决方法,一般直接锁死。但如有特殊需求,则需要从以下三方面分析
a、将xml中较大图片,改在程序中设置背景图(放在onCreate中):
destory时调用drawable.setCallback(null),防止Activity得不到释放。
b、将xml加载成view,放入容器里,调用this.setContentView(view),避免xml重复加载
c、 在页面切换时尽可能少地重复使用一些代码
如:重复调用数据库,反复使用某些对象等等......
6.Handler导致的内存泄露
原因分析:
由于Handler具有异步特点,也可能引发内存泄露。
解决方案:
使用static修饰handler,使其不依赖外部对象。
7.单例模式造成的内存泄漏
单例模式和application的生命周期是保持一致的,因此Activity对象无法被及时回收.同样需要用到弱引用进行优化
8.属性动画导致的内存泄露
属性动画中有一类无限循环的动画,如果没有在onDestory()方法中停止,那么将一直持有Activity的引用,则需要在Activity的onDestory方法中调用animator.cancel()来停止动画.
1.3.2常见问题之ANR异常
1.ANR简介(应用程序无响应Application Not Responding)
ANR一般有三种类型:
1) Activity(5秒) --主要类型,按键或触摸事件在特定时间内无响应
2) Broadcast (10秒)—小概率类型,BroadcastReceiver在特定时间内无响应
3) Service (20秒) --小概率类型,Service在特定的时间内无响应
2.ANR的原因
1) 当前的事件没得到处理(UI线程处理的事件没有及时完成或者looper机制阻塞)
2) 当前的事件正在处理,但没有及时完成(将耗时操作放到子线程执行)
3.查找ANR的方式:
1) 导出/data/data/anr/traces.txt,找出函数和调用过程,分析代码
2) 通过LOG查找
4.解决方案:将可能耗时的操作放到子线程处理,比如disk io, 网络访问等。
1.3.3其他注意事项
l 在Activity的onDestroy中释放资源
l 使用缓存,存储一些常用信息
l 对图片采样三级缓存机制
l 对一些全局的数据采样sp,db等进行存储
l 尽量不要用依赖注入框架(依赖注入框架通常会使用反射,会影响程序性能)
l 使用广播没有注销会产生内存泄露,需要及时注销。
l 构造Adapter时,使用缓存的 convertView和ViewHolder
l 使用优化的数据结构,如SparseArray和ArrayMap代替HashMap等; HashMap相对比较低效,因为它的键值对是object类型,即便把int传入, 自动转成Integer,也就是autoBoxing,它会按照对象的大小来分配内存,大概是32个字节,而不是4个字节(实际上如果不是特大型的项目,用hashmap也不要紧)
l 使用枚举通常会比使用静态常量要消耗两倍以上的内存,在Android开发当中我们应当尽可能地不使用枚举。 (影响也不是很大)
1.4 界面优化
详情参考:http://blog.csdn.net/qq_17766199/article/details/52863741
1.4.1.1 重用< include/>
< include>标签可以在一个布局中引入另外一个布局,这个的好处显而易见。类似于我们经常用到的工具类,随用随调。便于统一修改使用。
举例说明:首先写一个公共的布局title_bar.xml,app中常用的标题栏。
1.4.1.2 减少嵌套
首先我们心中要有一个大原则:尽量保持布局层级的扁平化。在这个大原则下我们要知道:
l 在不影响层级深度的情况下,使用LinearLayout而不是RelativeLayout。因为RelativeLayout会让子View调用2次onMeasure,LinearLayout 在有weight时,才会让子View调用2次onMeasure。Measure的耗时越长那么绘制效率就低。
l 如果非要是嵌套,那么尽量避免RelativeLayout嵌套RelativeLayout。这简直就是恶性循环,丧心病狂。
实现方法就不细说了,大家都是明白人。
1.4.1.3 ViewStub
我们通常是用visibility属性来实现隐藏或者显示,然后在代码中动态改变可见性,但是它的这样仍然会创建View,会影响性能。这时就可以用到ViewStub了,ViewStub是一个轻量级的View,不占布局位置,占用资源非常小
例子:比如我们请求网络加载列表,如果网络异常或者加载失败我们可以显示一个提示View,上面可以点击重新加载。当然一直没有错误时,我们就不显示。
用法很简单,记得一旦ViewStub可见或是被inflate了,ViewStub就不存在了,取而代之的是被inflate的Layout。所以它也被称做惰性控件。
1.4.1.4 < merge/>
< merge/>主要用来去除不必要的FrameLayout。它的使用最理想的情况就是你的根布局是FrameLayout,同时没有使用background等属性。这时可以直接替换。因为我们布局外层就是FrameLayout,直接“合并”。
1.4.1.5 用TextView同时显示图片和文字
这个就不细说了。当然EditView等也一样的,还有属性drawableBottom和drawableTop供你使用。同时利用代码setCompoundDrawables(Drawable left, Drawable top, Drawable right, Drawable bottom)可以让我们动态去设置图片。
1.4.1.6 使用TextView的行间距
lineSpacingExtra属性代表的是行间距,他默认是0,是一个绝对高度值。
lineSpacingMultiplier属性代表行间距倍数,默认为1.0f,是一个相对高度值。
当然了这两条属性可以同时使用,查看源码可以知道,他们的高度计算规则为mTextPaint.getFontMetricsInt(null) * 行间距倍数 + 行间距。
1.4.1.7 使用Spannable或Html.fromHtml
这个就不细说了。
1.4.1.8 用LinearLayout自带的分割线
android:divider="@drawable/divider"
android:showDividers="middle"
其中divider.xml是分隔线样式。
showDividers 是分隔线的显示位置,beginning、middle、end分别代表显示在开始位置,中间,末尾。
还有dividerPadding属性这里没有用到,意思很明确给divider添加padding。感兴趣可以试试。
1.4.1.9 Space控件
还是接着上面的例子,如果要给条目中间添加间距,怎么实现呢?当然也很简单,比如添加一个高10dp的View,或者使用android:layout_marginTop="10dp"等方法。但是增加View违背了我们的初衷,并且影响性能。而使用过多的margin其实会影响代码的可读性。
这时你就可以使用Space,他是一个轻量级的。
/**
* Draw nothing.
* @param canvas an unused parameter.
*/
@Override
public void draw(Canvas canvas) {
}
可以看到在draw方法没有绘制任何东西,那么性能也就几乎没有影响。
1.4.1.10 防止过度绘制
使用手机的开发者模式,打开显示过渡绘制选项,可以看到哪些地方有过渡绘制,从而根据具体情况减少不必要的绘制。
1.5 App启动优化
大部分app不需要用到, 特别大型的启动速度很慢的就需要优化了。
这里参考了其他人的文章总结的几点优化
APP的启动通常是由系统来控制的,一般来说不会出现什么问题,但是在对于启动速度方面,我们能够控制,主要优化涉及到以下三方面
- Activity 的 onCreate 流程,特别是 UI 的布局与渲染操作,如果布局过于复杂很可能导致严重的启动性能问题。(详细可参照上面的界面优化)
- Application 的 onCreate 流程,对于大型的 App 来说,通常会在这里做大量的通用组件的初始化操作。需要特别注意包含Disk IO操作,网络访问等耗时的任务。主要采用延迟加载进行优化,可以在application里面做延迟加载,也可以把一些初始化的操作延迟到组件真正被调用到的时候再做加载。
- 目前有部分 App 会提供自定义的启动窗口,这里可以做成品牌宣传界面或者是给用户提供一种程序已经启动的视觉效果
1.6 耗电量优化
主要是通过减少网络请求以及唤醒屏幕的次数来达到减少电量小号。
- 尽量减少唤醒屏幕的次数与持续时间,使用WakeLock来处理唤醒的问题,能够正确执行唤醒操作并根据设定及时关闭操作进入睡眠状态。
- 某些非必须马上执行的操作,例如上传歌曲,图片处理等,可以等到设备处于充电状态或者电量充足的时候才进行。
- 触发网络请求的操作,每次都会保持无线信号持续一段时间,我们可以把零散的网络请求打包进行一次操作,避免过多的无线信号引起的电量消耗。
- 根据设备电量和是否在充电的状态,进行不同的网络处理。
1.7 流量优化
流量优化用到最多的就是使用缓存,将一些不太变化的又经常需要用到的数据存起来,需要用到的使用优先从缓存里面取,从而减少网络请求。
- 首先要对请求网络的行为进行分类,区分需要立即更新和延迟的更新行为,根据不同的场景进行差异化处理。
- 其次我们可以减少客户端对服务器的轮询操作,比如说,我们在做购物车或者即时通讯的时候, 数据除了上传服务器之外,还可以保存一份在本地, 当用户再次访问数据时,直接到本地进行查询,这样不仅仅减少用户流量,同时还能减少服务器的压力.
- 再次我们还可以通过判断当前设备的状态(例如设备是否连接WiFi等)来决定更新的频率。
- 最后为了能够减小网络传输的数据量,我们需要对传输的数据做压缩的处理
- 使用WebP代替jpg,png图片,可以大幅减少流量消耗。
参考WebP: https://isux.tencent.com/introduction-of-webp.html
- 使用网络缓存,减少网络请求。
参考Okhttp缓存配置: http://blog.csdn.net/briblue/article/details/52920531