Android内存优化
MoMo Lv5
  1. 什么是OOM、内存泄漏、内存抖动?如何发生的? ⭐⭐⭐⭐⭐
  2. Handler导致的内存泄露你是如何解决的? ⭐⭐⭐⭐
  3. 知道如何定位内存泄漏吗?有什么工具?⭐⭐⭐⭐
  4. 请至少例举出5种内存泄漏情况。⭐⭐⭐⭐⭐
  5. 在 Android 中如何避免内存泄漏?⭐⭐⭐⭐⭐
  6. 谈谈你项目中内存优化的一些经验⭐⭐⭐

概述

常见的内存问题如下

  • 内存泄露
  • 内存抖动
  • 图片Bitmap相关
  • 代码质量 & 数量
  • 日常不正确使用

内存优化的目的是为了避免出现内存泄漏,内存溢出(OOM:Over Of Memory)、内存抖动等问题:

  • 内存泄漏:当一个对象A在程序中已经打算释放了,但有其他对象持有对象A的强引用,导致对象A不能正常被系统回收,继续占用着内存,如此反复使实际可使用内存越来越小。
  • 内存溢出:当内存使用量超过了虚拟机分配给当前程序的最大值时,就会发生内存溢出。出现这种情况很可能是加载的资源太多,如加载大图片,或者分配了很大的数组等变量,或者是太多的内存泄漏最终导致内存溢出;
  • 内存抖动:当内存频繁分配和回收导致内存不稳定,就会出现内存抖动,它通常表现为 频繁GC、内存曲线呈锯齿状;

内存优化的意义:

  • 通过减少OOM,可以提高程序的稳定性;
  • 减少程序的内存占用,提高程序在后台进程的存活率;
  • 减少卡顿,提高程序的流畅度;

发生内存泄露的本质原因

本该被回收的对象 因为某些原因 而不能被回收,从而继续停留在堆内存中

  1. “本该被回收”= 该对象 已不需再被使用
  2. “因某些原因 而 不能被回收”的原因=有另外1个正在使用的对象持有它的引用
  3. 无意识地持有对象

==本质上是持有引用者的生命周期>被引用者的生命周期,从而 当后者需结束生命周期被销毁时,无法被正确回收==

当1个对象已不需再被使用、本该被GC回收时,而因有另外1个正在使用的对象持有它的引用 从而导致它不能被程序回收 而停留在堆内存中

引起内存泄漏的场景及解决方法

单个的内存泄漏一般不会引起系统异常,但如果众多的内存泄漏则可能导致内存溢出,最后导致系统异常。因此, 一起看看有哪些可能造成内存泄漏的场景。

  1. 单例引起的内存泄漏
    1. 由于单例的静态特性导致它的生命周期和整个应用的生命周期一样长,如果有对象已经不再使用了,但又却被单例持有引用,那么就会导致这个对象就没办法被回收,从而导致内存泄漏。
    2. 在创建单例对象的时候,引入了一个Context上下文对象,如果把Activity注入进来,会导致这个Activity一直被单例对象持有引用,当这个Activity销毁的时候,对象也是没有办法被回收的。
    3. 在这里我们只需要让这个上下文对象指向应用的上下文即可(this.context=context.getApplicationContext()),因为应用的上下文对象的生命周期和整个应用一样长。

  1. 非静态内部类创建静态实例引起的内存泄漏

    1. 由于非静态内部类会默认持有外部类的引用,如果我们在外部类中去创建这个内部类对象,当频繁打开关闭Activity,会导致重复创建对象,造成资源的浪费,为了避免这个问题我们一般会把这个实例设置为静态,这样虽然解决了重复创建实例,但是会引发出另一个问题,就是静态成员变量它的生命周期是和应用的生命周期一样长的,然而这个静态成员变量又持有该Activity的引用,所以导致这个Activity销毁的时候,对象也是无法被回收的。
    2. 由于静态对象持有Activity的引用,导致Activity没办法被回收。
    3. 把非静态内部类改成静态内部类即可(static class TestResource)。

  2. Handler引起的内存泄漏

    1. 程序启动时在主线程中会创建一个Looper对象,这个Looper里维护着一个MessageQueue消息队列,这个消息队列里会按时间顺序存放着Message,然后上面的Handler是通过内部类来创建的,内部类会持有外部类的引用,也就是Handler持有Activity的引用,而消息队列中的消息target是指向Handler的,也就等同消息持有Handler的引用,也就是说当消息队列中的消息如果还没有处理完,这些未处理的消息(也可以理解成延迟操作)是持有Activity的引用的,此时如果关闭Activity,是没办法回收的,从而就会导致内存泄露。
    2. 把非静态内部类改成静态内部类(如果是Runnable类也需要改成静态),然后在Activity的onDestroy中移除对应的消息,再来需要在Handler内部用弱引用持有Activity,因为让内部类不再持有外部类的引用时,程序也就不允许Handler操作Activity对象了。

  3. WebView引起的内存泄露

    1. 不要在xml去定义,定义一个ViewGroup就行,然后动态在代码中new WebView(Context context)(传入的Context采取弱引用),再通过addView添加到ViewGroup中,最后在页面销毁执行onDestroy()的时候把WebView移除。

    2. 为WebView新开辟一个进程,在结束操作的时候直接System.exit(0)结束掉进程,这里需要注意进程间的通讯,可以采取Aidl,Messager,Content Provider,Broadcast等方式。

  4. Asynctask引起的内存泄露

    1. 和Handler比较像,因为内部类持有外部类引用,
    2. 改成静态内部类,然后在onDestory方法中取消任务即可。

  5. 资源对象未关闭引起的内存泄露

    1. 比如经常使用的广播接收者,数据库的游标,多媒体,文档,套接字等。
  6. 注册了EventBus没注销,添加Activity到栈中,销毁的时候没移除等。

Handler 内存泄漏

Handler发出一个消息后,消息会储存在消息队列里,而每个消息都会对应一个目标Handler,也称为target,target是Handler的一个引用。因此如果消息一直存放在消息队列里,将导致Handler无法被回收。

还有另一种情况,因为Handler一般是作为Activity的内部类,可以发送延迟执行的消息,如果在延迟阶段,我们把Activity关掉,此时因为消息队列还有未处理或者正在处理的消息,而消息队列里面的消息又持有Handler实例的引用,同时该Activity还被Handler这个内部类所持有,导致Activity无法被回收,没有真正退出并释放相关资源,最终就造成内存泄漏。

  • 将 Handler 定义成静态的内部类,在内部持有 Activity 的弱引用
  • 在Acitivity的onDestroy()中调用handler.removeCallbacksAndMessages(null)及时移除所有消息。
  • 将Handler抽离出来作为BaseHandler,然后每个Activity需要用到Handler的时候,就去继承BaseHandler:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 这个是BaseHandler
public abstract class BaseHandler<T> extends Handler {
private final WeakReference<T> mWeakReference; //弱引用

protected BaseHandler(T t) {
mWeakReference = new WeakReference<T>(t);
}

protected abstract void handleMessage(T t, Message msg);

@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (mWeakReference == null) {
return;
}

T t = mWeakReference.get();
if (t != null) {
handleMessage(t, msg);
}
}
}

//然后在某个Activity中使用
private static class H extends BaseHandler<XuruiActivity { //静态的内部类哦

public H(XuruiActivity activity) {
super(activity);
}

@Override
protected void handleMessage(XuruiActivity activity, Message msg) {
//do something
}

//同时Activity的onDestroy函数取消掉所有消息
@Override
protected void onDestroy() {
mMyHandler.removeCallbacksAndMessages(null);
super.onDestroy();
}
}

非静态内部类的静态实例

被 Static 关键字修饰的成员变量的生命周期 = 应用程序的生命周期

若使被 Static 关键字修饰的成员变量 引用耗费资源过多的实例(如Context),则容易出现该成员变量的生命周期 > 引用实例生命周期的情况,当引用实例需结束生命周期销毁时,会因静态变量的持有而无法被回收,从而出现内存泄露

1
2
3
4
5
6
7
8
9
public class ClassName {
// 定义1个静态变量
private static Context mContext;
//...
// 引用的是Activity的context
mContext = context;

// 当Activity需销毁时,由于mContext = 静态 & 生命周期 = 应用程序的生命周期,故 Activity无法被回收,从而出现内存泄露
}

非静态内部类依赖着外部类,因此会一直持有外部类的引用。如果外部类需要销毁时,非静态内部类还无法销毁,那么将导致外部类资源无法正常回收。

如果Handler定义为非静态内部类,就有可能导致所在的Activity无法正常被回收。

  • 将内部类设置为静态内部类
  • 将内部类抽取为一个单例
  • 尽量使用Application的Context,而不是Activity的Context(当然有时候必须使用Activity 的 Context)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 创建单例时,需传入一个Context
    // 若传入的是Activity的Context,此时单例 则持有该Activity的引用
    // 由于单例一直持有该Activity的引用(直到整个应用生命周期结束),即使该Activity退出,该Activity的内存也不会被回收
    // 特别是一些庞大的Activity,此处非常容易导致OOM
    public class SingleInstanceClass {
    private static SingleInstanceClass instance;
    private Context mContext;
    private SingleInstanceClass(Context context) {
    this.mContext = context; // 传递的是Activity的context
    this.mContext = context.getApplicationContext(); // 应该传递的是Application 的context
    }

    public SingleInstanceClass getInstance(Context context) {
    if (instance == null) {
    instance = new SingleInstanceClass(context);
    }
    return instance;
    }
    }

资源性对象

资源性对象,比如文件、图片、Cursor等,使用后,如果忘记调用close()函数,就直接退出Activity,那将造成内存泄漏,因此要记得在适当的地方关闭资源性对象。

WebView

WebView开发现在越来越常见,只要在应用中使用了一次WebView,内存就不会释放掉。 因此建议对WebView单独开启一个独立进程,只要在合适的时机销毁进程即可正常释放内存。

静态变量持有大数据对象

众所周知静态变量的生命周期比较长,那如果有一个大数据对象(大数据对象指的是占用内存较大的数据,比如有些需要保存本地的数据,因为放数据库会稍微麻烦,没有用sharePreference来得方便,就把这一大堆数据直接转为字符串保存本地)。如果把这个保存在sharePreference的大数据赋值给静态变量,该静态变量长期存在,就一直占有着内存。所以,数据量足够大的情况下,建议考虑数据库,推荐使用第三方库GreenDao。

注册后没有注销

比如在Activity里注册了广播接收器,当Activity销毁时没有注销,那么将导致内存泄漏。

单例对象

单例模式 由于其静态特性,其生命周期的长度 = 应用程序的生命周期

如果单例对象需要用到Context,那么很可能出现创建单例对象InstanceA时传入ActivityA的Context,当ActivityA退出时,InstanceA对象还存在并持有ActivityA的引用,这就导致了ActivityA无法被正常回收。 因此推荐尽量使用Application的Context。

集合只增不减

当一个对象放到集合后,集合就持有该对象的引用,如果不把对象从集合中移除,就可能导致该对象无法被系统正常回收。因此需要记得不再使用的集合对象需要及时remove,或者执行clear()方法清空集合。

1
2
3
4
5
6
7
8
9
// 通过 循环申请Object 对象 & 将申请的对象逐个放入到集合List
List<Object> objectList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Object o = new Object();
objectList.add(o);
o = null;
}
// 虽释放了集合元素引用的本身:o=null)
// 但集合List 仍然引用该对象,故垃圾回收器GC 依然不可回收该对象

解决方案

集合类添加集合元素对象后,在使用后必须从集合中删除。由于一个集合中有许多元素,故最简单的方法是:清空集合对象和设置为null。

1
2
3
// 释放objectList
objectList.clear();
objectList=null;

finalize()

如果错误的覆盖了finalize()方法,会导致资源无法正确的回收,所以要重写该方法时需要多注意。

匿名内部类

如下面代码,在实际开发中经常会使用到匿名内部类,而匿名内部类也和非静态内部类一样会持有外部类的引用。

1
2
3
4
5
6
7
Runnable beginFrontPhotoAutoTest =  new Runnable() {
@Override
public void run() {
beginFrontPhotoTest();
}
};
mHandler.postDelayed(beginFrontPhotoAutoTest, 3000); // 1

当执行了[注释1]后,就会出现和主线程异步的情况,就有可能造成Acitvity退出时,因为无法被正常回收的情况。因此一般在Acitvity 退出的时候,都需要将Handler消息清空。

图片

如果Bitmap对象不使用时没有调用recycle()释放内存,也会导致内存泄漏。当然在Android 3.0 以后,当Bitmap对象不使用后,将其引用设置为null即可,不需要再手动调用recycle()。而对图片的使用,推荐使用Glide或者LreCache等第三方库来做图片缓存管理。

解决方法有:

  • 图片使用完或者未使用时,及时执行recycle()进行资源回收;
  • 大图片建议先压缩再放到内存里,用到BitmapFactory类;
  • 如果是不允许压缩的大图,比如地图等需要按原图尺寸加载,此时不可能一次性加载整图到内存,局部加载可以用到BitmapRegionDecoder类;
  • 选择合适的图片解码方式,Android默认的Bitmp解码率格式是ARGB_8888,这是一种高质量的图片格式,但对于一些半透明图片来说,推荐使用GRB_565格式,内存开销约为前者的一半;
  • 推荐使用Glide或者LreCache等第三方库;

内存抖动

内存抖动是指在短时间内,内存中频繁地分配和释放大量对象,导致内存使用情况出现剧烈波动的现象。这种波动会使应用的性能下降,严重时可能导致卡顿甚至应用崩溃。

内存抖动可能导致页面卡顿,甚至造成OOM。如果频繁创建对象,需要频繁分配内存,最后导致系统内存不足,或者产生很多地址不连续的内存碎片。而不连续的内存碎片是不可以分配给程序的,最后就造成了OOM。

  • 卡顿:

    • 当内存抖动发生时,垃圾回收器(GC)会频繁地回收内存。因为 GC 在执行回收操作时,会暂停应用的主线程,以确保对象引用关系的准确性。如果 GC 过于频繁,主线程被暂停的时间就会累积,导致应用出现卡顿。例如,在一个动画播放的场景中,由于内存抖动引发频繁的 GC,动画的帧率可能会下降,用户会明显感觉到动画不流畅。
  • 性能下降:

    • 内存分配和释放操作本身也需要消耗一定的系统资源,包括 CPU 时间和内存带宽等。频繁的内存抖动会使这些资源被大量消耗在对象的分配和回收上,而不是应用的实际业务逻辑处理。例如,在一个对性能要求较高的游戏应用中,频繁的内存抖动可能会导致游戏的加载时间变长,场景切换变慢等问题。
  • 可能导致 OOM(内存溢出):

    • 如果内存抖动的情况非常严重,即频繁地创建大量对象,可能会使内存占用在短时间内迅速上升,超出应用的内存限制,从而导致内存溢出。例如,在一个处理大量图片的应用中,如果在循环中不断地创建新的大尺寸图片对象,且没有及时释放,就可能会耗尽内存,使应用崩溃。

导致内存抖动的常见场景有:

  • 循环中创建占用内存较大的对象

    1
    2
    3
    4
    5
    6
    for (int i = 0; i < imageWidth; i++) {
    for (int j = 0; j < imageHeight; j++) {
    Point point = new Point(i, j);
    // 对point对象进行一些操作,如计算像素位置等
    }
    }
    • 每次循环都会创建一个新的Point对象。如果图像尺寸较大,就会在短时间内创建大量的Point对象。这些对象在循环结束后可能很快就会变成垃圾对象,等待垃圾回收器回收,这就导致了内存的频繁波动,即内存抖动。
  • 自定义View在onDraw中创建对象,因为自定义View的onDraw方法会被频繁调用。如果频繁创建对象,甚至较大的对象,会导致内存增加,甚至内存抖动

  • 频繁地创建和销毁资源对象(如线程):

    1
    2
    3
    4
    5
        for (int i = 0; i < 100; i++) {
    new Thread(() -> {
    // 执行一些后台任务,如网络请求、文件读取等
    }).start();
    }
    • 频繁地创建和销毁线程会导致内存的波动。因为线程本身也会占用一定的内存资源,包括线程栈空间等。每次创建线程时,内存占用增加,线程结束后,内存才会被回收,这样频繁操作就容易引起内存抖动。

如何减少内存使用

不仅为了避免出现内存泄漏,内存抖动,在实际开发中,也需要掌握减少内存使用的方法。

  1. 减小对象本身的内存占用:在可以达到相同实现的诸多方法中,选择空间损耗较小的方法可以减少内存使用:

    • 尽量使用int代替Enum:使用Enum会导致编译后的dex文件大小变大,且在使用过程中也会产生额外的内存占用;

    • 使用轻量级的数据结构:经常可以看到使用HashMap等传统数据结构,相比起专为移动端操作系统使用的ArrayMap容器,前者效率较低,占用内存较大。还有SparesArray也更加的高效,避免了key和value的自动装箱和解箱;

    • StringBuilder:如果字符串通过’+’来拼接,会产生额外内存消耗,推荐使用StringBuilder来代替频繁的字符串拼接操作;

    • 使用ProGuard来剔除不需要的代码;

    • 慎用Static变量:因为Static变量的生命周期和程序的周期一样长;

    • 优化布局层次,减少内存消耗;

    • 不必为了实现某个单一的功能,就引入复杂的第三方库;

    • 尽量避免在循环体内或者频繁调用的函数里创建对象;

  2. 内存对象复用:使用到相同的对象或者资源,如果已经创建,则尽量复用以减少内存使用:

    • 线程池:使用到多线程的时候,推荐使用线程池,因为线程池自动的帮忙处理好多线程复用的问题;

    • 资源复用:Android系统内有自带很多资源,如字符串、颜色、图片、布局等,这些资源在App中是可以直接引用的,就不必再自己增加资源。同时,对于自己增加的资源,同样要考虑模块化和复用率;

    • 避免在onDraw方法里面执行对象的创建:类似onDraw等频繁调用的方法,一定需要注意避免在这里做创建对象的操作,因为他会迅速增加内存的使用,而且很容易引起频繁的gc,甚至是内存抖动。

内存分析工具和内存分析命令

内存分析的工具和命令的具体使用不属于本面试专栏的范围,因此在这里仅作为引导,具体使用自行百度。

内存分析工具

  1. MAT工具: Memory Analyzer是一个快速且功能丰富的Java堆分析器,可帮助您查找内存泄漏并减少内存消耗;
  2. LeakCanary工具:一个自动检测内存泄漏的工具,LeakCanary 本质上是一个基于 MAT 进行 Android 应用程序内存泄漏自动化检测的的开源工具;
  3. Android Studio自带工具
  • Memory Profiler工具:是 Android Profiler 中的一个组件,可帮助你分析应用卡顿、崩溃、内存泄漏等原因;
  • Heap Viewer工具:堆内存查看工具,用于监控App的某一时刻的内存堆上的具体使用情况,从而帮助找出内存泄露;
  • Allocation tracker工具:内存分配追踪工具,用于追踪一段时间的内存分配使用情况,能够知道执行一系列操作后,有哪些对象被分配空间。知道这些分配能使你调整相关的方法调用来优化app性能与内存使用。

内存分析命令

对Android系统性能进行监控,包括CPU使用率、CPU使用率TOP5的进程、内存、内存占用TOP5进程、网络速度、磁盘速度都参考Android系统性能监控最全面分析与实践

常用的内存分析指令有如下几个,具体使用详见参考文档:

  • dumpsys meminfo
  • procrank
  • cat /proc/meminfo
  • free
  • ps

内存优化总结

在平时的开发中,需要养成良好的代码习惯,从而避免内存泄漏,简单总结一下:

  • 对 Activity 等组件的引用应该控制在 Activity 的生命周期之内,如果可以使用Application的Context的情况下,优先使用Application的Context;
  • 静态变量或者静态内部类中慎用非静态外部成员变量,如果一定要用,则记得适当时机把非静态外部成员变量置空,同时建议用弱引用来引用外部类的变量;
  • 静态变量不要赋值大数据对象;
  • 将 Handler 定义成静态的内部类,在内部持有 Activity 的弱引用,并在Acitivity的onDestroy()中调用handler.removeCallbacksAndMessages(null)及时移除所有消息。更进一步建议将Handler抽离出来作为BaseHandler,然后每个Activity需要用到Handler的时候,就去继承BaseHandler。
  • 打开了文件、图片等资源性对象记得关闭;
  • 注册了BraodcastReceiver,ContentObserver等资源,注意在Activity销毁时注销掉;
  • 适当使用软引用,弱引用,确保对象可以在合适的时机回收;
  • 保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期,所以说意识很重要。

而对于内存溢出,我们可以用try…catch…方式捕获,但是OOM往往是由于内存泄漏造成的,泄漏的部分多数情况下不在try语句块里,反而catch后不久就会发生OOM。所以OOM优化还是要从内存泄漏优化出发。

image

Powered by Hexo & Theme Keep
Unique Visitor Page View