分析Android常见的内存泄露和解决方案
作者:huansky
一、前言
目前 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 内存泄露和解决方案的资料请关注脚本之家其它相关文章!