程序员人生 网站导航

[置顶] Android内存泄漏查找和解决

栏目:综合技术时间:2016-07-04 16:59:53

Android内存泄漏查找和解决

目录:

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

1.内存泄漏概念

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

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

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

2.内存泄漏的例子

内存泄漏例子1

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

内存泄漏例子2

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

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

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

class OuterClass { class InnerClass{ } }

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

内存泄漏例子1

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


反编译后

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

反编译源码1

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

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

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

复原反编译代码

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

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

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

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

查看内存泄漏1

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


1.先创建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对象有1个,存在的View对象有13个。

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

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

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

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


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

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

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

5.强援用与弱援用

看完上面的例子,了解到非静态内部类由于持有外部类的援用,极可能会造成泄漏。为何持有了外部类的援用会致使外部类不能被回收?

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


1.强援用(Strong Reference),就是我们常常使用的援用,写法以下

StringBuffer buffer = new StringBuffer();

上面创建了1个StringBuffer对象,并将这个对象的(强)援用存到变量buffer中。强援用最重要的就是它能够让援用变得强(Strong),这就决定了它和垃圾回收器的交互。具体来讲,如果1个对象可以从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中的弱援用

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

通过上面介绍的内容,我们了解到内存泄漏产生的缘由是对象在生命周期结束时被另外一个对象通过强援用持有而没法释放酿成的

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

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

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

这里写图片描述


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

这个例子改写成静态内部类+弱援用,其实不能完全解决内存泄漏的问题。
为何?只需要加上1句Log便可验证。
这里写图片描述

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

结果不言而喻,Log愈来愈多了,虽然Activity最后能够回收,但只是由于弱援用很弱,GC能够在内存不足的时候回收它,但并没有完全解决泄漏问题。

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

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

参考资料:
1. Android中Handler引发的内存泄漏

7.Context酿成的泄漏

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

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

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

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

这里写图片描述

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

1般来讲,对非控件类型的对象需要Context参数,最好优先斟酌全局ApplicationContext,来避免内存泄漏。

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

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

LeakCanary是甚么?它是1个傻瓜化并且可视化的内存泄漏分析工具。

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

下面来看1下如何集成:

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); } }

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

这里写图片描述

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

这里写图片描述

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

这里写图片描述

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


上面这类是最简单的默许配置,只对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方法。

1个毛病示例,比如监控1个Activity,放在onCreate就会大错特错了,那末你每次都会收到Activity的泄漏通知。

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

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

9.总结

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

场景

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

预防

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

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

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐