`
Z_萧晓
  • 浏览: 11290 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

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

阅读更多

前言

内存泄漏,一个说大不大说下不小的瑕疵。作为开发者,我们都很清楚内存泄漏是我们代码问题导致的。但是话说回来,泄漏后果会很严重嘛?这不好说,如果我们不泄漏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。它设计的是否合理,是否它应该是一个长时间存活的对象(比如单例)。

 

尾声

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

 

不管怎么说,大家技术还是要学好的。小编下面给大家分享一份成为高级工程师学习路线,如果想学习高级UI、性能优化、移动架构师、 NDK、混开发等Android高阶开发的朋友可以加下我的Android架构群:887084983,还有免费的学习资料及面试资料领取~

 

好了,文章到这里就结束了,如果你觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢~

0
0
分享到:
评论

相关推荐

    一个JAVA程序员面试32问

    - GC是Java的自动内存管理机制,负责回收不再使用的对象所占用的内存,避免内存泄漏。 - 有GC是因为Java程序员无需手动管理内存,提高了程序的可移植性和安全性。 以上这些知识点是Java程序员面试中常见的问题,...

    c c++不可不知的知识 分析内存 从内存学习c C++经典全是我多年整理的

    - `free`函数用于释放之前用`malloc`、`calloc`或`realloc`分配的内存,避免内存泄漏。 内存泄漏是C和C++编程中的常见问题,当程序员分配了内存但忘记释放时,这些内存就会被浪费,无法再被程序使用。为了防止内存...

    一个程序员应该有的编程修养.pdf

    - 内存管理:合理使用内存分配和释放,避免内存泄漏和野指针问题。 - 变量和类型命名:使用有意义的变量和函数命名,避免使用过于抽象或者不明确的命名,提高代码的可读性。 - 宏定义和常量:合理使用宏定义和常量,...

    C语言深度解剖 解开程序员面试笔试的秘密

    掌握这些函数的正确使用方法可以有效避免内存泄漏等问题。 #### 调试技巧:提升问题解决能力 在实际开发过程中,调试是不可避免的一部分。很多学生在学习C语言时往往忽视了调试技巧的重要性,导致在遇到问题时...

    优秀Java程序员必须了解的GC工作原理

    Java垃圾回收(GC)是Java编程中至关重要的一个部分,对于任何希望成为优秀Java程序员的人来说,理解其工作原理是必不可少的。GC的主要任务是自动管理内存,尤其是对象的分配和回收,以避免内存泄漏和资源浪费。在...

    企业招聘java程序员的面试题

    3. **垃圾回收机制**:Java的垃圾回收(GC)自动回收不再使用的对象所占用的内存,防止内存泄漏。常见的GC策略包括分代收集(根据对象生命周期分为新生代、老年代等)、标记-清除和复制算法。垃圾回收是异步进行的,...

    一个很好的内存管理理论知识

    一个优秀的程序员需要掌握内存管理,以避免内存泄漏、提高程序性能以及确保程序的稳定性和安全性。 C++的标准库并没有内置的垃圾回收机制,这使得程序员必须手动管理内存,通过`new`来分配内存,`delete`来释放内存...

    C语言实现内存管理

    虽然C语言本身不支持智能指针,但在C++中,智能指针如`std::unique_ptr`, `std::shared_ptr`等可以帮助自动管理内存,避免内存泄漏。 理解并熟练运用这些内存管理概念是成为优秀C程序员的关键。通过实践和优化,...

    JAVA程序员面试问题

    ### JAVA程序员面试问题详解 #### 1. Java与JVM的关系 Java是一种广泛使用的编程语言,而JVM(Java虚拟机)...同时,培养良好的编码习惯、文档撰写能力以及团队合作精神,也是成为一名优秀Java程序员不可或缺的素质。

    收集的内存管理内存分配资料

    5. 垃圾回收:在某些高级语言(如Java、Python)中,垃圾回收机制自动回收不再使用的内存,避免内存泄漏。但这也会引入额外的性能开销。 6. “瑜珈山夜话”系列可能深入探讨了内存分配的具体实现,例如操作系统如何...

    C++ 内存管理算法和实现

    而堆内存则需要程序员手动管理,通过`new`和`delete`关键字进行分配和释放,提供了更大的灵活性,但也容易引发内存泄漏和悬挂指针等问题。 1. 动态内存分配:C++中的`new`操作符用于在堆上分配内存,返回一个指向新...

    《三言两语说指针》指针对一个CC++程序员来讲重要性不言而喻

    在C++中,还存在智能指针,如std::unique_ptr和std::shared_ptr,它们在对象生命周期结束时自动释放内存,帮助避免内存泄漏问题。 总的来说,对指针的理解和熟练运用是成为一名优秀C++程序员的基础。通过掌握指针,...

    全面介绍Windows内存管理机制及C++内存分配实例

    程序员需手动释放内存,避免内存泄漏。 - **栈内存**:用于自动变量,由编译器自动分配和释放,速度快但大小受限。 - **静态存储区**:存放全局变量和静态变量,生命周期贯穿整个程序运行。 - **常量存储区**:...

    编程修养-程序员不可或缺的书

    ### 编程修养——程序员不可或缺的书 #### 1. 版权和版本 **版权和版本**不仅是对程序员个人劳动成果的一种尊重,也是对代码的一种规范管理方式。良好的版权注释能够清晰地表明代码的所有权归属及版本信息,这对于...

    C++内存管理小结(内存控制)

    掌握良好的内存管理技巧,不仅能避免内存泄漏,提高代码的稳定性和效率,还能在资源紧张的情况下优化程序的表现。然而,不当的内存管理会导致程序崩溃或行为异常,特别是在资源受限的嵌入式系统或高性能计算场景中。...

    内存区划分、内存分配、常量存储区、堆、栈、自由存储区、全局区[C++][内存管理]

    这部分内存由编译器管理,不可修改,程序运行期间一直存在。 2. **全局区/静态存储区**:存放全局变量和静态变量。这些变量在程序开始时分配,在程序结束时释放。如果全局变量未初始化,它们会被自动初始化为零或...

    解读C#程序员最易犯的7大错误

    在C#编程中,开发人员可能会遇到各种各样的问题,其中一些常见的错误如果不加以注意,可能会对程序性能和代码质量产生显著影响。...记住,不断学习和从错误中吸取经验是成为一个优秀程序员的必经之路。

    程序员 面试 经验 C C++ 语言

    【程序员面试经验 C C++ 语言】 在程序员的面试中,C++和...此外,熟悉常见的面试题,如编写代码片段解决特定问题,也是必不可少的准备环节。记住,优秀的程序员不仅掌握语法,更懂得如何利用这些工具高效地解决问题。

    C、C++内存管理.doc

    了解内存的分配方式、掌握堆与栈的区别、学会避免内存泄漏等都是成为优秀C/C++开发者的必备技能。随着现代C++的发展,许多新的特性如智能指针等已经被广泛应用于内存管理中,极大地提高了程序的健壮性和可维护性。...

Global site tag (gtag.js) - Google Analytics