Android 内存泄漏查找和解决 (长篇)

10,693 阅读16分钟

Android内存泄漏查找和解决

目录:

  1. 内存泄漏的概念
  2. 一个内存泄漏的例子
  3. Java中”失效”的private修饰符
  4. 回头看内存泄漏例子泄漏的重点
  5. 强引用与弱引用
  6. 解决内部类的内存泄漏
  7. Context造成的泄漏
  8. 使用LeakCanary工具查找内存泄漏
  9. 总结

一.内存泄漏概念

1.什么是内存泄漏?
用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。即所谓的内存泄漏。
其实说白了就是该内存空间使用完毕之后未回收

2.内存泄漏会导致的问题
内存泄露就是系统回收不了那些分配出去但是又不使用的内存, 随着程序的运行,可以使用的内存就会越来越少,机子就会越来越卡,直到内存数据溢出,然后程序就会挂掉,再跟着操作系统也可能无响应。

(在我们平时写应用的过程中,可能会无意的写了一些存在内存泄漏的代码,如果没有专业的工具,对内存泄漏的原理也不熟悉,要查内存泄漏出现在哪里是比较困难的)接下来先看一个内存泄漏的例子

二.内存泄漏的例子

内存泄漏例子1

这个例子存在的问题应该很容易能看出来,使用了handler延迟一定时间执行Runnable代码块,而在Activity结束的时候又没有释放执行的代码块,导致了内存泄漏。那么只要在Activity结束onDestroy的时候,释放延迟执行的代码块不就可以了,确实是,那么再看一看下面的例子。

内存泄漏例子2

这段代码是实际开发中存在内存泄漏的实例,稍微进行简化得到的。内存泄漏的关键点在哪里,怎么去解决,先留着这个问题,看下面一节的内容:”失效”的private修饰符。

三.Java中”失效”的private修饰符

相信大家都用过内部类,Java允许在一个类里面定义另一个类,类里面的类就是内部类,也叫做嵌套类。一个简单的内部类实现可以如下

class OuterClass {
    class InnerClass{
    }
}

下面回头看上面写的例子:

内存泄漏例子1

这其实是一个我们在编程中经常用到的场景,就是在一个内部类里面访问外部类的private成员变量或者方法,这是可以的。
这是为什么,不是private修饰的成员只能被成员所述的类才能访问么?难道private真的失效了么?
其实是编译器帮我们做了一些我们看不到的工作,下面我们通过反编译把这些看不到的工作都扒出来看看


反编译后

1.下面这一份是通过 dex2jar + jad 进行反编译得到的近似源码的java类

反编译源码1

可以看到这份反编译出来的代码,比我们编写的源码,要多了一些东西,在内部类MyRunnable里面多了一个MainActivity的成员变量,并且,在构造函数里面获得了外部类的引用。

2.再看看下面这一份文件,这是通过 apktool 反编译出来的 smali指令语言
在这里MainActivity分成了两个文件,分别是 MainActivity.smaliMainActivity$MyRunnable.smali。下面贴出的两份文件比较长,简单浏览一遍即可,详细看下面的解析,了解这份文件跟源码的对应关系。

MainActivity:

.class public Lcom/gexne/car/leaktest/MainActivity;
.super Landroid/app/Activity;
.source "MainActivity.java"


# annotations
.annotation system Ldalvik/annotation/MemberClasses;
    value = {
        Lcom/gexne/car/leaktest/MainActivity$MyRunnable;
    }
.end annotation


# instance fields
.field private handler:Landroid/os/Handler;

.field private test:Ljava/lang/String;


# direct methods
.method public constructor <init>()V
    .locals 1

    .prologue
    .line 18
    invoke-direct {p0}, Landroid/app/Activity;-><init>()V

    .line 20
    const-string v0, "TEST_STR"

    iput-object v0, p0, Lcom/gexne/car/leaktest/MainActivity;->test:Ljava/lang/String;

    .line 21
    new-instance v0, Landroid/os/Handler;

    invoke-direct {v0}, Landroid/os/Handler;-><init>()V

    iput-object v0, p0, Lcom/gexne/car/leaktest/MainActivity;->handler:Landroid/os/Handler;

    return-void
.end method

.method static synthetic access$000(Lcom/gexne/car/leaktest/MainActivity;)Ljava/lang/String;
    .locals 1
    .param p0, "x0"    # Lcom/gexne/car/leaktest/MainActivity;

    .prologue
    .line 18
    iget-object v0, p0, Lcom/gexne/car/leaktest/MainActivity;->test:Ljava/lang/String;

    return-object v0
.end method


# virtual methods
.method protected onCreate(Landroid/os/Bundle;)V
    .locals 4
    .param p1, "savedInstanceState"    # Landroid/os/Bundle;

    .prologue
    .line 32
    invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V

    .line 33
    const/high16 v0, 0x7f040000

    invoke-virtual {p0, v0}, Lcom/gexne/car/leaktest/MainActivity;->setContentView(I)V

    .line 34
    iget-object v0, p0, Lcom/gexne/car/leaktest/MainActivity;->handler:Landroid/os/Handler;

    new-instance v1, Lcom/gexne/car/leaktest/MainActivity$MyRunnable;

    invoke-direct {v1, p0}, Lcom/gexne/car/leaktest/MainActivity$MyRunnable;-><init>(Lcom/gexne/car/leaktest/MainActivity;)V

    const-wide/16 v2, 0x2710

    invoke-virtual {v0, v1, v2, v3}, Landroid/os/Handler;->postDelayed(Ljava/lang/Runnable;J)Z

    .line 36
    invoke-virtual {p0}, Lcom/gexne/car/leaktest/MainActivity;->finish()V

    .line 37
    return-void
.end method

在上面MainActivity.smali文件中,可以看到.field代表的是成员变量,.method代表的是方法,2个成员变量分别是Handler和String,方法则有3个分别是构造函数、onCreate()、 access$000()
嗯?在MainActivity中我们并没有定义access$000()这种方法,它是一个静态方法,接收一个MainActivity实例作为参数,并且返回MainActivity的test成员变量,所以, 它出现的目的就是为了得到MainActivity的私有属性。

MainActivity$MyRunnable.smali:

.class Lcom/gexne/car/leaktest/MainActivity$MyRunnable;
.super Ljava/lang/Object;
.source "MainActivity.java"

# interfaces
.implements Ljava/lang/Runnable;


# annotations
.annotation system Ldalvik/annotation/EnclosingClass;
    value = Lcom/gexne/car/leaktest/MainActivity;
.end annotation

.annotation system Ldalvik/annotation/InnerClass;
    accessFlags = 0x0
    name = "MyRunnable"
.end annotation


# instance fields
.field final synthetic this$0:Lcom/gexne/car/leaktest/MainActivity;


# direct methods
.method constructor <init>(Lcom/gexne/car/leaktest/MainActivity;)V
    .locals 0
    .param p1, "this$0"    # Lcom/gexne/car/leaktest/MainActivity;

    .prologue
    .line 23
    iput-object p1, p0, Lcom/gexne/car/leaktest/MainActivity$MyRunnable;->this$0:Lcom/gexne/car/leaktest/MainActivity;

    invoke-direct {p0}, Ljava/lang/Object;-><init>()V

    return-void
.end method


# virtual methods
.method public run()V
    .locals 2

    .prologue
    .line 26
    const-string v0, "test"

    iget-object v1, p0, Lcom/gexne/car/leaktest/MainActivity$MyRunnable;->this$0:Lcom/gexne/car/leaktest/MainActivity;

    # getter for: Lcom/gexne/car/leaktest/MainActivity;->test:Ljava/lang/String;
    invoke-static {v1}, Lcom/gexne/car/leaktest/MainActivity;->access$000(Lcom/gexne/car/leaktest/MainActivity;)Ljava/lang/String;

    move-result-object v1

    invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

    .line 27
    return-void
.end method

MyRunnable.smali文件中用同样的方法观察,发现多了一个成员变量MainActivity,方法分别是构造函数、run(),根据smali指令的含义可以看到构造函数是接收了一个MainActivity作为参数的,而run()方法中获取外部类中的test变量,则是调用access$000()方法获取。如果想了解smali指令语言可以自行google,这里不详细讲解。通过上面两个文件,重新还原一下源码。

复原反编译代码

这段代码基本上还原了编译器编译后指令的执行方式。内部类调用外部类,是通过一个外部类的引用进行调用的( 上面红色框框的两段代码是在还原的基础上加入的,用于解释内部类调用外部类的方式,调用方式1是我们常用的,而到的编译器编译后,实际调用方式是2),而外部类的private属性则通过编译器生成的我们看不见的静态方法,通过传入外部类实例引用获取出来。
通过还原,我们了解了非静态内部类跟外部类交互时的工作方式,以及非静态内部类为什么会持有外部类的引用。

参考资料:
1. 细话Java:”失效”的private修饰符
2. smali语法简析

四.通过dumpsys查看内存使用情况

继续回头看第一个内存泄漏的例子,稍微进行修改

查看内存泄漏1

对于这段代码,它会造成内存泄漏,那么对于外部类Activity来说,它能够被释放吗?
我们通过dumpsys来查看,了解怎么查看应用的内存使用情况,怎么看一个Activity有没有被顺利释放掉,而这个Activity能不能被回收。


1.先创建一个空Activity,如下代码所示,并安装到设备中

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

2.通过 adb shell dumpsys meminfo <packageName>来查看内存使用状况
在没有打开应用的情况下,该命令返回的数据是这样的:
dumpsys未打开应用

3.打开这个应用的MainActivity,再通过命令查看:
这里写图片描述

可以看到打印出来很多的信息,而对于我们查看Activity内存泄漏来说,只需要关注Activities和Views两个信息即可,在应用中存在的Activity对象有一个,存在的View对象有13个。

4.这时候我们退出这个Activity,在用命令查看一下:
这里写图片描述

可以看到,Activity对象和View对象都在极短的时间内被回收掉了。再次打开,退出,多次尝试,发现情况都是一样的。我们可以通过这种方式来简单判断一个Activity是否存在内存泄漏,最后是否能够被回收。

5.再运行刚才的泄漏的例子,用命令查看一下:
这里写图片描述

当我们连续打开退出同一个页面,然后使用命令查看时,发现Activity存在13个,而View则存在了234个,而且没有很快被回收,依次判断应该是存在内存泄漏了。
等待10多秒,再次查看,发现Activity和View的数量都变成了0。
这里写图片描述
所以,结论是能够被回收,只要Runnable代码块执行完毕,释放了Activity的引用,Activity就能被回收。


上面的例子,是Handler临时性内存泄漏,只要Handler post的代码块执行完毕,被引用的Activity就能够释放。
除了临时性内存泄漏,还有危害更大,直到程序结束才能被释放的内存泄漏。例如:
这里写图片描述

内存泄漏例子2
对于第一个例子,比较容易看出来,MyRunnable内部类持有了Activity的引用,而它自身一直不释放,导致Activity也一直无法释放,使用dumpsys meminfo查看可以验证,多次打开后退Activities的数量只会增加不会减少,直到手动结束整个应用。
而第二个例子也不难看出,只是引用链稍微长了点,TelephonyManager注册了内部类PhoneStateListener,持有了这个内部类的引用,PhoneStateListener持有了ViewHolder的引用,ViewHolder同时也是一个内部类,持有了ViewAdapter的引用,而ViewAdapter则持有了Activity的引用,最后TelephonyManager又没有做反注册的操作,导致了内存泄漏。
很多时候我们写代码,都忽略了释放工作,特别是写Java写多了,都觉得这些资源会自动释放,不用写释放方法,不用操心去做释放工作,然后内存泄漏就这样出现了。

参考资料:
1. 使用meminfo分析Android单个进程内存信息

五.强引用与弱引用

看完上面的例子,了解到非静态内部类因为持有外部类的引用,很可能会造成泄漏。为什么持有了外部类的引用会导致外部类不能被回收?

在解决内存泄漏之前,先了解Java的引用方式。Java有四种引用方式,分别是强引用、弱引用、虚引用、软引用。这里只介绍强引用以及弱引用,更详细的资料可以自行查找。


1.强引用(Strong Reference),就是我们经常使用的引用,写法如下

StringBuffer buffer = new StringBuffer();

上面创建了一个StringBuffer对象,并将这个对象的(强)引用存到变量buffer中。强引用最重要的就是它能够让引用变得强(Strong),这就决定了它和垃圾回收器的交互。具体来说,如果一个对象可以从GC Roots通过强引用到达时,那么这个对象将不会被GC回收。

2.弱引用(Weak Reference),弱引用简单来说就是将对象留在内存的能力不是那么强的引用。使用WeakReference,垃圾回收器会帮你来决定引用的对象何时回收并且将对象从内存移除。创建弱引用如下

WeakReference<Widget> weakWidget = new WeakReference<Widget>(widget);

使用weakWidget.get()就可以得到真实的Widget对象,因为弱引用不能阻挡垃圾回收器对其回收,你会发现(当没有任何强引用到widget对象时)使用get时突然返回null,所以对于弱引用要记得做判空处理后再使用,否则很容易出现NPE异常。

参考资料:
1. GC Roots
2. 理解Java中的弱引用

六.解决内部类的内存泄漏

通过上面介绍的内容,我们了解到内存泄漏产生的原因是 对象在生命周期结束时被另一个对象通过强引用持有而无法释放造成的

怎么解决这个问题,思路就是避免使用非静态内部类,定义内部类时,要么是放在单独的类文件中,要么就是使用静态内部类。因为静态的内部类不会持有外部类的引用,所以不会导致外部类实例的内存泄露。当你需要在静态内部类中调用外部的Activity时,我们可以使用弱引用来处理。
这里写图片描述

这种解决方法,对于临时性内存泄漏适用,其中包括但不限于自定义动画的更新回调,网络请求数据后更新页面的回调等, 更具体一点的例子有当我们在页面触发了网络请求加载时,希望它把数据加载完毕,当加载完毕时如果页面还在活动状态则更新显示内容。其实在Android中很多的内存泄露都是由于在Activity中使用了非静态内部类导致的,所以当我们使用时要非静态内部类时要格外注意。

在Android Studio里面,当你定义一个内部类Handler的时候,会出现贴心提示, This Handler class should be static or leaks might occur,提醒你把Handler改成静态类。

这里写图片描述


解决了上面的内存泄漏问题,再看看下面这个例子:
这里写图片描述

这个例子改写成静态内部类+弱引用,并不能完全解决内存泄漏的问题。
为什么?只需要加上一句Log即可验证。
这里写图片描述

多次进入退出页面,看一下打印出来的Log
这里写图片描述

结果显而易见,Log越来越多了,虽然Activity最后能够回收,但只是因为弱引用很弱,GC能够在内存不足的时候回收它,但并没有完全解决泄漏问题。

使用dumsys meminfo同样可以验证,每一次打开Activity并退出,等GC回收掉Activity后,发现Local Binder的数量并没有减少,而且比上一次多了1。
这里写图片描述

对于注册到服务中的回调(包括系统服务,自定义服务),使用静态内部类+弱引用的方式只能部分解决内存泄漏问题,这种问题需要释放资源时进行反注册才能根本解决,因为这种服务会长期存在系统中,注册了的callback对象会一直存在于服务中,每次callback来了都会执行callback中的代码块,只不过执行到弱引用部分由于弱引用获取到的对象为null而不会执行下一步操作。例如Broadcast,例如systemServer.listen等。

参考资料:
1. Android中Handler引起的内存泄露

七.Context造成的泄漏

了解完内部类的泄漏以及修复方法,再来看一下另一种泄漏,由context造成的泄漏。
这里写图片描述

这也是一个开发中的例子,稍作修改得到。

可以看到,蓝色框框内是一个标准的懒汉式单例。单例是我们比较简单常用的一种设计模式,然而如果单例使用不当也会导致内存泄露。比如这个例子,DashBoardTypeface需要持有一个Context作为成员变量,并且使用该Context创建字体资源。
instance作为静态对象,其生命周期要长于普通的对象,其中也包含Activity,当我们退出Activity,默认情况下,系统会销毁当前Activity,然后当前的Activity被一个单例持有,导致垃圾回收器无法进行回收,进而产生了内存泄露。

解决的方法就是不持有Activity的引用,而是持有Application的Context引用。

这里写图片描述

在任何使用到Context的地方,都要多加注意,例如我们常见的Dialog,Menu,悬浮窗,这些控件都需要传入Context作为参数的,如果要使用Activity作为Context参数,那么一定要保证控件的生命周期跟Activity的生命周期同步。窗体泄漏也是内存泄漏的一种,就是我们常见的leak window,这种错误就是依赖Activity的控件生命周期跟Activity不同步造成的。

一般来说,对于非控件类型的对象需要Context参数,最好优先考虑全局ApplicationContext,来避免内存泄漏。

参考资料:
1. 避免Android中Context引起的内存泄露

八.使用LeakCanary工具查找内存泄漏

LeakCanary是什么?它是一个傻瓜化并且可视化的内存泄露分析工具。

它的特点是简单,易于发现问题,人人都可参与,只要配置完成,简单的黑盒测试通过手工点击就能够看到详细的泄漏路径。

下面来看一下如何集成:

dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta2'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
   testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
 }

创建Application并加入LeakCanary代码:

public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    LeakCanary.install(this);
  }
}

这样已经完成最简单的集成,可以开始进行测试了。
在进行尝试之前再看一段代码:

这里写图片描述

思考完这段代码的问题后,我们来尝试一下使用LeakCanary寻找问题。如上面的配置,配置好应用,安装后可以看到,应用多了一个入口,如图所示。

这里写图片描述

这个入口就是当应用在使用过程中发生内存泄漏,可以从这个入口看到详细的泄漏位置。

这里写图片描述

从LeakCanary给出来的分析能轻易找到内存泄漏出现在responseHandler里面,跟刚才思考分析的答案是否一致呢?如果一致那你对内存泄漏的知识已经掌握不少了。


上面这种是最简单的默认配置,只对Activity进行了检测。但需要检测的对象肯定不只有Activity,例如Fragment、Service、Broadcast。这需要做更多的配置,在Application中留下RefWatcher的引用,使用它来检测其他对象。

public class MyApplication extends Application {
    private static RefWatcher sRefWatcher;


    @Override
    public void onCreate() {
        super.onCreate();
        sRefWatcher = LeakCanary.install(this);
    }

    public static RefWatcher getRefWatcher() {
        return sRefWatcher;
    }
}

在有生命周期的对象的onDestroy()中进行监控,例如Service。

public class CoreService extends Service {
    @Override
    public void onDestroy() {
        super.onDestroy();
        MyApplication.getRefWatcher().watch(this);
    }
}

监控需要设置在对象(很快)被释放的时候,如Activity和Fragment的onDestroy方法。

一个错误示例,比如监控一个Activity,放在onCreate就会大错特错了,那么你每次都会收到Activity的泄露通知。

更详细的资料可以到LeakCanary的github仓库中查看。

参考资料:
1. Android内存泄漏检测利器:LeakCanary
2. LeakCanary

九.总结

关于内存泄漏的知识,如何定位内存泄漏,如何修复,已经讲解完了。
最后做一个总结:

场景

  • 非静态内部类的静态实例
    非静态内部类会维持一个到外部类实例的引用,如果非静态内部类的实例是静态的,就会间接长期维持着外部类的引用,阻止被回收掉。
  • 资源对象未关闭
    资源性对象如Cursor、File、Socket,应该在使用后及时关闭。未在finally中关闭,会导致异常情况下资源对象未被释放的隐患。
  • 注册对象未反注册
    未反注册会导致观察者列表里维持着对象的引用,阻止垃圾回收。
  • Handler临时性内存泄露
    Handler通过发送Message与主线程交互,Message发出之后是存储在MessageQueue中的,有些Message也不是马上就被处理的。在Message中存在一个 target,是Handler的一个引用,如果Message在Queue中存在的时间越长,就会导致Handler无法被回收。如果Handler是非静态的,则会导致Activity或者Service不会被回收。
    由于AsyncTask内部也是Handler机制,同样存在内存泄漏的风险。
    此种内存泄露,一般是临时性的。

预防

  • 不要维持到Activity的长久引用,对activity的引用应该和activity本身有相同的生命周期。
  • 尽量使用context-application代替context-activity
  • Activity中尽量不要使用非静态内部类,可以使用静态内部类和WeakReference代替。

参考资料:
1. Android内存泄漏研究