首页 > 编程语言 > 分析Android常见的内存泄露和解决方案
2021
09-29

分析Android常见的内存泄露和解决方案

一、前言

目前 java 垃圾回收主流算法是虚拟机采用 GC Roots Tracing 算法。算法的基本思路是:通过一系列的名为 GC Roots (GC 根节点)的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径,当一个对象到GC Roots没有任何引用链相连(图论说:从GC Roots 到这个对象不可达)时, 证明此对象是不可用的。

关于可达性的对象,便是能与 GC Roots 构成连通图的对象,如下图:

根搜索算法的基本思路就是通过一系列名为 "GC Roots" 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链 ( Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。

从上图,reference1、reference2、reference3 都是 GC Roots,可以看出:

reference1-> 对象实例1;

reference2-> 对象实例2;

reference3-> 对象实例4;

reference3-> 对象实例4 -> 对象实例6;

可以得出对象实例1、2、4、6都具有 GC Roots 可达性,也就是存活对象,不能被 GC 回收的对象。

而对于对象实例3、5直接虽然连通,但并没有任何一个 GC Roots 与之相连,这便是 GC Roots 不可达的对象,这就是 GC 需要回收的垃圾对象。

在了解 GC 之后,开始去了解 Android 的内存泄露情况了。

二、Android 内存泄露场景

下面会详细介绍一些常见的内存泄露场景,以及对应的修复办法。

2.1、非静态内部类的静态实例

比如我们在 Activity 内部定义了一个内部类InnerClass,同时定义了一个静态变量inner,并给予赋值。假设你在 onDestory 的时候没有将 inner 置 null;那么就会引起内存泄露。原因是静态变量持有了内部类的实例,内部类会对外部类有个引用,从而导致 Activity 得不到释放。

private static Object inner;
void createInnerClass() {
    class InnerClass {
    } 
    inner = new InnerClass();
}
View icButton = findViewById(R.id.ic_button);
    icButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        createInnerClass();
        nextActivity();
    }
});

记得在生命周期结束的时候,将不需要的静态变量置 null。

2.2、多线程相关的匿名内部类/非静态内部类

和非静态内部类一样,匿名内部类也会持有外部类实例的引用。多线程相关的类有 AsyncTask 类,Thread 类和 Runnable 接口的类等,它们的匿名内部类如果做耗时操作

就可能发生内存泄露,这里以 AsyncTask 的匿名内部类举例,如下所示:

void startAsyncTask() {
    new AsyncTask<Void, Void, Void>() {
        @Override protected Void doInBackground(Void... params) {
            while(true);
        }
    }.execute();
}

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View aicButton = findViewById(R.id.at_button);
aicButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        startAsyncTask();
        nextActivity();
    }
});

当异步任务在后台执行耗时任务期间,Activity 不幸被销毁了(比如:用户退出,系统回收),这个被 AsyncTask 持有的 Activity 实例就不会被垃圾回收器回收,直到异步任务结束。

解决方法是继承 AsyncTask 新建一个静态内部类,用静态内部类创建实例就不会存在对外部实例的引用了。

2.3、Handler 内存泄露

同样道理,Handler 的 message 被传递到消息队列MessageQueue中,在Message消息没有被处理之前,handler 的实例也不无法被回收,如果 handler 实例不是静态的,就会导致引用它的 activity 或者 service 不能被回收,于是就会发生内存泄漏。

void createHandler() {
    new Handler() {
        @Override public void handleMessage(Message message) {
            super.handleMessage(message);
        }
    }.sendMessageDelayed(Message.obtain(), 60000);
}


View hButton = findViewById(R.id.h_button);
hButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        createHandler();
        nextActivity();
    }
});

对于上述问题,有两种解决办法,一种是使用一个静态的 handler 内部类,并且其持有的对象都改成弱引用形式进行引用。还有一种是在销毁 activity 的时候,将发送的消息进行移除。

myHandler.removeCallbackAndMessages(null);

这种有个问题就是 Handler 中的消息可能无法全部被处理完。

另外还有一个要注意的是,最好不要直接使用 View#post 来做一些操作。如果要用,确保要用的话,确保 view 已经被 attach 到了 window。

2.4、静态 Activity 或 View

在类中定义了静态Activity变量,把当前运行的Activity实例赋值于这个静态变量。
如果这个静态变量在Activity生命周期结束后没有清空,就导致内存泄漏。因为 static 变量是贯穿这个应用的生命周期的,所以被泄漏的Activity就会一直存在于应用的进程中,不会被垃圾回收器回收。

static Activity activity;
void setStaticActivity() {
    activity = this;
}

View saButton = findViewById(R.id.sa_button);
saButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
    setStaticActivity();
    nextActivity();
    }
});

为了能够被回收,需要在不需要使用的时候进行置 null 操作。比如销毁当前 activity 的时候。

特殊情况:如果一个 View 初始化耗费大量资源,而且在一个Activity生命周期内保持不变,那可以把它变成 static,加载到视图树上 (View Hierachy),像这样,当Activity被销毁时,应当释放资源。

static view;
void setStaticView() {
    view = findViewById(R.id.sv_button);
}

View svButton = findViewById(R.id.sv_button);
svButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
    setStaticView();
    nextActivity();
    }
});

同样的,为了解决内存泄露的问题,在 Activity 销毁的时候把这个 static view 置 null 即可,但是还是不建议用这个 static view的方法。

2.5、Eventbus 等注册监听造成的内存泄露

相信很多同学都在项目里面会用到 Eventbus。对于一些没有经验的同学在使用的时候经常会出现一些问题。比如说在 onCreate 的时候进行注册,却忘了反注册,或者说,在onStop的时候进行反注册,这些都会导致 Eventbus 的内存泄露。

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    EventBus.getDefault().register(this);// 注意在onCreate()方法中注册
}

@Override
public void onDestroy() {
    EventBus.getDefault().unregister(this);// 注意在onDestory()方法中注册
    super.onDestroy();
}

注册和反注册(取消注册)是对应的,必须要添加,否则会引起组件的内存泄漏。因为注册的时候组件是被 EventBus 内部的单例队列所持有引用的。

如果你是在 View 里面注册 Eventbus 的,记得是在 View 的生命周期 onAttachedToWindow 和 onDetachedFromWindow 的时候进行注册和反注册。

最近跟我的同事进行聊天的时候发现,他们为了解决 eventbus 导致的内存泄露问题(已经成对注册和反注册还是存在内存泄露问题),于是打算创建一个 object 的实例,用这个来进行注册与反注册,这样即使发生内存泄露也只会占用很小的内存空间。

2.6、单例引起的内存泄露

项目中,经常会存在很多单例。有时候需要我们将当前 Activity 实例传给单例,然后去做一些事情。如下面的代码:

public class SingleInstance {
    private Context mContext;
    private static SingleInstance instance;
 
    private SingleInstance(Context context) {
        this.mContext = context;
    }
 
    public static SingleInstance getInstance(Context context) {
        if (instance == null) {
            instance = new SingleInstance(context);
        }
        return instance;
    }
}

上述单例中传入一个 context ,就会导致 context 的生命时长和应用的生命时长一样。就会造成内存泄露。

对于这种有三种解决办法:

1、采用弱引用的方式进行引用,确保能够被回收;

2、在对应的 context 要被销毁的时候,进行置 null;确保不会长于原本的生命时长;

3、看是否能够使用 APP context;这样就不会存在内存泄露的问题了。

2.7、资源对象没关闭造成内存泄漏

当我们打开资源时,一般都会使用缓存。比如读写文件资源、打开数据库资源、使用 Bitmap 资源等等。当我们不再使用时,应该关闭它们,使得缓存内存区域及时回收。虽然有些对象,如果我们不去关闭,它自己在 finalize() 函数中会自行关闭。但是这得等到 GC 回收时才关闭,这样会导致缓存驻留一段时间。如果我们频繁的打开资源,内存泄漏带来的影响就比较明显了。

解决办法:及时关闭资源

2.8、WebView

不同的Android 版本的 webView 会有差异,加上不同的厂商定制的 ROM 的 webView 差异,这就导致 webView 存在很大的兼容性问题。weView 都会存在内存泄露问题,在应用中只要使用一次,内存就不会被释放。通常的做法是为 webView 单独开一个进程,使用 AIDL 与应用的主进程进程通信。webView 进程可以根据业务的需求,在合适的时机进行销毁。

以上就是分析Android常见的内存泄露和解决方案的详细内容,更多关于Android 内存泄露和解决方案的资料请关注自学编程网其它相关文章!

编程技巧