资讯专栏INFORMATION COLUMN

一个优秀程序员不可避免的问题:内存泄漏

tanglijun / 1115人阅读

摘要:前言内存泄漏,一个说大不大说下不小的瑕疵。所以今天咱们来聊一聊中的内存泄漏。主线程进行耗时操作,每一个开发者都明白这意味着什么所以内存泄漏足够严重,其危害还是很严重的。

前言

内存泄漏,一个说大不大说下不小的瑕疵。作为开发者,我们都很清楚内存泄漏是我们代码问题导致的。但是话说回来,泄漏后果会很严重嘛?这不好说,如果我们不泄漏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 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>() {
                   
                    @Override
                    public void onResponse(Call> call,
                                           Response> response) {
                        int numberOfMovies = response.body().size();
                        mNoOfMoviesThisWeek.setText("No of movies this week: " + String.valueOf(numberOfMovies));
                    }

                    @Override
                    public void onFailure(Call> 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。它设计的是否合理,是否它应该是一个长时间存活的对象(比如单例)。

尾声

关于内存泄漏,还是需要咱们平时多注意,对自己写的每一行代码都多思考。毕竟这东西“不是病,但疼起来真要命”。

我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,以及我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/7353.html

相关文章

  • 优秀代码必知必会(一)?

    摘要:但由于模式本身有严重的缺陷,由于构造方法在多次调用中被分割,导致可能处于不一致的状态,并且还需要额外增加工作以确保线程安全。方法必须遵从类指定的常规约定,将不同的哈希码分配给不同的实例对象。 1.使用静态工厂方法替代构造方法 静态工厂方法的优点: 不像构造方法,它是有名字的。 它不需要每次调用时都创建一个新对象。 它可以返回 其返回类型的任何子类型的对象。 返回对象的类可以根...

    ChanceWong 评论0 收藏0
  • JavaScript中内存泄漏以及如何处理

    摘要:本文将会讨论中的内存泄漏以及如何处理,方便大家在使用编码时,更好的应对内存泄漏带来的问题。当内存不再需要时进行释放大部分内存泄漏问题都是在这个阶段产生的,这个阶段最难的问题就是确定何时不再需要已分配的内存。中的相同对象称为全局。 随着现在的编程语言功能越来越成熟、复杂,内存管理也容易被大家忽略。本文将会讨论JavaScript中的内存泄漏以及如何处理,方便大家在使用JavaScript...

    itvincent 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<