关于Android应用程序内存泄漏 你需要知道的一切
原文:https://blog.aritraroy.in/everything-you-need-to-know-about-memory-leaks-in-android-apps-655f191ca859
作者:Aritra Roy
垃圾收集器是你的朋友,但并非总是如此
Java内置了专用的内存管理系统,可以在适当的时候自动释放内存,那为什么我需要关注这一切。垃圾收集器有问题吗?
不,肯定不是。**垃圾收集器的工作正常,**但是我们自己的编程错误,有时会阻止垃圾收集器的正常工作。
所以是我们自己的错,导致了所有混乱。垃圾收集器是Java最好的成就之一,值得尊重。
关于垃圾收集器的更多内容
垃圾收集器如何工作的:
每个Android(或Java)应用程序都有一个起点,从对象开始实例化并调用方法。因此,我们可以将此起点视为内存树的“根”。一些对象直接引用“根”,其他对象从它们实例化,持有对这些对象的引用,依此类推形成了引用链。
因此,垃圾收集器从GC根开始并遍历直接或间接链接到根的对象。在此过程结束时,有一些对象从未被GC访问过。
这些是你的垃圾(或死亡的对象),这些是我们心爱的垃圾收集器有资格收集的垃圾。
到目前为止,这个故事看起来像是一个童话故事。现在让我们深入挖掘它,真正的乐趣开始了。
了解更多有关垃圾收集器的信息:
https://www.youtube.com/watch?v=UnaNQgzw4zY
http://www.cubrid.org/blog/dev-platform/understanding-java-garbage-collection/
什么是内存泄漏?
简单来说,当一个对象目的达到,很久之后你仍然持有此对象时,就会发生内存泄漏。
每个对象都有自己的生命周期,之后需要说再见并释放内存。但是如果某些其他对象(直接或间接)持有此对象,则垃圾收集器将无法收集它。而这正是一个内存泄漏。
但好消息是,您不必过多担心应用中发生的每一次内存泄漏。并非所有内存泄漏都会损害您的应用。
有一些泄漏实际上很小(泄漏了几千字节的内存),并且Android框架本身有一些(是的,你读得正确;-)),你不能或不需要修复。这些通常会对您应用的效果产生最小影响,并且可以安全地忽略。
但是还有其他一些情况可能会让你的应用程序崩溃。这些是你需要注意的。
为什么需要关注修复内存泄漏?
没有人想使用一个缓慢,滞后,消耗大量内存或使用几分钟后崩溃的应用程序。这是一个非常糟糕的体验,如果长久以来一直如此,那么你很可能会永远失去该用户。
当用户使用您的应用程序时,堆内存也会不断增加,如果您的应用程序中存在内存泄漏,则GC无法释放堆中未使用的内存。因此,应用程序的堆内存将不断增加,直到达到临界点,从而无法为您的应用程序分配更多内存,从而导致可怕的OutOfMemoryError并最终导致应用程序崩溃。
您还必须记住垃圾收集过程会消耗性能,因此垃圾收集器运行的越少,您的应用程序性能就越好。
随着应用程序的使用和堆内存不断增加,一个简短的GC将启动并尝试清除直接死对象。现在这些短的GC并发(在一个单独的线程上),轻微减慢你的应用程序(2-5毫秒暂停)。
但是如果你的应用程序在隐藏了一些严重的内存泄漏,那么这些短的GC将无法回收内存,并且堆将继续增加,从而迫使更大的GC启动,这通常是“stop the world“GC。触发这个GC将暂停整个应用程序主线程(大约50-100毫秒),从而使您的应用程序严重卡顿,有时几乎无法使用一段时间。
如何检测这些内存泄漏?
Android Studio有一个非常有用和强大的工具,监视器(Monitors)。监视器不仅用于内存使用,还用于网络,CPU和GPU使用(这里有更多信息)。
在调试应用程序时,您应该密切关注此内存监视器。内存泄漏的第一个症状是,当您使用应用程序时,内存占用会不断增加,即使您将应用程序置于后台,内存占用也不会下降。
该分配追踪器就派上用场了,你可以用它来检查应用程序的内存分配情况,查看不同类型的对象占用内存的百分比。您可以清楚地了解哪些对象占用的内存最多,需要专门解决。
您现在需要使用Dump Java Heap选项来创建heap dump,也就是内存快照。
我们的工程师往往很懒,这正是LeakCanary**拯救我们的地方。**该库与您的应用程序一起运行,在需要时转储内存,查找潜在的内存泄漏并为您提供通知,其中包含干净且有用的堆栈跟踪以查找泄漏的根本原因。
*LeakCanary使任何人都可以非常轻松地检测应用程序中的泄漏。*非常感谢Py(来自Square)开发了一个如此节约生命的库。
详细了解如何充分利用此库,请查看此内容。
一些常见的内存泄漏场景以及解决方案
未取消注册的监听器
在许多情况下,您在Activity(或Fragment)中注册了一个监听器,但忘记取消注册它。如果你运气不好,这很容易导致巨大的内存泄漏。所以如果你在某个地方注册了监听,你还需要在那里取消注册。
现在让我们通过一个简单的例子来看看它。假设您想在应用程序中接收位置更新,那么您需要做的就是获取LocationManager系统服务并注册一个用于位置更新的监听器。
private void registerLocationUpdates(){
mManager = (LocationManager)getSystemService(LOCATION_SERVICE);
mManager.requestLocationUpdates(LocationManager. GPS_PROVIDER , TimeUnit. MINUTES .toMillis(1), 100, this);
}
您在Activity中实现了监听器接口,*因此LocationManager保留了对它的引用。*现在当你的Activity死掉的时候,Android框架会调用它上面的onDestroy()
,但垃圾收集器将无法从内存中删除实例,因为LocationManager仍然保留了对它的强引用。
**解决方案非常简单。**只需在onDestroy()
方法中取消注册监听器,就可以了。这就是我们大多数人忘记或甚至不知道的事情。
内部类
内部类在Java中非常常见,并且由于其简单性而被许多Android开发人员用于各种任务。但由于使用不当,这些内部类也可能导致潜在的内存泄漏。
让我们再次借助一个简单的例子来看看:
public class BadActivity extends Activity {
private TextView mMessageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_bad_activity);
mMessageView = (TextView) findViewById(R.id.messageView);
new LongRunningTask().execute();
}
private class LongRunningTask extends AsyncTask<Void, Void, String> {
@Override
protected String doInBackground(Void... params) {
return "Am finally done!";
}
@Override
protected void onPostExecute(String result) {
mMessageView.setText(result);
}
}
}
这是一个非常简单的Activity,它在后台线程中启动一个长时间运行的任务(可能是复杂的数据库查询或慢速网络调用)。任务完成后,结果显示在TextView中。好像一切正常?
不,当然不是。这里的问题是非静态内部类包含对外部封闭类的隐式引用(即,Activity本身)。现在,如果我们旋转屏幕或者这个LongRunningTask
的任务比Activity的生命周期长,那么它不会让垃圾收集器从内存中收集整个Activity实例。一个简单的错误导致巨大的内存泄漏。
但是解决方案也非常简单:
public class GoodActivity extends Activity {
private AsyncTask mLongRunningTask;
private TextView mMessageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_good_activity);
mMessageView = (TextView) findViewById(R.id.messageView);
mLongRunningTask = new LongRunningTask(mMessageView).execute();
}
@Override
protected void onDestroy() {
super.onDestroy();
mLongRunningTask.cancel(true);
}
private static class LongRunningTask extends AsyncTask<Void, Void, String> {
private final WeakReference<TextView> messageViewReference;
public LongRunningTask(TextView messageView) {
this.messageViewReference = new WeakReference<>(messageView);
}
@Override
protected String doInBackground(Void... params) {
String message = null;
if (!isCancelled()) {
message = "I am finally done!";
}
return message;
}
@Override
protected void onPostExecute(String result) {
TextView view = messageViewReference.get();
if (view != null) {
view.setText(result);
}
}
}
}
正如你所看到的,我们将非静态内部类改为静态内部类,静态内部类不包含对其封闭外部类的任何隐式引用。 但是现在我们无法从静态上下文访问外部类的非静态变量(如TextView),因此我们必须通过其构造函数将所需的对象引用传递给内部类。
强烈建议在WeakReference中包装这些对象引用以防止进一步的内存泄漏。你需要开始学习有关Java中的引用类型以及如何善用它们,以避免内存泄漏。
匿名类
匿名类是许多开发人员的最爱,因为它们的定义方式使得使用它们编写代码变得非常简单和简洁。但根据我的经验,这些匿名类是内存泄漏的最常见原因。
匿名类只是非静态内部类,它可能导致潜在的内存泄漏,因为我之前谈到的原因相同。您一直在应用中的多个位置使用它,但不知道如果使用不当会严重影响您应用的性能。
public class MoviesActivity extends Activity {
private TextView mNoOfMoviesThisWeek;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_movies_activity);
mNoOfMoviesThisWeek = (TextView) findViewById(R.id.no_of_movies_text_view);
MoviesRepository repository = ((MoviesApp) getApplication()).getRepository();
repository.getMoviesThisWeek()
.enqueue(new Callback<List<Movie>>() {
@Override
public void onResponse(Call<List<Movie>> call,
Response<List<Movie>> response) {
int numberOfMovies = response.body().size();
mNoOfMoviesThisWeek.setText("No of movies this week: " + String.valueOf(numberOfMovies));
}
@Override
public void onFailure(Call<List<Movie>> call, Throwable t) {
// Oops.
}
});
}
}
在这里,我们使用非常流行的库Retrofit进行网络调用并在TextView中显示结果。很明显,Callable对象保持对封闭的Activity类的引用。
现在,如果此网络调用在非常慢的连接上运行,并且在调用结束之前,以某种方式旋转或销毁Activity,则 整个Activity实例将被泄露。
始终建议尽可能使用静态内部类而不是匿名类。并不是说我突然告诉你完全停止使用匿名类,但你必须理解并判断何时使用它们是安全的,何时不能。
位图
您在应用中看到的每个图像都只是包含图像的整个像素数据的位图对象。
现在这些位图对象通常很重,如果处理不当会导致严重的内存泄漏,最终可能会因OutOfMemoryError导致应用程序崩溃。与您在应用程序中使用的图像资源相关的位图内存全部由框架本身自动管理,但如果您手动处理位图,请确保在使用后 recycle()
回收它们。
您还必须学习如何正确管理这些位图,比如通过缩小位图来加载大位图,并尽可能使用位图缓存和位图池来减少内存使用。这是一个很好的资源,可以详细了解位图处理。
上下文
内存泄漏的另一个常见原因是滥用上下文实例。Context仅仅是一个抽象类,有许多类(如Activity,Application,Service等)是其扩展,以提供他们自己的功能。
如果你想在Android中完成任务,那么Context对象就是你的最佳选择。
但这些Context之间存在差异。理解活动级上下文和应用程序级上下文之间的区别以及在什么情况下应该使用哪个上下文非常重要。
在错误的位置使用活动上下文可以保持对整个活动的引用并导致潜在的内存泄漏。这是一篇很好的文章,供您开始使用。
结论
现在您必须知道垃圾收集器的工作原理,内存泄漏是什么以及它们如何对您的应用产生重大影响。您还了解了如何检测和修复这些内存泄漏。
让我们从现在开始构建高质量,高性能的Android应用程序。检测和修复内存泄漏不仅会使您的应用程序的用户体验更好,而且会慢慢使您成为更优秀的开发人员。
本文最初发布于TechBeacon。