前言
内存泄漏, 一个说大不大说下不小的瑕疵. 作为开发者, 我们都很清楚内存泄漏是我们代码问题导致的. 但是话说回来, 泄漏后果会很严重嘛? 这不好说, 如果我们不泄漏 Bitmap 这种大内存的对象, 那么修补内存泄漏就像鸡肋一样,"食之无味, 弃之可惜". 就比如说我们项目组, 近 2000w 的 DAU, 只要不明显影响用户体验, 一切以上需求为主...
但是这作为一个 996 福报码农, 不能只挖坑, 不填坑, 毕竟技术债都是要还的. 所以今天咱们来聊一聊 Android 中的内存泄漏. 这篇文章总结翻译了外国友人的一篇文章: 原文如下
techbeacon.com/App-dev-tes...
一, 理论
先上一张图:
解释一下这张图, 每个 Android(或 Java)应用程序都有一个起点(GC Root), 从这个点中实例化对象, 调用方法.. 一些对象直接引用 GC Root, 另一些对象又引用了这些对象. 因此, 形成了引用链, 就像上图一样. 因此垃圾收集器从 GC Root 开始并遍历直接或间接链接到 GC Root 的对象. 在此过程结束时, 脱离 GC Root 的对象 / 对象链将被回收.
接下来咱们再想另一个问题:
什么是内存泄漏?
有了上图, 理解内存泄漏的概念就很简单, 说白了就是: 长生命周期对象 A 持有了短生命周期的对象 B, 那么只要 A 不脱离 GC Root 的链, 那么 B 对象永远没有可能被回收, 因此 B 就泄漏了.
有什么危害?
危害的话, 如开篇所说. 如果泄漏的内存很小, 几字节, 几 kb.... 对于现在的机器性能, 就像星爵打灭霸..."伤害" 基本无视. 但是如果泄漏的足够多, 普通的 GC 无法回收这些泄漏的内存, 那么堆将持续增加, 当堆足够大的时候, 就会触发 "stop-the-world" GC, 直接在主线程进行耗时的 GC.
主线程进行耗时操作, 每一个 Android 开发者都明白这意味着什么....
所以内存泄漏足够严重, 其危害还是很严重的.
二, 实践
对于我们日常开发来说, 有比较多的场景稍不注意就会存在内存泄漏的风险. 让我们一起留意一下:
2.1, 内部类 Inner classes
内部类存在内存泄漏的风险, 是一个老生常谈的话题. 说白了就是因为我们在 new 一个内部类时, 编译器会在编译时让这个内部类的实例持有外部对象.
这也就是, 为啥我们的内部类可以引用到外部类变量, 方法的原因.
上段代码:
- 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);
- }
- }
- }
大家应该都能看出这里的问题吧. 作为非静态内部类的 LongRunningTask, 会持有 BadActivity. 并且 LongRunningTask 是一个长时间任务, 也就是说, 在这个任务没有完成时, BadActivity 是不会被回收的, 因此我们的 BadActivity 就被泄漏了. 那么怎么改呢?
解决原理
首先我不能让 LongRunningTask 持有 BadActivity. 那么我们需要使用静态内部类(static class). 这样的确不会持有 BadActivity, 但是问题来了, 我们 LongRunningTask 不持有 BadActivity, 也就意味着没办法引用到 BadActivity 中的变量, 那么我们的更新 UI 的操作就做不了, 也就是说还是要显示的传一个 BadActivity 中我们需要的变量进来... 但是这样有造成了同样的泄漏问题.
因此, 我们需要对传入的变量使用 WeakReference 进行包一层. 但发生 GC 的时候, 告诉 GC 收集器 "我" 可以被回收.
上改造后的代码:
- 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);
- }
- }
- }
- }
2.2, 匿名类 Anonymous classes
这一类和 2.1 很类似. 本质都是持有外部对象的引用.
上一段很常见的代码:
- 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.
- }
- });
- }
- }
2.3, 注册 Listener
- SingleInstance.setMemoryLeakListener(new OnMemoryLeakListener(){
- //.....
- })
这里写了段很常见的伪码, 一个单例的对象, register 了一个 Listener, 并且这个 Listener 被单例的一个成员变量引用.
OK, 那么问题很明显了. 单例作为静态变量, 肯定是一直存在的. 而其内部持有了 Listener, 而 Listener 作为一个匿名类, 有持有了外部对象的引用. 因此这条 GC 链上的所有对象都不会被释放.
解决也很简单, 适当的时机, 在单例中将 Listener 的引用置为 null. 这样, Listener 和单例之间的引用关系断了, Listener 链上的所有内容就可以被正常释放掉了. 也就是咱们常做的在 onDestory()进行 unRegisterListener 的操作.
类似不注意的内容, 还包括 Lambda. 不过有一点值得注意的, 在 Kotlin 的 Lambda 中, 如果我们没有使用外部对象的变量或者方法, 那么 Kotlin 在编译时, 这个 Lambda 是不会持有外部对象的引用的. 也算是 Kotlin 的一些优化吧
2.4,Contexts
上下文的滥用, 也是泄漏的大客户. 不过大家针对这类问题应该比较熟悉.
比如: 长时间存活的对象, 不建议持有 Activity 的 context, 而是使用 ApplicationContext. 如果 ApplicationContext 没办法完成业务, 那么就需要好好考虑一下: 这个长时间存活的对象, 为什么必须要持有 Activity 的 context. 它设计的是否合理, 是否它应该是一个长时间存活的对象(比如单例).
尾声
关于内存泄漏, 还是需要咱们平时多注意, 对自己写的每一行代码都多思考. 毕竟这东西 "不是病, 但疼起来真要命".
最后
来源: http://www.jianshu.com/p/9c887bf9aa23