Android性能优化-内存优化详解
内存的概念
内存是计算机中最重要的部件之一,是硬盘与CPU之间沟通的桥梁,所有程序都是运行其上,会对程序的性能造成很大的影响。
Why 内存优化?
减少Crash
减少因为内存问题引起的Crash,其中最典型的就是OOM
运行流畅
当内存紧张时,就会导致频繁触发
GC。当触发GC时,所有线程都要停止,会导致所有运行被搁置,导致运行卡顿。
延长后台运行时间
Android会按照特定的机制进行进程清理,按照
前台进程-可见进程-服务进程-后台进程-空进程的顺序优先清理后面的进程。当应用占用内存过多时,切到后台时有更高的几率被Kill。
内存管理机制
系统层面
LowMemoryKiller
每隔一段时间检查一次,当系统剩余可用内存较低时,便会触发杀进程的策略。
按照
进程优先级来回收资源,如果进程优先级一致的情况下,会优先Kill消耗内存更多的进程。
进程优先级
前台进程(Foreground Process)
优先级最高的进程,正处于用户交互的进程。
优先级最高,基本不会被回收。
判断条件:
- 持有一个与用户交互的Activity
- 持有一个Service(
startForeground() / 与可交互Activity绑定)
可见进程(Visible Process)
不含任何前台组件,但是依然可见
除非前台进程内存耗尽,否则不会轻易终止。
判断条件:
- 持有一个处于
pause状态的Activity,例如显示了一个Dialog - 持有一个与可见Activity绑定的Service
服务进程(Service Process)
可能在播放音乐或者下载文件
除非系统内存不足,否则系统尽量维持服务进程运行
判断条件:
- 持有一个Service,且是通过
startService()启动的
后台进程(Background Process)
处于用户不可见的状态,例如切到后台的应用
通过LruCache进行管理,系统会适当清理后台进程。占用内存越大越容易被清理
判断条件:
- 持有一个处于
stop状态的Activity,但尚未调用onDestroy()
空进程(Empty Process)
不包含任何活跃的应用组件
主要为了加快下次启动进程的速度。
进程层面
每个进程都是一个单独的虚拟机,使用的内存空间都是独立的。
不管是哪种虚拟机类型Dalvik和Art虚拟机,当分配对象所占用的内存空间不足时会触发GC。
GC类型
GC_FOR_MALLOC:表示在堆上分配对象时内存不足触发的GCGC_CONCURRENT:当应用程序的堆内存达到一定量时,系统自动触发的GC操作GC_EXPLICIT:调用了System.gc()时触发的GCGC_BEFORE_OOM:在准备抛出OOM异常前进行的GC
内存分配过程
//TODO
内存监听
获取系统内存信息
Android提供了
ActivityManager.getMemoryInfo()去获取系统的内存信息。
1 | |
最终得到的就是MemoryInfo对象
1 | |
获取应用内存信息
通过
ActivityManager也可以获取到应用可用的内存信息
1 | |
实时获取应用使用内存
上述操作获取的都是系统为应用配置的属性,但是无法实时的获取应用使用内存
1 | |
其中的memInfo结构如下:
1 | |
获取应用内存状态
实现需要获取状态的Activity或Application的onTrimMemory()
1 | |
优化目标
减少内存的占用。
内存问题大致可以分为两类:
- 无用的数据依然占用内存——内存泄漏
- 有用的数据占用内存过多——图片加载/内存抖动
上述两类问题最后都容易导致内存溢出(OOM)
内存泄漏
一般采用
可达性分析做为内存对象是否存活的判断方式,通过与GC Roots对象是否有关联判断是否需要进行回收。
内存泄漏:当前的对象已经不再使用,但是依然被GC Roots对象所引用,导致无法进行回收,依然占用内存。
触发原因
静态变量导致静态变量会一直持有外部类的引用,导致外部类对象无法被回收。
单例模式导致单例传入了外部参数,例如传入
Activity的context,就会持有对Activity的引用属性动画导致播放无限循环动画时,没有在Activity关闭时及时停止,导致View持有了Activity
Handler导致Handler的Message会持有Handler的引用,而Handler持有Looper的引用,Looper由
sThreadLocal 这个静态对象管理。所以导致内存泄漏的GC Roots对象为sThreadLocal。匿名内部类/非静态内部类匿名内部类会隐式持有对所在Activity的引用资源对象没关闭一般资源对象都使用了缓冲,不及时关闭的话,缓冲依然存在。
图片加载
图片资源基本是占用内存最多的,如果使用图片不当的话就很容易会导致OOM的发生。
本地或者网络图片最终都会转换成
bitmap。
支持图片格式
目前移动端Android平台原生支持的图片格式主要有以下几种:
JPEG广泛使用的有损压缩图像标准格式,不支持透明度和多帧动画,只有
RGB三个通道。PNG无损压缩图像标准格式,支持完整的透明通道。支持
ARGB四个通道WebP支持
有损压缩和无损压缩,也支持透明度。在Andorid4.0之后添加的系统支持,在4.3之后支持了无损和透明的WebP展示。
Bitmap占用内存
所有像素的内存占用总和。
可以通过getByteCount()和getAllocationByteCount()去获取Bitmap所占用的内存。
一般情况下Bitmap占用内存大小计算公式为:
图片长度 x 图片宽度 x 单位像素占用的字节数。
其中单位像素占用的字节数来自颜色深度。
颜色深度:每个像素显示的颜色数,显示的越多,色彩就越丰富。
Android系统提供如下几种:
颜色深度 每个像素占用内存 ARGB_8888( 默认颜色深度)4 byte / 32 bit ARGB_4444 2 byte / 16 bit RGB_565 2 byte / 16 bit ALPHA_8 1 byte / 8 bit 实际应用中建议使用
ARGB_8888(需要透明度)和RGB_565(不需要透明度)但
RGB565在部分场景下显示效果较差,例如大图展示。
加载网络或者本地图片(非Drawable文件夹)
占用内存大小:图片宽度 * 图片长度 * 单位像素占用字节数。
假设100 * 100且颜色深度为ARGB_8888的本地图片,转换Bitmap占用大小为100 * 100 * 4。
加载Drawable下文件资源(/res/drawable/)
占用内存大小:图片宽度 * 图片长度 * (inTargetDensity / inDensity) ^ 2 * 单位像素占用字节数。
inDensity:图片所在文件夹对应的密度
inTargetDensity:当前系统的屏幕密度
占用内存存储位置
在Android 2.3 之前 占用内存是在 native上分配的,并且生命周期不可控,还需要用户自己回收。
在Android 2.3 - 7.1 之间,占用内存位于Java堆上
在Android 8.0 之后,占用内存重新在native上分配,并且不需要主动执行回收。
内存抖动
短时间内有大量的对象被创建与回收,有短时间内快递的上升和下落的趋势,内存呈锯齿状。
此时频繁触发GC,造成卡顿,甚至OOM
触发原因
- 频繁创建对象,例如在
for循环创建对象
解决方案
- 尽量避免在循环体中创建对象
- 尽量不要在
onDraw()中创建对象 - 对于能够复用的对象,考虑使用
对象池进行缓存以便复用
内存溢出
OutOfMemoryError,应用申请的内存超出单个应用的最大可用内存。可用最大内存配置位于/system/build.prop下的 dalvik.vm.heapgrowthlimit
触发原因
- 内存泄漏积累到一定量之后导致OOM
- 一次性申请很多内存,例如
一次创建大的数组或者显示大型文件(图片)
其他问题
数据容器
使用了HashMap之类的容器,针对每一个键值对,都需要额外的Entry对象
强引用
针对某些低频使用对象使用强引用,当GC触发时不能去回收这些对象
数据相关
使用SP存储数据时,第一次读取时都需要将所有数据缓存到内存中,有时为了一些数据,就需要缓存整个SP。
缓存
针对一些大量重复使用对象,但是很快就要被替代,导致频繁发生GC。
优化工具
主要是针对内存泄漏场景的优化分析。
Lint分析
主要是扫描静态代码,从代码实现方面进行内存泄漏分析。
识别不太准确且覆盖率不高,不推荐使用。
Memory Profiler
AS 提供的性能分析工具,包含了CPU、内存、网络以及电量的分析信息。
可以实时观测应用的内存使用情况,用于查看是否发生内存抖动(上下波动明显),内存泄漏(切换Activity时内存明显上升)
一般情况下会结合下面的
MAT一起使用。
主要有以下作用:
- 实时图表展示应用内存使用量
- 用于识别内存泄漏、内存抖动等
- 提供捕获堆转储、强制GC以及查看内存分配详情
多次点击强制GC后,再点击堆转储,等待一会儿会得到hprof文件,如果想用MAT查看该文件,还需要执行一次转换。
1 | |
转换得到的mat.hprof就可以通过MAT打开。
Memory Analyzer Tool
Memory Profiler只能查看对应内存的分配,不能判断是否发生了内存泄漏。
MAT可以提供完整的Java Heap分析功能,并可以生成对应的内存分配报告以及分析内存问题。
如何使用
使用Mat打开的上一步生成的mat.hprof文件,打开后会显示一个预览页。
预览页上主要显示以下组件:
Histogram列举内存中所有
实例类型对象和个数以及大小,并在顶部的regex区域支持正则表达式查找。主要显示以下内容:
Shallow Heap:对象自身占用的内存Retained Heap:对象自身占用的内存 + 对象引用对象所占用内存Objects:对象个数
Dominator Tree列举
最大的对象及其依赖存活的Object。相比Histogram可以更方便的看出引用关系Top Consumers通过
图形的方式列出占用内存比较多的对象Leak Suspects列出
有内存泄漏的地方
排查方式
找到当前Activity(
任何猜测可能发生内存泄漏的类)通过顶部的
Regex输入具体类名,或使用group by package查找对应包下的类在
Histogram选择对应类的List Objects的with incoming reference就可以查看类的实例with incoming reference:哪些对象引用了它with outgoing reference:它引用了哪些对象
看到实例后,右键点击,选择
Path to GC Roots的exclude all phantom/weak/soft etc references排除掉
虚 / 弱 / 软引用,剩下的就是强引用根据引用链分析是否发生内存泄漏
高级使用
有两个hprof文件中,通过Compare Basket进行比较,可以快速生成对比结果,直接进行对应实例对象的比较。
hprof文件介绍
{% post_link Hprof文件解析%}LeakCanary
{% post_link Android性能优化-LeakCanary%}优化技巧
图片高效加载方式
图片的主要载体形式为Bitmap,一般通过BitmapFactory.decodeFile()或BitmapFactory.decodeResource()去进行加载。
1 | |
其中最主要的就是BitmapFactory.Options。通过设置其中的参数进行高效加载
1 | |
Options关键参数
inPreferredConfig
根据需求选择合适的
颜色深度,可以有效减少占用内存。
实质用的就是上面介绍的颜色深度
1 | |
inJustDecodeBounds
是否去加载图片。
- 设置
true:只会去加载图片的原始宽高信息,但不会真正加载图片到内存。- 设置
false:图片加载到内存中
1 | |
一般配合inSampleSize使用,可以提前设置采样率
inDensity/inTargetDensity
inDensity默认表示图片资源文件夹的densityDpi
inTargetDensity默认表示设备的densityDpi
上面讲到加载Drawable下文件资源时,计算占用内存大小时,需要用到上述两个参数。
所以可以通过调整这两个参数,优化一部分的图片内存占用。
inSampleSize
设置图片的采样率,同时作用于图片的宽和高
inSampleSize取值总是2的指数,如果传进来的值不为2的次方,就会向下取整并取到2的次方的值来代替。
1 | |
使用优化的数据容器
可以使用SparseArray和ArrayMap替换HashMap。
如果key为int,可以直接使用SparseArray
AutoBoxing的处理
核心就是基础数据类型转换成对应的复杂类型,例如int <=> Integer。
在自动装箱发生时,每次都会产生一个新的对象,就会导致更多的内存占用和性能开销。
尽量使用基础数据类型,减少自动装箱。
减少使用枚举类型
一般情况下使用枚举类型的dex size是普通常量定义的dex size的13倍以上,同时运行时的内存分配,一个enum值的生命也会消耗至少20Bytes。
建议使用IntDef和StringDef替代枚举类型。简单的枚举的话,可以直接使用静态常量代替。
内存复用
- 资源复用:通用的字符串、颜色定义、简单页面布局的复用(
<merge>、<include>) - 视图复用:使用ViewHolder实现ConvertView复用
- 对象池:创建对象池,实现复用逻辑,对相同类型的数据使用同一块内存空间。不要使用new Message()而是使用Message.obtain()以复用Message对象
- Bitmap复用:使用
inBitmap属性告知BitmapDecoder尝试使用已经存在的内存区域。在Android 4.4之前只能重用相同大小的Bitmap内存,4.4之后的只要后来的Bitmap比之前的小即可。
可用内存过低主动清理
通过实现onTrimMemory()或onLowMemory()在其中去执行释放资源的操作以减少内存占用。
自动化内存检测

参考链接
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!

