Android内存泄漏代码场景与规避

1,007 阅读8分钟

作 者: 十年磨剑

创建日期: 2021-12-27

更新日期: 2019-10-29

0、内存泄露描述

内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

对于Java来说,就是new出来的Object 放在Heap上无法被GC回收。

Java 中的内存分配

  1. 静态储存区:编译时就分配好,在程序整个运行期间都存在。它主要存放静态数据和常量;
  2. 栈区:当方法执行时,会在栈区内存中创建方法体内部的局部变量,方法结束后自动释放内存;
  1. 堆区:通常存放 new 出来的对象。由 Java 垃圾回收器回收。

四种引用类型的介绍

  1. 强引用(StrongReference):JVM 宁可抛出 OOM ,也不会让 GC 回收具有强引用的对象;
  2. 软引用(SoftReference):只有在内存空间不足时,才会被回的对象;
  1. 弱引用(WeakReference):在 GC 时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存;
  2. 虚引用(PhantomReference):任何时候都可以被GC回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为GC回收Object的标志。

我们常说的内存泄漏是指new出来的Object无法被GC回收,即为强引用。


对于Android而言,内存泄露大部分是由于视图的生命周期与普通对象的生命周期不匹配,导致普通对象强引用持有了视图对象的实例导致视图没办法被及时回收。

1、非静态内部类引起泄露

非静态内部类,编译时产生的Xxx$1.class的class,在构造函数自动引用其宿主类的强引用,如果该内部类使用不当,极易导致内存泄漏。

样例代码:

// Main.java
public class Main {
    public static void main(String[] args) {
        // TODO
    }
    
    private String name;
    private int age;
    
    private ITest iTest = new ITest() {
    	
        @Override
        public void showData(String data) {
        	// TODO
            System.out.println("Hello world!");
        }
    };
    
    public interface ITest {
        void showData(String data);
    }
}

编译后生成的class文件:

// Main.class
public class Main {
    private String name;
    private int age;
    private Main.ITest iTest = new Main.ITest() {
        public void showData(String var1) {
            System.out.println("Hello world!");
        }
    };

    public Main() {
    }

    public static void main(String[] var0) {
    }

    public interface ITest {
        void showData(String var1);
    }
}

// Main$ITest.class
public interface Main$ITest {
    void showData(String var1);
}

// Main$1.class
class Main$1 implements ITest {
    Main$1(Main var1) {
        this.this$0 = var1;
    }

    public void showData(String var1) {
        System.out.println("Hello world!");
    }
}

可以看到,编译后生成的 Main$1.class,在构造函数上,自动添加了对Main的强引用,因此内存泄露的产生也就是由于这个强引用引起的,在没有对其进行合理的释放,就造成了内存泄露。


这里说的非静态内部类,在Android中可以总结为一下几种情况,大家都有遇到过。

场景样例说明
setOnXXXListener
addOnXXXListener
setOnClickListener(xxx)
setOnLongClickListener(xxx)
在Android中,Activity、Fragment、View等等,有很多监听接口,在使用这些接口时,一般都是在该类文件中直接调用的。这个时候,在设置相关的回调时就需要特别注意,匿名内部类会持有对应类的实例,没有及时释放就会导致内存泄露。
一般做法有两种:
1)匿名内部类改为静态内部类,需要Context的时候优先使用getApplicationContext,或者使用弱引用持有;
2)在页面销毁使,主动调用setOnXXXListener(null),及时清空;
自己实现的回调方法setOnXXXCallback类封装的时候,通常都会需要一些回调方法进行交互,这个时候也会像上面说的情况一样,解决办法也是一样的。

2、不注意使用Context引起泄露

2.1、单例模式引起的内存泄露

由于单例模式的静态特性,使得它的生命周期和我们的应用一样长,如果让单例无限制的持有Activity的强引用就会导致内存泄漏。

public class UserInfoBean {
    private static UserInfoBean userInfoBean;

    private Context mContext;

    private UserInfoBean(Context context) {
        // 这里的mContext会长期持有Context实例,因此在使用的时候,就会出现内存泄露。
        this.mContext = context;
        
        // 这里可以将context改成 context.getApplicationContext(),这样就不用当心外部使用者不注意时造成内存泄露。
    }

    public static UserInfoBean getUserInfoBean(Context context) {
        if (userInfoBean == null) {
            synchronized (UserInfoBean.class) {
                if (userInfoBean == null) {
                    userInfoBean = new UserInfoBean(context);
                }
            }
        }
        return userInfoBean;
    }
}

同时,还需要注意下,单例模式中,成员属性持有的内容,需要在不在使用的时候及时清理掉,不然也会造成内存泄露以及内存空间的浪费。

2.2、注册监听器引起的内存泄露

系统服务可以通过Context.getSystemService 获取,它们负责执行某些后台任务,或者为硬件访问提供接口。如果Context 对象想要在服务内部的事件发生时被通知,那就需要把自己注册到服务的监听器中。然而,这会让服务持有Activity 的引用,如果在Activity onDestory时没有释放掉引用就会内存泄漏。

// 这里直接使用页面的Context
mSensorManager = (SensorManager) this.getSystemService(Context.SENSOR_SERVICE);

// 需要改成ApplicationContext
mSensorManager = (SensorManager) getApplicationContext().getSystemService(Context.SENSOR_SERVICE);

2.3、Context被当作参数传递到另外一个单例对象的回调中引起内存泄露

// 这是一个单例模式的方法
private void openLiteAppInner(Context context, LiteAppBean liteAppBean, OnOpenCallback callback) {
    // 先显示 Loading UI
    showLoadingUI(context);

    OnOpenCallback onOpenCallback = new OnOpenCallback() {
        @Override
        public void onSuccess(int type) {
            if (null != callback) {
                callback.onSuccess(type);
            }
        }

        @Override
        public void onFailed(int type, String code, String message) {
            if (null != callback) {
                callback.onFailed(type, code, message);
            }

            // 失败的时候需要主动关闭 Loading UI.
            // 这里使用context导致内存泄露
            hideLoadingUI(context);
        }
    };

    // 保存打开小程序的历史记录
    saveOpenHistoryRecord(liteAppBean);

    // 根据类型,调用不同的服务打开小程序
    switch (liteAppBean.getType()) {
        case LiteAppBean.TYPE_OWN:
            openOwnLiteApp(liteAppBean, onOpenCallback);
            break;
        case LiteAppBean.TYPE_WECHAT:
            openWeChatLiteApp(liteAppBean, onOpenCallback);
            break;
    }
}

// 修改方案
private void openLiteAppInner(Context context, LiteAppBean liteAppBean, OnOpenCallback callback) {
    // 先显示 Loading UI
    showLoadingUI(context);

    // 避免内存泄露
    WeakReference<Context> refContext = new WeakReference<>(context);
    OnOpenCallback onOpenCallback = new OnOpenCallback() {
        @Override
        public void onSuccess(int type) {
            if (null != callback) {
                callback.onSuccess(type);
            }
        }

        @Override
        public void onFailed(int type, String code, String message) {
            if (null != callback) {
                callback.onFailed(type, code, message);
            }

            // 失败的时候需要主动关闭 Loading UI.
            // 这里改成弱引用
            hideLoadingUI(refContext.get());
        }
    };

    // 保存打开小程序的历史记录
    saveOpenHistoryRecord(liteAppBean);

    // 根据类型,调用不同的服务打开小程序
    switch (liteAppBean.getType()) {
        case LiteAppBean.TYPE_OWN:
            openOwnLiteApp(liteAppBean, onOpenCallback);
            break;
        case LiteAppBean.TYPE_WECHAT:
            openWeChatLiteApp(liteAppBean, onOpenCallback);
            break;
    }
}

对需要使用Context进行调用和获取的内容,优先使用getApplicationContext(),不能满足的情况下,再使用弱引用方式持有进行实现。

3、Handler引起的内存泄露

当Handler中有延迟的的任务或是等待执行的任务队列过长,由于消息持有对Handler的引用,而Handler又持有对其外部类的潜在引用,这条引用关系会一直保持到消息得到处理,而导致了Activity无法被垃圾回收器回收,而导致了内存泄露。因此,使用Handler的地方,都需要在视图销毁时,主动使用handler.removeCallbacksAndMessages(null); 进行清除。

@Override
protected void doOnDestroy() {        
    super.doOnDestroy();        
    if (mHandler != null) {
        mHandler.removeCallbacksAndMessages(null);
    }
    mHandler = null;
    mRenderCallback = null;
}

Thread、TimerTask 等异步任务都有可能存在类似的问题。

4、静态对象没有及时清理导致的内存泄露

public class MainActivity extends AppCompatActivity {

    private static Info sInfo;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (sInfo != null) {
            sInfo = new Info(this);
        }
    }
}

class Info {
    public Info(Activity activity) {
    }
}

Info 作为 Activity 的静态成员,并且持有 Activity 的引用,但是 sInfo 作为静态变量,生命周期肯定比 Activity 长。

所以当 Activity 退出后,sInfo 仍然引用了 Activity,Activity 不能被回收,这就导致了内存泄露。

在Android开发中,静态持有很多时候都有可能因为其使用的生命周期不一致而导致内存泄露,所以我们在新建静态持有的变量的时候需要多考虑一下各个成员之间的引用关系,并且尽量少地使用静态持有的变量,以避免发生内存泄露。当然,我们也可以在适当的时候讲静态量重置为null,使其不再持有引用,这样也可以避免内存泄露。

5、属性动画造成的内存泄露

动画同样是一个耗时任务,比如在Activity中启动了属性动画(ObjectAnimator),但是在销毁的时候,没有调用cancle方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在的控件引用Activity,这就造成Activity无法正常释放。因此同样要在Activity销毁的时候cancel掉属性动画,避免发生内存泄漏。

@Override
protected void onDestroy() {
    super.onDestroy();
    // 需要及时释放掉
    mAnimator.cancel();
}

6、WebView造成内存泄露

因为WebView在加载网页后会长期占用内存而不能被释放,因此我们在Activity销毁后要调用它的destory()方法来销毁它以释放内存。

@Override
public void onDestroy() {
    if (webView != null) {
        // 在销毁WebView之前需要先将WebView从父容器中移除,然后在销毁WebView。
        // 详细分析过程请参考这篇文章:WebView内存泄漏解决方法。
        // https://blog.csdn.net/xygy8860/article/details/53334476
        ViewParent parent = webView.getParent();
        if (null != parent) {
            ((ViewGroup) parent).removeView(webView);
        }
        webView.stopLoading();
        // 退出时调用此方法,移除绑定的服务,否则某些特定系统会报错
        webView.getSettings().setJavaScriptEnabled(false);
        webView.clearHistory();
        webView.clearView();
        webView.removeAllViews();
        webView.destroy();
        webView = null;
    }

    super.onDestroy();
}

7、循环引用导致实例不能被释放

避免代码设计模式的错误造成内存泄露;譬如循环引用,A持有B,B持有C,C持有A,这样的设计谁都得不到释放。