内存泄漏简介

    Java可以保证当没有引用指向对象的时候,对象会被垃圾回收器回收,与c语言自己申请的内存自己释放相比,java程序员轻松了很多,但是并不代表java程序员不用担心内存泄漏。当java程序发生内存泄漏的时候往往具有隐蔽性。




定义

内存泄漏用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。从程序员的角度来看“内存泄漏”,其实就是一个对象的生命周期超出了程序员所预期的长度,那么这个对象就泄漏了。




Android开发中的内存泄漏

Android应用程序本身系统分配的内存很少,一旦发生泄漏,程序很快就会变得非常卡顿,直至OOM崩溃。本文将通过两个案例,来介绍内存泄漏分析工具MAT,以及内存分析的技巧。




需要工具

准备内存泄漏的分析工具,可以安装eclipse插件mat。如果使用AndroidStudio开发,可以将hprof文件导出再另外使用mat进行分析,不过会缺少某些功能,但是也够用了,本文适应的场景是AndroidStudio+MAT。


Android开发中常见的内存泄漏


•对象没有反注册


•数据库cursor没有关闭


•Bitmap没有回收


•ListViewitem没有复用


•Handler在Activity中定义为非static的匿名内部类




人为制造一个内存泄漏

自定义一个ActivityManager,提供两个方法,分别用来注册与反注册Activity

package com.example.qinghua_liu.myapplication;

importandroid.app.Activity;
importandroid.util.Log;
importjava.util.ArrayList;
importjava.util.List;
publicclass  ActivityManager{
     private List<Activity>mActivities=newArrayList<>();
     private static ActivityManagersInstance;

     private ActivityManager(){
     };

     public static ActivityManagerinstance() {
         if (sInstance==null){
             sInstance=newActivityManager();
         }
         return sInstance;
     }

     public void registActivity(Activityactivity) {
         mActivities.add(activity);
         Log.d("TAG","mActivities.size()="+mActivities.size());
     }

     public void unRigistActivity(Activityactivity) {
         mActivities.remove(activity);
     }

 }


在MainActivity的onCreate与onDestroy中分别调用registActivity和registActivity方法进行注册与反注册。但是Main2Activity忘记了反注册

public classMainActivityextendsAppCompatActivity{ 
   Button mBtn;
@Override
protectedvoid onCreate(BundlesavedInstanceState){
     super.onCreate(savedInstanceState);
     setContentView(R.layout.activity_main);mBtn = (Button)findViewById(R.id.mybutton);
mBtn("newActivity");
mBtnView.OnClickListener(){
     @Override
     public void onClick(Viewv) {
         Intent it = newIntent(MainActivity.this,
                 Main2Activity.class);
         try {
             startActivity(it);
         } catch (Exceptione) {
             Log.e(TAG,e.toString());
         }
    }
 });
     ActivityManager.instance().registActivity(this);}
@Override
protectedvoid onDestroy(){
     super.onDestroy();
     ActivityManager.instance().unRigistActivity(this);}}
 

 
public classMain2ActivityextendsActivity{
private Object[] mObjs= new Object[10000];
@Override
protectedvoid onCreate(BundlesavedInstanceState){
     super.onCreate(savedInstanceState);
     setContentView(R.layout.activity_main);//模拟快速消耗大量内存,使效果明显
for (inti=0;i<mObjs.length;i++){
     mObjs[i] =new Object();
 }ActivityManager.instance().registActivity(this);
}
 
@Override
protectedvoid onDestroy(){
     super.onDestroy();
     //ActivityManager.instance().unRigistActivity(this);// if forget unRigist
}}


AndroidStudio Monitor Debug

Android postDelay内存泄漏 android 内存泄漏分析_MAT


Monitor界面可以大概显现内存,CPU,GPU,NetWork的占用消耗情况。

    详细分析还要依靠Mat。APP操作多次叫起和销毁MainActivity以及Main2Activity。

      导出Memory hprof 文件:

      在AS Monitor Debug界面按DumpJava Heap,工具会自动生成一份hprof后缀文件。




hprof文件分析

Android Studio 生成的hprof文件 MAT还不能直接使用,需用转换成标准的hprof文件。

转换可借助AndroidSdkPlatform toolhprof具体命令下图:

Android postDelay内存泄漏 android 内存泄漏分析_MAT_02




MAT分析工具


经转换得到的hprof文件可以由MAT直接打开了。

Android postDelay内存泄漏 android 内存泄漏分析_内存泄露_03

MAT分析

Mat会dump一个内存快照出来,然后从分析报告中点击“Leaksuspects”这里会列出可能泄漏的对象,其中你会发现“com.example.qinghua_liu.myapplication.Main2Activity”的身影。Main2Activity这个类有49个实例,CodeAuthor应该一下子就会发现,原来是Main2Activity泄漏了。发现它泄漏之后,如何找出是哪一个对象持有了Main2Activity对象的引用呢?



找出引用链


使用OQL对象查询语言查询出泄漏的对象,和SQL非常相似,语法简单易懂,却非常强大。select* from com.example.qinghua_liu.myapplication.Main2Activity筛选出Main2Activity这一类对象。

Android postDelay内存泄漏 android 内存泄漏分析_Handler_04

 然后选择“excludeweak/soft references”筛选出除了软引用和弱引用之外(即强引用)的对象。


Android postDelay内存泄漏 android 内存泄漏分析_MAT_05



然后找出的GC的Root,从下图可以看出,原来Activity对象被ActivityManager里面的ArrayList给hold住了,所以接下来的工作就是在Main2Activity检查反注册,内存泄漏的原因很快就可以搞清楚了。


Android postDelay内存泄漏 android 内存泄漏分析_android_06



链接


Handler使用过程中可能引发的内存泄漏

我们UI编程时一般会使用Handler进行UI更新,环境会有下面一段温馨的提示:ThisHandler class should be static or leaksmightoccur

    另外还有段详细英文描述,大概意思就是:

   一旦Handler被声明为内部类,那么可能导致它的外部类不能够被垃圾回收。如果Handler是在其他线程(通常是workerthread)使用Looper或MessageQueue(消息队列),而不是main线程(UI线程),那么就没有这个问题。如果Handler使用Looper或MessageQueue在主线程(mainthread),你需要对Handler的声明做如下修改:
    声明Handler为static类;在外部类中实例化一个外部类的WeakReference(弱引用)并且在Handler初始化时传入这个对象给你的Handler;将所有引用的外部类成员使用WeakReference对象。(与Bitmap工作线程弱引用相似?)




解决方案

private static class CopyFileHandlerextendsHandler{
     WeakReference<MainActivity>mActivity;
     public CopyFileHandler(MainActivityactivity) {
         mActivity=newWeakReference<>(activity);
     }

     public void handleMessage(Messagemsg){
         final MainActivityactivity =mActivity.get();
         if(null!=activity&&null!=activity.btn){
         //handle you message here!
             activity.btn.setText("clickme");
             activity.btn.setClickable(true);
         }     }
} 
private CopyFileHandlermHandler;
@Override
protected void  onCreate(BundlesavedInstanceState){
mHandler=newCopyFileHandler(this);
}
private void  startCopyFileThread(){
     Log.d(TAG,"startCopyDBThread");
     new Thread(newRunnable() {
         @Override
         public void run(){
             //DOSOMETHING LIKE:  copyDBFile();
             Messagemsg=mHandler.obtainMessage();
             mHandler.sendMessageDelayed(msg,1000);         }
     }).start();
 }


为什么会内存泄漏

这与几个关键词有关:内部类、Handler的消息循环(Looper)、Java垃圾回收机制。需要强调一下,并不是每次使用Handler都会引发内存泄漏,这里面有一定的几率,需要满足特定条件才会引起泄漏。
  内部类会有一个指向外部类的(硬)引用。垃圾回收机制中约定,当内存中的一个对象的引用计数为0时,将会被回收。
  Handler作为Android上的异步消息处理机制,它的工作是需要Looper和MessageQueue配合的。简单的说,要维护一个循环体(Looper)处理消息队列(MessageQueue)。每循环一次就从MessageQueue中取出一个Message,然后回调相应的消息处理函数。

onDestroy方法一直不执行就是这个原因。




另一个解决方案的尝试

提示描述中提到了也可以让Handler在workerthread中使用Looper或MessageQueue。

private class  WorkThreadextendsThread{
         @Override
         public void run(){
             super.run();
             Looper.prepare();
             testHandler=newHandler(){
                 publicvoid handleMessage(Messagemsg){                     mybutton1.post(newRunnable(){
                        @Override
                         publicvoid  run(){
                             mybutton1.setText("clickme");
                             mybutton1.setClickable(true);
                         }
                     });
                 }
             };
             Looper.loop();
         }
     } 

 
private WorkThreadmThread;
@Override
protectedvoid onCreate(BundlesavedInstanceState){   mThread=newWorkThread();
    if(Thread.State.NEW==mThread.getState()){
         mThread.start();
    }}
@OnClick({R.id.hello,R.id.mybutton1,R.id.radioButton})
publicvoid onClick(Viewview) {
     switch (view.getId()){
         case R.id.mybutton1:             mybutton1.setText("clicked");
             mybutton1.setClickable(false);
             testHandler.sendEmptyMessageDelayed(1,3000);             break;
         case R.id.radioButton:
             break;
     }
 }