安卓内存泄漏_安卓手机内存溢出和内存泄漏 - 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内存泄漏分析工具。找出来利用自己对原理的理解解决这些问题是很简单的。

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

    展开全文
  • 如上图所示,GC会选择一些它了解还存活的对象作为内存遍历的根节点(GC Roots),比方说thread stack中的变量,JNI中的全局变量,zygote中的对象(class loader加载)等,然后开始对heap进行遍历。到最后,部分没有...

     尊重原创作者,转载请注明出处:

    http://blog.csdn.net/gemmem/article/details/13017999

    此文承接我的另一篇文章:Android进程的内存管理分析

     首先了解一下dalvik的Garbage Collection:




    如上图所示,GC会选择一些它了解还存活的对象作为内存遍历的根节点(GC Roots),比方说thread stack中的变量,JNI中的全局变量,zygote中的对象(class loader加载)等,然后开始对heap进行遍历。到最后,部分没有直接或者间接引用到GC Roots的就是需要回收的垃圾,会被GC回收掉。如下图蓝色部分。





    Java内存泄漏指的是进程中某些对象(垃圾对象)已经没有使用价值了,但是它们却可以直接或间接地引用到gc roots导致无法被GC回收。无用的对象占据着内存空间,使得实际可使用内存变小,形象地说法就是内存泄漏了。下面分析一些可能导致内存泄漏的情景。

     

    常见的内存泄漏

     

     1、非静态内部类的静态实例容易造成内存泄漏

    public class MainActivityextends Activity
    {
             static Demo sInstance = null;
            
        @Override
        public void onCreate(BundlesavedInstanceState)
        {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            if (sInstance == null)
            {
               sInstance= new Demo();
            }
        }
        class Demo
        {
        voiddoSomething()
        {
                   System.out.print("dosth.");
        }
        }
    }


    上面的代码中的sInstance实例类型为静态实例,在第一个MainActivity act1实例创建时,sInstance会获得并一直持有act1的引用。当MainAcitivity销毁后重建,因为sInstance持有act1的引用,所以act1是无法被GC回收的,进程中会存在2个MainActivity实例(act1和重建后的MainActivity实例),这个act1对象就是一个无用的但一直占用内存的对象,即无法回收的垃圾对象。所以,对于lauchMode不是singleInstance的Activity, 应该避免在activity里面实例化其非静态内部类的静态实例。

     

    2、activity使用静态成员

    private static Drawable sBackground;  
    @Override  
    protected void onCreate(Bundle state) {  
        super.onCreate(state);  
      
        TextView label = new TextView(this);  
        label.setText("Leaks are bad");  
      
        if (sBackground == null) {  
            sBackground = getDrawable(R.drawable.large_bitmap);  
        }  
        label.setBackgroundDrawable(sBackground);  
      
        setContentView(label);  
    } 


    由于用静态成员sBackground 缓存了drawable对象,所以activity加载速度会加快,但是这样做是错误的。因为在android 2.3系统上,它会导致activity销毁后无法被系统回收。

    label .setBackgroundDrawable函数调用会将label赋值给sBackground的成员变量mCallback。

    上面代码意味着:sBackground(GC Root)会持有TextView对象,而TextView持有Activity对象。所以导致Activity对象无法被系统回收。

    下面看看android4.0为了避免上述问题所做的改进。

    先看看android 2.3的Drawable.Java对setCallback的实现:

        public final void setCallback(Callback cb){

            mCallback = cb;

    }

    再看看android 4.0的Drawable.Java对setCallback的实现:

        public final void setCallback(Callback cb){

            mCallback = newWeakReference<Callback> (cb);

    }

    在android 2.3中要避免内存泄漏也是可以做到的, 在activity的onDestroy时调用

    sBackgroundDrawable.setCallback(null)。

     

    以上2个例子的内存泄漏都是因为Activity的引用的生命周期超越了activity对象的生命周期。也就是常说的Context泄漏,因为activity就是context。

     

    想要避免context相关的内存泄漏,需要注意以下几点:

    ·不要对activity的context长期引用(一个activity的引用的生存周期应该和activity的生命周期相同)

    ·如果可以的话,尽量使用关于application的context来替代和activity相关的context

    ·如果一个acitivity的非静态内部类的生命周期不受控制,那么避免使用它;正确的方法是使用一个静态的内部类,并且对它的外部类有一WeakReference,就像在ViewRootImpl中内部类W所做的那样。

    3、使用handler时的内存问题

     

    我们知道,Handler通过发送Message与其他线程交互,Message发出之后是存储在目标线程的MessageQueue中的,而有时候Message也不是马上就被处理的,可能会驻留比较久的时间。在Message类中存在一个成员变量 target,它强引用了handler实例,如果Message在Queue中一直存在,就会导致handler实例无法被回收,如果handler对应的类是非静态内部类 ,则会导致外部类实例(Activity或者Service)不会被回收,这就造成了外部类实例的泄露。 所以正确处理Handler等之类的内部类,应该将自己的Handler定义为静态内部类,并且在类中增加一个成员变量,用来弱引用外部类实例,如下:

    public class OutterClass
    {
            ......
            ......
            static class InnerClass
            {
            	private final WeakReference<OutterClass> mOutterClassInstance;
            	......
            	......
            }
    }



    HandlerThread的使用也需要注意:

      当我们在activity里面创建了一个HandlerThread,代码如下:

    public classMainActivity extends Activity
    {
        @Override
        public void onCreate(BundlesavedInstanceState)
        {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            Thread mThread = newHandlerThread("demo", Process.THREAD_PRIORITY_BACKGROUND); 
            mThread.start();
    MyHandler mHandler = new MyHandler( mThread.getLooper( ) );
    …….
    …….
    …….
    }
        @Override
        public void onDestroy()
        {
        super.onDestroy();
        }
    }


    这个代码存在泄漏问题,因为HandlerThread实现的run方法是一个无限循环,它不会自己结束,线程的生命周期超过了activity生命周期,当横竖屏切换,HandlerThread线程的数量会随着activity重建次数的增加而增加。

    应该在onDestroy时将线程停止掉:mThread.getLooper().quit();

    另外,对于不是HandlerThread的线程,也应该确保activity消耗后,线程已经终止,可以这样做:在onDestroy时调用mThread.join();

     

    4、注册某个对象后未反注册

    注册广播接收器、注册观察者等等,比如:

    假设我们希望在锁屏界面(LockScreen)中,监听系统中的电话服务以获取一些信息(如信号强度等),则可以在LockScreen中定义一个PhoneStateListener的对象,同时将它注册到TelephonyManager服务中。对于LockScreen对象,当需要显示锁屏界面的时候就会创建一个LockScreen对象,而当锁屏界面消失的时候LockScreen对象就会被释放掉。

      但是如果在释放LockScreen对象的时候忘记取消我们之前注册的PhoneStateListener对象,则会导致LockScreen无法被GC回收。如果不断的使锁屏界面显示和消失,则最终会由于大量的LockScreen对象没有办法被回收而引起OutOfMemory,使得system_process进程挂掉。

    虽然有些系统程序,它本身好像是可以自动取消注册的(当然不及时),但是我们还是应该在我们的程序中明确的取消注册,程序结束时应该把所有的注册都取消掉。

    5、集合中对象没清理造成的内存泄露

      我们通常把一些对象的引用加入到了集合中,当我们不需要该对象时,如果没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。

     

    比如某公司的ROM的锁屏曾经就存在内存泄漏问题:

    这个泄漏是因为LockScreen每次显示时会注册几个callback,它们保存在KeyguardUpdateMonitor的ArrayList<InfoCallback>、ArrayList<SimStateCallback>等ArrayList实例中。但是在LockScreen解锁后,这些callback没有被remove掉,导致ArrayList不断增大, callback对象不断增多。这些callback对象的size并不大,heap增长比较缓慢,需要长时间地使用手机才能出现OOM,由于锁屏是驻留在system_server进程里,所以导致结果是手机重启。

    6、资源对象没关闭造成的内存泄露

      资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于Java虚拟机内,还存在于Java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄露。因为有些资源性对象,比如SQLiteCursor(在析构函数finalize(),如果我们没有关闭它,它自己会调close()关闭),如果我们没有关闭它,系统在回收它时也会关闭它,但是这样的效率太低了。因此对于资源性对象在不使用的时候,应该立即调用它的close()函数,将其关闭掉,然后再置为null.在我们的程序退出时一定要确保我们的资源性对象已经关闭。

      程序中经常会进行查询数据库的操作,但是经常会有使用完毕Cursor后没有关闭的情况。如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在长时间大量操作的情况下才会复现内存问题,这样就会给以后的测试和问题排查带来困难和风险。

    7、一些不良代码成内存压力

    有些代码并不造成内存泄露,但是它们或是对没使用的内存没进行有效及时的释放,或是没有有效的利用已有的对象而是频繁的申请新内存,对内存的回收和分配造成很大影响的,容易迫使虚拟机不得不给该应用进程分配更多的内存,增加vm的负担,造成不必要的内存开支。

    7.1,Bitmap使用不当

        第一、及时的销毁。

        虽然,系统能够确认Bitmap分配的内存最终会被销毁,但是由于它占用的内存过多,所以很可能会超过Java堆的限制。因此,在用完Bitmap时,要及时的recycle掉。recycle并不能确定立即就会将Bitmap释放掉,但是会给虚拟机一个暗示:“该图片可以释放了”。


        第二、设置一定的采样率。

        有时候,我们要显示的区域很小,没有必要将整个图片都加载出来,而只需要记载一个缩小过的图片,这时候可以设置一定的采样率,那么就可以大大减小占用的内存。如下面的代码:

    private ImageView preview;  
    BitmapFactory.Options options = newBitmapFactory.Options();  
    options.inSampleSize = 2;//图片宽高都为原来的二分之一,即图片为原来的四分之一  
    Bitmap bitmap =BitmapFactory.decodeStream(cr.openInputStream(uri), null, options); preview.setImageBitmap(bitmap); 


     

    第三、巧妙的运用软引用(SoftRefrence)

    有些时候,我们使用Bitmap后没有保留对它的引用,因此就无法调用Recycle函数。这时候巧妙的运用软引用,可以使Bitmap在内存快不足时得到有效的释放。如下:

    SoftReference<Bitmap>  bitmap_ref  = new SoftReference<Bitmap>(BitmapFactory.decodeStream(inputstream)); 
    ……
    ……
    if (bitmap_ref .get() != null)
              bitmap_ref.get().recycle();


    7.2,构造Adapter时,没有使用缓存的 convertView

      以构造ListView的BaseAdapter为例,在BaseAdapter中提共了方法:

      public View getView(intposition, View convertView, ViewGroup parent)

      来向ListView提供每一个item所需要的view对象。初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的view对象,同时ListView会将这些view对象缓存起来。当向上滚动ListView时,原先位于最上面的list item的view对象会被回收,然后被用来构造新出现的最下面的list item。这个构造过程就是由getView()方法完成的,getView()的第二个形参 View convertView就是被缓存起来的list item的view对象(初始化时缓存中没有view对象则convertView是null)。

      由此可以看出,如果我们不去使用convertView,而是每次都在getView()中重新实例化一个View对象的话,即浪费时间,也造成内存垃圾,给垃圾回收增加压力,如果垃圾回收来不及的话,虚拟机将不得不给该应用进程分配更多的内存,造成不必要的内存开支。ListView回收list item的view对象的过程可以查看:

      android.widget.AbsListView.Java--> void addScrapView(View scrap) 方法。

      Java代码:

    public View getView(int position, View convertView, ViewGroupparent) {
      View view = newXxx(...);
      return view;
      }


      修正示例代码:

      Java代码:

      public View getView(intposition, View convertView, ViewGroup parent) {
      View view = null;
      if (convertView != null){
      view = convertView;
      populate(view, getItem(position));
      } else {
      view = new Xxx(...);
      }
      return view;
      }


    7.3、不要在经常调用的方法中创建对象,尤其是忌讳在循环中创建对象。可以适当的使用 hashtable , vector 创建一组对象容器,然后从容器中去取那些对象,而不用每次 new 之后又丢弃。

     

    关于内存泄漏的调试

    (1).内存监测工具 DDMS --> Heap
     无论怎么小心,想完全避免bad code是不可能的,此时就需要一些工具来帮助我们检查代码中是否存在会造成内存泄漏的地方。Android tools中的DDMS就带有一个很不错的内存监测工具Heap(这里我使用eclipse的ADT插件,并以真机为例,在模拟器中的情况类似)。用 Heap监测应用进程使用内存情况的步骤如下:
    1. 启动eclipse后,切换到DDMS透视图,并确认Devices视图、Heap视图都是打开的;
    2. 将手机通过USB链接至电脑,链接时需要确认手机是处于“USB调试”模式,而不是作为“MassStorage”;
    3. 链接成功后,在DDMS的Devices视图中将会显示手机设备的序列号,以及设备中正在运行的部分进程信息;
    4. 点击选中想要监测的进程,比如system_process进程;
    5. 点击选中Devices视图界面中最上方一排图标中的“Update Heap”图标;
    6. 点击Heap视图中的“Cause GC”按钮;
    7. 此时在Heap视图中就会看到当前选中的进程的内存使用量的详细情况。
     说明:
    a) 点击“Cause GC”按钮相当于向虚拟机请求了一次gc操作;
    b) 当内存使用信息第一次显示以后,无须再不断的点击“CauseGC”,Heap视图界面会定时刷新,在对应用的不断的操作过程中就可以看到内存使用的变化;
    c) 内存使用信息的各项参数根据名称即可知道其意思,在此不再赘述。
      如何才能知道我们的程序是否有内存泄漏的可能性呢。这里需要注意一个值:Heap视图中部有一个Type叫做dataobject,即数据对象,也就是我们的程序中大量存在的类类型的对象。在data object一行中有一列是“Total Size”,其值就是当前进程中所有Java数据对象的内存总量,一般情况下,这个值的大小决定了是否会有内存泄漏。可以这样判断:
    a) 不断的操作当前应用,同时注意观察data object的Total Size值;
    b) 正常情况下Total Size值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况,所以说虽然我们不断的操作会不断的生成很多对象,而在虚拟机不断的进行GC的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平;
    c) 反之如果代码中存在没有释放对象引用的情况,则dataobject的Total Size值在每次GC后不会有明显的回落,随着操作次数的增多Total Size的值会越来越大,
      直到到达一个上限后导致进程OOM被kill掉。

     

    (2).内存分析工具 MAT(Memory Analyzer Tool) 

    并不是所有的内存泄漏都可以用观察heap size的方法检测出来,因为有的程序只是泄漏了几个对象,而且泄漏的对象个数不会随着程序的运行而增加,这种内存泄漏不会直接导致OOM,但是无用对象无法回收,无疑是对内存的浪费,会影响到程序的性能,我们需要使用MAT工具才能发现这种比较隐蔽的内存泄漏。
    使用MAT之前有2个概念是要掌握的:Shallowheap和Retained heap。Shallow heap表示对象本身所占内存大小,一个内存大小100bytes的对象Shallow heap就是100bytes。Retained heap表示通过回收这一个对象总共能回收的内存,比方说一个100bytes的对象还直接或者间接地持有了另外3个100bytes的对象引用,回收这个对象的时候如果另外3个对象没有其他引用也能被回收掉的时候,Retained heap就是400bytes。

    MAT使用Dominator Tree这样一种来自图形理论的概念。


    所谓Dominator,就是Flow Graph中从源节点出发到某个节点的的必经节点。那么根据这个概念我们可以从上图左侧的Flow Graph构造出右侧的Dominator Tree。这样一来很容易就看出每个节点的Retained heap了。Shallow heap和Retained heap在MAT中是非常有用的概念,用于内存泄漏的分析。

    我们做一个Demo。在工程的MainActivity当中加入如下代码:

    publicclassMainActivityextends Activity{
        static Leaky leak=null;
        classLeaky{
            voiddoSomething(){
                System.out.println("Wheee!!!");
            }
        }
        @Override
        publicvoidonCreate(Bundle savedInstanceState){
            super.onCreate(savedInstanceState);
            if(leak==null){
                leak =new Leaky();
            }
            ...


    上面这段代码,对Java熟悉的同学都应该了解非静态内部类对象默认持有外部类对象引用,而leak作为静态变量在非空判断下只产生了一个对象,因此当旋转屏幕时生成新的Activity的时候旧的Activity的引用依然被持有,如下图: 




    通过观察旋转屏幕前后Log中GC的信息也能看出heap的data object分配往上涨了一点,并且在GC执行完heap的分配稳定之后并没有降下来,这就是内存泄漏的迹象。

    我们通过MAT来进行分析。先下载MAT,可以作为Eclipse插件下载,也可以作为RCP应用下载,本质上没有区别。DDMS中选中应用对应的进程名,点击Dump HPROF file的按钮,等一小段时间生成HPROF文件,如果是Eclipse插件的话,Eclipse会为这个HPROF自动转化成标准的HPROF并自动打开MAT分析界面。如果是作为RCP应用的话,需要用sdk目录tools中的hprof-conv工具来进行转化,也就是上文提及的命令hprof-convorig.hprof converted.hprof,这种方式保存HPROF文件的位置选择更为自主,你也可以修改Eclipse的设置让Eclipse提示保存而不是自动打开,在Preferences -> Android -> DDMS中的HPROFAction由Open in Eclipse改为Save todisk。打开MAT,选择转化好的HPROF文件,可以看到Overview的界面如下图:

     

     

    中间的饼状图就是根据我们上文所说的Retained heap的概念得到的内存中一些Retained Size最大的对象。点击饼状图能看到这些对象类型,但对内存泄漏的分析还远远不够。再看下方Action中有Dominator Tree和Histogram的选项,这一般来说是最有用的工具。还记得我们上文说过的DominatorTree的概念吗,这就是我们用来跟踪内存泄漏的方式。点开Dominator Tree,会看到以Retained heap排序的一系列对象,如下图:




    Resources类型对象由于一般是系统用于加载资源的,所以Retained heap较大是个比较正常的情况。但我们注意到下面的Bitmap类型对象的Retained heap也很大,很有可能是由于内存泄漏造成的。所以我们右键点击这行,选择Path To GC Roots ->exclude weak references,可以看到下图的情形:


     

    Bitmap最终被leak引用到,这应该是一种不正常的现象,内存泄漏很可能就在这里了。MAT不会告诉哪里是内存泄漏,需要你自行分析,由于这是Demo,是我们特意造成的内存泄漏,因此比较容易就能看出来,真实的应用场景可能需要你仔细的进行分析。

    根据我们上文介绍的Dominator的概念,leak对象是该Bitmap对象的Dominator,应该出现在Dominator Tree视图里面,但实际上却没有。这是由于MAT并没有对weak references做区别对待,这也是我们选择exclude weakreferences的原因。如果我们Path To GC Roots ->with all references,我们可以看到下图的情形:

     

     

    可以看到还有另外一个对象在引用着这个Bitmap对象,了解weak references的同学应该知道GC是如何处理weak references,因此在内存泄漏分析的时候我们可以把weak references排除掉。

     

    有些同学可能希望根据某种类型的对象个数来分析内存泄漏。我们在Overview视图中选择Actions -> Histogram,可以看到类似下图的情形:

     

    上图展示了内存中各种类型的对象个数和Shallow heap,我们看到byte[]占用Shallow heap最多,那是因为Honeycomb之后Bitmap Pixel Data的内存分配在Dalvik heap中。右键选中byte[]数组,选择List Objects -> with incomingreferences,可以看到byte[]具体的对象列表:







    我们发现第二个byte[]的Retained heap较大,内存泄漏的可能性较大,因此右键选中这行,Path To GC Roots -> exclude weak references,同样可以看到上文所提到的情况,我们的Bitmap对象被leak所引用到,这里存在着内存泄漏。




    在Histogram视图中第一行<Regex>中输入com.example.android.hcgallery,过滤出我们自己应用中的类型,如下图:



    我们发现本应该只有一个MainActivity现在却有两个,显然不正常。右键选择List Objects-> with incoming references,可以看到这两个具体的MainActivity对象。右键选中Retained heap较大的MainActivity,Path To GC Roots -> exclude weak references,再一次可疑对象又指向了leak对象。





    以上是MAT一些基本的用法,如果你感兴趣,可以自行深入的去了解MAT的其他功能。

    展开全文
  • 你应该管理好应用的内存 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

    展开全文
  • 什么是内存泄漏(Memory Leak)?答: 进程中某些对象已经没有使用价值了,但是他们却还可以直接或者间接地被引用到GC Root,导致无法被回收。什么是内存溢出(Memory Leak)? 答 : 简单来说—>当程序向系统申请内存...

    什么是内存泄漏(Memory Leak)?

    答: 进程中某些对象已经没有使用价值了,但是他们却还可以直接或者间接地被引用到GC Root,导致无法被回收。

    什么是内存溢出(Memory Leak)?
    答 : 简单来说—>当程序向系统申请内存时,系统没有足够的内存供其使用就会造成内存溢出。
    专业来说—>当应用占用的heap(内存)资源超过了Dalvik虚拟机分配的内存就会内存溢出。比如:加载大图片。

    内存泄漏的影响

    内存泄漏是造成应用程序OOM(内存溢出)的主要原因之一。我们知道Android系统为每个应用程序分配的内存是有限的,而当一个应用中产生的内存泄漏比较多时,这就难免会导致应用所需要的内存超过系统分配的内存限额,这就造成了内存溢出从而导致应用崩溃。

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

    1、单例造成的内存泄漏

       由于单例的静态特性使得其生命周期和应用的生命周期一样长,如果一个对象已经不再需要使用了,而单例对象还持有该对象的引用,就会使得该对象不能被正常回收,从而导致了内存泄漏。
      当Activity 已经持有了对单例的引用,当屏幕切换横竖屏的时候,Activity 会重建,那么就会重新创建一个单例并且引用,先前的那个对象无法被回收,也就导致了内存泄露
    
    public class AppManager {
        private static AppManager instance;
        private Context context;
        private AppManager(Context context) {
            this.context = context;
        }
        public static AppManager getInstance(Context context) {
        // 使用了单例模式
            if (instance != null) {
                instance = new AppManager(context);
            }
            return instance;
        }
    }
    

    解决办法:在MyApplication中得到Context ,因为MyApplication中的context 的声明周期和 Activity 一样长

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

    例如,有时候我们可能会在启动频繁的Activity中,为了避免重复创建相同的数据资源,可能会出现如下写法:

    public class MainActivity extends AppCompatActivity {
    
        private static TestResource mResource = null;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            if(mResource == null){
                mResource = new TestResource();
            }
            //...
        }
    
        class TestResource {
        //...
        }
    }
    
    这样在Activity内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据。虽然这样避免了资源的重复创建,但是这种写法却会造成内存泄漏。因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,从而导致Activity的内存资源不能被正常回收。

    解决方法:将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,就使用Application的Context。

    3、Handler造成的内存泄漏
    示例:创建匿名内部类的静态对象

    ublic class MainActivity extends AppCompatActivity {
    
        private final Handler handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                // ...
            }
        };
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    // ...
                    handler.sendEmptyMessage(0x123);
                }
            });
        }
    }
    
    

    *1、从Android的角度
    当Android应用程序启动时,该应用程序的主线程会自动创建一个Looper对象和与之关联的MessageQueue。当主线程中实例化一个Handler对象后,它就会自动与主线程Looper的MessageQueue关联起来。所有发送到MessageQueue的Messag都会持有Handler的引用,所以Looper会据此回调Handle的handleMessage()方法来处理消息。只要MessageQueue中有未处理的Message,Looper就会不断的从中取出并交给Handler处理。另外,主线程的Looper对象会伴随该应用程序的整个生命周期。
    2、 Java角度
    在Java中,非静态内部类和匿名类内部类都会潜在持有它们所属的外部类的引用,但是静态内部类却不会。
    对上述的示例进行分析,当MainActivity结束时,未处理的消息持有handler的引用,而handler又持有它所属的外部类也就是MainActivity的引用。这条引用关系会一直保持直到消息得到处理,这样阻止了MainActivity被垃圾回收器回收,从而造成了内存泄漏。*

    解决办法: 可以在onDestroy方法中把handler 置为null ,或者在MyApplication 中的onCreate()方法中创建handler ,这样handler 的生命周期和activity 的生命周期 一样长。

    4、线程造成的内存泄漏

    示例:AsyncTask和Runnable

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            new Thread(new MyRunnable()).start();
            new MyAsyncTask(this).execute();
        }
    
        class MyAsyncTask extends AsyncTask<Void, Void, Void> {
    
            // ...
    
            public MyAsyncTask(Context context) {
                // ...
            }
    
            @Override
            protected Void doInBackground(Void... params) {
                // ...
                return null;
            }
    
            @Override
            protected void onPostExecute(Void aVoid) {
                // ...
            }
        }
    
        class MyRunnable implements Runnable {
            @Override
            public void run() {
                // ...
            }
        }
    }

    *AsyncTask和Runnable都使用了匿名内部类,那么它们将持有其所在Activity的隐式引用。如果任务在Activity销毁之前还未完成,那么将导致Activity的内存资源无法被回收,从而造成内存泄漏。
    解决方法:将AsyncTask和Runnable类独立出来或者使用静态内部类,这样便可以避免内存泄漏*

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

    对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,从而造成内存泄漏
    1)比如在Activity中register了一个BraodcastReceiver,但在Activity结束后没有unregister该BraodcastReceiver。
    2)资源性对象比如Cursor,Stream、File文件等往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于 java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。
    3)对于资源性对象在不使用的时候,应该调用它的close()函数将其关闭掉,然后再设置为null。在我们的程序退出时一定要确保我们的资源性对象已经关闭。
    4)Bitmap对象不在使用时调用recycle()释放内存。2.3以后的bitmap应该是不需要手动recycle了,内存已经在java层了。

    6、使用ListView时造成的内存泄漏

    初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的View对象,同时ListView会将这些View对象缓存起来。当向上滚动ListView时,原先位于最上面的Item的View对象会被回收,然后被用来构造新出现在下面的Item。这个构造过程就是由getView()方法完成的,getView()的第二个形参convertView就是被缓存起来的Item的View对象(初始化时缓存中没有View对象则convertView是null)。
    
    构造Adapter时,没有使用缓存的convertView。

    解决方法:在构造Adapter时,使用缓存的convertView。

    7、集合容器中的内存泄露

    我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。

    解决方法:在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。

    8、WebView造成的泄露

    当我们不要使用WebView对象时,应该调用它的destory()函数来销毁它,并释放其占用的内存,否则其长期占用的内存也不能被回收,从而造成内存泄露。

    解决方法:

    为WebView另外开启一个进程,通过AIDL与主线程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。

    1、在涉及使用Context时,对于生命周期比Activity长的对象应该使用Application的Context。凡是使用Context优先考虑Application的Context,当然它并不是万能的,对于有些地方则必须使用Activity的Context。

    2、对于需要在静态内部类中使用非静态外部成员变量(如:Context、View ),可以在静态内部类中使用弱引用来引用外部类的变量来避免内存泄漏。
    3、对于不再需要使用的对象,显示的将其赋值为null,比如使用完Bitmap后先调用recycle(),再赋为null。
    4、保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期。
    5、对于生命周期比Activity长的内部类对象,并且内部类中使用了外部类的成员变量,可以这样做避免内存泄漏:
    1)将内部类改为静态内部类
    2)静态内部类中使用弱引用来引用外部类的成员变量

    展开全文
  • 1. 静态变量引起的内存泄露 public class CommUtil { private static CommUtil instance; private Context context; private CommUtil(Context context){ this.context = context; } ...
    1. 静态变量引起的内存泄露
     public class CommUtil {
            private static CommUtil instance;
            private Context context;
            private CommUtil(Context context){
            this.context = context;
            }
    
            public static CommUtil getInstance(Context mcontext){
            if(instance == null){
                instance = new CommUtil(mcontext);
            }
        //        else{
        //            instance.setContext(mcontext);
        //        }
            return instance;
            }
    
    

    当调用getInstance时,如果传入的context是Activity的context。只要这个单利没有被释放,那么这个
    Activity也不会被释放一直到进程退出才会释放。

     CommonUtils.getInstance(getActivity());
    

    正确的写法应该是

    CommonUtils.getInstance(getApplicationContext());
    
    2. 非静态内部类引起的内存泄漏

    最常见的就是我们平常总用的handler

    
    //错误的示范:
        //mHandler是匿名内部类的实例,会引用外部对象MainActivity.this。如果Handler在Activity退出的时候,它可能还活着,这时候就会一直持有Activity。
        private Handler mHandler = new Handler(){
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                switch (msg.what){
                    case 0:
                        //加载数据
                        break;
    
               }
            }
        };
    
    

    正确的handler写法

    public static class MyHandler extends Handler {
        //声明一个弱引用对象
        WeakReference<MainActivity> mReference;
     
        MyHandler(MainActivity activity) {
            //在构造器中传入Activity,创建弱引用对象
            mReference = new WeakReference<MainActivity>(activity);
        }
     
        public void handleMessage(Message msg) {
            //在使用activity之前先判空处理
             MainActivity main =  mReference.get();
            if (main == null && main .isFinishing()) {
                return;
            }
        }
    }
    
    
    
    3. 不需要用的监听未移除会发生内存泄露
    例子1//        tv.setOnClickListener();//监听执行完回收对象
            //add监听,放到集合里面
            tv.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
                @Override
                public void onWindowFocusChanged(boolean b) {
                    //监听view的加载,view加载出来的时候,计算他的宽高等。
    
                    //计算完后,一定要移除这个监听
                    tv.getViewTreeObserver().removeOnWindowFocusChangeListener(this);
                }
            });
    
        例子2:
                SensorManager sensorManager = getSystemService(SENSOR_SERVICE);
            Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
            sensorManager.registerListener(this,sensor,SensorManager.SENSOR_DELAY_FASTEST);
            //不需要用的时候记得移除监听
            sensorManager.unregisterListener(listener);
    
    
    
    
    4. 资源未关闭引起的内存泄露情况

    比如:BroadCastReceiver、Cursor、Bitmap、IO流、自定义属性attribute
    attr.recycle()回收。
    当不需要使用的时候,要记得及时释放资源。否则就会内存泄露。

    5. 无限循环动画

    没有在onDestroy中停止动画,否则Activity就会变成泄露对象。
    比如:轮播图效果。

    展开全文
  •   android中的内存泄露通常是Activity或者Fragment的泄露。下文分析以Activity展开,Fragment同理。 1. 非静态内部类、匿名内部类 2. 静态的View 3. Handler 4. 监听器(各种需要注册的Listener...
  • 这篇文章主要给大家介绍了关于Android内存泄漏的轻松解决方法,文中通过示例代码介绍的非常详细,对各位Android具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧 前言 内存管理的目的就是让我们在开发...
  • 1Java内存回收方式 Java判断对象是否可以回收使用的而是可达性分析算法。 在主流的商用程序语言中(Java和C#),都是使用可达性分析算法判断对象是否存活的。这个算法的基本思路就是通过一系列名为...
  • Android内存泄漏的检测流程、捕捉以及分析简述:一个APP的性能,重度关乎着用户体验,而关于性能检测的一个重要方面,就是内存泄漏,通常内存泄漏的隐藏性质比较强,不同于异常导致的程序Crash,在异常导致的Crash中...
  • 最近在整理Android内存泄漏相关内容,目前整理出了以下八种情形,后期还会继续补充,请持续关注~单例造成的内存泄漏非静态内部类创建静态实例造成的内存泄漏Handler造成的内存泄漏线程造成的内存泄漏资源未关闭造成...
  • java/Android内存泄漏和内存溢出详解java内存泄漏和溢出跟内存栈堆也是有一些关系,这里不解释! 这里主要讲解一下内存泄漏和溢出的区别和联系。 之前我跟别人说这两个的区别就说了:内存泄漏是因为内存对象一直被...
  • 常见的Android内存泄漏问题以及解决办法什么是内存泄漏以及其危害编写代码的时候因为错误或者疏忽,导致一部分内存空间不能被垃圾回收机制回收,造成这部分的内存空间浪费,再也不能被程序使用到,这就叫做内存泄漏...
  • 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,就是该被释放的对象没有释放,一直被某个或某些实例所持有却不再被使用导致 GC 不能回收。...
  • 内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏。内存泄露有时不严重且不易察觉,这样开发者就不知道存在内存泄露,但有时也会很严重,会...
  • 1. 内存泄露简介 内存泄露,即Memory Leak,指程序中不再使用到的对象因某种原因从而无法被GC正常回收。发生内存泄露,会导致一些不再使用到的对象没有及时释放,这些对象占用了宝贵的内存空间,很容易导致后续需要...
  • Android Studio的内存分析界面 一般分析内存泄露, ...图中蓝色区域,就是程序使用的内存, 灰色区域就是空闲内存当然,Android内存分配机制是对每个应用程序逐步增加, 比如你程序当前使用30M内存, 系统可能会给你分
  • 本文整理自:【技术公开课】Android内存泄漏案例分析(点击链接,观看视频),演讲PPT>>下载地址。 一款优秀的Android应用,不仅要有完善的功能,也要有良好的体验,而性能是影响体验的一个重要因素。内存泄露...
  • 前言 在Android开发中,内存泄露 十分常见 1.内存泄露的定义:本该被回收... 这就导致了内存泄漏。 本文将详细讲解内存泄露的其中一种情况:在Handler中发生的内存泄露 1. 问题描述 Handler的一般用法 = 新建...
  • 首先了解什么是内存泄露 http://liuwangshu.cn/application/performance/ram-3-memory-leak.html   1Leakcancary的优势 LeakCanary是一个可视化的内存泄露分析工具,他具备以下优势 · 简单:只需设置一段...
  • Android内存泄漏相关

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

安卓内存泄漏