在 Android 系统中,垃圾回收是自动的,比较隐蔽,这就导致一些内存问题表现的并不明显,出现问题后难以定位。常见的内存问题有内存泄漏、内存溢出(Out of Memory)、内存抖动等。
我们做内存优化的主要原因有以下几点:
- 降低 OOM 率,内存问题最常见的就是 OOM,申请的内存得不到释放就有可能造成 OOM;
- 减少卡顿, 在 Android 系统中造成卡顿的原因有很多,其中就有内存引起的卡顿。内存问题之所以会影响到界面流畅度,是因为垃圾回收机制,在 GC 的时候,所有的线程都要停止工作,包括主线程。因此,当 GC 和绘制界面的操作同时触发时,绘制的任务就会被搁置,导致掉帧,也就是界面卡顿;
- 增加应用存活时间,Android 系统会按照特定的流程清理进程,优先考虑清理后台进程,如果某个应用在后台运行且占用的内存很多,就会被优先清理掉。如果我们希望 APP 可以存活的久一点,不使用的内存应该尽快清理掉;
至于哪些对象是需要回收的?使用跟搜索算法/可达性算法,选定一些对象作为 GC Roots,向下搜索,搜索所经过的路径称为引用链,当一个对象到 GC Roots 没有应用链相连,则表明此对象需要回收。以下是可以作为 GC Roots 的对象:
- 本地方法栈中引用的对象;
- 虚拟机栈中引用的对象;
- 全局静态变量引用的对象;
- 全局常量引用的对象;
1 内存泄漏
内存泄漏指的是我们的应用已经不需要某些对象了,但是,该对象的某些引用依旧没有释放,导致这些对象没有办法被回收。从内存上来看,就是某块内存已经不再被使用了,但是却没有办法被回收掉,从而造成了内存浪费。 比如说匿名内部类持有外部类的引用,导致外部类无法被回收,就会出现内存泄漏的问题。
内存泄漏会导致无法回收的内存越来越多,可用的内存越来越少,直到应用无更多可用的内存申请,严重的甚至出现 OOM。
2 内存溢出(OOM Out Of Memory)
内存泄漏一般导致应用卡顿,极端情况会导致 OOM,OOM 的原因是因为超过内存的阈值。原因主要有两方面:
- 内存泄漏,导致无法及时释放导致 OOM;
- 一些逻辑消耗了大量内存,无法及时释放或者超过导致 OOM;
能够消耗大量内存的,绝大多数是因为图片加载。这是 OOM 出现最频繁的地方。图片加载,一个是控制每次加载的数量,二是保证每次滑动的时候不进行加载,滑动完进行加载。一般情况使用先进后出,而不是先进先出。 不过一般我们图片加载都是使用 fresco 或者 Glide 等开源库。
下面是导致内存溢出的两种情况:
通过命令行查看内存消耗情况(adb shell dumpsys meminfo 包名 -d):
对于图片的内存优化:
- 图片缩放,比如说一张图片的宽高为 200 x 200,但是 View 的宽高为 100 x 100,这个时候就要对图片进行缩放,通过 inSampleSize 实现;
- 减少每个像素所占用的内存,在 API 29 中,将 Bitmap 分为 ALPHA_8、RGB_565、ARGB_4444、RGBA_F16、HARDWARE 六个等级:
- ALPHA_8:不存储颜色信息,每个像素占 1 个字节;
- RGB_565:仅存储 RGB 通道,每个像素占 2 个字节,对 Bitmap 色彩没有高要求,可以使用该模式;
- ARGB_4444:已弃用,用 ARGB_8888 代替;
- ARGB_8888:每个像素占用 4 个字节,保持高质量的色彩保真度,默认使用该模式;
- RGBA_F16:每个像素占用 8 个字节,适合宽色域和 HDR;
- HARDWARE:一种特殊的配置,减少了内存占用同时也加快了 Bitmap 的绘制;
- 每个等级每个像素所占用的字节是不一样的,所存储的色彩值也不同,同一张 100 像素的图片,ARGB_8888 占用了 400 字节,RGB_565 才占用 200 字节。所以在某些场景中,修改图片格式也可以达到减少一半内存的效果。
- 内存复用,避免重复分配内存。Bitmap 所占用的内存比较大,如果频繁的创建和回收 Bitmap,很容易造成内存抖动,所以应该尽量复用 Bitmap 内存。在 Android 3.0(API 级别 11)开始,系统引入了 BitmapFactory.Options.inBitmap 字段。如果设置了此选项,那么采用 Options 对象的解码方法会在生成目标 Bitmap 时尝试复用 inBitmap,这意味着 inBitmap 的内存得到了重复使用,从而提高了性能,同时移除了内存分配和取消分配。不过 inBitmap 的使用方式存在某些限制,在 Android 4.4(API 级别 19)之前系统仅支持复用大小相同的位图,4.4 之后只要 inBitmap 的大小比目标 Bitmap 大即可
- 大图局部加载策略:对于图片加载还有种情况,就是单个图片非常巨大,并且还不允许压缩。比如显示:世界地图、清明上河图、微博长图等。首先不压缩,按照原图尺寸加载,那么屏幕肯定是不够大的,并且考虑到内存的情况,不可能一次性整图加载到内存中。所以这种情况的优化思路一般是局部加载,通过 BitmapRegionDecoder 来实现
上面所说的这些关于 Bitmap 的内存优化策略其实都比较简单,而且我们在开发中可能很少用到
因为我们常用的图片框架比如 Glide 已经将这些都封装在里面了,所以一般情况下我们加载图片时不需要做这些特殊操作。
3 内存抖动
在短时间内频繁的创建或者销毁的对象,容易触发 GC,会引起内存抖动,比如在一个 for 循环中创建临时对象实例。
因为 GC 的时候所有的线程都会停止工作,因此导致卡顿。为了避免内存抖动,应该避免以下操作:
- 尽量避免在循环体中创建对象,如果可以的话把创建的对象移到循环体外;
- 尽量不要在自定义 View.onDraw() 方法中创建对象,因为这个方法会被频繁调用;
- 对于能够复用的对象,可以考虑使用对象池把它们缓存起来。需要注意的是,使用结束后要手动释放对象池里的对象;