Android内存泄漏检测工具LeakCanary

一、LeakCanary简介
LeakCanary是Square公司开源的一个检测内存的泄露的函数库,可以方便地和你的项目进行集成,在Debug版本中监控Activity、Fragment等的内存泄露;
LeakCanary集成到项目中之后,在检测到内存泄露时,会发送消息到系统通知栏。点击后打开名称DisplayLeakActivity的页面,并显示泄露的跟踪信息,Logcat上面也会有对应的日志输出。同时如果跟踪信息不足以定位时,DisplayLeakActivity还为开发者默认保存了最近7个dump文件到App的目录中,可以使用MAT等工具对dump文件进行进一步的分析;
二、内存泄漏简介
在了解了LeakCanary的接入方式后,我们肯定着急想见识见识LeakCanary的威力。在跟大家演示LeakCanary检测和处理构成之前,大家应该明应该对内存泄露有基本的了解和认识;
1.为什么会产生内存泄漏?
当一个对象不需要使用本该回收时,有另外一个正在使用的对象持有它的引用,从而导致它不能回收停留在堆内存中,这就产生了内存泄漏;
2.内存泄露对程序产生的影响?
内存泄漏是造成应用程序OOM的主要原因之一。Android系统为每个应用程序分配有限的内存,当应用中内存泄漏较多时,就难免会导致应用所需要的内存超出系统分配限额,从而导致OOM应用Crash;
3.Android常见的内存泄露?
相信内存泄露对大家都早有耳闻,但是它不像一些Java异常情况,会立即造成程序的Crash,却有让大家比较“陌生”。下面我们就列举出日常开发中常见的内存泄露类型,让大家对内存泄露的认识不仅仅停留在“有所耳闻 ”的层面;
 单例造成:由于单例静态特性使得单例的生命周期和应用的生命周期一样长,如果一个对象(如Context)已经不使用了,而单例对象还持有对象的引用造成这个对象不能正常被回收;
 非静态内部类创建静态实例造成:在Acitivity内存创建一个非静态内部类单例,避免每次启动资源重新创建。但是因为非静态内部类默认持有外部类(Activity)的引用,并且使用该类创建静态实例。造成该实例和应用生命周期一样长,导致静态实例持有引用的Activity和资源不能正常回收;
 Handler造成:子线程执行网络任务,使用Handler处理子线程发送消息。由于handler对象是非静态匿名内部类的对象,持有外部类(Activity)的引用。在Handler-Message中Looper线程不断轮询处理消息,当Activity退出还有未处理或者正在处理的消息时,消息队列中的消息持有handler对象引用,handler又持有Activity,导致Activity的内存和资源不能及时回收;
 线程造成:匿名内部类Runnalbe和AsyncTask对象执行异步任务,对当前Activity隐式引用。当Activity销毁之前,任务还没有执行完,将导致Activity的内存和资源不能及时回收;

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


在项目中引入LeakCanary
  1. 配置 build.gradle文件,添加后我们点Sync Now ,或者build一下
  debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
  releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
  testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
 2.在项目里添加一个类叫MyApplication 继承自Application ,并在其中“安装” LeakCannary:

public class BaseApplication extends Application {
    public static RefWatcher refWatcher;
    @Override
    public void onCreate() {
        super.onCreate();
        //初始化LeakCanary
        refWatcher = LeakCanary.install(this);
    }

}

写一个内存泄漏的Demo

  UI布局
 
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.bwie.neicunjiance.MainActivity">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="开启异步线程" />
    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="跳转" />

</LinearLayout>
在MainActivity中添加代码
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private TextView mTv;
    private Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mButton = (Button) findViewById(R.id.button);
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startAsyncTask();
            }
        });
    }

    public void startAsyncTask() {
        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                Log.v("家俊", "doInBackground");
                SystemClock.sleep(10000);
                return null;
            }
        }.execute();
    }
运行代码,点击button后等待10秒,LeakCanary提示内存泄漏。

分析其中的原因:

AsyncTask是一个匿名的内部类,隐式的持有外部类(MainActivity)的引用,
当activity被销毁的时候,如果AsyncTask(代码sleep 20秒,模拟了一个耗时操作)没有执行完成,
则MainActivity将会泄漏

关于隐式引用----内部类可以直接去调用外部类的成员(属性和方法),
如果没有持有外部类的引用,内部类是没办法去调用外部类的成员,
但是内部类又没有显示的去指定声明引用,所以称之为隐式引用。

//解决方式
与内部类一样,匿名类也会持有外部类的引用,改成静态内部类
public class MainActivity extends AppCompatActivity {
    private TextView mTv;
    private Button mButton;
    private Button btn;
    private AppManager instance;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mButton = findViewById(R.id.button);
        btn = findViewById(R.id.btn);
        instance = AppManager.getInstance(this);
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
               new MyAsyncTask().execute();
            }
        });
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(MainActivity.this,Main2Activity.class));
            }
        });
    }

    static class MyAsyncTask extends AsyncTask<Void, Void, Void> {
        @Override
        protected Void doInBackground(Void... voids) {
            Log.v("家俊", "doInBackground");
            SystemClock.sleep(10000);
            return null;
        }
    }

    @Override


    protected void onDestroy() {
        super.onDestroy();
        //BaseApplication.refWatcher.watch(instance);
    }
}

LeakCanary使用方式

    监控 Activity 泄露(默认)
    由于经常把 Activity 当作为 Context 对象使用,在不同场合由各种对象引用 Activity。
    所以,Activity 泄漏是一个重要的需要检查的内存泄漏之一。

    监控Fragment泄漏
    -使用RefWatcher监控
    此类的对象的获取方式:RefWatcher refWatcher=LeakCanary.install(this);
    可以在刚才自定义的全局MyApplication 中去获取。代码如下:

   public class MyApplication extends Application {
      public static RefWatcher refWatcher;

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

然后,在需要检测Fragment回收的地方,加入refWatcher.watch(this);
-代码如下

public abstract class BaseFragment extends Fragment {
    @Override 
    public void onDestroy() {
        super.onDestroy();
        RefWatcher refWatcher = MyApplication.refWatcher;
        refWatcher.watch(this);
    }
}
当 Fragment.ondestroy()被调用之后,如果这个 fragment 实例没有被销毁,
那么就会从通知栏和 logcat 里看到相应的泄漏信息。


LeakCanary 的机制如下:
1.RefWatcher.watch() 会以监控对象来创建一个 KeyedWeakReference 弱引用对象
2.在 AndroidWatchExecutor 的后台线程里,来检查弱引用已经被清除了,如果没被清除,则执行一次 GC
3.如果弱引用对象仍然没有被清除,说明内存泄漏了,系统就导出 hprof 文件,保存在 app 的文件系统目录下
4.HeapAnalyzerService 启动一个单独的进程,使用 HeapAnalyzer 来分析 hprof 文件。它使用另外一个开源库 HAHA。
5.HeapAnalyzer 通过查找 KeyedWeakReference 弱引用对象来查找内在泄漏
6.HeapAnalyzer 计算 KeyedWeakReference 所引用对象的最短强引用路径,来分析内存泄漏,并且构建出对象引用链出来。
7.内存泄漏信息送回给 DisplayLeakService,它是运行在 app 进程里的一个服务。然后在设备通知栏显示内存泄漏信息。


1.1 静态变量引用Activity对象

通过静态变量引用Activty对象时,会导致Activty对象所占内存内漏。主要是因为,静态变量是驻扎在JVM的方法区,因此,静态变量引用的对象是不会被GC回收的,因为它们所引用的对象本身就是GC ROOT(这块不清楚的请参考我的另一篇文章《JVM理解其实并不难! 》)。即最终导致Activity对象不被回收,从而也就造成内存泄漏。

看个简单例子,比如说,你应用启动Activty的场景很多,你希望定义一个工具类Util.java,在这个类中,定义一个启动Activty的方法startActivity(Class nextActivity);以此来简化启动Activty的代码。另外,加入你当前的Activty启动另一个Activty的代码使用率也特别高。为了使得参数尽可能的少,你提供setFirstActivty,保存当前的Activty。代码如下:

Util.java

/**
 * Created by HuaChao on 2016/8/13.
 */
public class Util {

    private static Activity sActivity;

    public static void setActivity(Activity activity) {
        sActivity = activity;
    }

    public static void startActivity(Class nextActivity) {
        Intent intent = new Intent(sActivity, nextActivity);
        sActivity.startActivity(intent);
    }
}

在当前的Activty中,只需在onCreate中调用Util.setFirstActivity(this);,在需要启动另一个Activty处调用Util.startActivity(SecondActivity.class);

在上面代码中,如果当前的Activty不再使用且Util中的sActivity对象没有更改,会导致当前Activty一直驻留在内存中。

1.2 静态View

有时,当一个Activity经常启动,但是对应的View读取非常耗时,我们可以通过静态View变量来保持对该Activity的rootView引用。这样就可以不用每次启动Activity都去读取并渲染View了。这确实是一个提高Activity启动速度的好方法!但是要注意,一旦View attach到我们的Window上,就会持有一个Context(即Activity)的引用。而我们的View有事一个静态变量,所以导致Activity不被回收。当然了,也不是说不能使用静态View,但是在使用静态View时,需要确保在资源回收时,将静态View detach掉。

1.3 内部类

我们知道,非静态内部类持有外部类的一个引用。因此,如果我们在一个外部类中定义一个静态变量,这个静态变量是引用内部类对象。将会导致内存泄漏!因为这相当于间接导致静态引用外部类。

static InnerClass innerClass;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main); 
    innerClass = this.new InnerClass(); 
}

class InnerClass { 
}

1.4 匿名类

与内部类一样,匿名类也会持有外部类的引用。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);


    new AsyncTask<Void, Void, Void>() {
        @Override
        protected Void doInBackground(Void... params) {
            //另一个线程中持有Activity的引用,并且不释放
            while (true) ;
        }
    }.execute();

}

1.5 Handler

我们知道,主线程的Looper对象不断从消息队列中取出消息,然后再交给Handler处理。如果在Activity中定义Handler对象,那么Handler肯定是持有Activty的引用。而每个Message对象是持有Handler的引用的(Message对象的target属性持有Handler引用),从而导致Message间接引用到了Activity。如果在Activty destroy之后,消息队列中还有Message对象,Activty是不会被回收的。当然了,如果消息正在准备(处于延时入队期间)放入到消息队列中也是一样的。

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


    handler.postDelayed(new Runnable() {
        @Override
        public void run() { /* ... */ }
    }, Integer.MAX_VALUE); 
}

解决办法就是,将Handler放入单独的类或者将Handler放入到静态内部类中(静态内部类不会持有外部类的引用)。如果想要在handler内部去调用所在的外部类Activity,可以在handler内部使用弱引用的方式指向所在Activity,这样不会导致内存泄漏。

1.6 Threads和TimerTask

Threads和Timer导致内存泄漏的原因跟内部类一样。虽然在新的线程中创建匿名类,但是只要是匿名类/内部类,它都会持有外部类引用。

void spawnThread() {
    new Thread() {
        @Override public void run() {
            while(true);
        }
    }.start();
}

void scheduleTimer() {
    new Timer().schedule(new TimerTask() {
        @Override
        public void run() {
            while(true);
        }
    }, Long.MAX_VALUE >> 1);
}

1.7 监听器

当我们需要使用系统服务时,比如执行某些后台任务、为硬件访问提供接口等等系统服务。我们需要把自己注册到服务的监听器中。然而,这会让服务持有 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);
}

2 集合对象造成的泄漏

当我们定义一个静态的集合类时,请注意,这可能会导致内存泄漏!前面我们提到过,静态变量所引用的对象是不会被回收掉的。而我的静态集合类中,包含有大量的对象,这些对象不会被回收。另外,如果集合中保存的对象又引用到了其他的大对象,如超长字符串、Bitmap、大数组等,很容易造成OOM。

3 资源对象没关闭造成内存泄漏

当我们打开资源时,一般都会使用缓存。比如读写文件资源、打开数据库资源、使用Bitmap资源等等。当我们不再使用时,应该关闭它们,使得缓存内存区域及时回收。虽然有些对象,如果我们不去关闭,它自己在finalize()函数中会自行关闭。但是这得等到GC回收时才关闭,这样会导致缓存驻留一段时间。如果我们频繁的打开资源,内存泄漏带来的影响就比较明显了。

4 使用对象池避免频繁创建对象

在我们需要频繁创建使用某个类时,或者是在for循环里面创建新的对象时,导致JVM不断创建同一个类。我们知道,在使用Message对象时,不是直接new出来的,而是通过obtain方法获取,以及recycle方法回收。这是典型的享元模式(不熟悉的同学参考《从Android代码中来记忆23种设计模式 》)。我们可以通过使用对象池来实现.

import android.support.v4.util.Pools;

/**
 * Created by HuaChao on 2016/8/13.
 */
public class MyObject {
    private static final Pools.SynchronizedPool<MyObject> MY_POOLS = new Pools.SynchronizedPool<>(10);

    public static MyObject obtain() {
        MyObject object = MY_POOLS.acquire();
        if (object == null)
            object = new MyObject();
        return object;
    }

    public void recycle() {
        MY_POOLS.release(this);
    }
}




 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值