android 内存泄漏_android内存泄漏怎样确定是kernel内存泄漏 - CSDN
  • 面试中最常问的就是:“你了解Android内存泄漏和Android内存溢出的原因吗,请简述一下” ,然后大多数的人都能说出原因及其例子和解决办法,但是实际项目中稍微不注意还是会导致内存泄漏,今天就来梳理一下那些是...

    前言

    面试中最常问的就是:“你了解Android内存泄漏和Android内存溢出的原因吗,请简述一下” ,然后大多数的人都能说出原因及其例子和解决办法,但是实际项目中稍微不注意还是会导致内存泄漏,今天就来梳理一下那些是常见的内存泄漏写法和解决方法。

    原因

    内存泄漏的原理很多人都明白,但是为了加强大家的防止内存泄漏的意识,我再来说一遍。说到内存泄漏的原理就必须要讲一下Java的GC的。Java之所以这么流行不仅仅是他面向对象编程的方式,还有一个重要的原因是因为,它能帮程序员免去释放内存的工作,但Java并没有我们想象的那么智能,它进行内存清理还得依靠固定的判断逻辑。

    Java的GC可分为

    引用计数算法

    给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;在任何时刻计数器的值为0的对象就是不可能再被使用的,也就是可被回收的对象。这个原理容易理解并且效率很高,但是有一个致命的缺陷就是无法解决对象之间互相循环引用的问题。如下图所示

    image

    可达性分析算法

    针对引用计数算法的致命问题,可达性分析算法能够轻松的解决这个问题。可达性算法是通过从GC root往外遍历,如果从root节点无法遍历该节点表明该节点对应的对象处于可回收状态,如下图中obj1、obj2、obj3、obj5都是可以从root节点出发所能到达的节点。反观obj4、obj6、obj7却无法从root到达,即使obj6、obj7互相循环引用但是还是属于可回收的对象最后被jvm清理。

    image

    看了这些知识点,我们再来寻找内存泄漏的原因,Android是基于Java的一门语言,其垃圾回收机制也是基于Jvm建立的,所以说Android的GC也是通过可达性分析算法来判定的。**但是如果一个存活时间长的对象持有另一个存活时间短的对象就会导致存活时间短的对象在GC时被认定可达而不能被及时回收也就是我们常说的内存泄漏。**Android对每个App内存的使用有着严格的限制,大量的内存泄漏就可能导致OOM,也就是在new对象请求空间时,堆中没有剩余的内存分配所导致的。

    既然知道了原理那么平时什么会出现这种问题和怎么合理的解决这种问题呢。下面来按实例说话。

    image

    内存泄漏的例子

    Handler

    说到Handler这个东西,大家平时肯定没少用这玩意,但是要是用的不好就非常容易出现问题。举个例子

    public Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
          super.handleMessage(msg);
          toast("handlerLeakcanary");
        }
      };
    
    private void handlerLeakcanary(){
        Message message = new Message();
        handler.sendMessageDelayed(message,TIME);
      }
    

    老实说写过代码的人肯定很多。其中不乏了解内存泄漏原理的人。但是平时需要多的时候一不小心就可能写下这气人的代码。

    了解Handler机制的人都明白,但message被Handler send出去的时候,会被加入的MessageQueue中,Looper会不停的从MessageQueue中取出Message并分发执行。但是如果Activity 销毁了,Handler发送的message没有执行完毕。那么Handler就不会被回收,但是由于**非静态内部类默认持有外部类的引用。**Handler可达,并持有Activity实例那么自然jvm就会错误的认为Activity可达不就行GC。这时我们的Activity就泄漏,Activity作为App的一个活动页面其所占有的内存是不容小视的。那么怎么才能合理的解决这个问题呢

    1、使用弱引用

    Java里面的引用分为四种类型强引用、软引用、弱引用、虚引用。如果有不明白的可以先去了解一下4种引用的区别

     public static class MyHandler extends Handler{
        WeakReference<ResolveLeakcanaryActivity> reference;
    
        public MyHandler(WeakReference<ResolveLeakcanaryActivity> activity){
          reference = activity;
        }
    
        @Override
        public void handleMessage(Message msg) {
          super.handleMessage(msg);
          if (reference.get()!=null){
            reference.get().toast("handleMessage");
          }
        }
      }
    

    引用了弱引用就不会打扰到Activity的正常回收。但是在使用之前一定要记得判断弱引用中包含对象是否为空,如果为空则表明表明Activity被回收不再继续防止空指针异常

    2、使用Handler.removeMessages();
    知道原因就很好解决问题,Handler所导致的Activity内存泄漏正是因为Handler发送的Message任务没有完成,所以在onDestory中可以将handler中的message都移除掉,没有延时任务要处理,activity的生命周期就不会被延长,则可以正常销毁。

    单例所导致的内存泄漏

    在Android中单例模式中经常会需要Context对象进行初始化,如下简单的一段单例代码示例

    public class MyHelper {
    
      private static MyHelper myHelper;
    
      private Context context;
    
      private MyHelper(Context context){
        this.context = context;
      }
    
      public static synchronized MyHelper getInstance(Context context){
        if (myHelper == null){
          myHelper = new MyHelper(context);
        }
        return myHelper;
      }
    
      public void doSomeThing(){
    
      }
      
    }
    
    

    这样的写法看起来好像没啥问题,但是一旦如下调用就会产生内存溢出

      public void singleInstanceLeakcanary(){
        MyHelper.getInstance(this).doSomeThing();
      }
    

    首先单例中有一个static实例,实例持有Activity,但是static变量的生命周期是整个应用的生命周期,肯定是会比单个Activity的生命周期长的,所以,当Activity finish时,activity实例被static变量持有不能释放内存,导致内存泄漏。
    解决办法:
    1.使用getApplicationContext()

      private void singleInstanceResolve() {
        MyHelper.getInstance(getApplicationContext()).doSomeThing();
      }
    

    2.改写单例写法,在Application里面进行初始化。

    匿名内部类导致的异常

     /**
       * 匿名内部类泄漏包括Handler、Runnable、TimerTask、AsyncTask等
       */
      public void anonymousClassInstanceLeakcanary(){
        new Thread(new Runnable() {
          @Override
          public void run() {
            try {
              Thread.sleep(TIME);
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
          }
        }).start();
      }
    

    这个和Handler内部类导致的异常原理一样就不多说了。改为静态内部类+弱引用方式调用就行了。

    静态变量引用内部类

      private static Object inner;
      public void innearClassLeakcanary(){
    
        class InnearClass{
    
        }
        inner = new InnearClass();
      }
    

    因为静态对象引用了方法内部类,方法内部类也是持有Activity实例的,会导致Activity泄漏
    解决方法就是通过在onDestory方法中置空static变量

    网络请求回调接口

        Retrofit retrofit = new Retrofit.Builder()
            .addConverterFactory(GsonConverterFactory.create())
            .baseUrl("http://gank.io/api/data/")
            .build();
        Api mApi = retrofit.create(Api.class);
        Call<AndroidBean> androidBeanCall = mApi.getData(20,1);
        androidBeanCall.enqueue(new Callback<AndroidBean>() {
          @Override
          public void onResponse(Call<AndroidBean> call, Response<AndroidBean> response) {
            toast("requestLeakcanary");
          }
    
          @Override
          public void onFailure(Call<AndroidBean> call, Throwable t) {
    
          }
        });
    

    这是一段很普通的请求代码,一般情况下Wifi请求很快就回调回来了,并不会导致什么问题,但是如果是在弱网情况下就会导致接口回来缓慢,这时用户很可能就会退出Activity不在等待,但是这时网络请求还未结束,回调接口为内部类依然会持有Activity的对象,这时Activity就内存泄漏的,并且如果是在Fragment中这样使用不仅会内存泄漏还可能会导致奔溃,之前在公司的时候就是写了一个Fragment,里面包含了四个网络请求,由于平时操作的时候在Wi-Fi情况下测试很难发现在这个问题,后面灰度的时候出现Crash,一查才之后当所附属的Activity已经finish了,但是网络请求未完成,首先是Fragment内存泄漏,然后调用getResource的时候返回为null导致异常。这类异常的原理和非静态内部类相同,所以可以通过static内部类+弱引用进行处理。由于本例是通过Retrofit进行,还可以在onDestory进行call.cancel进行取消任务,也可以避免内存泄漏。

    RxJava异步任务

    RxJava最近很火,用的人也多,经常拿来做网络请求和一些异步任务,但是由于RxJava的consumer或者是Observer是作为一个内部类来请求的时候,内存泄漏问题可能又随之而来

      @SuppressLint("CheckResult")
      public void rxJavaLeakcanary(){
        AppModel.getData()
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(
            new Consumer<Object>() {
              @Override
              public void accept(Object o) throws Exception {
                toast("rxJavaLeakcanary");
              }
            });
      }
    

    这个代码很常见,但是consumer这个为内部类,如果异步任务没有完成Activity依然是存在泄漏的风险的。好在RxJava有取消订阅的方法可通过如下方法解决

      @Override
      protected void onDestroy() {
        super.onDestroy();
        if (disposable!=null && !disposable.isDisposed()){
          disposable.dispose();
        }
      }
    

    Toast显示

    看到这个可能有些人会惊讶,为啥Toast会导致内存泄漏,首先看一下

    Toast.makeText(this,"toast",Toast.LENGTH_SHORT);
    

    这个代码大家都很熟悉吧,但是如果直接这么做就可能会导致内存泄漏
    ,这里传进去了一个Context,而Toast其实是在界面上加了一个布局,Toast里面有一个LinearLayout,这个Context就是作为LinearLayout初始化的参数,它会一直持有Activity,大家都知道Toast显示是有时间限制的,其实也就是一个异步的任务,最后让其消失,但是如果在Toast还在显示Activity就销毁了,由于Toast显示没有结束不会结束生命周期,这个时候Activity就内存泄漏了。
    解决方法就是不要直接使用那个代码,自己封装一个ToastUtil,使用ApplicationContext来调用。或者通过getApplicationContext来调用,还有一种通过toast变量的cancel来取消这个显示

     private void toast(String msg){
        Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
      }
    

    总结

    看了那么多是不是感觉其实内存泄漏的原理很简单,变来变去其实只是形式变了,换汤不换药。但是在编码中不注意还是可能会出现这些问题。还有很多示例我没有举出来,但是不表示就没有其他情况了,这需要大家编程的时候自己耐心去发觉,有一些View或者其他对象默认持有Activity对象的时候,如果不小心被其他生命周期更长的对象引用了,依然会导致内存泄漏。并且只要有一个强引用应用了Activity即使你对其他很多地方做了防泄漏处理结果还是一样的—Activity泄漏(这就很扎心了),所以这就是一个细节与编程规范的问题,但是可有利用一些工具来找出我们粗心写下的代码,比如leakcanary内存泄漏分析工具。找出来利用自己对原理的理解解决这些问题是很简单的。

    了解原理之后就去写代码吧 ?

    展开全文
  • 你应该管理好应用的内存 Random-access memory(随机存取存储器RAM)在任何软件开发环境中都是宝贵的资源,而对于物理内存经常受到限制的移动操作系统来...你仍然需要避免引入内存泄漏。 内存溢出(OO...

    本文主要介绍以下两个主题:

    内存泄露的检测方法:通过LeakCanary&MAT检测应用中潜在的内存泄漏。
    内存泄露的解决方法:常见内存泄漏场景以及解决方案,如何避免写出泄漏的代码。

    篇幅较长,各位可以根据自己的需求选择阅读。

    你应该管理好应用的内存


    Random-access memory(随机存取存储器RAM)在任何软件开发环境中都是宝贵的资源,而对于物理内存经常受到限制的移动操作系统来说,它就更具价值了。 尽管Android Runtime(ART)和Dalvik虚拟机都会执行常规的垃圾收集(GC),但这并不意味着你可以忽略你的应用分配和释放内存的时间和位置。你仍然需要避免引入内存泄漏。

    内存溢出(OOM)和内存泄漏(Leak)


    内存溢出(OutOfMemoryError)

    为了允许多进程,Android为每个应用程序分配的堆大小设置了硬性限制。 确切的堆大小限制根据设备有多少内存总量而有所不同。 如果你的应用程序使用的内存已达到该限制并尝试分配更多内存时,系统就会抛出OutOfMemoryError。

    内存泄漏(Memory Leak)

    是指应用在申请内存后,无法释放已申请的内存空间,是对内存资源的浪费。最坏的情况下,内存泄漏会最终导致内存溢出。
    #内存泄漏的危害


    一次内存泄漏危害并不大,但也不能放任不管,最坏的情况下,你的 APP 可能会由于大量的内存泄漏而内存耗尽,进而闪退,但它并不总是这样。相反,内存泄漏会消耗大量的内存,但却不至于内存耗尽,这时,APP 会由于内存不够分配而频繁触发GC。而GC是非常耗时的操作,会导致严重的卡顿。另外,当你的应用处于LRU列表中(即切换到后台,变为后台进程)时,由于内存泄漏而消耗了更多内存,当系统资源不足而需要回收一部分缓存进程时,你的应用被系统杀死的可能性就更大了。

    Tips1:为什么我们在平时开发中并不太在意的GC会导致卡顿?你需要了解GC相关知识,包括“Full GC / Minor GC”、“GC停顿”等。感兴趣的读者可以看我的JVM基础(二) - 垃圾收集器与内存分配策略

    Tips2:应用进程在整个LRU列表中消耗的内存越少,保留在列表中并且能够快速恢复的机会就越大。这一部分的相关知识可以参考我的Android 内存管理机制

    LeakCanary


    LeakCanary是大家所熟知的内存泄漏检测工具,它简单易用,集成以后能在应用发生泄漏时发出警告,并显示发生泄漏的堆栈信息,新版本还会显示具体泄漏的内存大小,作为被动监控泄漏的工具非常有效,但LeakCanary功能有限,不能提供更详细的内存快照数据,并且需要嵌入到工程中,会在一定程度上污染代码,所以一般都只在build version中集成,release version中则应该去掉。

    本文的重点并不是LeakCanary,所以这里不做详细讲述,但仍然强烈推荐大家看看以下博客,这是LeakCanary的研发人员写的LeakCanary的由来,并简单诙谐的道出了LeakCanary的实现原理:

    用 LeakCanary 检测内存泄漏

    LeakCanary的原理

    虽然本文重点不是LeakCanary,但是笔者还是很好奇它是如何工作的。在此,我们简单概括一下LeakCanary的原理:

    1. 监听Activity生命周期,当onDestroy被调用时,调用RefWatcher.watch(activity)检查泄漏。
    2. RefWatcher.watch() 会创建一个 KeyedWeakReference 到要被监控的对象。
      KeyedWeakReference是WeakReference的子类,只不过附加了一个key和name作为成员变量,方便后续查找这个KeyedWeakReference对象。这一步创建KeyedWeakReference时使用了WeakReference的一个构造方法WeakReference(T referent, ReferenceQueue<? super T> q),这个构造方法很关键,下一步会用到。
    3. 然后在后台线程检查引用是否被清除,如果没有,调用GC。
      究竟如何检查?这就要得益于上一步构造KeyedWeakReference对象时传入的ReferenceQueue了,关于这个类,有兴趣的可以直接看Reference的源码。我们这里需要知道的是,每次WeakReference所指向的对象被GC后,这个弱引用都会被放入这个与之相关联的ReferenceQueue队列中。所以此时我们去检查ReferenceQueue,如果其中没有这个KeyedWeakReference,那么它所指向的这个对象很可能存在泄漏,不过为了防止误报,LeakCanary会进行二次GC确认,也就是主动触发一次GC。
    4. 如果引用还是未被清除,把 heap 内存 dump 到 APP 对应的文件系统中的一个 .hprof 文件中。
    5. 在另外一个进程中的 HeapAnalyzerService 有一个 HeapAnalyzer 使用HAHA 解析这个文件。
    6. 得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位内存泄漏。
    7. HeapAnalyzer 计算 到 GC roots 的最短强引用路径,并确定是否是泄漏。如果是的话,建立导致泄漏的引用链。
    8. 引用链传递到 APP 进程中的 DisplayLeakService, 并以通知的形式展示出来。

    更详细的LeakCanary源码分析,可以参考以下博客:

    LeakCanary 内存泄漏监测原理研究

    当然LeakCanary还有更多高级用法,比如可以添加忽略(一些第三方库甚至android sdk本身的泄漏你可能无法解决,但又不想LC总是报警)、可以定制ReferenceWatcher以监控特定的类等等,这些可以参考其GitHub文档:

    LeakCanary on GitHub

    笔者习惯使用LeakCanary作为监控工具,再结合MAT作为分析工具。MAT相对LeakCanary功能更加强大,当然用法也更复杂一些,它能提供详尽的内存分析数据,并且不需要嵌入工程中。下面就来介绍一下MAT的使用方法。
    #MAT简介


    MAT(Memory Analyzer Tool),一个基于Eclipse的内存分析工具,是一个快速、功能丰富的JAVA heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。使用内存分析工具从众多的对象中进行分析,快速的计算出在内存中对象的占用大小,看看是谁阻止了垃圾收集器的回收工作,并可以通过报表直观的查看到可能造成这种结果的对象。

    除了Eclipse插件版,MAT也有独立的不依赖Eclipse的版本,只不过这个版本在调试Android内存的时候,需要将DDMS生成的文件进行转换,才可以在独立版本的MAT上打开。因为DDMS生成的是Android格式的HPROF(堆转储)文件,而MAT只能识别JAVA格式的HPROF文件。不过Android SDK中已经提供了这个Tools,所以使用起来也是很方便的。

    要调试内存,首先需要获取HPROF文件,HPROF文件存储的是特定时间点,java进程的内存快照。有不同的格式来存储这些数据,总的来说包含了快照被触发时java对象和类在heap中的情况。由于快照只是一瞬间的事情,所以heap dump中无法包含一个对象在何时、何地(哪个方法中)被分配这样的信息。

    官方文档
    Basic Tutorial

    MAT中一些概念介绍


    要看懂MAT的列表信息,Shallow heap、Retained Heap、GC Root这几个概念一定要弄懂。

    Shallow heap

    Shallow size就是对象本身占用内存的大小,不包含其引用的对象。

    • 常规对象(非数组)的Shallow size有其成员变量的数量和类型决定。
    • 数组的shallow size有数组元素的类型(对象类型、基本类型)和数组长度决定。

    因为不像c++的对象本身可以存放大量内存,java的对象成员都是些引用。真正的内存都在堆上,看起来是一堆原生的byte[], char[], int[],所以我们如果只看对象本身的内存,那么数量都很小。所以我们看到Histogram图是以Shallow size进行排序的,排在第一位第二位的一般是byte,char 。

    Retained Set

    Retained Set是指当一个对象X被GC时,会因为X的释放而同时被GC掉的所有对象的集合。

    Retained Heap

    Retained Heap则表示一个对象X的Retained Set中所有对象的Shallow Size的总和。换句话说,Retained Heap就表示如果一个对象被释放掉,那会因此而被释放的总的heap大小。于是,如果一个对象的某个成员new了一大块int数组,那这个int数组也可以计算到这个对象中。相对于shallow heap,Retained heap可以更精确的反映一个对象实际占用的大小(因为如果该对象释放,retained heap都可以被释放)。

    这里要说一下的是,Retained Heap并不总是那么有效。例如我在A里new了一块内存,赋值给A的一个成员变量。此时我让B也指向这块内存。此时,因为A和B都引用到这块内存,所以A释放时,该内存不会被释放。所以这块内存不会被计算到A或者B的Retained Heap中。为了纠正这点,MAT中的Leading Object(例如A或者B)不一定只是一个对象,也可以是多个对象(Leading Set)。此时,(A, B)这个组合的Retained Set就包含那块大内存了。
    Retained_Heap
    很显然,从上面的对象引用图计算Retained Memory并不那么直观高效。比如A和B的Retained Memory只有它们自身,而E的Retained Memory则是E和G,G的Retained Memory也只是它自身。为了更直观的计算Retained Memory,MAT引入了Dominator(统治者) Tree的概念。

    Dominator Tree

    dominator tree
    在Dominator Tree中,有下面一些非正式的定义:

    • 在对象图中,若每一条从开始节点(或根节点)到节点y的路径都必须经过节点x,那么节点x就dominates节点y。
    • 在Dominator Tree中,每一个节点都是其子节点的直接Dominator。

    Dominator Tree还有以下重要属性:

    • x的sub-tree就代表了x的retained set。
    • 如果x是y的直接dominator,那么x的直接dominator同样dominates y,以此类推。
    • Dominator Tree的边缘并不直接对应于对象引用树中的引用关系。比如在引用图中,C是A和B的子节点,而在Dominator Tree中,三者却是平级的。

    对应到MAT UI上,在dominator tree这个view中,显示了每个对象的shallow heap和retained heap。而点击右键,可以List objects中选择with outgoing references和with incoming references。这是个真正的引用图的概念,

    • outgoing references :表示该对象的出节点(被该对象引用的对象)。
    • incoming references :表示该对象的入节点(引用到该对象的对象)。

    GC Roots

    首先要说一下GC的原则:

    垃圾回收器会尝试回收所有非GC roots的对象。所以如果你创建一个对象,并且移除了这个对象的所有指向,它就会被回收掉。但是如果你将一个对象设置成GC root,那它就不会被回收掉。那么GC又如何判断某个对象是否可以被回收呢?在垃圾回收过程中,当一个对象到GC Roots 没有任何引用链(或者说,从GC Roots 到这个对象不可达)时,垃圾回收器就会释放掉它

    而GC Roots是一些由虚拟机自身保持存活的对象。比如运行中的线程、当前处于调用栈中的对象、由system class loader加载的类等等。

    反过来,从一个对象到一个GC Roots的引用链(path to GC Root),就解释了为什么这个对象无法被GC。这个path就可以帮助我们解决典型的内存泄漏。

    一个gc root就是一个对象,这个对象从堆外可以访问读取。以下一些方法可以使一个对象成为gc root:

    • System class:被Bootstrap或者system class loader加载的类,比如位于rt.jar里的所有类(如java.util.*);
    • JNI local:native代码里的local变量,比如用户定义的JNI代码和JVM的内部代码;
    • JNI global:native代码里的global变量,比如用户定义的JNI代码和JVM的内部代码;
    • Thread block:当前活跃的线程block中引用的对象;
    • Thread:已经启动并且没有stop的线程;
    • busy monitor:调用了wait()或者notify()或者被同步的对象,比如调用了synchronized(Object) 或使用了synchronized方法。静态方法指的是类,非静态方法指的是对象;
    • java local:local变量,比如仍然存在于线程栈中的方法的入参和方法内创建的对象;
    • native stack:native代码里的出入参数,比如file/net/IO方法以及反射的参数;
    • finalizable:在一个队列里等待它的finalizer 运行的对象;
    • unfinalized:一个有finalize方法的对象,还没有被finalize,同时也没有进入finalizer队列等待finalize;
    • unreachable:被MAT标记为root,并且无法通过任何其他root到达的对象,这个root的作用是retain那些不这么做就无法包含在分析中的objects;
    • java stack frame:一个持有本地变量的java栈帧。只有在dump被解析且在preferences里设置把栈帧当做对象对待时才会产生;
    • unknown:未知root类型的对象。

    Java的引用级别

    从最强到最弱,不同的引用(可到达性)级别反映了对象的生命周期。

    • 强引用:通过 new 关键字创建出来的对象引用都是强引用,只有去掉强引用,对象才会被回收。请记住,JVM宁可抛出OOM也不会去回收一个有强引用的对象
    • 软引用:只要有足够的内存,就一直保持对象,直到发现一次GC后内存仍然不够,系统会在将要发生OOM之前针对此类对象进行二次回收。如果此次回收还没有足够的内存,才会抛出OOM。一般可用来实现缓存,通过java.lang.ref.SoftReference类实现。
    • 弱引用:比Soft Ref更弱,被弱引用关联的对象只能生存到下一次GC发生之前。在GC执行时,无论当前内存是否足够,都会立刻回收只被弱引用关联的对象。通过java.lang.ref.WeakReference和java.util.WeakHashMap类实现。
    • 虚引用:也成为幽灵引用或幻影引用。虚引用完全不会影响对象的生存时间,你只能使用Phantom Ref本身,而无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被GC时收到一个系统通知。一般用于在进入finalize()方法后进行特殊的清理过程,通过 java.lang.ref.PhantomReference实现。

    获取HPROF(堆转储)文件


    HPROF(堆转储)文件可以使用DDMS导出,在Android Studio中选择“Tools → Android → Android Device Monitor”(为方便使用,可以将该按钮固定在AS工具栏面板中),DDMS中在Devices上面有一排按钮,选择一个进程后(即在Devices下面列出的列表中选择你要调试的应用程序的包名),点击Dump HPROF file 按钮:
    export hprof

    Tips:在Android Studio3.0以后,DDMS不再可以通过Android Studio打开了,打开方式有以下两种:

    • 进入Android SDK安装路径,在tools目录下双击“monitor”文件
    • 在命令行直接输入命令:monitor

    选择存储路径保存后就可以得到对应进程的HPROF文件。不过该文件是Android格式的,你可以直接拖入AS中进行浏览,但功能有限,若要做更深入的内存分析,一般要用专门的分析工具,比如Eclipse的MAT或者Oracle的jhat(jdk6以后提供的)。MAT有两个版本:Eclipse插件版和客户端版,插件版可以把上面的工作一键完成。只需要点击Dump HPROF file图标,然后MAT插件就会自动转换格式,并且在eclipse中打开分析结果。eclipse中还专门有个Memory Analysis视图 ,得到对应的文件后,如果安装了Eclipse插件,那么切换到Memory Analyzer视图。

    使用独立安装的MAT,则须使用Android SDK自带的工具(hprof-conv 位置在sdk/platform-tools/hprof-conv)将上述Android格式的HPROF文件转换为java格式的HPROF文件:

    hprof-conv [-z] com.test.myproject.hprof com.test.myproject_conv.hprof
    -z:排除非APP泄漏的干扰,比如zygote
    

    (Windows系统可能需要进入上述路径找到hprof-conv.exe安装一次)
    转换过后的.hprof文件即可使用MAT工具打开了。

    Tips:堆转储文件的导出和格式转换工作,Android Studio也可以帮我们完成,在AS3.0以前是Android Monitor(Logcat窗口旁边的tab页),而AS3.0以上版本则是Android Profiler,并且它们也都可以做一些内存分析。

    MAT一般使用步骤


    首先需要通过 Android Profile 或者 DDMS 得到一个堆转储文件(方法见上一节),然后就可以按照以下方法分析:

    1.打开经过转换的hprof文件:
    mat step 1
    如果选择了第一个,则会生成一个报告。这个无大碍,是工具帮你分析的有泄漏嫌疑的对象,可以作为一个快速参考。
    mat step 2
    2.选择OverView界面:
    mat step 3
    上方的“Unreachable Objects Histogram”指的是当前可以被GC的对象,只是由于系统还未触发GC,所以仍然存活于heap中,这个一般不需要关心。

    常用的是Histogram和Dominator Tree。

    Histogram

    列出每个类分配了多少个实例,以及实例的大小。
    mat step 4
    排在前两位的基本是byte[]和char[],一般不需要理会。Histogram视图中,默认不显示Retained Heap,如果想查看Retained Heap大小,可以点击工具栏中按钮:
    button_to_calculate_retained_heap
    为了方便查看,快速找到自己的类的问题,可以“Group by package”:
    mat step 5

    Dominator Tree

    列出最大的对象以及其依赖存活的Object (大小是以Retained Heap为标准排序的),多了一列Percentage。
    mat step 6
    同样也可以“Group by package”

    我们看到MobUIShell本身占用了408Byte的内存,并且如果它被回收,将释放出201840Byte的内存空间。

    右键选择Merge Shortest Paths to GC Roots - exclude weak/soft references,就可以分析出其对象引用关系,为什么选择exclude weak/soft references呢?因为通常情况下weak/soft references不会导致内存泄漏。
    mat step 7
    可以看到,SizeHelper泄漏了MobUIShell的一个实例,这个实例本身占用了200Byte的内存,而它同时还持有了201160Byte的其他对象的内存,换句话说,一旦它被清理了,就可以释放200K左右的内存。
    mat step 8
    其实这和LeakCanary报告的结果是一致的:
    SizeHelper_leaks_lc
    至此,我们知道了SizeHelper泄漏了MobUIShell,接下来当然就是要分析为什么会泄漏,以及如何解决泄漏,首先看看SizeHelper.java的内容:

    public class SizeHelper {
    	public static float designedDensity = 1.5f;
    	public static int designedScreenWidth = 540;
    	private static Context context = null;
    	
    	protected static SizeHelper helper;
    	
    	private SizeHelper() {
    	}
    	
    	public static void prepare(Context c) {
    		if(context == null || context != c.getApplicationContext()) {
    			context = c;
    		}
    	}
    	
    	public static int fromPx(int px) {
    		return ResHelper.designToDevice(context, designedDensity, px);
    	}
    	
    	public static int fromPxWidth(int px) {
    		return ResHelper.designToDevice(context, designedScreenWidth, px);
    	}
    	
    	public static int fromDp(int dp) {
    		int px = ResHelper.dipToPx(context, dp);
    		return ResHelper.designToDevice(context, designedDensity, px);
    	}
    }
    

    上述代码是一种最常见也最简单的泄漏 - 由静态变量引起的泄漏。静态全局变量context:

    private static Context context = null;
    

    如何解决此处的泄漏?三种方案:

    方案一:不改变代码,使用时prepare方法中传入Application Context而非Activity Context,虽然可以避免泄漏,但无法保证其他人在使用SizeHelper时不会传入Activity Context,这里就有泄漏隐患。所以不可取。要从根本上杜绝泄漏隐患,就必须重构代码。

    方案二:重构代码,强制让SizeHelper仅持有ApplicationContext的实例。

    	public static void prepare(Context c) {
    		if(context == null || context != c.getApplicationContext()) {
        // 强制从Context获取ApplicationContext,让SizeHelper持有ApplicationContext的实例
    			context = c.getApplicationContext();
    		}
    	}
    

    该方案仍然允许你使用static关键字修饰context,虽然context仍然无法被GC,但它本身所持有的只是ApplicationContext实例,而非Activity Context实例,所以并不会造成某个Activity的泄漏。但个别情况下无法使用该方案,比如只能使用Activity Context(Dialog相关时就不能使用ApplicationContext)时,此时只能另辟蹊径,比如方案三。

    方案三:重构代码,不再持有Context实例。

    public class SizeHelper {
    	public static final float designedDensity = 1.5f;
    	public static final int designedScreenWidth = 540;
    
    	public static int fromPx(Context context, int px) {
    		return ResHelper.designToDevice(context, designedDensity, px);
    	}
    
    	public static int fromPxWidth(Context context, int px) {
    		return ResHelper.designToDevice(context, designedScreenWidth, px);
    	}
    
    	public static int fromDp(Context context, int dp) {
    		int px = ResHelper.dipToPx(context, dp);
    		return ResHelper.designToDevice(context, designedDensity, px);
    	}
    }
    

    Context不再被设置为静态全局变量,而是作为方法内的局部变量被使用,这样看起来,SizeHelper本身已经不可能再发生泄漏了,但它实际上是调用ResHelper的方法,所以我们还要确认一下ResHelper会不会造成泄漏。下面是ResHelper的部分代码:

    public class ResHelper {
        private static float density;
        private static int deviceWidth;
        private static Object rp;
        private static Uri mediaUri;
    
        public ResHelper() {
        }
        ...
        public static int designToDevice(Context context, float designScreenDensity, int designPx) {
            if(density <= 0.0F) {
                density = context.getResources().getDisplayMetrics().density;
            }
            return (int)((float)designPx * density / designScreenDensity + 0.5F);
        }
        ...
    }
    

    ResHelper就是一个普通的类,提供了一系列静态方法,并且它没有把Context保存成静态全局变量,所以它并不会造成context对象的泄漏。

    好了,我们再运行一次看看MAT的分析结果:
    resolved 1
    resolved 2
    我们看到MobUIShell的泄漏明显降低了,现在它的Retained Heap只有680Byte,内存百分比也已经降到了0.01%,被释放的内存为201840Byte-680Byte=201160Byte(这和前面图表中MAT侦测到此处泄漏时所报告的数据完全一致),虽然没有完全解决,但剩下这一部分明显是Android SDK的InputMethodManager造成的,已经和SizeHelper没有关系了。

    还有一个更快捷的对比方式,在Histogram页面,点击“Compare to another Heap Dump”按钮,可以选择与之前的Heap Dump做对比,我们看到MobUIShell的Shallow Heap相比于修改之前,减少了200Byte,而整个应用的内存泄漏,减少了213224Byte(200K左右)。说明一点,下图是在修改后的Heap Dump中选择与修改前的Heap Dump做对比,所以显示为“-200”。如果在修改前的Heap Dump中选择与修改后的Heap Dump做对比,就会显示为“200”,意思是修改前的相比修改后的多了200Byte的内存。
    resolved 3
    还可以通过immediate dominator找到责任对象,对于快速定位一组对象的持有者非常有用,这个操作直接解决了“谁让这些对象alive”的问题,而不是“谁有这些对象的引用”的问题,更直接高效。

    “严格模式”:StrictMode


    其实在性能调优中,从Android 6.0开始,系统还提供了一个严格模式,它是用来检测程序中违例情况的开发者工具。最常用的场景就是检测主线程中本地磁盘和网络读写等耗时的操作,也包括内存泄漏,一旦发现问题,就会打error日志或者强制应用崩溃。需要通过代码打开该模式,但只建议在debug模式下打开。严格模式也很简单,想了解的读者可以参考这篇文章:

    Android性能调优利器StrictMode

    常见的内存泄漏及解决方案


    通过以上章节的介绍,我们了解到了如何使用MAT分析内存泄露问题,本章节主要介绍常见的几种内存泄漏和解决方案。在这之前,让我们再多了解一下Android中的内存泄漏。

    传统的内存泄漏是由忘记释放分配的内存导致的,比如用完Stream或者DB Connection以后忘记close,而逻辑上的内存泄漏(Logical Leak)则是由于忘记在对象不再被使用的时候释放对它的引用导致的。如果一个对象仍然存在强引用,垃圾回收器就无法对其进行回收。在安卓平台,泄漏 Context 对象问题尤其严重。这是因为Acitivity指向Window,而Window又拥有整个View继承树,除此之外,Activity还可能引用其他占用大量内存的资源(比如Bitmap)。如果 Context 对象发生了内存泄漏,那它引用的所有对象都被泄漏了。

    如果一个对象的合理生命周期没有清晰的定义,那判断逻辑上的内存泄漏将是一个见仁见智的问题。幸运的是,activity 有清晰的生命周期定义,使得我们可以很明确地判断 activity 对象是否被内存泄漏。onDestroy() 函数将在 activity 被销毁时调用,无论是程序员主动销毁 activity,还是系统为了回收内存而将其销毁。如果 onDestroy 执行完毕之后,activity 对象仍被 heap root 强引用,那垃圾回收器就无法将其回收。所以我们可以把生命周期结束之后仍被引用的 activity 定义为被泄漏的 activity。

    Activity 是非常重量级的对象,所以我们应该极力避免妨碍系统对其进行回收。然而有多种方式会让我们无意间就泄露了 activity 对象。我们把可能导致 activity 泄漏的情况分为两大类,一类是使用了进程全局(process-global)的静态变量,无论 APP 处于什么状态,都会一直存在,它们持有了对 activity 的强引用进而导致内存泄漏,另一类是生命周期长于 activity 的线程,它们忘记释放对 activity 的强引用进而导致内存泄漏。下面我们就来详细分析一下这些可能导致 activity 泄漏的情况。

    Tips:为什么说静态变量会导致泄漏呢?这要从java基础说起,static修饰的变量称为静态变量,又称类变量,从命名就能看出类变量的生命周期是绑定在类对象(class)上的,而非某个具体的实例,而类对象的生命周期是从被类加载器加载一直到应用结束为止,几乎就等于应用的生命周期。所以一旦某个静态变量持有了Activity的强引用,那么就会造成泄漏。

    静态Activity

    private static Context context;
    proteced void onCreate(Bundle savedInstanceState) {
        context = this;
    }
    

    尽量避免使用static关键字修饰context,如果一定要用,就必须保证context只能是ApplicationContext,不能是Activity Context。也就是要结合以下代码:

    this.context = this.getApplicationContext();
    

    或者在Activity生命周期结束前,清除这个引用:

    protected void onDestroy() {
        context = null;
    }
    

    静态View

    有时候我们可能有一个创建起来非常耗时的 View,在同一个 activity 不同的生命周期中都保持不变,所以让我们为它实现一个单例模式。

    private static View view;
    
    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();
      }
    });
    

    你又泄漏了Activity!因为一旦 view 被加入到界面中,它就会持有 context 的强引用,也就是我们的 activity。由于我们通过一个静态成员引用了这个 view,所以我们也就引用了 activity,因此 activity 就发生了泄漏。所以一定不要把加载的 view 赋值给静态变量,如果你真的需要,那一定要确保在 activity 销毁之前将其从 view 层级中移除。

    void removeView (View view)
    

    单例

    单纯的单例模式并没有什么问题,但如果在单例模式中,将一个context对象作为全局变量,就会造成泄漏。

    class Singleton {
    	private static Singleton instance;
    	private Context context;
    
    	private Singleton(Context context) {
    		this.context = context;
    	}
    
    	public static Singleton getInstance(Context context) {
    		if (instance == null) {
    			instance = new Singleton(context);
    		}
    		return instance;
    	}
    }
    

    上述代码是一个线程不安全的单例模式,但不影响我们分析单例导致的泄漏。

    同样地,要么保证context只能是ApplicationContext,要么不要将context写成全局变量。

    可以改造一下构造方法:

    private Singleton(Context context) {
    	this.context = context.getApplicationContext();
    }
    

    当然了,如果这个单例是和Dialog有关的,那么就无法使用ApplicationContext,此时就只能重构代码,不将context写成全局变量了。

    非静态内部类

    我们在编程时经常会用到内部类,这样做的原因有很多,比如增加封装性和可读性。如果我们创建了一个内部类的对象,并且通过静态变量持有了该内部类对象的引用,那也会发生 activity 泄漏。

    	private boolean b = false;
    	private static InnerClass inner;
    
    	void createInnerClass() {
    		inner = new InnerClass();
    	}
    
    	class InnerClass {
    		private boolean bool;
    		
    		public InnerClass() {
    			this.bool = b;
    		}
    	}
    

    内部类的一大优势就是能够直接引用外部类的成员,这是通过隐式地持有外部类的引用来实现的,而这又恰恰是造成 activity 泄漏的原因。

    可见,在使用非静态内部类时,一定要注意引用的生命周期,避免内部类的生命周期超出外部类,这样引用就没有问题了:

    private InnerClass inner;
    

    但是在实际开发中,我们仍然要尽量避免使用非静态内部类,而要改用静态内部类,因为静态内部类并不会持有外部类的引用,也就不会泄漏外部类了,但相对的,静态内部类无法访问外部类的成员。如果你的代码结构必须访问外部类的成员,那么请使用静态内部类+弱引用,让静态内部类持有外部类的弱引用,既不会造成泄漏,又能解决访问外部类的成员变量的问题。

    	private boolean b = false;
    	private static InnerClass inner;
    
    	void createInnerClass() {
    		inner = new InnerClass(this);
    	}
    
    	static class InnerClass {	// 静态内部类
    		private boolean bool;
    		private WeakReference<MainActivity> activityWeakReference;	// 外部类的弱引用
    
    		public InnerClass(MainActivity activity) {
    			activityWeakReference = new WeakReference<>(activity);
    			MainActivity mainActivity = activityWeakReference.get();
    			if (mainActivity != null) {	// 使用弱引用时要注意判空,因为弱引用的对象可能会被GC
    				this.bool = mainActivity.b;	// 如此访问外部类的成员
    			}
    		}
    	}
    

    匿名内部类

    匿名内部类和非静态内部类导致内存泄露的原理一样,因为匿名内部类也同样隐式持有外部类的引用。在Android开发中有一种典型的场景就是使用Handler,很多开发者在使用Handler时是这样写的:

    public class MainActivity extends AppCompatActivity {
        
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            start();
        }
    
        private void start() {
            Message msg = Message.obtain();
            msg.what = 1;
            mHandler.sendMessage(msg);
        }
    
        private Handler mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                if (msg.what == 1) {
                    // 做相应逻辑
                }
            }
        };
    }
    

    看起来并没有问题,mHandler并未作为静态变量持有Activity引用,生命周期可能不会比Activity长,应该不会导致内存泄露啊,显然不是这样的!

    这要从Handler消息机制说起,mHandler会作为成员变量保存在发送的消息msg中,即msg持有mHandler的引用,而mHandler是Activity的非静态内部类实例,即mHandler持有Activity的引用,那么我们就可以理解为msg间接持有Activity的引用。msg被发送后先放到消息队列MessageQueue中,然后等待Looper的轮询处理(MessageQueue和Looper都是与线程相关联的,MessageQueue是Looper引用的成员变量,而Looper是保存在ThreadLocal中的)。那么当Activity退出后,msg可能仍然存在于消息队列MessageQueue中未处理或者正在处理,那么这样就会导致Activity无法被回收。

    套用前面说的“静态内部类+弱引用”的方法,重构代码:

    public class MainActivity extends AppCompatActivity {
    
        private Handler mHandler;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mHandler = new MyHandler(this);
            start();
        }
    
        private void start() {
            Message msg = Message.obtain();
            msg.what = 1;
            mHandler.sendMessage(msg);
        }
    
        private static class MyHandler extends Handler {
    
            private WeakReference<MainActivity> activityWeakReference;
    
            public MyHandler(MainActivity activity) {
                activityWeakReference = new WeakReference<>(activity);
            }
    
            @Override
            public void handleMessage(Message msg) {
                MainActivity activity = activityWeakReference.get();
                if (activity != null) {
                    if (msg.what == 1) {
                        // 做相应逻辑
                    }
                }
            }
        }
    }
    

    mHandler通过弱引用的方式持有Activity,当GC执行垃圾回收时,遇到Activity就会回收并释放所占据的内存单元。这样就不会发生内存泄露了。

    上面的做法确实避免了Activity的泄露,发送的msg不再持有Activity的强引用了,但是msg还是有可能存在消息队列MessageQueue中,所以更好的是在Activity销毁时就将mHandler的回调和发送的消息给移除掉。

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }
    

    让我们再来看一个很常见的场景:用Handler实现的计时器。比如发送短信验证码后一般会有个1分钟的倒计时才能重新发送,如果没有在适当的时候主动关闭计时器,而该计时器又正好间接持有了activity context的引用,那么在计时器结束之前就会将该activity泄漏。

    以下是短信GUI中使用Handler实现的倒数计时器:

    	private void countDown() {
    		runOnUIThread(new Runnable() {
    			public void run() {
    				time--;
    				setResendText(time);
    				if (time <= 0) {
    					time = 60;
    				} else {
    					runOnUIThread(this, 1000);
    				}
    			}
    		}, 1000);
    	}
    

    其中的runOnUIThread是位于FakeActivity类中的方法:

        public void runOnUIThread(final Runnable r, long delayMillis) {
            UIHandler.sendEmptyMessageDelayed(0, delayMillis, new Callback() {
                public boolean handleMessage(Message msg) {
                    r.run();
                    return false;
                }
            });
        }
    

    这里参数中传入的Callback其实就是一个FakeActivity的匿名内部类,它持有外部类FakeActivity的强引用,而FakeActivity又持有着实际的Activity context的强引用,于是在计时器停止前,当前页面会被泄漏。解决该问题的方法就是在离开当前页面时主动停止计时器:

    	@Override
    	public void onDestroy() {
    		super.onDestroy();
    		// 离开该页面前停止读秒计时器
    		stopCountDown();
    	}
    	private void stopCountDown() {
    		time = 1;
    	}
    

    需要注意的是,不止是onDestroy方法中要停止计时器,同时输入正确验证码后跳转下一个页面时也要停止(此时并不会调用onDestroy哦)!

    匿名内部类造成泄漏的场景还有很多,比如在Activity中定义一个匿名的AsyncTask,如果Activity结束时没有正确的结束AsyncTask,那么就会妨碍GC对Activity的回收,直到AsyncTask执行结束才能回收。同样地,通过匿名内部类创建的Thread和TimerTask,也很可能因为没有正确的结束而泄漏Activity。另外,常用的listener和callback对象等(无论是通过内部类实现还是通过让Activity直接implements Callback实现)都有可能泄漏Activity,这些Callback的实例很可能会通过多次引用传递最终被某个类的类变量(比如某个单例)或者某个生命周期较长的线程所持有,最终导致Activity被泄漏。我们的AsyncImageView中就发生了这样的情况,AsyncImageView是我们自定义的View,它本身持有activity context,为了处理图片,其内部通过匿名内部类创建了一个Callback对象传给BitmapProcess类以接收图片处理结果,而BitmapProcess中又经过几次传递,最终将Callback对象保存在一个静态的ArrayList对象中。为了解决这个问题,必须在Callback使用结束后显示的清除对它的引用(设置为null)。

    SensorManager以及广播接收器

    系统服务可以通过 context.getSystemService 获取,它们负责执行某些后台任务,或者为硬件访问提供接口。如果 context 对象想要在服务内部的事件发生时被通知,那就需要把自己注册到服务的监听器中。然而,这会让服务持有 activity 的引用,如果程序员忘记在 activity 销毁时取消注册,那就会导致 activity 泄漏了。

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

    Memory Leak_SensorManager
    注册广播也是同理,如果在Activity销毁时忘记注销广播接收器,也会导致Activity的泄漏。

    集合中的对象未清理造成内存泄露

    这个比较好理解,如果一个对象放入到ArrayList、HashMap等集合中,这个集合就会持有该对象的引用。当我们不再需要这个对象时,也并没有将它从集合中移除,这样只要集合还在使用(而此对象已经无用了),这个对象就造成了内存泄露。并且如果集合被静态引用的话,集合里面那些没有用的对象更会造成内存泄露了。所以在使用集合时要及时将不用的对象从集合remove,或者clear集合,以避免内存泄漏。

    资源未关闭或释放导致内存泄露

    在使用IO、File流或者Sqlite、Cursor等资源时要及时关闭。这些资源在进行读写操作时通常都使用了缓冲,如果不及时关闭,这些缓冲对象就会一直被占用而得不到释放,以致发生内存泄露。

    属性动画造成内存泄露

    动画同样是一个耗时任务,比如在Activity中启动了属性动画(ObjectAnimator),但是在销毁的时候,没有调用cancle方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在的控件引用Activity,这就造成Activity无法正常释放。因此同样要在Activity销毁的时候cancel掉属性动画,避免发生内存泄漏。

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mAnimator.cancel();
    }
    

    WebView造成内存泄露

    关于WebView的内存泄露,因为WebView在加载网页后会长期占用内存而不能被释放,因此我们在Activity销毁后要调用它的destroy()方法来销毁它以释放内存。

    另外在查阅WebView内存泄露相关资料时看到这种情况:

    Webview下面的Callback持有Activity引用,造成Webview内存无法释放,即使是调用了Webview.destory()等方法都无法解决问题(Android5.1之后)。

    最终的解决方案是:在销毁WebView之前需要先将WebView从父容器中移除,然后再销毁WebView。详细分析过程请参考这篇文章:
    WebView内存泄漏解决方法

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 先从父控件中移除WebView
        mWebViewContainer.removeView(mWebView);
        mWebView.stopLoading();
        mWebView.getSettings().setJavaScriptEnabled(false);
        mWebView.clearHistory();
        mWebView.removeAllViews();
        mWebView.destroy();
    }
    

    总结:如何避免写出内存泄漏的代码


    1. 谨慎使用static关键字,尤其不要用static修饰Activity context;
    2. 注意不要让类变量直接或间接地持有Activity context引用;
    3. 尽量不要在单例中使用Activity context,如果要用,不能将其作为全局变量;
    4. 时刻注意内部类(尤其是Activity的内部类)的生命周期,尽量使用静态内部类代替内部类,如果内部类需要访问外部类的成员,可以用“静态内部类+弱引用”代替;内部类的生命周期不应该超出外部类,外部类结束前,应该及时结束内部类生命周期(停止线程、AsyncTask、TimerTask、Handler消息等,移除类变量或长生命周期的线程对Callback、listener等的强引用);
    5. 及时注销广播以及一些系统服务的监听器;
    6. 属性动画在Activity销毁前记得cancel;
    7. 文件流、Cursor等资源用完及时关闭;
    8. Activity销毁前WebView的移除和销毁;
    9. 使用别人的方法(尤其是第三方库),遇到需要传递context时尽量使用ApplicationContext,而不要轻易使用Activity context,因为你不知道别人的代码内部会不会造成该context的泄漏。比如微信支付SDK就有泄漏的隐患,微信支付初始化时需要传入context,最终由WXApiImpl这个类持有了context,如果你传入的是activity context,就会被WXApiImpl泄漏。
      Memory Leak_wechat

    知识点梳理


    1.GC如何判断某个对象是否可以被回收:

    在垃圾回收过程中,当一个对象到GC Roots 没有任何引用链(或者说,从GC Roots 到这个对象不可达)时,垃圾回收器就会释放掉它。

    2.Java的引用级别:

    强引用 - 软引用 - 弱引用 - 虚引用

    3.JVM宁可抛出OOM也不会去回收一个有强引用的对象
    4.GC Root:

    有多种方法使得一个对象成为GC Root,GC Root是由虚拟机自身保持存活的对象,所以它不会被回收,由GC Root强引用的对象也无法被回收。

    5.内部类和静态内部类:

    内部类的一大优势就是可以直接引用外部类的成员,这是通过隐式地持有外部类的引用来实现的;而静态内部类,由于不再隐式地持有外部类的引用,也就无法直接引用外部类的成员了。

    6.如何避免内部类造成的泄漏:

    为避免内部类泄漏外部类,应该使用静态内部类。但静态内部类又无法访问外部类的成员,为解决该问题,可以使用“静态内部类+弱引用”,让静态内部类持有外部类的弱引用,既不会造成泄漏,又能解决访问外部类的成员变量的问题。

    7.LeakCanary如何检查是否存在内存泄漏:

    WeakReference + ReferenceQueue

    参考文献


    本文是笔者做了大量参考学习,并结合自身实践总结得出,特此感谢:

    Eight Ways Your Android App Can Leak Memory
    Android内存优化——常见内存泄露及优化方案
    LeakCanary on GitHub
    用LeakCanary检测内存泄露
    Overview of memory management
    Vidoe:Memory management on Android Apps

    展开全文
  • 内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。 原因: 1.内存中加载的数据量过于庞大,...

    内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。

    原因:
    1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
    2.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
    3.代码中存在死循环或循环产生过多重复的对象实体;
    4.使用的第三方软件中的BUG;
    5.启动参数内存值设定的过小

    内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

    内存泄露会最终会导致内存溢出! 大家都知道虚拟机针对每一个应用都会分配给一定量的内存,当你的请求量超过这个值的时候,就是内存溢出。

    内存泄露解决方案:

    静态变量导致的内存泄露(解决办法:在适当的时候讲静态量重置为null,使其不再持有引用,这样也可以避免内存泄露。比如静态的context,静态的view;静态变量存储在方法区,它的生命周期从类加载开始,到整个进程结束。一旦静态变量初始化后,它所持有的引用只有等到进程结束才会释放)

    静态单例模式持有Activity或者services无法被释放(解决办法:引用全局的context 单例模式对应应用程序的生命周期,所以我们在构造单例的时候尽量避免使用Activity的上下文,而是使用Application的上下文)

    非静态内部类导致内存泄露(情况一)(例如:Handler,非静态内部类(包括匿名内部类)默认就会持有外部类的引用,当非静态内部类对象的生命周期比外部类对象的生命周期长时,就会导致内存泄露。
    mHandler会作为成员变量保存在发送的消息msg中,即msg持有mHandler的引用,而mHandler是Activity的非静态内部类实例,即mHandler持有Activity的引用,那么我们就可以理解为msg间接持有Activity的引用。msg被发送后先放到消息队列MessageQueue中,然后等待Looper的轮询处理(MessageQueue和Looper都是与线程相关联的,MessageQueue是Looper引用的成员变量,而Looper是保存在ThreadLocal中的)。那么当Activity退出后,msg可能仍然存在于消息对列MessageQueue中未处理或者正在处理,那么这样就会导致Activity无法被回收,以致发生Activity的内存泄露。
    解决办法:
    通常在Android开发中如果要使用内部类,但又要规避内存泄露,一般都会采用静态内部类+弱引用的方式。(当GC执行垃圾回收时,遇到Activity就会回收并释放所占据的内存单元),但是msg还是有可能存在消息队列MessageQueue中,所以更好的是在Activity销毁时就将mHandler的回调和发送的消息给移除掉。

    public class MainActivity extends AppCompatActivity {
    
        private Handler mHandler;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mHandler = new MyHandler(this);
            start();
        }
    
        private void start() {
            Message msg = Message.obtain();
            msg.what = 1;
            mHandler.sendMessage(msg);
        }
    
        private static class MyHandler extends Handler {
    
            private WeakReference<MainActivity> activityWeakReference;
    
            public MyHandler(MainActivity activity) {
                activityWeakReference = new WeakReference<>(activity);
            }
    
            @Override
            public void handleMessage(Message msg) {
                MainActivity activity = activityWeakReference.get();
                if (activity != null) {
                    if (msg.what == 1) {
                        // 做相应逻辑
                    }
                }
            }
        }
    }
    

    非静态内部类导致内存泄露(情况二)
    非静态内部类造成内存泄露还有一种情况就是使用Thread或者AsyncTask
    比如在Activity中直接new一个子线程Thread:(newThread new Runable方式;新建的子线程Thread和AsyncTask都是匿名内部类对象,默认就隐式的持有外部Activity的引用,导致Activity内存泄露。要避免内存泄露的话还是需要像上面Handler一样使用静态内部类+弱应用的方式(代码就不列了,参考上面Hanlder的正确写法)。)

    未取消注册或回调导致内存泄露
    比如我们在Activity中注册广播,如果在Activity销毁后不取消注册,那么这个刚播会一直存在系统中,同上面所说的非静态内部类一样持有Activity引用,导致内存泄露。因此注册广播后在Activity销毁后一定要取消注册。
    比如我们在Activity中注册广播,如果在Activity销毁后不取消注册,那么这个刚播会一直存在系统中,同上面所说的非静态内部类一样持有Activity引用,导致内存泄露。因此注册广播后在Activity销毁后一定要取消注册。

    Timer和TimerTask导致内存泄露
    Timer和TimerTask在Android中通常会被用来做一些计时或循环任务,比如实现无限轮播的ViewPager:
    当我们Activity销毁的时,有可能Timer还在继续等待执行TimerTask,它持有Activity的引用不能被回收,因此当我们Activity销毁的时候要立即cancel掉Timer和TimerTask,以避免发生内存泄漏。

    集合中的对象未清理造成内存泄露
    这个比较好理解,如果一个对象放入到ArrayList、HashMap等集合中,这个集合就会持有该对象的引用。当我们不再需要这个对象时,也并没有将它从集合中移除,这样只要集合还在使用(而此对象已经无用了),这个对象就造成了内存泄露。并且如果集合被静态引用的话,集合里面那些没有用的对象更会造成内存泄露了。所以在使用集合时要及时将不用的对象从集合remove,或者clear集合,以避免内存泄漏。

    资源未关闭或释放导致内存泄露
    在使用IO、File流或者Sqlite、Cursor等资源时要及时关闭。这些资源在进行读写操作时通常都使用了缓冲,如果及时不关闭,这些缓冲对象就会一直被占用而得不到释放,以致发生内存泄露。因此我们在不需要使用它们的时候就及时关闭,以便缓冲能及时得到释放,从而避免内存泄露。

    属性动画(repeatCount为无限模式)线程优化
    动画同样是一个耗时任务,比如在Activity中启动了属性动画(ObjectAnimator),但是在销毁的时候,没有调用cancle方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在的控件引用Activity,这就造成Activity无法正常释放。因此同样要在Activity销毁的时候cancel掉属性动画,避免发生内存泄漏。

    WebView造成内存泄露
    关于WebView的内存泄露,因为WebView在加载网页后会长期占用内存而不能被释放,因此我们在Activity销毁后要调用它的destory()方法来销毁它以释放内存。
    另外在查阅WebView内存泄露相关资料时看到这种情况:
    Webview下面的Callback持有Activity引用,造成Webview内存无法释放,即使是调用了Webview.destory()等方法都无法解决问题(Android5.1之后)。

    最终的解决方案是:在销毁WebView之前需要先将WebView从父容器中移除,然后在销毁WebView。详细分析过程请参考这篇文章:WebView内存泄漏解决方法。

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 先从父控件中移除WebView
        mWebViewContainer.removeView(mWebView);
        mWebView.stopLoading();
        mWebView.getSettings().setJavaScriptEnabled(false);
        mWebView.clearHistory();
        mWebView.removeAllViews();
        mWebView.destroy();
    }

    LeakCanary是一个傻瓜化并且可视化的内存泄露分析工具
    原理:监视一个即将被销毁的对象
    白话:对被监控对象创建一个弱引用对象,后台线程检查这个对象是否被清除,如果没有就触发垃圾回收,垃圾回收之后如果这个弱引用对象还存在,说明发生了内存泄漏
    1.RefWatcher.watch()函数会为被监控的对象创建一个KeyedWeakReference弱引用对象,是WeakReference对的子类,增加了键值对信息,后面会根据指定的键key找到弱引用对象;
    2.在后台线程AndroidWatchExecutor中,检查KeyedWeakReference弱引用是否已经被清除。如果还存在,则触发一次垃圾回收之后。垃圾回收之后,如果弱引用对象依然存在,说明发生了内存泄露;

    内存泄露在Android内存优化是一个比较重要的一个方面,很多时候程序中发生了内存泄露我们不一定就能注意到,所有在编码的过程要养成良好的习惯
    构造单例的时候尽量别用Activity的引用;
    静态引用时注意应用对象的置空或者少用静态引用;
    使用静态内部类+软引用代替非静态内部类;
    及时取消广播或者观察者注册;
    耗时任务、属性动画在Activity销毁时记得cancel;
    文件流、Cursor等资源及时关闭;
    Activity销毁时WebView的移除和销毁。

    展开全文
  • 这篇文章主要给大家介绍了关于Android内存泄漏的轻松解决方法,文中通过示例代码介绍的非常详细,对各位Android具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧 前言 内存管理的目的就是让我们在开发...

    这篇文章主要给大家介绍了关于Android内存泄漏的轻松解决方法,文中通过示例代码介绍的非常详细,对各位Android具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧

    前言

    内存管理的目的就是让我们在开发过程中有效避免我们的应用程序出现内存泄露的问题。内存泄露相信大家都不陌生,我们可以这样理解:「没有用的对象无法回收的现象就是内存泄露」。

    如果程序发生了内存泄露,则会带来以下这些问题

    • 应用可用的内存减少,增加了堆内存的压力
    • 降低了应用的性能,比如会触发更频繁的 GC
    • 严重的时候可能会导致内存溢出错误,即 OOM Error

    下面我们从基础说起

    基础知识

    Java 的内存分配简述

    • 方法区(non-heap):编译时就分配好,在程序整个运行期间都存在。它主要存放静态数据和常量;
    • 栈区:当方法执行时,会在栈区内存中创建方法体内部的局部变量,方法结束后自动释放内存;
    • 堆区(heap):通常用来存放 new 出来的对象。由 GC 负责回收。

    Java四种不同的引用类型

    • 强引用(Strong Reference):JVM 宁愿抛出 OOM,也不会让 GC 回收存在强引用的对象。
    • 软引用(Soft Reference) :一个对象只具有软引用,在内存不足时,这个对象才会被 GC 回收。
    • 弱引用(weak Reference):在 GC 时,如果一个对象只存在弱引用,那么它将会被回收。
    • 虚引用(Phantom Reference):任何时候都可以被 GC 回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为 GC 回收 Object 的标志。

    与 Android 中的差异:在 2.3 以后版本中,即使内存够用,Android 系统会优先将 SoftReference 的对象提前回收掉, 其他和 Java 中是一样的。
    因此谷歌官方建议用LruCache(least recentlly use 最少最近使用算法)。会将内存控制在一定的大小内, 超出最大值时会自动回收, 这个最大值开发者自己定。

    什么是内存泄漏?

    • 对于 C++ 来说,内存泄漏就是 new 出来的对象没有 delete,俗称野指针;
    • 而对于 java 而言,就是存放在堆上的 Object 无法被 GC 正常回收。

    内存泄漏根本原因

    长生命周期的对象持有短生命周期对象**强/软引用**,导致本应该被回收的短生命周期的对象却无法被正常回收。

    例如在单例模式中,我们常常在获取单例对象时需要传一个 Context 。单例对象是一个长生命周期的对象(应用程序结束时才终结),而如果我们传递的是某一个 Activity 作为 context,那么这个 Activity 就会因为引用被持有而无法销毁,从而导致内存泄漏。

    内存泄漏的危害

    • 运行性能的问题: Android在运行的时候,如果内存泄漏将导致其他组件可用的内存变少,一方面会使得GC的频率加剧,在发生GC的时候,所有进程都必须进行等待,GC的频率越多,从而用户越容易感知到卡顿。另一方面,内存变少,将可能使得系统会额外分配给你一些内存,而影响整个系统的运行状况。
    • 运行崩溃问题: 内存泄露是内存溢出(OOM)的重要原因之一,会导致 Crash。如果应用程序在消耗光了所有的可用堆空间,那么再试图在堆上分配新对象时就会引起 OOM(Out Of Memory Error) 异常,此时应用程序就会崩溃退出。

    内存泄漏的典型案例

    永远的单例(Singleton)

    由于单例模式的静态特性,使得它的生命周期和我们的应用一样长,一不小心让单例无限制的持有 Activity 的强引用就会导致内存泄漏。

    解决方案

    把传入的 Context 改为同应用生命周期一样长的 Application 中的 Context。

    通过重写 Application,提供 getContext 方法,那样就不需要在获取单例时传入 context。

    public class BaseApplication extends Application{
     private static ApplicationContext sContext;
     @Override
     public void onCreate(){
     super.onCreate();
     sContext = getApplicationContext();
     }
     public static Context getApplicationContext(){
     return sContext;
     }
    }

    Handler引发的内存泄漏

    由于 Handler 属于 TLS(Thread Local Storage)变量,导致它的生命周期和 Activity 不一致。因此通过 Handler 来更新 UI 一般很难保证跟 View 或者 Activity 的生命周期一致,故很容易导致无法正确释放。

    例如:

    public class HandlerBadActivity extends AppCompatActivity {
     private final Handler handler = new Handler(){//非静态内部类,持有外部类的强引用
     @Override
     public void handleMessage(Message msg) {
     super.handleMessage(msg);
     }
     };
     @Override
     protected void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
     setContentView(R.layout.activity_handler_bad);
     // 延迟 5min 发送一个消息
     handler.postDelayed(new Runnable() {
     //内部会将该 Runable 封装为一个 Message 对象,同时将 Message.target 赋值为 handler
     @Override
     public void run() {
     //do something
     }
     }, 1000 * 60 * 5);
     this.finish();
     }
    }

    上面的代码中发送了了一个延时 5 分钟执行的 Message,当该 Activity 退出的时候,延时任务(Message)还在主线程的 MessageQueue 中等待,此时的 Message 持有 Handler 的强引用(创建时通过 Message.target 进行指定),并且由于 Handler 是 HandlerBadActivity 的非静态内部类,所以 Handler 会持有一个指向 HandlerBadActivity 的强引用,所以虽然此时 HandlerBadActivity 调用了 finish 也无法进行内存回收,造成内存泄漏。

    解决方法

    将 Handler 声明为静态内部类,但是要注意**如果用到 Context 等外部类的 非static 对象,还是应该使用 ApplicationContext 或者通过弱引用来持有这些外部对象**。

    public class HandlerGoodActivity extends AppCompatActivity {
     private static final class MyHandler extends Handler{//声明为静态内部类(避免持有外部类的强引用)
     private final WeakReference<HandlerGoodActivity> mActivity;
     public MyHandler(HandlerGoodActivity activity){
     this.mActivity = new WeakReference<HandlerGoodActivity>(activity);//使用弱引用
     }
     @Override
     public void handleMessage(Message msg) {
     HandlerGoodActivity activity = mActivity.get();
     if (activity == null || activity.isFinishing() || activity.isDestroyed()) {//判断 activity 是否为空,以及是否正在被销毁、或者已经被销毁
     removeCallbacksAndMessages(null);
     return;
     }
     // do something
     }
     }
     private final MyHandler myHandler = new MyHandler(this);
    }

    慎用 static 成员变量

    static 修饰的变量位于内存的方法区,其生命周期与 App 的生命周期一致。 这必然会导致一系列问题,如果你的 app 进程设计上是长驻内存的,那即使 app 切到后台,这部分内存也不会被释放。

    解决方法

    不要在类初始化时初始化静态成员,也就是可以考虑懒加载。架构设计上要思考是否真的有必要这样做,尽量避免。如果架构需要这么设计,那么此对象的生命周期你有责任管理起来。

    当然,Application 的 context 不是万能的,所以也不能随便乱用,对于有些地方则必须使用 Activity 的 Context,对于Application,Service,Activity三者的Context的应用场景如下:

    功能 Application Service Activity
    Start an Activity NO1 NO1 YES
    Show a Dialog NO NO YES
    Layout Inflation YES YES YES
    Start an Service YES YES YES
    Bind an Service YES YES YES
    Send a Broadcast YES YES YES
    Register BroadcastReceiver YES YES YES
    Load Resource Values YES YES YES
    • NO1 表示 Application 和 Service 可以启动一个 Activity,不过需要创建一个新的 task 任务队列。
    • 对于 Dialog 而言,只有在 Activity 中才能创建。

    使用系统服务引发的内存泄漏

    为了方便我们使用一些常见的系统服务,Activity 做了一些封装。比如说,可以通过 getPackageManager在 Activtiy 中获取 PackageManagerService,但是,里面实际上调用了 Activity 对应的 ContextImpl 中的 getPackageManager 方法

    ContextWrapper#getPackageManager

    @Override
    public PackageManager getPackageManager() {
     return mBase.getPackageManager();
    }

    ContextImpl#getPackageManager

    @Override
    public PackageManager getPackageManager() {
     if (mPackageManager != null) {
     return mPackageManager;
     }
     IPackageManager pm = ActivityThread.getPackageManager();
     if (pm != null) {
     // Doesn't matter if we make more than one instance.
     return (mPackageManager = new ApplicationPackageManager(this, pm));//创建 ApplicationPackageManager
     }
     return null;
    }

    ApplicationPackageManager#ApplicationPackageManager

    ApplicationPackageManager(ContextImpl context,
        IPackageManager pm) {
     mContext = context;//保存 ContextImpl 的强引用
     mPM = pm;
    }
    
    private UserManagerService(Context context, PackageManagerService pm,
     Object packagesLock, File dataDir) {
     mContext = context;//持有外部 Context 引用
     mPm = pm;
     //代码省略
    }

    PackageManagerService#PackageManagerService

    public class PackageManagerService extends IPackageManager.Stub {
     static UserManagerService sUserManager;//持有 UMS 静态引用
     public PackageManagerService(Context context, Installer installer,
     boolean factoryTest, boolean onlyCore) {
      sUserManager = new UserManagerService(context, this, mPackages);//初始化 UMS
     }
    }

    遇到的内存泄漏问题是因为在 Activity 中调用了 getPackageManger 方法获取 PMS ,该方法调用的是 ContextImpl,此时如果ContextImpl 中 PackageManager 为 null,就会创建一个 PackageManger(ContextImpl 会将自己传递进去,而 ContextImpl 的 mOuterContext 为 Activity),创建 PackageManager 实际上会创建 PackageManagerService(简称 PMS),而 PMS 的构造方法中会创建一个 UserManger(UserManger 初始化之后会持有 ContextImpl 的强引用)。
    只要 PMS 的 class 未被销毁,那么就会一直引用着 UserManger ,进而导致其关联到的资源无法正常释放。

    解决办法

    将getPackageManager()改为 getApplication()#getPackageManager() 。这样引用的就是 Application Context,而非 Activity 了。

    远离非静态内部类和匿名类

    因为使用非静态内部类和匿名类都会默认持有外部类的引用,如果生命周期不一致,就会导致内存泄漏。

    public class NestedClassLeakActivity extends AppCompatActivity {
    
     class InnerClass {//非静态内部类
    
     }
    
     private static InnerClass sInner;//指向非静态内部类的静态引用
    
     @Override
     protected void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
     setContentView(R.layout.activity_nested_class);
     if (sInner == null) {
      sInner = new InnerClass();//创建非静态内部类的实例
     }
     }
    }

    非静态内部类默认会持有外部类的引用,而外部类中又有一个该非静态内部类的静态实例,该静态实例的生命周期和应用的一样长,而静态实例又持有 Activity 的引用,因此导致 Activity 的内存资源不能正常回收。

    解决方法

    将该内部类设为静态内部类 也可以将该内部类抽取出来封装成一个单例

    集合引发的内存泄漏

    我们通常会把一些对象的引用加入到集合容器(比如ArrayList)中,当我们不再需要该对象时(通常会调用 remove 方法),并没有把它的引用从集合中清理掉(其中的一种情况就是 remove 方法没有将不再需要的引用赋值为 null),下面以 ArrayList 的 remove 方法为例

    public E remove( int index) {
     // 数组越界检查
     RangeCheck(index);
     modCount++;
     // 取出要删除位置的元素,供返回使用
     E oldValue = (E) elementData[index];
     // 计算数组要复制的数量
     int numMoved = size - index - 1;
     // 数组复制,就是将index之后的元素往前移动一个位置
     if (numMoved > 0)
     System. arraycopy(elementData, index+1, elementData, index,
       numMoved);
     // 将数组最后一个元素置空(因为删除了一个元素,然后index后面的元素都向前移动了,所以最后一个就没用了),好让gc尽快回收
     elementData[--size ] = null; // Let gc do its work
     return oldValue;
    }

    WebView 引发的内存泄漏

    WebView 解析网页时会申请Native堆内存用于保存页面元素,当页面较复杂时会有很大的内存占用。如果页面包含图片,内存占用会更严重。并且打开新页面时,为了能快速回退,之前页面占用的内存也不会释放。有时浏览十几个网页,都会占用几百兆的内存。这样加载网页较多时,会导致系统不堪重负,最终强制关闭应用,也就是出现应用闪退或重启。
    由于占用的都是Native 堆内存,所以实际占用的内存大小不会显示在常用的 DDMS Heap 工具中( DMS Heap 工具看到的只是Java虚拟机分配的内存,即使Native堆内存已经占用了几百兆,这里显示的还只是几兆或十几兆)。只有使用 adb shell 中的一些命令比如 dumpsys meminfo 包名,或者在程序中使用 Debug.getNativeHeapSize()才能看到 Native 堆内存信息。

    据说由于 WebView 的一个 BUG,即使它所在的 Activity(或者Service) 结束也就是 onDestroy() 之后,或者直接调用 WebView.destroy()之后,它所占用这些内存也不会被释放。

    解决方法

    把使用了 WebView 的 Activity (或者 Service) 放在单独的进程里。

    • 系统在检测到应用占用内存过大有可能被系统干掉
    • 也可以在它所在的 Activity(或者 Service) 结束后,调用 System.exit(0),主动Kill掉进程。由于系统的内存分配是以进程为准的,进程关闭后,系统会自动回收所有内存。

    使用 WebView 的页面(Activity),在生命周期结束页面退出(onDestory)的时候,主动调用WebView.onPause()==以及==WebView.destory()以便让系统释放 WebView 相关资源。

    其他常见的引起内存泄漏原因

    Android 3.0 以下,Bitmap 在不使用的时候没有使用 recycle() 释放内存。

    非静态内部类的静态实例容易造成内存泄漏:即一个类中如果你不能够控制它其中内部类的生命周期(譬如Activity中的一些特殊Handler等),则尽量使用静态类和弱引用来处理(譬如ViewRoot的实现)。

    警惕线程未终止造成的内存泄露;譬如在 Activity 中关联了一个生命周期超过 Activity 的 Thread,在退出 Activity 时切记结束线程。

    一个典型的例子就是 HandlerThread 的 run 方法。该方法在这里是一个死循环,它不会自己结束,线程的生命周期超过了 Activity 生命周期,我们必须手动在 Activity 的销毁方法中中调用 thread.getLooper().quit() 才不会泄露。

    对象的注册与反注册没有成对出现造成的内存泄露;譬如注册广播接收器、注册观察者(典型的譬如数据库的监听)等。
    创建与关闭没有成对出现造成的泄露;譬如Cursor资源必须手动关闭,WebView必须手动销毁,流等对象必须手动关闭等。

    避免代码设计模式的错误造成内存泄露;譬如循环引用,A 持有 B,B 持有 C,C 持有 A,这样的设计谁都得不到释放。

    总结

    以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对脚本之家的支持。

    阅读原文

    您可能感兴趣的文章:

    展开全文
  • Android Studio的内存分析界面 一般分析内存泄露, ...图中蓝色区域,就是程序使用的内存, 灰色区域就是空闲内存当然,Android内存分配机制是对每个应用程序逐步增加, 比如你程序当前使用30M内存, 系统可能会给你分

    Android Studio的内存分析界面

    一般分析内存泄露, 首先运行程序,打开日志控制台,有一个标签Memory ,我们可以在这个界面分析当前程序使用的内存情况, 一目了然, 我们再也不需要苦苦的在logcat中寻找内存的日志了。

    图中蓝色区域,就是程序使用的内存, 灰色区域就是空闲内存

    当然,Android内存分配机制是对每个应用程序逐步增加, 比如你程序当前使用30M内存, 系统可能会给你分配40M, 当前就有10M空闲, 如果程序使用了50M了,系统会紧接着给当前程序增加一部分,比如达到了80M, 当前你的空闲内存就是30M了。 当然,系统如果不能再给你分配额外的内存,程序自然就会OOM(内存溢出)了。 每个应用程序最高可以申请的内存和手机密切相关,比如我当前使用的华为Mate7,极限大概是200M,算比较高的了, 一般128M 就是极限了, 甚至有的手机只有可怜的16M或者32M,这样的手机相对于内存溢出的概率非常大了。

    我们怎么检测内存泄露呢

    首先需要明白一个概念, 内存泄露就是指,本应该回收的内存,还驻留在内存中。一般情况下,高密度的手机,一个页面大概就会消耗20M内存,如果发现退出界面,程序内存迟迟不降低的话,可能就发生了严重的内存泄露。我们可以反复进入该界面,然后点击dump java heap 这个按钮,然后Android Studio就开始干活了,下面的图就是正在dump

    正在dump

    dump成功后会自动打开 hprof文件,文件以Snapshot+时间来命名

    内存分析结果

    通过Android Studio自带的界面,查看内存泄露还不是很智能,我们可以借助第三方工具,常见的工具就是MAT了,下载地址 http://eclipse.org/mat/downloads.php ,这里我们需要下载独立版的MAT. 下图是MAT一开始打开的界面, 这里需要提醒大家的是,MAT并不会准确地告诉我们哪里发生了内存泄漏,而是会提供一大堆的数据和线索,我们需要自己去分析这些数据来去判断到底是不是真的发生了内存泄漏。

    LeakCanary

    上面介绍了MAT检测内存泄露, 再给大家介绍LeakCanary。 项目地址:https://github.com/square/leakcanaryLeakCanary 会检测应用的内存回收情况,如果发现有垃圾对象没有被回收,就会去分析当前的内存快照,也就是上边MAT用到的.hprof文件,找到对象的引用链,并显示在页面上。这款插件的好处就是,可以在手机端直接查看内存泄露的地方,可以辅助我们检测内存泄露.

    手机端查看内存泄露.png

    使用: 在build.gradle文件中添加,不同的编译使用不同的引用:

    dependencies {  
          debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3' 
          releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
     }
    

    在应用的Application onCreate方法中添加LeakCanary.install(this),如下:

    public class ExampleApplication extends Application  
      @Override  
      public void onCreate() { 
          super.onCreate();  
          LeakCanary.install(this);
       } 
    }
    

    应用运行起来后,LeakCanary会自动去分析当前的内存状态,如果检测到泄漏会发送到通知栏,点击通知栏就可以跳转到具体的泄漏分析页面。Tips:就目前使用的结果来看,绝大部分泄漏是由于使用单例模式hold住了Activity的引用,比如传入了context或者将Activity作为listener设置了进去,所以在使用单例模式的时候要特别注意,还有在Activity生命周期结束的时候将一些自定义监听器的Activity引用置空。

    作者:于连林520wcf链接:https://www.jianshu.com/p/216b03c22bb8來源:简书著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    是什么?

    一言以蔽之:LeakCanary是一个傻瓜化并且可视化的内存泄露分析工具

    为什么需要LeakCanary?

    因为它简单,易于发现问题,人人可参与。

    • 简单:只需设置一段代码即可,打开应用运行一下就能够发现内存泄露。而MAT分析需要Heap Dump,获取文件,手动分析等多个步骤。
    • 易于发现问题:在手机端即可查看问题即引用关系,而MAT则需要你分析,找到Path To GC Roots等关系。
    • 人人可参与:开发人员,测试测试,产品经理基本上只要会用App就有可能发现问题。而传统的MAT方式,只有部分开发者才有精力和能力实施。

    如何集成

    尽量在app下的build.gradle中加入以下依赖

    1
    2
    3
    4
    5
    
     dependencies {
       debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1' // or 1.4-beta1
       releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // or 1.4-beta1
       testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // or 1.4-beta1
     }

    在Application中加入类似如下的代码

    1
    2
    3
    4
    5
    6
    7
    
    public class ExampleApplication extends Application {
    
      @Override public void onCreate() {
        super.onCreate();
        LeakCanary.install(this);
      }
    }
    

    到这里你就可以检测到Activity的内容泄露了。其实现原理是设置Application的ActivityLifecycleCallbacks方法监控所有Activity的生命周期回调。内部实现代码为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    
    public final class ActivityRefWatcher {
        private final ActivityLifecycleCallbacks lifecycleCallbacks = new ActivityLifecycleCallbacks() {
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
            }
    
            public void onActivityStarted(Activity activity) {
            }
    
            public void onActivityResumed(Activity activity) {
            }
    
            public void onActivityPaused(Activity activity) {
            }
    
            public void onActivityStopped(Activity activity) {
            }
    
            public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
            }
    
            public void onActivityDestroyed(Activity activity) {
                ActivityRefWatcher.this.onActivityDestroyed(activity);
            }
        };
        private final Application application;
        private final RefWatcher refWatcher;
    
        public static void installOnIcsPlus(Application application, RefWatcher refWatcher) {
            if(VERSION.SDK_INT >= 14) {
                ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
                activityRefWatcher.watchActivities();
            }
        }
    ....
    }
    

    想要检测更多?

    首先我们需要获得一个RefWatcher,用来后续监控可能发生泄漏的对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    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;
        }
    }
    

    监控某个可能存在内存泄露的对象

    1
    
    MyApplication.getRefWatcher().watch(sLeaky);
    

    哪些需要进行监控

    默认情况下,是对Activity进行了检测。另一个需要监控的重要对象就是Fragment实例。因为它和Activity实例一样可能持有大量的视图以及视图需要的资源(比如Bitmap)即在Fragment onDestroy方法中加入如下实现

    1
    2
    3
    4
    5
    6
    7
    
    public class MainFragment extends Fragment {
        @Override
        public void onDestroy() {
            super.onDestroy();
            MyApplication.getRefWatcher().watch(this);
        }
    }
    

    其他也可以监控的对象

    • BroadcastReceiver
    • Service
    • 其他有生命周期的对象
    • 直接间接持有大内存占用的对象(即Retained Heap值比较大的对象)

    何时进行监控

    首先,我们需要明确什么是内存泄露,简而言之,某个对象在该释放的时候由于被其他对象持有没有被释放,因而造成了内存泄露。

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

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

    如何解决

    常用的解决方法思路如下

    • 尽量使用Application的Context而不是Activity的
    • 使用弱引用或者软引用
    • 手动设置null,解除引用关系
    • 将内部类设置为static,不隐式持有外部的实例
    • 注册与反注册成对出现,在对象合适的生命周期进行反注册操作。
    • 如果没有修改的权限,比如系统或者第三方SDK,可以使用反射进行解决持有关系

    加入例外

    有些特殊情况,我们需要忽略一些问题,这时候就需要添加例外规则。比如ExampleClass.exampleField会导致内存泄漏,我们想要忽略,如下操作即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    // ExampleApplication is defined in "Customizing and using the no-op dependency"
    public class DebugExampleApplication extends ExampleApplication {
      protected RefWatcher installLeakCanary() {
        ExcludedRefs excludedRefs = AndroidExcludedRefs.createAppDefaults()
            .instanceField("com.example.ExampleClass", "exampleField")
            .build();
        return LeakCanary.install(this, DisplayLeakService.class, excludedRefs);
      }
    }
    

    如何实现的

    LeakCanary实际上就是在本机上自动做了Heap dump,然后对生成的hprof文件分析,进行结果展示。和手工进行MAT分析步骤基本一致。

    如何不影响对外版APK

    是的,这个问题确实值得关注,因为LeakCanary确实是影响程序运行的,尤其是heap dump操作,不过好在这件事Square已经考虑了,即在我们增加依赖时

    1
    2
    3
    
    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1' // or 1.4-beta1
    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // or 1.4-beta1
    testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // or 1.4-beta1
    

    其中releaseCompile和testCompile这两个的依赖明显不同于debugCompile的依赖。它们的依赖属于NOOP操作。

    NOOP,即No Operation Performed,无操作指令。常用的编译器技术会检测无操作指令并出于优化的目的将无操作指令剔除。

    因而,只要配置好releaseCompile和testCompile的依赖,就无需担心对外版本的性能问题了。

    实践中的问题

    • 目前LeakCanary已经完美支持运行时权限,大家可以放心使用。

    注意

    • 目前LeakCanary一次只能报一个泄漏问题,如果存在内存泄漏但不是你的模块,并不能说明这个模块没有问题。建议建议将非本模块的泄漏解决之后,再进行检测。
    转载:http://droidyue.com/blog/2016/03/28/android-leakcanary/
    Android内存泄漏解决方案(OOM)
    

    为什么会有内存泄漏?

    一个不会被使用的对象,因为另一个正在使用的对象持有该对象的引用,导致它不能正常被回收,而停留在堆内存中,内存泄漏就产生了

    Android系统为每个应用分配的内存是有限的,内存泄漏会使我们的应用内存随着时间不断的增加,造成应用OOM(Out Of Memory)错误,使应用崩溃.

    如何解决内存泄漏?

    当我们在解决内存泄漏的时候常常使用 LeakCanary工具,它是一个自动检测内存泄漏的开源工具,使用它我们就可以明确的知道那个地方发生了泄漏

    持有Context造成的内存泄漏

    在Android中有两种context对象:Activity和Application.当我们给一个类传递context的时候经常使用第一种,而这样就导致了改类持有对Activity的全部引用,当Activity关闭的时候因为被其他类持有,而导致无法正常被回收,而导致内存泄漏

    解决方案:

    在给其他给传递context的时候使用Application对象,这个对象的生命周期和共存亡,而不依赖activity的声明周期.  而对context的引用不要超过他本身的生命周期,谨慎对context使用static关键字.

    Handler造成的内存泄漏

      public class SampleActivity extends Activity {
      private final Handler mLeakyHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
          // ... 
        }
      }
    }
    

    这样来使用Handler会造成严重的内存泄漏. 
    假设Hanlder中有延迟的任务或是等在执行的任务队列过长,由于消息队列持有对handler的引用,而handler又持有activity的隐式引用,这个引用会保持到消息得到处理,而导致activity无法被垃圾回收器进行回收,而导致内存泄漏

    解决方案:

    1. 可以把Handler放到单独的类中,或者使用静态的内部类(静态内部类不会引用activity)避免泄漏
    2. 如果想要在handler内部去调用Activity中的资源,可以在Handler中使用弱引用的方式指向所在的Activity,使用static+WeakReference的方式断开handler与activity的关系

    最终代码:

    public static class MyHandler extends Handler {
        //声明一个弱引用对象
        WeakReference<MainActivity> mReference;
    
        MyHandler(MainActivity activity) {
            //在构造器中传入Activity,创建弱引用对象
            mReference = new WeakReference<MainActivity>(activity);
        }
    
        public void handleMessage(Message msg) {
            //在使用activity之前先判空处理
            if (mReference != null && mReference.get() != null) {
                mReference.get().text.setText("hello word");
            }
        }
    }
    

    使用单利模式造成的内存泄漏

    在我们使用单利模式的时候如果使用不当也会造成内存泄漏.因为单利模式的静态特征使得单利模式的生命周期和应用一样的长,这说明了当一个对象不需要使用了,而单利对象还存在该对象的引用,那么这个对象就不能正常的被回收,就造成了内存泄漏

    解决方案:

    XXUtils.getInstance(this);
    

    这句代码默认传入的是Activity的Context,而Activity是间接继承自Context的,当Activity退出之后,单利对象还持有他的引用,所以在为了避免传Activity的Context,在单利中通过传入的context获取到全局的上下文对象,而不使用Activity的Context就解决了这个问题.

    public class XXUtils {
        private Context mContext;
        private XXUtils(Context context) {
            mContext = context.getApplicationContext();
        }
        private static XXUtils instance;
        public static XXUtils getInstance(Context context) {
            if (instance == null) {
                synchronized (XXUtils.class) {
                    if (instance == null) {
                        instance = new XXUtils(context);
                    }
                }
            }
            return instance;
        }
    }
    

    非静态内部类创建静态实例造成的内存泄漏

    在项目中我们为了避免多次的初始化资源,常常会使用静态对象去保存这些对象,这种情况也很容易引发内存泄漏.

    why?

    1. 非静态的内部类默认会持有外部类的引用
    2. 而我们又使用非静态内部类创建了一个静态的实例
    3. 该静态实例的声明周期和应用一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity不能正常回收

    解决方案:

    1. 将内部类修改成静态的,这样它对外部类就没有引用
    2. 将该对象抽取出来封装成一个单利.

    最终代码:

    private static TestResource mTestResource;
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.btn).setOnClickListener(this);
    }
    private void initData() {
        if (mTestResource == null) {
            mTestResource = new TestResource();
        }
    }
    public void onClick(View v) {
        initData();
    }
    //非静态内部类默认会持有外部类的引用
    //修改成就太之后正常被回收,是因为静态的内部类不会对Activity有引用
    private static class TestResource {
    }
    

    线程造成的内存泄漏

    当我们在使用线程的时候,一般都使用匿名内部类,而匿名内部类会对外部类持有默认的引用,当Acticity关闭之后如果现成中的任务还没有执行完毕,就会导致Activity不能正常回收,造成内存泄漏

    解决方案

    创建一个静态的类,实现Runnable方法,在使用的时候实例化他.

    最终代码:

    private void loadData() {
        new Thread(new MyThread()).start();
    }
    private static class MyThread implements Runnable {
        public void run() {
            SystemClock.sleep(20000);
        }
    }
    

    资源未关闭造成的内存泄漏

    对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源的代码,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

    监听器没有注销造成的内存泄漏

    在Android程序里面存在很多需要register与unregister的监听器,我们需要确保及时unregister监听器。

    集合中的内存泄漏

    我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时, 
    并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。 
    所以要在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。

    转载:http://blog.csdn.net/imuhao/article/details/51694144
    展开全文
  • Android内存泄漏分析

    2018-08-04 17:59:27
    Android系统查找native进程的内存泄漏 问题描述 在系统运行过程中,使用android自带的procrank工具,查看系统内存情况,例如 130|shell@android:# procrank | head warning: could not create process ...
  • Android中常见的内存泄漏汇总 集合类泄漏 集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 ...
  • Android内存泄露总结

    2016-06-01 09:15:24
    JAVA和C++之间有一堵由内存分配和垃圾收集技术所围成的高墙,墙外的人想进来,墙里面的人想出来。
  •   android中的内存泄露通常是Activity或者Fragment的泄露。下文分析以Activity展开,Fragment同理。 1. 非静态内部类、匿名内部类 2. 静态的View 3. Handler 4. 监听器(各种需要注册的Listener...
  • Android内存泄漏的检测流程、捕捉以及分析简述:一个APP的性能,重度关乎着用户体验,而关于性能检测的一个重要方面,就是内存泄漏,通常内存泄漏的隐藏性质比较强,不同于异常导致的程序Crash,在异常导致的Crash中...
  • java/Android内存泄漏和内存溢出详解java内存泄漏和溢出跟内存栈堆也是有一些关系,这里不解释! 这里主要讲解一下内存泄漏和溢出的区别和联系。 之前我跟别人说这两个的区别就说了:内存泄漏是因为内存对象一直被...
  • 最近在整理Android内存泄漏相关内容,目前整理出了以下八种情形,后期还会继续补充,请持续关注~单例造成的内存泄漏非静态内部类创建静态实例造成的内存泄漏Handler造成的内存泄漏线程造成的内存泄漏资源未关闭造成...
  • 常见的Android内存泄漏问题以及解决办法什么是内存泄漏以及其危害编写代码的时候因为错误或者疏忽,导致一部分内存空间不能被垃圾回收机制回收,造成这部分的内存空间浪费,再也不能被程序使用到,这就叫做内存泄漏...
  • android内存泄漏和内存溢出
  • 1. 内存泄露简介 内存泄露,即Memory Leak,指程序中不再使用到的对象因某种原因从而无法被GC正常回收。发生内存泄露,会导致一些不再使用到的对象没有及时释放,这些对象占用了宝贵的内存空间,很容易导致后续需要...
  • 本文整理自:【技术公开课】Android内存泄漏案例分析(点击链接,观看视频),演讲PPT>>下载地址。 一款优秀的Android应用,不仅要有完善的功能,也要有良好的体验,而性能是影响体验的一个重要因素。内存泄露...
  • Android 内存泄漏总结

    2016-03-15 15:05:45
    Android 内存泄漏总结 android性能优化ATA内存管理内存泄漏 摘要 Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,...
  • Android内存泄露专题——leakCanary源码分析 目录 Android内存泄露专题——leakCanary源码分析 一、内存泄露的检测机制 1、初始化 2、开启显示内存泄漏信息的页面 3、初始化一个ServiceHeapDumpListener,这是...
  • Android 内存泄漏的检测和解决前言MAT工具下载地址Android ...Android内存泄漏的产生的原因:一个长生命周期的对象持有一个短生命周期对象的引用 通俗讲就是该回收的对象,因为引用问题没有被回收,最终会产生OOM...
  • Android内存泄漏相关

    2019-09-03 10:19:25
    内存泄漏是指一个不再被使用的对象被一个还存活着的对象引用,此时垃圾回收器会跳过它,不去回收它。比如常见的Activity泄漏,Activity组件在Android中扮演着重要的作用,如果它泄漏,很可能导致Activity所引用的...
1 2 3 4 5 ... 20
收藏数 51,523
精华内容 20,609
关键字:

android 内存泄漏