内存泄露是指,由于调用栈中持有对象的强引用,使得垃圾回收器无法回收内存中的这个对象。
文章目录
- 一、8 种内存泄露核查
- 二、Android 中的6种内存泄漏场景
- 1 资源
- 2 Handler
- 3 Thread
- 4 Context
- 5 集合
- 6 非静态内部类
- 三、检测内存泄漏
- dumpsys
- Profile
- 使用 LeakCamary 自动检测APP中的内存泄露
- P.S. 推荐阅读
一、8 种内存泄露核查
8 种内存泄露的 Demo:https://github.com/NimbleDroid/Memory-Leaks
全局进程的static变量(措施:销毁 Activity 时将静态变量置空):
- static Activity:类中定义了静态Activity变量,泄露 Activity
- static View:类中定义了静态 View 变量,泄露 Context -> Activity
- static Inner Class:Activity中定义了一个非静态的内部类(包括匿名内部类),创建该内部类对象由静态变量引用;泄露 Outer Class -> Activity
活在 Activity 生命周期之外的线程(措施:清空对 Activity 的强引用):
- Anonymous AsyncTask:匿名的 AsyncTask 在后台执行耗时任务,Activity 不幸销毁,在异步任务结束前,泄露 Outer Class -> Activity
- Handler:定义匿名的 Runnable 发送给 Handler,或用非静态内部类 Handler 执行消息,在 Message 执行前,泄露 MessageQueue -> Message -> Handler/Runnable -> Outer Calss -> Activity
- 非静态内部类(包括匿名内部类) Thread:在 Thread 运行结束前,泄露 OuterClass
- 非静态内部类(包括匿名内部类) TimerTask:在 TimerTask 运行结束前,泄露 OuterClass
- 系统服务未取消监听器:注册监听器时,服务持有了 Context 引用,销毁时如果没有取消监听器,泄露 Context
防止内存泄露的手段:
- 上述有 4 种都和非静态内部类有关,非静态内部类会隐式地强引用外部类,所以常常是建议使用 static 内部类,包括 static AsyncTask、Handler、Thread、TimerTask,如果坚持使用匿名类,记得在 Activity 生命周期结束时中断线程;
- 避免在 static 变量中保存可能引用 Context 的对象
- 弱引用代替强引用
- 在 Activity.onDestroy 时注销监听器
二、Android 中的6种内存泄漏场景
1 资源
资源性对象(Cursor,File等)往往用了缓冲,不仅存在于Java虚拟机中,还存在于Java虚拟机外。仅仅把引用设置为null,而不关闭,往往会内存泄漏。
但不会导致内存溢出,因为资源性对象的finalize方法中往往会再调用一次close方法,但效率低,频繁GC影响系统性能。
Tips:在资源性对象不再使用时,先调用close函数关闭,再置为null。
2 Handler
Handler的运行涉及到Message、MessageQuere、Looper。
主线程的Looper除非程序退出,否则一直存在。存在两条可能内存泄漏的引用链:
- Looper -> MessageQueue -> Message -> Handler -> Activity
- Looper -> MessageQueue -> Message -> Runnable -> Activity
Tips:Handler和Runnable使用静态内部类或lambda表达式创建的匿名类。
(JDK 1.8之后,通过lambda表达式创建的匿名类中,如果没有引用外部类,则编译时生成的内部类不引用外部类)
Tips:在Activity的onDestroy时,移除自定义Handler的消息。
@Override protected void onDestroy() { super.onDestroy(); handler.removeCallbacksAndMessaged(null); }
3 Thread
线程的run方法执行结束前,线程不会销毁,可能会泄漏所引用的Activity。
Tips:创建新线程时,要么切断线程对Activity的引用,要么在Activity的onDestroy方法中停止线程。
HandlerThread可以调用quitSafely,普通Thread可以用interrupt设置中断标记来停止(stop太暴力)。
class MyThread extends Thread { @Override pulic void run() { while (!isInterrupted()) { // do something } } } @Override protected void onDestroy() { super.onDestroy(); mMyThread.interrupt(); }
4 Context
应用中Context数量 = Activity个数 + Service个数 + 1(ApplicationContext)
当生命周期长于 Activity/Service的对象,持有其引用时,会泄漏Context。比如,ToastUitls。
Tips:生命周期长于 Activity/Service的对象,使用ApplicationContext。
5 集合
Tips:如果确定一个键不再使用了,先从集合中remove,再置为null;或者考虑使用WeakHashMap,弱引用Key,并且会自动清除。
6 非静态内部类
三、检测内存泄漏
dumpsys
查看退出Activity和Service后,AppContexts和Activities数量是否减少
Profile
Android Studio调试时,主动触发GC后,可以再生成堆的存储信息 hprof 文件。
通过platform-tools中的hprof-conv.exe将抓取的hprof 转换成eclipse MAT中可打开的格式。
通过独立版本的eclipse MAT(Memory Analyzer Tool)定位分析发生泄漏的代码:http://www.eclipse.org/mat/downloads.php
举个例子:
public class ToastUtils {
private static ToastUtils sInstance = null;
private Toast mToast;
private ToastUtils() {
}
public static ToastUtils getInstance() {
if (sInstance == null) {
sInstance = new ToastUtils();
}
return sInstance;
}
public void show(Context context, String msg, int duration) {
if (mToast != null) {
mToast.cancel();
mToast = null;
}
mToast = Toast.makeText(context, msg, duration);
mToast.show();
}
public void cancel() {
if (mToast != null) {
mToast.cancel();
mToast = null;
}
}
}
调用show,而没有调用cancel会导致mToast保存在静态引用中,导致Context的泄漏。
public class ContextLeakActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_context_leak);
}
public void switchToAnotherActivity(View view) {
ToastUtils.getInstance().show(this, "show a toast", Toast.LENGTH_LONG);
startActivity(new Intent(this, MainActivity.class));
finish();
}
}
MAT 分析引用用链(只保留强引用):
使用 LeakCamary 自动检测APP中的内存泄露
及其简单,一步到位:
dependencies {
// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
}