前言:本章知识不是为具体的一个性能问题提供详细的解决方案,而是梳理和总结Android中性能优化有关的问题,让我们在以后的开发中遇到性能有关的问题时能够快速分析,定位,并解决问题.
ANR异常
什么是ANR:
Application Not responding 应用程序无响应,在android四大组件都是运行在主线程当中的,android的,ActivityManager和WindowManager会时刻监视这应用的响应状态.如果因为阻塞(死锁)或者耗时操作(Activity中5s内不能响应用户,Recever中方法执行时间超过10s),那么系统就会弹出ANR的对话框提示用户目前应用处于无响应的状态.
为了避免ANR:
1.对于耗时操作都是通过AsyncTask,HandlerThread,IntentService去解决.避免UI线程的做耗时操作.
2.避免UI线程阻塞
分析ANR:
在发生Anr异常以后,dalvik虚拟机会在data/anr目录下生成trace文件.用于分析Anr异常.
在trace文件中主要包含以下:
1.ANR进程id,时间以及名称
2.线程的名称,优先级,线程锁id以及线程的状态,通过线程状态可以知道耗时操作还是阻塞引起ANR
3.导致anr的调用栈信息.
举个简单的例子:
DALVIK THREADS:
(mutexes: tll=0 tsl=0 tscl=0 ghl=0 hwl=0 hwll=0)
"main" prio=5 tid=1 NATIVE
| group="main" sCount=1 dsCount=0 obj=0x40025340 self=0xd180
| sysTid=1071 nice=0 sched=0/0 cgrp=default handle=-1344994080
| schedstat=( 2355584448 1199910712 3410 )
at java.net.InetAddress.getaddrinfo(Native Method)
at java.net.InetAddress.lookupHostByName(InetAddress.java:540)
at java.net.InetAddress.getAllByNameImpl(InetAddress.java:333)
at java.net.InetAddress.getAllByName(InetAddress.java:295)
at org.apache.harmony.luni.internal.net.www.protocol.http.HttpConnection.<init>(HttpConnection.java:100)
at org.apache.harmony.luni.internal.net.www.protocol.http.HttpConnection.<init>(HttpConnection.java:79)
at org.apache.harmony.luni.internal.net.www.protocol.http.HttpConnection$Address.connect(HttpConnection.java:353)
at org.apache.harmony.luni.internal.net.www.protocol.http.HttpConnectionPool.get(HttpConnectionPool.java:120)
at org.apache.harmony.luni.internal.net.www.protocol.http.HttpURLConnectionImpl.getHttpConnection(HttpURLConnectionImpl.java:316)
at org.apache.harmony.luni.internal.net.www.protocol.http.HttpURLConnectionImpl.makeConnection(HttpURLConnectionImpl.java:298)
at org.apache.harmony.luni.internal.net.www.protocol.http.HttpURLConnectionImpl.connect(HttpURLConnectionImpl.java:236)
at org.apache.harmony.luni.internal.net.www.protocol.http.HttpURLConnectionImpl.getOutputStream(HttpURLConnectionImpl.java:645)
at com.song.cool.util.URLUtil.invokeURL(URLUtil.java:136)//对进行耗时操作 导致没用在5秒中响应点击事件
at com.song.cool.activity.WoDeJianYiActivity$1.onClick(MainActivity.java:173)//定位到ANR
at android.view.View.performClick(View.java:2535)
at android.view.View$PerformClick.run(View.java:9129)
at android.os.Handler.handleCallback(Handler.java:618)
at android.os.Handler.dispatchMessage(Handler.java:123)
at android.os.Looper.loop(SourceFile:351)
at android.app.ActivityThread.main(ActivityThread.java:3821)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:538)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:969)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:727)
at dalvik.system.NativeStart.main(Native Method)
UI卡顿
Android系统会每隔16ms发出信号,进行ui的渲染,如果每次渲染都成功,则用户就可以看到流畅的画面,为什么是16ms,因为人们眼睛所能感觉到最大帧数就是60帧,高于60帧以上人们的视觉无法感觉,但是低于60fps时用户能够感觉到不顺畅,也就是有些卡顿,所以说16ms就是每一次渲染的最大时间.所以为了保证ui的流畅性,所以程序的大多操作都需要在16ms内完成.
在执行动画和一些listview滑动的时候有时候会出现卡顿,这就说明这部分操作很发杂,所以造成cpu或者gpu的负载过重.
造成卡顿的常见原因.
1.在ui线程中作轻微的耗时操作,导致Ui线程卡顿(轻量级的ANR)
2.布局layout过于复杂,无法在16ms内完成渲染
3.同一时间动画执行次数过多
4.View的过度绘制,导致同一个像素点绘制很多次
5.内存频繁的GC过多,在执行GC时所有线程都处于暂停,导致暂时阻塞UI绘制(内存抖动)
内存抖动:
内存抖动是指在短时间内创建大量的对象又被回收的现行.内存抖动的主要原因是频繁的在循环当中创建对象,由于频繁创建对象则需要在新生代内存区占用很大的空间,所以GC垃圾回收器就会频繁调用,GC执行的时候所有的线程都会挂起,如果一个帧时间内(16ms)频繁调用GC,那势必会印象页面的渲染,造成UI卡顿.
如果你在Memory Monitor里面查看到短时间发生了多次内存的涨跌,这意味着很有可能发生了内存抖动。
解决UI卡顿
布局优化,避免layout嵌套过多(尽量用GONE代替INVISIBLE,合理使用merge标签,在布局很复杂的时候考虑自定义控件)
列表优化:这部分主要体现在ListView上,尽量复用ListView的Item,而不是每次都去填充渲染,还有就是在滑动的时候不会加载图片.
渲染优化:避免对一个像素点的多次渲染,可以用开发者选项中的现实过度绘制功能查看.级别有红黄绿蓝.
内存优化:避免内存泄露,内存抖动问题,减少GC执行次数.
性能优化:避免ANR ,避免在主线程中耗时操作
OOM内存溢出
当前占用的内存的资源和申请的资源超出了dalvike虚拟机的最大资源就会抛出outofmemony异常.
在Android中OOM主要分为两点:
1.对对象的创建以及管理姿势不正确导致的OOM,最常见的就是对Bitmap对象的处理.
2.内存泄露导致的OOM
1.有关bitmap优化:
1.1 图片显示:比如在listview滑动的时候不去加载图片.
1.2 及时释放内存:因为bitmap的生成是由jni实现,加载bitmap到内存是由两部分实现的:一部分java区域,一部分为native区域,java内存区域通过GC回收.需要recycle()来释放native的内存.
1.3图片压缩:
对加载的大图片会直接超出dalvike给分配的内存引起OOM,所以在加载图片之前,通过Bitmap的inSimpleSize来对图片进行压缩,
1.4 inBitmap属性:
可以复用bitmap在对内存中占用的内存区域.
1.5捕获OOM异常
在实例化bitmap中对oom异常进行捕获,不过需要捕获Error.
其他对象的优化
ListView:复用convertView对象,避免每次都去填充加载一个新的view.
避免在draw中执行创建的对象,由于draw方法会调用很多次,如果在其中创建大量的对象时,会导致内存急剧增加,轻则内存抖动,严重的话就会造成OOM
Bitmap优化
1.recycle:
释放bitmap内存的时候会释放跟bitmap对象有关的native内存.官方建议不必调用.
但是如果需要加载大量的图片图片资源,造成有大量图片垃圾进行回收时,那必然GC需要很频繁的调用,为了造成避免GC太频繁造成ANR,这时可以手动调用recycle方法,将GC任务进行分散处理.
2.LRU算法:
存储bitmap时三级缓存时使用,LRU算法是一个最近最少使用算法.内部是一个LinkHashMap,提供了put,get方法完成了缓存的添加和获取操作.当缓存满的时候,LRU算法会调trimToSize方法清楚最近最少使用的对象,并添加缓存新对象.
trimToSzie方法内部判断当前大小是否判断现在缓存的大小是否大于所能提供的最大缓存大小,如果大于,则循环remove最近最少使用的缓存对象,知道此大小.
3.缩略图
由于对于大图片来说我们不能直接加载它到内存,否则会引起OOM.所以需要对大图片进行压缩操作,这部分操作通过inJustDecodeBounds来获得需要加载图片的尺寸,然后计算出图片尺寸和控件尺寸计算出一个缩放比例,设置给inSimpleSize,这个缩放比例如果是2的话就是每有两个像素点,我们只取一个,然后再去加载到内存,虽然对大图的加载是缩略的,但是由于控件的尺寸也比较小,所以在视觉方面没有影响.
4.三级缓存
为了提高用户体验.对图片的加载都需要进行三级缓存,减少图片的加载,减少使用流量,三级缓存分为,网络缓存,本地缓存,内存缓存.
流程: 先从内存缓存(LRU)获取,如果没有从本地缓存获取,都找不到以后再去请求服务器上的图片.在图片请求成功后在从将他缓存到本地和内存中.
内存泄露
内存泄露是指无用对象(也就是不再使用的对象)还会被GCroot的引用链引用使的持续占用空间,导致内存得不到释放,导致的内存空间浪费.当内存泄露到严重到一定程度时也会造成内存溢出OOM.(可达,但是无用,GC无法回收)
Android中常见的内存泄露:
1.单例
由于单例的特性似的单例的对象一直由静态变量引用保存在内存的方法区,这部分内存的生命周期很长与应用的生命周期相同.所以很难被回收,在我们创建单例的时候避免传入使单例对象引用到Activity对象,否则会造成Activity销毁后无法对其占用的空间进行回收,所以在需要Context时通过context.getApplcationContext()来获取app的context对象而不是Activity.
2.非静态内部类或非静态匿名内部类:
在Java中非静态内部类会持有外部类的引用,如果在内部类中创建一个静态实例时,因为静态实例的生命周期会和app的生命周期一样长,并且该静态实例保存这外部类的引用,这就会导致外部类对象无法的得到回收, 正确的写法是将非静态内部类或者非静态匿名内部类写成静态内部类.这样就不会持有外部类对象的引用.
3.Handler:
最常见的,原因也是因为非静态内部类持有外部类对象的实例子.我们长在Activity或Service中通过new Handler复写HandMassage方法创建一个匿名内部类,这时候handler匿名内部类就会持有外部类的引用,在Activity销毁的时候,消息队列中还有没处理的消息或者正在处理的消息,那么Message的target引用这handler,这是会导致Activity无法回收.正确写法也是将handler写成一个静态内部类.
4.避免使用static.
因为static的生命周期过长.尽量使用懒加载,如果必须用Static修饰的话,需要对他的生命周期进行管理.
5.资源未关闭
比如socket,文档,cursor,广播接受者,contentProvider未关闭资源的会导致资源无法回收.
6.AsyncTask:
和Handler类似,需要在Activity销毁的时候调用AsyncTask的cancel方法.
内存检测常用工具:
1.god eyes :静态代码扫描,可以扫描流关闭,cursor关闭等问题.
2.Memory Monitor:这是android studio自带内存分析工具 他可以只管的现实当前时间内存的使用情况,同时可以通过Java Heap生成hprof文件,通过这个文件可以显示,无法回收的对象的引用关系.
3.Leakcanary:这是一个更直观检测内存泄露的工具,只需要在项目中添加leakcanary的依赖,并在appliction启动的时候进行初始化,然后使用APP时,他检测到存在内存泄露会发送通知,而且会说明原因.
内存管理
操作系统会对每个进程合理的分配资源.由于android是移动操作系统,所以内存资源是很有限的.google为android提供了一套特有的内存管理机制,作为应用的开发者,我们必须清楚并且遵守这个机制,才能使得我们的app与android能够和谐共存,降低被系统杀死的可能性.
Android对每个进程内存的管理:
分配机制: 在为每个进程分配内存的时候采用的是一个弹性的分配方式,在系统开始不会为进程分配太多的内存,而是根据ram尺寸分配一个小额的量,当内存不够使用时,android会为这个进程提供一个额外的内存大小,但是这个大小是有限制的.
回收机制: Android是根据linux开发的操作系统,在内存管理上android和linux一样,会尽可能多的保存内存资源,使得每次应用重新开启时不再重新开启初始化一个进程,从而提高了速度.
但是当android内存不够使用时他就会去杀死一些进程,在选择杀死哪些进程,这里有一个进程优先级的概念.
Android中进程分为:前台进程,可见进程,服务进程,后台进程,空进程
正常情况下前台进程,可见进程,服务进程是不会被系统杀死的,后台进程则会被系统存放在一个类似LRU的缓存列表中.在回收时android还会考虑一个回收效益的问题.就是说android会优先杀死一个能回收更多内存的进程.这样就能做到杀死进程越少,用户的影响也就越小.
内存优化方法:
1.如果应用占用内存更少,则被回收的可能越小.
2.在Android内存不足时,主动的释放不必要的资源.通过onTrimMomey方法中释放.
3.在合理的生命周期是保存一些数据,在再次打开应用时,系统可以复用这些数据,而不重新创建.
4.避免滥用bitmap造成的内存资源的浪费
5.通过android提供优化过得容器比如sparseArray和ArrayMap代替Hashmap,他们两个底层数据结构都是由两个数组实现的,一个存放key一个存放Value.一般数据量较小是用他们替换HashMap,因为他们对内存进行了优化,但是在效率上没有散列hashmap效率高.SparseArray用于key为int值时,避免了自动拆装箱,其他类型的key用ArrayMap.
6.使用多进程,对于对内存较多的模块和长期运行在后台的模块为其开辟一个新进程,比如定位和推送一般都自己独占一个进程.但是要考虑清楚解决多进程之间数据传输以及安全的问题.