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 协议 ,转载请注明出处!