目录:
一、内存优化的意义
二、Android 内存管理
三、内存问题分类
四、内存优化辅助工具
五、内存优化建议
六、实现内存监控
七、总结
一、内存优化的意义
内存优化就是对内存问题的一个预防和解决,做内存优化能让应用挂得少、活得好和活得久。
挂的少:
“挂”指的是 Crash,内存问题导致 Crash 的具体表现就是内存溢出异常 OOM。
活得好:
活得好指的是使用流畅,Android 中造成界面卡顿的原因有很多种,其中一种就是由内存问题引起的。内存问题之所以会影响到界面流畅度,是因为垃圾回收(GC,Garbage Collection),在 GC 时,所有线程都要停止,包括主线程,当 GC 和绘制界面的操作同时触发时,绘制的执行就会被搁置,导致掉帧,也就是界面卡顿。
活得久:
活得久指的是我们的应用在后台运行时不会被干掉。Android 会按照特定的机制清理进程,清理进程时优先会考虑清理后台进程。清理进程的机制就是LowMemoryKiller。在 Android 中不同的进程有着不同的优先级,当两个进程的优先级相同时,低杀会优先考虑干掉消耗内存更多的进程。也就是如果我们应用占用的内存比其他应用少,并且处于后台时,我们的应用能在后台活下来,这也是内存优化为我们应用带来竞争力的一个直接体现。
内存占用是否越少越好?
当系统 内存充足 的时候,我们可以多用 一些获得更好的性能。当系统 内存不足 的时候,我们希望可以做到 ”用时分配,及时释放“。内存优化并不能一刀切。
二、Android 内存管理
我们都知道,应用程序的内存分配和垃圾回收都是由Android虚拟机完成的,在Android 5.0以下,使用的是Dalvik虚拟机,5.0及以上,则使用的是ART虚拟机。
Android虚拟机Dalvik和ART
1、内存区域划分
JVM运行时数据区划分 方法区、堆、虚拟机栈、本地方法栈、程序计数器,其中 虚拟机栈、本地方法栈、程序计数器 为线程私有的,线程私有的意思是指,JVM每遇到一个新的线程就会为他们分配 虚拟机栈、本地方法栈和程序计数器。
程序计数器无内存溢出异常,其他四个区域都可能会抛出OutofMemoryRrror异常。
详细请看以下两篇文章(建议全看):
java内存四大区_JVM内存区域划分
Android 内存机制
2、内存回收
垃圾收集的标记算法(找到垃圾):
- 引用计数算法
- 可达分析法 (JVM使用此算法来分析哪些对象需要回收)
垃圾收集算法(回收垃圾):
- 标记-清除算法
- 复制算法
- 标记-整理算法
- 分代收集算法
引用类型:强引用、软引用、弱引用、虚引用
对象的有效性=可达性+引用类型
JAVA垃圾回收机制-史上最容易理解看这一篇就够了
Android:玩转垃圾回收机制与分代回收策略
android中还存在低杀机制,这种情况属于系统整机内存不足,直接把应用进程杀掉的情况。
Android后台杀死系列:LowMemoryKiller原理
三、内存问题分类
1、内存溢出
系统会给每个App分配内存空间也就是heap size值,当app占用的内存加上申请的内存超过这个系统分配的内存限额,最终导致OOM(OutOfMemory)使程序崩溃。
通过命令 getprop |grep dalvik.vm.heapsize 可以获取系统允许的最大
注意:在设置了heapgrowthlimit的状况下,单个进程可用最大内存为heapgrowthlimit值。在android开发中,若是要使用大堆,须要在manifest中指定android:largeHeap为true,这样dvm heap最大可达heapsize。
关于heapsize & heapgrowthlimit
2、内存泄漏
Android系统虚拟机的垃圾回收是通过虚拟机GC机制来实现的。GC会选择一些还存活的对象作为内存遍历的根节点GC Roots,通过对GC Roots的可达性来判断是否需要回收。内存泄漏就是 在当前应用周期内不再使用的对象被GC Roots引用,造成该对象无法被系统回收,以致该对象在堆中所占用的内存单元无法被释放而造成内存空间浪费,使实际可使用内存变小。简言之,就是 对象被持有导致无法释放或不能按照对象正常的生命周期进行释放。
Android常见内存泄漏汇总
3、内存抖动
指的是在短时间内大量的新对象被实例化,运行时可能无法承载这样的内存分配,在这种情况下就会导致垃圾回收事件被大量调用,影响到应用程序的UI和整体性能,最终可能导致卡顿和OOM。
常见情况:在一些被频繁调用的方法内不断地创建对象。例如在View 的onDraw方法内new 一些新的对象。
注意内存抖动也会导致 OOM,主要原因有如下两点:
- 频繁创建对象,导致内存不足及碎片(不连续)。
- 不连续的内存片无法被分配,导致OOM。
四、内存优化辅助工具
1、Android Studio Profiler
作用
- 实时图表展示应用内存使用量。
- 用于识别内存泄漏、抖动等。
- 提供捕获堆转储、强制GC以及根据内存分配的能力。
优点
- 方便直观
- 线下使用
内存抖动问题处理实战
理解内存抖动的概念的话,我们就能明白只要能找到抖动过程中所产生的对象及其调用栈,我们就能解决问题,刚好Android Studio 的Porfiler里面的Memory工具就能帮我们记录下我们操作过程中或静止界面所产生的新对象,并且能清晰看到这些对象的调用栈。
选择Profile 中 的Memory ,选择 Record Java/Kotlin allocations,再点击Record开始记录, Record Java/Kotlin allocations 选项会记录下新增的对象。
操作完成之后,点击如图所示的红脑按钮,停止记录。
停止记录后,我们就可以排序(点击 Allocations可以排序)看看哪些对象或基本类型在短时间被频繁创建多个,点击这些新增的对象就可以看到它的完成的调用链了,进而就找找到导致内存抖动的地方在哪里了。
2、利用DDMS 和 MAT(Memory Analyzer tool)来分析内存泄漏
我们利用工具进行内存泄漏分析主要是用对比法:
a.先打开正常界面,不做任何操作,先抓取一开始的堆文件。
b.一顿胡乱操作,回到原来操作前的界面。主动触发一两次GC,过10秒再抓取第二次堆文件。
c.通过工具对比,获取胡乱操作后新增的对象,然后分析这些新增的对象。
DDMS作用:抓取堆文件,主动触发GC。(其实也是可以用Android Studio 的Profile里面的Memory工具来抓取堆文件的,但是我这边在利用Profile 主动触发gc 的时候会导致程序奔溃,也不知道是不是手机的问题,所以没用Android Studio的Profiler)
MAT作用:对堆文件进行对比,找到多出的对象,找到对象的强引用调用链。
以下是详细的过程:
步骤1.打开DDMS,选择需要调试的应用,打开初始界面,点击下图的图标(Dump Hprof File)先获取一次堆文件。
步骤2.对应用随便操作后,回到一开始的界面,先多触发几次GC ,点击下图的图标(Cause Gc)来主动触发GC,然后再次点击 Dump Hprof File 图标来获取堆文件。
步骤3.通过Android Studio Profile 或者 DDMS dump 的堆文件无法在MAT 打开,需要借助android sdk包下的一个工具hprof-conv.exe来转换。
格式为 hprof-conv 旧文件路径名 要转换的名称;
例如:hprof-conv 2022-04-13_17-54-40_827.hprof change.hprof
步骤4.把两份堆文件导入MAT,然后选择其中第二次获取的堆文件,点击 如图所示的 Histogram查看。
步骤5.点击下图图标,Compare To Another Heap Dump ,选择另一份堆文件。
6.会得出下图所示的 Hitogram 展示,我们主要看Objects 这一列。 如下图所示 “+ 2” 则代表前面两份堆文件对比,这个对象多了两个,我们主要就是要分析这些多了出来,没有被回收的对象。
7.加入我们从增加的对象中,看到了MainActivity ,则需要从一开始打开的Hitogram 展示里面找到这个对象的调用栈。如下图所示,搜索MainActivity
8.看到下图所示解雇,然后鼠标右键点击下图红色圈圈着的MainActivity ,选择 Merger Shortest Paths to Gc Roots ,再选择 exclude all phantom/weak/soft etc.references ,就可以看到这个MainActivity 对象的强引用链,至此我们就可以找到MainActivity对象是被什么引用导致无法回收了。
3、内存泄露检测神器之LeakCanary(线下集成)
自行学习了解,接入简单,使用简单,基本可以解决大部分内存泄漏问题。
github地址:https://github.com/square/leakcanary/
学习地址:https://square.github.io/leakcanary/changelog/#version-22-2020-02-05
五、内存优化建议
针对内存抖动的建议:
避免在被频繁调用的方法内中进行内存分配,如 View.onDraw() ,我们知道每次View重绘的时候该方法都会被调用。如果在这些被频繁调用的方法内实例化对象,每次会分配大量的内存。而且该方法内的对象一旦不被使用会快会被回收,都会导致一次或者多次垃圾回收。
合理的使用对象复用(尤其是bitmap)。就像系统的listView,RecyclerView滑动过程中对象复用机制这样。
对象池模式对于重用已分配对象非常有帮助,目的在于面对需要大量分配高成本对象时,通过对象重用来尽量减少内存分配以及垃圾回收对系统产生的影响。例如线程池、okhttp 里面的 ConnectionPool。
享元模式,通过节省对象所共享的状态,以减少内存的量(和对象池是有区别的)
字符串拼接使用StringBuilder 、StringBuffer。
针对内存泄漏问题的建议:
对于生命周期比Activity长的对象(单例),要避免直接引用Activity的context,可以考虑使用ApplicationContext,静态变量不使用时及时置空;
Handler持有的引用最好使用弱引用,在Activity被释放的时候要记得清空Message,取消Handler对象的Runnable;
非静态内部类、非静态匿名内部类会自动持有外部类的引用,为避免内存泄露,可以考虑把内部类声明为静态的;
广播接收器、EventBus等的使用过程中,注册/反注册应该成对使用,但凡有注册的都应该有反注册;
不再使用的资源对象Cursor、File、Bitmap等要记住正确关闭;
集合里面的东西有加入就应该对应有相应的删除。
属性动画及时取消,注意webview内存泄漏问题。
针对内存溢出问题的建议(主要就是要减少内存占用):
图片内存优化(重点)
推荐参考githubdemo:https://github.com/yangchong211/YCBannerView使用经过优化的数据容器。如ArrayMap 、SparseArray。
Android内存优化之取代HashMap(SparseArray和ArrayMap解析)当后台Service任务运行完成后,及时关闭。其实没用的Service还不关闭也属于内存泄漏的情况。
GC可以在空闲时主动调用。
apk打包签名时,使用zipalign工具对齐。
使用ProGuard去除不必要代码。
杜绝内存抖动问题。
养成良好的编码习惯。
六、实现图片、内存、GC监控
建议参考:
深入探索 Android 内存优化(炼狱级别)
七、总结
对于 优化的大方向,我们应该优先去做见效快的地方,主要有以下三部分:内存泄漏、内存抖动、Bitmap。完善监控机制也是我们的重点,能帮助我们对内存问题快速分析和处理。
参考:
深入探索 Android 内存优化(炼狱级别)