八个造成 Android 应用内存泄露的原因

简介: 本文讲的是八个造成 Android 应用内存泄露的原因,诸如 Java 这样的 GC (垃圾回收)语言的一个好处就是免去了开发者管理内存分配的必要。这样降低了段错误导致应用崩溃或者未释放的内存挤爆了堆的可能性,因此也能编写更安全的代码。
本文讲的是八个造成 Android 应用内存泄露的原因,

诸如 Java 这样的 GC (垃圾回收)语言的一个好处就是免去了开发者管理内存分配的必要。这样降低了段错误导致应用崩溃或者未释放的内存挤爆了堆的可能性,因此也能编写更安全的代码。不幸的是,Java 里仍有一些其他的方式会导致内存“合理”地泄露。最终,这意味着你的 Android 应用可能会浪费一些非必要内存,甚至出现 out-of-memory (OOM) 错误。

传统的内存泄露发生的时机是:所有的相关引用已不在域范围内,你忘记释放内存了。另一方面,逻辑内存的泄漏,是忘记去释放在应用中不再使用的对象引用的结果。如果对象仍然存在强引用(译者注:这里可以去关注下 Java 的弱引用),GC 就无法从内存中回收对象。这在 Android 开发中尤其是个大问题:如果你碰巧泄露了 Context。这是因为像 Activity 一样的 Context 持有大量的内存引用,例如:view 层级和其他资源。如果你泄漏了 Context,就意味着你泄漏了它引用的所有东西。Android 应用通常运行在内存受限的手机设备中,如果你的应用泄漏太多内存的话就会导致 out-of-memory (OOM) 错误。

如果对象的有用存在期没有被明确定义的话,探查逻辑内存泄漏将会变成一件很主观的事情。幸好,Activity 明确定义了 生命周期,使得我们可以简单地知道一个 Activity 对象是否被泄漏了。在 Activity 的生命末期,onDestroy() 方法被调用来销毁 Activity ,这样做的原因可能是程序本身的意愿或者是 Android 需要回收一些内存。如果这个方法完成了,但是 Activity 的实例被堆根的一个强引用链持有着,那么 GC 就无法标记它为可回收 —— 尽管原本是想删掉它。因此,我们可以将一个泄露的 Activity 对象定义为一个超过其自然生命周期的对象。

Activity 是非常重的对象,所以你从来就不应该选择无视 Android 框架对它们的处理。然而,Activity 实例也有一些泄漏是非意愿造成的。在 Android 中,所有的可能导致内存泄漏的陷阱都围绕着两个基本场景:第一个是由独立于应用状态存在的全局静态对象对 Activity 的链式引用造成的;另一个是由独立于 Activity 生命周期的一个线程持有 Activity 的引用链造成。下面我们来解释一些你可能遇到这些场景的方式。

1. 静态 Activity

泄漏一个 Activity 最简单的方法是:定义 Activity 时在内部定义一个静态变量,并将其值设置为处于运行状态的 Activity 。如果在 Activity 生命周期结束时没有清除引用的话,这个 Activity 就会泄漏。这是因为这个对象表示这个 Activity 类(比如:MainActivity )是静态的并且在内存中一直保持加载状态。如果这个类对象持有了对 Activity 实例的引用,就不会被选中进行 GC 了。

void setStaticActivity() {
  activity = this;
}

View saButton = findViewById(R.id.sa_button);
saButton.setOnClickListener(new View.OnClickListener() {
  @Override public void onClick(View v) {
    setStaticActivity();
    nextActivity();
  }
});

内存泄漏 1 - 静态 Activity

2. 静态 View

一个相似的情况是:对于经常访问到的 Activity 实现了单例模式,并且保持它的实例在内存中的加载状态使之有利于快速读写。然而,正如刚才提到的原因,违背了 Activity 既定的生命周期并且在内存中长久存在是一件极其危险和不必要的实践 —— 并且应该被完全禁止。

但是假如我们有一个特定的 View :花费极大的代价来初始化,但是在同一个 Activity 的不同生命时间内没怎么变化过,我们该怎么办呢?我们可以简单地在初始化后就把这个 View 设为静态的,然后附加到 View 的层次关系中,就像我们在这里做的。现在假如 Activity 被销毁了,我们应该可以释放它占用的大部分内存。

void setStaticView() {
  view = findViewById(R.id.sv_button);
}

View svButton = findViewById(R.id.sv_button);
svButton.setOnClickListener(new View.OnClickListener() {
  @Override public void onClick(View v) {
    setStaticView();
    nextActivity();
  }
});

内存泄漏 2 - 静态 View

稍等,有一点奇怪的地方。正如你知道的,在这种情况下,我们的 Activity 中,一个被附加的 View 会持有对它的 Context 的引用。通过使用一个 View 的静态引用,我们给 Activity 设定了一个持久化的引用链并且泄露了它。不要使附加的 View 静态化,如果你必须这么做的话,至少让它们在 Activity 完成之前从 View 层级关系的同一点上分离出来。

3. 内部类

继续,让我们讨论下在 Activity 类中定义一个内部类的情况。程序员一般选择这样做是有一些原因的,诸如提升可靠性和封装性等。假如我们创建了一个内部类的实例然后对其持有了一个静态引用呢?你肯定猜到了必然会发生内存泄漏。

void createInnerClass() {
    class InnerClass {
    }
    inner = new InnerClass();
}

View icButton = findViewById(R.id.ic_button);
icButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        createInnerClass();
        nextActivity();
    }
});

内存泄漏 3 - 内部类

不幸的是,因为内部类的一个特性是它们可以访问外部类的变量,所以它们必然持有了对外部类实例的引用以至于 Activity 会发生泄漏。

4. 匿名类

同样的,匿名类同样持有了内部定义的类的引用。因此如果你在 Activity 中匿名地声明并且实例化了一个 AsyncTask的话就会发生泄漏。如果在 Activity 销毁后它仍在后台工作的话,对于 Activity 的引用会持续并且直到后台工作完成才会进行 GC。

void startAsyncTask() {
    new AsyncTask<void, void,="" void="">() {
        @Override protected Void doInBackground(Void... params) {
            while(true);
        }
    }.execute();
}

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View aicButton = findViewById(R.id.at_button);
aicButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        startAsyncTask();
        nextActivity();
    }
});</void,>

内存泄漏 4 - AsyncTask

5. Handler

相同的情况同样适用于这样的后台任务:被一个 Runnable 对象定义并被一个 Handler 对象加入执行队列。这个 Runnable 对象将会隐式地引用定义它的 Activity 然后会作为 Message 提交到 Handler 的 MessageQueue(消息队列)。只要 Activity 销毁前消息还没有被处理,那么引用链就会使 Activity 保留在内存里并导致泄漏。

void createHandler() {
    new Handler() {
        @Override public void handleMessage(Message message) {
            super.handleMessage(message);
        }
    }.postDelayed(new Runnable() {
        @Override public void run() {
            while(true);
        }
    }, Long.MAX_VALUE >> 1);
}

View hButton = findViewById(R.id.h_button);
hButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        createHandler();
        nextActivity();
    }
});

内存泄漏 5 - Handler

6. Thread

我们在使用 Thread 和 TimerTask 时,可能会犯同样的错误。

void spawnThread() {
    new Thread() {
        @Override public void run() {
            while(true);
        }
    }.start();
}

View tButton = findViewById(R.id.t_button);
tButton.setOnClickListener(new View.OnClickListener() {
  @Override public void onClick(View v) {
      spawnThread();
      nextActivity();
  }
});

内存泄漏 6 - Thread

7. TimerTask

只要 TimerTask 被定义并且匿名实例化,即使任务执行在独立的线程里,它们也会在 Activity 销毁后保持对其的引用链,从而导致泄漏。

void scheduleTimer() {
    new Timer().schedule(new TimerTask() {
        @Override
        public void run() {
            while(true);
        }
    }, Long.MAX_VALUE >> 1);
}

View ttButton = findViewById(R.id.tt_button);
ttButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        scheduleTimer();
        nextActivity();
    }
});

内存泄漏 7 - TimerTask

8. SensorManager

最后,有一些 Context 可以通过调用 getSystemService 来检索的系统服务。这些服务运行在它们独立的线程,辅助应用去与硬件设备进行接口通讯。如果 Context 想要时刻监听到 Service 中发生的事件,它就需要注册自己为 Listener。然而,这将会造成 Service 持有 Activity 的引用,如果在 Activity 销毁前忘记注销作为 Listener 的 Activity 的话,GC 就无法回收从而导致泄漏。

void registerListener() {
       SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
       Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
       sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
}

View smButton = findViewById(R.id.sm_button);
smButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        registerListener();
        nextActivity();
    }
});

内存泄漏 8 - SensorManager

现在你已经见识了这么多内存泄漏的情况,一不留神就泄漏大量内存实在是太容易发生了。记住,尽管最严重的内存泄漏情况才会造成应用内存溢出并崩溃,但并不总会发生这样的情况,取而代之的是,这将浪费应用大量内存空间。在这种情况下,应用给其他对象的可分配内存就少了,然后你的 GC 就不得不时常为新对象释放空间。GC 是代价很大的操作并会让用户感到速度下降。当你在 Activity 中初始化对象的时候,留心潜在的引用链,并且经常测试内存泄漏!






原文发布时间为:2016年06月22日

本文来自云栖社区合作伙伴掘金,了解相关信息可以关注掘金网站。
目录
相关文章
|
5天前
|
搜索推荐 Java Android开发
打造个性化安卓应用:从设计到发布的全程指南
【9月更文挑战第15天】本篇文章将带领读者踏上一段激动人心的旅程,从构思一个独特的安卓应用想法开始,直至将其变为现实并成功发布。我们将一起探索如何捕捉灵感、设计界面、编写代码以及最终将应用推向市场。无论你是编程新手还是有经验的开发者,这篇文章都将为你提供宝贵的洞见和实用的技巧,让你的应用在竞争激烈的市场中脱颖而出。
35 17
|
1天前
|
开发框架 搜索推荐 开发工具
打造个性化安卓应用:从零开始的Flutter之旅
【8月更文挑战第51天】本文是一篇面向初学者的Flutter入门教程,旨在通过简单易懂的语言和实际代码示例,引导读者步入跨平台移动应用开发的世界。文章首先介绍了Flutter的基本概念和优势,然后逐步展示了如何搭建开发环境、创建第一个Flutter应用,并实现了一个简单的待办事项列表。最后,文章探讨了Flutter在实现高性能和美观界面方面的潜力,鼓励读者发挥创意,探索更多可能。
28 15
|
1天前
|
Java Android开发 UED
🧠Android多线程与异步编程实战!告别卡顿,让应用响应如丝般顺滑!🧵
在Android开发中,为应对复杂应用场景和繁重计算任务,多线程与异步编程成为保证UI流畅性的关键。本文将介绍Android中的多线程基础,包括Thread、Handler、Looper、AsyncTask及ExecutorService等,并通过示例代码展示其实用性。AsyncTask适用于简单后台操作,而ExecutorService则能更好地管理复杂并发任务。合理运用这些技术,可显著提升应用性能和用户体验,避免内存泄漏和线程安全问题,确保UI更新顺畅。
13 5
|
2天前
|
前端开发 Java 数据库
💡Android开发者必看!掌握这5大框架,轻松打造爆款应用不是梦!🏆
在Android开发领域,框架犹如指路明灯,助力开发者加速应用开发并提升品质。本文将介绍五大必备框架:Retrofit简化网络请求,Room优化数据库访问,MVVM架构提高代码可维护性,Dagger 2管理依赖注入,Jetpack Compose革新UI开发。掌握这些框架,助你在竞争激烈的市场中脱颖而出,打造爆款应用。
25 3
|
2天前
|
存储 API Android开发
"解锁Android权限迷宫:一场惊心动魄的动态权限请求之旅,让你的应用从平凡跃升至用户心尖的宠儿!"
随着Android系统的更新,权限管理成为应用开发的关键。尤其在Android 6.0(API 级别 23)后,动态权限请求机制的引入提升了用户隐私保护,要求开发者进行更精细的权限管理。
14 2
|
3天前
|
搜索推荐 Java 测试技术
打造个性化安卓应用:从设计到发布的完全指南
【9月更文挑战第17天】在这个数字时代,拥有一款个性化的安卓应用无疑是展现创意、实现梦想的一大步。本文将带你走进安卓应用的开发世界,从设计理念的孕育到实际代码的编写,再到最终的应用发布,我们将一步步揭开应用开发的神秘面纱。无论你是编程新手还是希望提升现有技能,这篇文章都将是你的宝贵资源。让我们开始这段激动人心的旅程吧!
|
7天前
|
监控 算法 数据可视化
深入解析Android应用开发中的高效内存管理策略在移动应用开发领域,Android平台因其开放性和灵活性备受开发者青睐。然而,随之而来的是内存管理的复杂性,这对开发者提出了更高的要求。高效的内存管理不仅能够提升应用的性能,还能有效避免因内存泄漏导致的应用崩溃。本文将探讨Android应用开发中的内存管理问题,并提供一系列实用的优化策略,帮助开发者打造更稳定、更高效的应用。
在Android开发中,内存管理是一个绕不开的话题。良好的内存管理机制不仅可以提高应用的运行效率,还能有效预防内存泄漏和过度消耗,从而延长电池寿命并提升用户体验。本文从Android内存管理的基本原理出发,详细讨论了几种常见的内存管理技巧,包括内存泄漏的检测与修复、内存分配与回收的优化方法,以及如何通过合理的编程习惯减少内存开销。通过对这些内容的阐述,旨在为Android开发者提供一套系统化的内存优化指南,助力开发出更加流畅稳定的应用。
19 0
|
1月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
|
2月前
|
存储 分布式计算 Hadoop
HadoopCPU、内存、存储限制
【7月更文挑战第13天】
189 14
|
23天前
|
存储 监控 Docker
如何限制docker使用的cpu,内存,存储
如何限制docker使用的cpu,内存,存储

热门文章

最新文章