本文来自于腾讯Bugly公众号(weixinBugly),原文地址:https://mp.weixin.qq.com/s/2MsEAR9pQfMr1Sfs7cPdWQ
工欲善其事必先利其器,想要优化App的内存占用,那么还是需要先了解Android系统的内存分配和回收机制。
一、Android内存分配回收机制
从宏观角度上来看Android系统可以分为三个层次
- Application Framework,
- Dalvik 虚拟机
- Linux内核。
这三个层次都有各自内存相关工作:
1. Application Framework
Anroid基于进程中运行的组件及其状态规定了默认的五个回收优先级:
- Empty process(空进程)
- Background process(后台进程)
- Service process(服务进程)
- Visible process(可见进程)
- Foreground process(前台进程)
系统需要进行内存回收时最先回收空进程,然后是后台进程,以此类推最后才会回收前台进程(一般情况下前台进程就是与用户交互的进程了,如果连前台进程都需要回收那么此时系统几乎不可用了)。
由此也衍生了很多进程保活的方法(提高优先级,互相唤醒,native保活等等),出现了国内各种全家桶,甚至各种杀不死的进程。
Android中由ActivityManagerService 集中管理所有进程的内存资源分配。
2. Linux内核
3. Dalvik虚拟机
Android中有Native Heap和Dalvik Heap。Android的Native Heap言理论上可分配的空间取决了硬件RAM,而对于每个进程的Dalvik Heap都是有大小限制的,具体策略可以看看android dalvik heap 浅析[4]。
Android App为什么会OOM呢?其实就是申请的内存超过了Dalvik Heap的最大值。这里也诞生了一些比较”黑科技”的内存优化方案,比如将耗内存的操作放到Native层,或者使用分进程的方式突破每个进程的Dalvik Heap内存限制。
Android Dalvik Heap与原生Java一样,将堆的内存空间分为三个区域,Young Generation,Old Generation, Permanent Generation。
最近分配的对象会存放在Young Generation区域,当这个对象在这个区域停留的时间达到一定程度,它会被移动到Old Generation,最后累积一定时间再移动到Permanent Generation区域。系统会根据内存中不同的内存数据类型分别执行不同的gc操作。
GC发生的时候,所有的线程都是会被暂停的。执行GC所占用的时间和它发生在哪一个Generation也有关系,Young Generation中的每次GC操作时间是最短的,Old Generation其次,Permanent Generation最长。
GC时会导致线程暂停,导致卡顿,Google在新版本的Android中优化了这个问题, 在ART中对GC过程做了优化揭秘 ART 细节 —— Garbage collection[5],据说内存分配的效率提高了10倍,GC的效率提高了2-3倍(可见原来效率有多低),不过主要还是优化中断和阻塞的时间,频繁的GC还是会导致卡顿。
上面就是Android系统内存分配和回收相关知识,回过头来看,现在各种手机厂商鼓吹人工智能手机,号称18个月不卡顿,越用越快,其实很大一部分Android系统的内存优化有关,无非就是利用一些比较成熟的基于统计,机器学习的算法定时清理数据,清理内存,甚至提前加载数据到内存。
二、Android常见内存问题和对应检测,解决方式
1. 内存泄露
不止Android程序员,内存泄露应该是大部分程序员都遇到过的问题,可以说大部分的内存问题都是内存泄露导致的,Android里也有一些很常见的内存泄露问题[6],这里简单罗列下:
- 单例(主要原因还是因为一般情况下单例都是全局的,有时候会引用一些实际生命周期比较短的变量,导致其无法释放)
- 静态变量(同样也是因为生命周期比较长)
- Handler内存泄露[7]
- 匿名内部类(匿名内部类会引用外部类,导致无法释放,比如各种回调)
- 资源使用完未关闭(BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap)
对Android内存泄露业界已经有很多优秀的组件其中LeakCanary最为知名(Square出品,Square可谓Android开源界中的业界良心,开源的项目包括okhttp, retrofit,otto, picasso, Android开发大神Jake Wharton就在Square),其原理是监控每个activity,在activity ondestory后,在后台线程检测引用,然后过一段时间进行gc,gc后如果引用还在,那么dump出内存堆栈,并解析进行可视化显示。使用LeakCanary可以快速地检测出Android中的内存泄露。
正常情况下,解决大部分内存泄露问题后,App稳定性应该会有很大提升,但是有时候App本身就是有一些比较耗内存的功能,比如直播,视频播放,音乐播放,那么我们还有什么能做的可以降低内存使用,减少OOM呢?
2. 图片分辨率相关
分辨率适配问题。很多情况下图片所占的内存在整个App内存占用中会占大部分。我们知道可以通过将图片放到hdpi/xhdpi/xxhdpi等不同文件夹进行适配,通过xml android:background设置背景图片,或者通过BitmapFactory.decodeResource()方法,图片实际上默认情况下是会进行缩放的。在Java层实际调用的函数都是或者通过BitmapFactory里的decodeResourceStream函数
1 | public static Bitmap decodeResourceStream(Resources res, TypedValue value, |
decodeResource在解析时会对Bitmap根据当前设备屏幕像素密度densityDpi的值进行缩放适配操作,使得解析出来的Bitmap与当前设备的分辨率匹配,达到一个最佳的显示效果,并且Bitmap的大小将比原始的大,可以参考下腾讯Bugly的详细分析Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存?
关于Density、分辨率、-hdpi等res目录之间的关系:
举个例子,对于一张1280×720的图片,如果放在xhdpi,那么xhdpi的设备拿到的大小还是1280×720而xxhpi的设备拿到的可能是1920×1080,这两种情况在内存里的大小分别为:3.68M和8.29M,相差4.61M,在移动设备来说这几M的差距还是很大的。
尽管现在已经有比较先进的图片加载组件类似Glide,Facebook Freso, 或者老牌Universal-Image-Loader,但是有时就是需要手动拿到一个bitmap或者drawable,特别是在一些可能会频繁调用的场景(比如ListView的getView),怎样尽可能对bitmap进行复用呢?这里首先需要明确的是对同样的图片,要 尽可能复用,我们可以简单自己用WeakReference做一个bitmap缓存池,也可以用类似图片加载库写一个通用的bitmap缓存池,可以参考GlideBitmapPool[8]的实现。
我们也来看看系统是怎么做的,对于类似在xml里面直接通过android:background或者android:src设置的背景图片,以ImageView为例,最终会调用Resource.java里的loadDrawable:
1 | Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException |
可以看到实际上系统也是有一份全局的缓存,sPreloadedDrawables, 对于不同的drawable,如果图片时一样的,那么最终只会有一份bitmap(享元模式),存放于BitmapState中,获取drawable时,系统会从缓存中取出这个bitmap然后构造drawable。而通过BitmapFactory.decodeResource()则每次都会重新解码返回bitmap。所以其实我们可以通过context.getResources().getDrawable再从drawable里获取bitmap,从而复用bitmap,然而这里也有一些坑,比如我们获取到的这份bitmap,假如我们执行了recycle之类的操作,但是假如在其他地方再使用它是那么就会有”Canvas: trying to use a recycled bitmap android.graphics.Bitmap”异常。
3. 图片压缩
BitmapFactory 在解码图片时,可以带一个Options,有一些比较有用的功能,比如:
- inTargetDensity 表示要被画出来时的目标像素密度
- inSampleSize 这个值是一个int,当它小于1的时候,将会被当做1处理,如果大于1,那么就会按照比例(1 / inSampleSize)缩小bitmap的宽和高、降低分辨率,大于1时这个值将会被处置为2的倍数。例如,width=100,height=100,inSampleSize=2,那么就会将bitmap处理为,width=50,height=50,宽高降为1 / 2,像素数降为1 / 4
- inJustDecodeBounds 字面意思就可以理解就是只解析图片的边界,有时如果只是为了获取图片的大小就可以用这个,而不必直接加载整张图片。
- inPreferredConfig 默认会使用ARGB_8888,在这个模式下一个像素点将会占用4个byte,而对一些没有透明度要求或者图片质量要求不高的图片,可以使用RGB_565,一个像素只会占用2个byte,一下可以省下50%内存。
- inPurgeable和inInputShareable 这两个需要一起使用,BitmapFactory.java的源码里面有注释,大致意思是表示在系统内存不足时是否可以回收这个bitmap,有点类似软引用,但是实际在5.0以后这两个属性已经被忽略,因为系统认为回收后再解码实际会反而可能导致性能问题
- inBitmap 官方推荐使用的参数,表示重复利用图片内存,减少内存分配,在4.4以前只有相同大小的图片内存区域可以复用,4.4以后只要原有的图片比将要解码的图片大既可以复用了。
4. 缓存池大小
现在很多图片加载组件都不仅仅是使用软引用或者弱引用了,实际上类似Glide 默认使用的事LruCache,因为软引用 弱引用都比较难以控制,使用LruCache可以实现比较精细的控制,而默认缓存池设置太大了会导致浪费内存,设置小了又会导致图片经常被回收,所以需要根据每个App的情况,以及设备的分辨率,内存计算出一个比较合理的初始值,可以参考Glide的做法。
5. 内存抖动
什么是内存抖动呢?Android里内存抖动是指内存频繁地分配和回收,而频繁的gc会导致卡顿,严重时还会导致OOM。
一个很经典的案例是string拼接创建大量小的对象(比如在一些频繁调用的地方打字符串拼接的log的时候), 见Android优化之String篇[9]。
而内存抖动为什么会引起OOM呢?
主要原因还是有因为大量小的对象频繁创建,导致内存碎片,从而当需要分配内存时,虽然总体上还是有剩余内存可分配,而由于这些内存不连续,导致无法分配,系统直接就返回OOM了。
其他
常用数据结构优化,ArrayMap及SparseArray是android的系统API,是专门为移动设备而定制的。用于在一定情况下取代HashMap而达到节省内存的目的,具体性能见HashMap,ArrayMap,SparseArray源码分析及性能对比[10],对于key为int的HashMap尽量使用SparceArray替代,大概可以省30%的内存,而对于其他类型,ArrayMap对内存的节省实际并不明显,10%左右,但是数据量在1000以上时,查找速度可能会变慢。
枚举,Android平台上枚举是比较争议的,在较早的Android版本,使用枚举会导致包过大,在个例子里面,使用枚举甚至比直接使用int包的size大了10多倍 在stackoverflow上也有很多的讨论, 大致意思是随着虚拟机的优化,目前枚举变量在Android平台性能问题已经不大,而目前Android官方建议,使用枚举变量还是需要谨慎,因为枚举变量可能比直接用int多使用2倍的内存。
ListView复用,这个大家都知道,getView里尽量复用conertView,同时因为getView会频繁调用,要避免频繁地生成对象
谨慎使用多进程,现在很多App都不是单进程,为了保活,或者提高稳定性都会进行一些进程拆分,而实际上即使是空进程也会占用内存(1M左右),对于使用完的进程,服务都要及时进行回收。
尽量使用系统资源,系统组件,图片甚至控件的id
减少view的层级,对于可以 延迟初始化的页面,使用viewstub
数据相关:序列化数据使用protobuf可以比xml省30%内存,慎用shareprefercnce,因为对于同一个sp,会将整个xml文件载入内存,有时候为了读一个配置,就会将几百k的数据读进内存,数据库字段尽量精简,只读取所需字段。
dex优化,代码优化,谨慎使用外部库, 有人觉得代码多少于内存没有关系,实际会有那么点关系,现在稍微大一点的项目动辄就是百万行代码以上,多dex也是常态,不仅占用rom空间,实际上运行的时候需要加载dex也是会占用内存的(几M),有时候为了使用一些库里的某个功能函数就引入了整个庞大的库,此时可以考虑抽取必要部分,开启proguard优化代码,使用Facebook redex使用优化dex(好像有不少坑)。
文中引用参考链接
1.Android 操作系统的内存回收机制
https://www.ibm.com/developerworks/cn/opensource/os-cn-android-mmry-rcycl/2.阿里巴巴的Android内存优化分享
http://www.infoq.com/cn/presentations/android-memory-optimization3.Android进程的内存管理分析
http://blog.csdn.net/gemmem/article/details/89200394.android dalvik heap 浅析
http://blog.csdn.net/cqupt_chen/article/details/110681295.揭秘 ART 细节 —— Garbage collection
http://www.cnblogs.com/jinkeep/p/3818180.html6.Android性能优化之常见的内存泄漏
http://blog.csdn.net/u010687392/article/details/499094777.Android App 内存泄露之Handler
http://blog.csdn.net/zhuanglonghai/article/details/382330698.GlideBitmapPool
https://github.com/amitshekhariitbhu/GlideBitmapPool9.Android 性能优化之String篇
http://blog.csdn.net/vfush/article/details/5303843710.HashMap,ArrayMap,SparseArray源码分析及性能对比
http://www.jianshu.com/p/7b9a1b38626511.MAT使用教程
http://blog.csdn.net/itomge/article/details/4871952712.MAT - Memory Analyzer Tool 使用进阶
http://www.lightskystreet.com/2015/09/01/mat_usage/