It's our wits that make us men.

Android内存优化!

Posted on By yan zi jie

First POST build by Jekyll.github:https://github.com/nishibaiyang

Android内存优化

参考:https://www.jianshu.com/p/c4b283848970

介绍

内存这东西为什么被经常被提及。因为内存条贵呀,前端时间电脑的内存条涨的多凶呀,都买不起了。开个玩笑啦,不过确实内存这东西不便宜,尤其是移动平台上。iphone内存才多大,android稍微大些不过再大还是不够用。作为开发者需要反省下我们到底对内存负责了吗?

内存(RAM)在平常感觉讲的很多,经常就被人写一遍。但是在实际开发中感觉跟内存直接关系不大,这肯定是错误的。开发过程中与内存的关系都在无形之中体现出来。可以说基本所有都与内存牵扯上关系,比如图片、activity、service、资源等等。你的application运行在手机上还不是要根据RAM的大小分配这个application所能占用的最大内存。

总之一句话:内存在开发过程中看起来是由垃圾回收机制进行回收资源的,但是管理的好不好还是靠开发者的功底。因此内存优化经常在面试中被提及。

内存分配

从Android的系统架构上来看,这内存可以细分下1.方法区 2.虚拟机栈 3.本地方法栈 4.堆 5.程序计数器,如图: name

方法区:方法区存放的是类信息、常量、静态变量,所有线程共享区域。

虚拟机栈:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,线程私有区域。

本地方法栈:与虚拟机栈类似,区别是虚拟机栈为虚拟机执行Java方法服务,本地方法栈为虚拟机使用到的Native方法服务。

堆:JVM管理的内存中最大的一块,所有线程共享;用来存放对象实例,几乎所有的对象实例都在堆上分配内存;此区域也是垃圾回收器(Garbage Collection)主要的作用区域,内存泄漏就发生在这个区域。

程序计数器:可看做是当前线程所执行的字节码的行号指示器;如果线程在执行Java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址;如果执行的是Native方法,这个计数器的值为空(Undefined)。

内存回收

1.Android FrameWork:FrameWork会根据进程优先级进行回收。进程优先级依次是:空进程、后台进程、服务进程、可见进程、前台进程。

2.JVM虚拟机:Android为JVM分配了内存大小,Application出现oom就是申请的内存大于当前Application JVM内存的最大值。

3.Linux内核:这里最简单的理解就是ActivityManagerService会对所有进程进行评分(存放在变量adj中),然后再讲这个评分更新到内核,由内核去完成真正的内存回收(lowmemorykiller, Oom_killer)。

补充:Android Dalvik Heap与原生Java一样,将堆的内存空间分为三个区域,Young Generation,Old Generation, Permanent Generation。最近分配的对象会存放在Young Generation区域,当这个对象在这个区域停留的时间达到一定程度,它会被移动到Old Generation,最后累积一定时间再移动到Permanent Generation区域。系统会根据内存中不同的内存数据类型分别执行不同的gc操作。

现在各种手机厂商鼓吹人工智能手机,号称18个月不卡顿,越用越快,其实很大一部分Android系统的内存优化有关,无非就是利用一些比较成熟的基于统计,机器学习的算法定时清理数据,清理内存,甚至提前加载数据到内存。

内存回收算法

1.内存清除法

分为“标记”和“清除”两个阶段,首先,标记出所有需要回收的对象,然后统一回收所有被标记的对象。

缺点:1.效率问题,标记和清除两个过程的效率都不高; 2.空间问题,标记清除之后会产生大量的不连续的内存碎片。

2.复制算法

将内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存将用完了,就将还存活着的对象复制到另一块内存上面,然后再把已使用过的内存空间一次清理掉。

1.优点:实现简单,运行高效;每次都是对整个半区进行内存回收,内存分配时也不需要考虑内存碎片等情况,只要移动堆顶指针,按顺序分配内存即可; 2.缺点:粗暴的将内存缩小为原来的一半,代价实在有点高。

3.标记-整理算法

先标记需要回收的对象(标记过程与“标记-清除”算法一样),然后把所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

特点:

避免了内存碎片; 避免了“复制”算法50%的空间浪费。

内存回收依据

如何判断内存被回收了呢?

引用计数法:给对象中添加一个引用计数器,每当有一个地方引用该对象时,计数器值加1;引用失效时,计数器值减1;任意时刻计数器为0的对象就是不可能再被使用的,表示该对象不存在引用关系。

优点:实现简单,判定效率也很高; 缺点:难以解决对象之间相互循环引用导致计数器值不等于0的问题。

可达性分析算法:以一系列成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(GC Roots到这个对象不可达),则证明此对象是不可用的。 name

内存管理

Android系统的ART和Dalvik虚拟机扮演了常规的内存垃圾自动回收的角色, 使用paging 和 memory-mapping来管理内存,这意味着不管是因为创建对象还是使用使用内存页面造成的任何被修改的内存,都会一直存在于内存中,App唯一释放内存的方法就是释放App持有的对象引用,使GC可以回收。

内存回收

在Android的高级系统版本里面针对Heap空间有一个Generational Heap Memory的模型,最近分配的对象会存放在Young Generation区域,当这个对象在这个区域停留的时间达到一定程度,它会被移动到Old Generation,最后累积一定时间再移动到Permanent Generation区域。系统会根据内存中不同的内存数据类型分别执行不同的gc操作。例如,刚分配到Young Generation区域的对象通常更容易被销毁回收,同时在Young Generation区域的gc操作速度会比Old Generation区域的gc操作速度更快。

共享内存

Android应用的进程都是从一个叫做Zygote的进程fork出来的。Zygote进程在系统启动并且载入通用的framework的代码与资源之后开始启动。为了启动一个新的程序进程,系统会fork Zygote进程生成一个新的进程,然后在新的进程中加载并运行应用程序的代码。这使得大多数的RAM pages被用来分配给framework的代码,同时使得RAM资源能够在应用的所有进程之间进行共享。

大多数static的数据被mmapped到一个进程中。这不仅仅使得同样的数据能够在进程间进行共享,而且使得它能够在需要的时候被paged out。常见的static数据包括Dalvik Code,app resources,so文件等。

大多数情况下,Android通过显式的分配共享内存区域(例如ashmem或者gralloc)来实现动态RAM区域能够在不同进程之间进行共享的机制。例如,Window Surface在App与Screen Compositor之间使用共享的内存,Cursor Buffers在Content Provider与Clients之间共享内存。

应用切换

Android系统并不会在用户切换应用的时候做交换内存的操作。Android会把那些不包含Foreground组件的应用进程放到LRU Cache中。例如,当用户开始启动了一个应用,系统会为它创建了一个进程,但是当用户离开这个应用,此进程并不会立即被销毁,而是会被放到系统的Cache当中,如果用户后来再切换回到这个应用,此进程就能够被马上完整的恢复,从而实现应用的快速切换。

如果你的应用中有一个被缓存的进程,这个进程会占用一定的内存空间,它会对系统的整体性能有影响。因此当系统开始进入Low Memory的状态时,它会由系统根据LRU的规则与应用的优先级,内存占用情况以及其他因素的影响综合评估之后决定是否被杀掉。

常见内存问题

内存泄露

内存泄漏通俗的讲就是长生命周期的对象持有短生命周期的对象,导致短生命周期的对象在该回收的时候不能被正常回收,造成内存泄漏。

内存抖动

内存抖动是由于短时间内有大量对象进出Young Generiation区导致的,它伴随着频繁的GC。在as中表现是这样的: name 内存抖动会造成oom,主要原因还是有因为大量小的对象频繁创建,导致内存碎片,从而当需要分配内存时,虽然总体上还是有剩余内存可分配,而由于这些内存不连续,导致无法分配,系统直接就返回OOM了。 ###常见内存问题解决办法 常见的问题包括:1.单例(主要原因还是因为一般情况下单例都是全局的,有时候会引用一些实际生命周期比较短的变量,导致其无法释放)

2.静态变量(同样也是因为生命周期比较长)

3.Handler内存泄露

4.匿名内部类(匿名内部类会引用外部类,导致无法释放,比如各种回调)

5.资源使用完未关闭(BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap)

LeakCanary或Android Studio检测工具检查内存泄漏

简单说下LeakCanary原理是监控每个activity,在activity ondestory后,在后台线程检测引用,然后过一段时间进行gc,gc后如果引用还在,那么dump出内存堆栈,并解析进行可视化显示。

拒绝亡羊补牢,内存优化

图片压缩

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以后只要原有的图片比将要解码的图片大既可以复用了。

缓存池大小

现在很多图片加载组件都不仅仅是使用软引用或者弱引用了,实际上类似Glide 默认使用的事LruCache,因为软引用 弱引用都比较难以控制,使用LruCache可以实现比较精细的控制,而默认缓存池设置太大了会导致浪费内存,设置小了又会导致图片经常被回收,所以需要根据每个App的情况,以及设备的分辨率,内存计算出一个比较合理的初始值,可以参考Glide的做法。

常用数据结构优化

ArrayMap及SparseArray是android的系统API,是专门为移动设备而定制的。用于在一定情况下取代HashMap而达到节省内存的目的,具体性能见HashMap,ArrayMap,SparseArray源码分析及性能对比[10],对于key为int的HashMap尽量使用SparceArray替代,大概可以省30%的内存,而对于其他类型,ArrayMap对内存的节省实际并不明显,10%左右,但是数据量在1000以上时,查找速度可能会变慢。 #####谨慎使用多进程 现在很多App都不是单进程,为了保活,或者提高稳定性都会进行一些进程拆分,而实际上即使是空进程也会占用内存(1M左右),对于使用完的进程,服务都要及时进行回收。

数据相关

序列化数据使用protobuf可以比xml省30%内存,慎用shareprefercnce,因为对于同一个sp,会将整个xml文件载入内存,有时候为了读一个配置,就会将几百k的数据读进内存,数据库字段尽量精简,只读取所需字段。

节制地使用Service

如果应用程序当中需要使用Service来执行后台任务的话,请一定要注意只有当任务正在执行的时候才应该让Service运行起来。另外,当任务执行完之后去停止Service的时候,要小心Service停止失败导致内存泄漏的情况。

当我们启动一个Service时,系统会倾向于将这个Service所依赖的进程进行保留,这样就会导致这个进程变得非常消耗内存。并且,系统可以在LRU cache当中缓存的进程数量也会减少,导致切换应用程序的时候耗费更多性能。严重的话,甚至有可能会导致崩溃,因为系统在内存非常吃紧的时候可能已无法维护所有正在运行的Service所依赖的进程了。

当界面不可见或内存紧张时释放内存

在Activity中重写onTrimMemory()方法,然后在这个方法中监听TRIM_MEMORY_UI_HIDDEN这个级别,一旦触发了之后就说明用户已经离开了我们的程序,那么此时就可以进行资源释放操作。应用程序正在运行时的回调还有如下回调:

1.TRIM_MEMORY_RUNNING_MODERATE 表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经有点低了,系统可能会开始根据LRU缓存规则来去杀死进程了。

2.TRIM_MEMORY_RUNNING_LOW 表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经非常低了,我们应该去释放掉一些不必要的资源以提升系统的性能,同时这也会直接影响到我们应用程序的性能。

3.TRIM_MEMORY_RUNNING_CRITICAL 表示应用程序仍然正常运行,但是系统已经根据LRU缓存规则杀掉了大部分缓存的进程了。这个时候我们应当尽可能地去释放任何不必要的资源,不然的话系统可能会继续杀掉所有缓存中的进程,并且开始杀掉一些本来应当保持运行的进程,比如说后台运行的服务。

程序目前是被缓存的,则会收到以下几种类型的回调:

1、TRIM_MEMORY_BACKGROUND 表示手机目前内存已经很低了,系统准备开始根据LRU缓存来清理进程。这个时候我们的程序在LRU缓存列表的最近位置,是不太可能被清理掉的,但这时去释放掉一些比较容易恢复的资源能够让手机的内存变得比较充足,从而让我们的程序更长时间地保留在缓存当中,这样当用户返回我们的程序时会感觉非常顺畅,而不是经历了一次重新启动的过程。

2、TRIM_MEMORY_MODERATE 表示手机目前内存已经很低了,并且我们的程序处于LRU缓存列表的中间位置,如果手机内存还得不到进一步释放的话,那么我们的程序就有被系统杀掉的风险了。

3、TRIM_MEMORY_COMPLETE 表示手机目前内存已经很低了,并且我们的程序处于LRU缓存列表的最边缘位置,系统会最优先考虑杀掉我们的应用程序,在这个时候应当尽可能地把一切可以释放的东西都进行释放。