事无巨细的Android内存泄漏

发布时间:19-11-1023:07

前言

这又是一篇关于内存泄漏的文章,我猜有朋友应该有影响之前是写过一篇类似的文章。那么为啥有整了一篇呢?那是因为,无意中发现的这篇文章写的的确很不错,特别适合并没尝试过分析内存泄漏的你~

正文

开始翻译~~

demo涉及的版本环境:Kotlin 1.3,Android 4.4,Android Studio 3.4

内存泄漏是Android应用程序崩溃的常见原因。每个Android开发人员都应该了解它们并知道如何避免它们。

在本教程中,您将学习如何使用Android Profiler和LeakCanary检测内存泄漏。

一、Android Profiler和LeakCanary

Android Profiler取代了Android Monitor tools,依附于Android Studio 3.0及更高版本。它实时测量应用程序性能的多个方面,例如:

电量流量CPU内存二、内存泄漏

内存泄漏往往发生在你为对象分配内存,但却无法对其进行有效的释放。发生这种情况的原因可能很多,您稍后将了解这些原因。

无论是什么原因,当发生某内存泄漏时,垃圾收集器都认为仍然需要这个对象,因为该对象仍被其他对象引用。即使这些对象应该已经释放。

PS:其实小量的内存泄漏并不会造成太大的问题…但是如果泄漏的内存比较大,很容易出现“崩溃”。我们当时遇到过一次,用户反馈某页面进入几次后就“崩溃”了。当时我们一直没有找到日志,后来才意识到,是因为内存不足,出发了lowmemorykill,被系统强制在前台杀死了…

三、入门

首页,在本教程中,您将使用TripLog和GuessCount(这个是作者的demo,因为不能贴外链,需要的可以私聊我~)。

PS:略过作者对俩个demo app的介绍,界面大概是这个样子:

四、使用Android Profiler查找泄漏

开始使用Android Profiler 对GuessCount入门应用程序进行性能分析。如图打开Profiler:

在秒数字段中输入240秒,然后按开始按钮。您会看到一个loading。预期,您应该数到240并按Guess,但是为了创造泄漏,请直接Back按钮。

按下Dump Java heap:

8.0以上的手机会发现没有这个功能。

该CountActivity没有释放,尽管在屏幕上没有显示。当然这不一定说明泄漏了,也有可能没有触发GC,让我们强制GC一下:

现在生成一个新的Dump:

很明显,咱们的CountActivity的确被泄漏了,这时我们要进行分析,单击CountActivity行以查看更多详细信息:

在“instance view”中,您将看到根据mFinished = true。说明Activity已经finsh,此时就应该被GC掉。但是它没有,这是很明显的内存泄漏,因为CountActivity仍在内存中,但是没有人需要它。

等待几分钟。重复强制GC并再次Dump的过程。您将看到Activity最终被释放:

这实际上是暂时的内存泄漏。在此示例中,暂时泄漏CountActivity并不是一个大问题。但是请记住,泄漏整个活动可能真的很糟糕,因为您将保留其所有视图及其引用的每个对象

要分析此问题,请打开第一个Dump:

“References”窗格中的列表显示了所有引用CountActivity的对象。第一个this$0 in CountActivity$timeoutRunnable$1是CountActivity中的变量。因此,打开CountActivity.kt文件并搜索此变量:

private val timeoutRunnable = object : Runnable { override fun run() { this@CountActivity.stopCounting() this@CountActivity.showTimeoutResults() } }

很常见的问题,匿名内部类持有外部对象的引用,也就是说持有了Activity。

这就是为什么此代码可以调用stopCounting()和showTimeoutResults()的原因。如果右键单击该timeoutRunnable变量并选择Find Usages,则会看到从此方法调用该变量:

private fun startCounting() { startTimestamp = System.currentTimeMillis() val timeoutMillis = userCount * 1000 + 10000L timeoutHandler.postDelayed(timeoutRunnable, timeoutMillis) }

Activity的onCreate()中调用了startCounting()。在这里,您会看到timeoutHandler延迟执行timeoutRunnable。如果您不按Guess按钮,则应用会一直延迟到Runnable执行完毕。

继续研究CountActivity类,您将发现Activity退出timeoutHandler也不会取消。因为,它需要延迟timeoutMillis时间后,执行timeoutRunnable的代码。在这种情况下,Activity必须被保留,从而产生泄漏。

五、常见内存泄漏

您遇到了一种内存泄漏的情况。现在,您将学习其他一些常见情况及其成因。您还将学习如何使用Android Profiler和Leak Canary检测并避免它们。

5.1、匿名类

有时,匿名类实例的寿命比容器(外部对象)实例的预期寿命更长。如果此匿名类实例调用任何方法,或读取或写入容器类的任何属性,它将保留该容器实例。这将泄漏容器的内存。

注意:如果匿名类实例不调用任何方法,也不读取或写入容器类的任何属性,则它不会保留它。在那种情况下,您不会泄漏容器的内存。

简单的使用Leak Canary分析问题,gradle声明:

debugImplementation'com.squareup.leakcanary :leakcanary-android:2.0-alpha-3'

重复刚才的操作:

LeakCanary监视被破坏的Activity。它等待五秒钟,然后强制进行GC。如果Activity仍然存在,LeakCanary会认为Activity可能泄漏。

可以看出来和咱们Profiler分析的一样…解决这类问题,很简单…找到合适的时机,停掉runable就好了。

override fun onDestroy() { stopCounting() super.onDestroy() }

5.2、内部类

内部类是内存泄漏的另一个常见来源,因为它们也可以引用外部类。例如,假设您具有以下内容AsyncTask:

class MyActivity : AppCompatActivity() { ... inner class MyTask : AsyncTask<Void, Void, String>() { override fun doInBackground(vararg params: Void): String { // Perform heavy operation and return a result } override fun onPostExecute(result: String) { this@MyActivity.showResult(result) } }}

如果您离开Activity并且任务未完成,则将产生泄漏。你可以尝试取消AsyncTask的onDestroy()方法。但是,由于AsyncTask工作原理,该doInBackground()方法不会取消,您仍然会继续泄漏该Activity。

要修复它,您应该删除inner修饰符以转换MyTask为静态类。静态内部类无权访问容器类,因此您将无法泄漏该Activity。但是您都不能打调用showResult()。

因此,您可能会考虑将Activity作为参数传递,如下所示:

class MyActivity : AppCompatActivity() { ... class MyTask(private val activity: MainActivity) : AsyncTask<Void, Void, String>() { override fun doInBackground(vararg params: Void): String { // Perform heavy operation and return a result } override fun onPostExecute(result: String) { activity.showResult(result) } }}

但是,您也会泄漏该Activity。一个可能的解决方案是使用WeakReference:

class MyActivity : AppCompatActivity() { ... class MyTask(activity: MainActivity) : AsyncTask<Void, Void, String>() { private val weakRef = WeakReference<MyActivity>(activity) override fun doInBackground(vararg params: Void): String { // Perform heavy operation and return a result } override fun onPostExecute(result: String) { weakRef.get()?.showResult(result) } }}

WeakReference可以正常使用传递进来的Activity引用,但不会强引用Activity。因此,当GC通过并且未找到对该对象的任何强引用时,它将收集该对象。任何WeakReference引用收集到的对象的对象都将设置为null。

5.3、静态变量

companion object中的变量是静态变量。这些变量与一个Class相关联,而不与该类的实例相关联。因此,由于系统加载了类,它们将一直存活。

您应该避免使用静态变量引用Activity。因为即使不再需要它们也不会被垃圾回收。

5.4、注册Listener

在许多Android API和外部SDK上,您通常会注册Listener,而Listener使用,通常也是一种匿名内部类,所以和5.1的情况类似。如果您忘记remove Listener。通常,您注册的对象的寿命比您的Activity的寿命更长,这可能会导致内存泄漏。

尾声

这篇文章到此就算是结束了,基本上可以涵盖咱们开发的日常各种情况~、

返回顶部