(七)结合leakcanary探讨handler和AsyncTask的内存泄露

版权声明:本文为博主原创文章,转载请注明出处,O(∩_∩)O谢谢 https://blog.csdn.net/sinat_20059415/article/details/79392633

前言:之前看handler和AsyncTask的相关博客,都提及了内存泄露,但是只是理论上提及并且提出了修改方案,并没有可视性,搜索的时候发现有个叫做leakcanary的神器可以监控内存泄露,赶紧了解看下。


参考博客:

1.五分钟体验内存泄露检测LeakCanary

2.Android内存优化(六)LeakCanary使用详解 

3.LeakCanary 中文使用说明


我的github demo地址:

点击打开链接


1. LeakCanary简单介绍

leakCanary,我Google翻译了下,叫做泄露金丝雀,简单说来就是用来做内存泄露检测的。它是开源的,代码存放路径如下:

leakCanary源码地址

可以用git clone git@github.com:square/leakcanary.git 将其下载导入到Android studio中。

1.1 LeakCanary使用演示

我下载了leakCanary的源码导入到Android studio中后,界面如下所示:

可以看出源码中给出了leakcanary的sample,可以先体验一下这个sample。

这个sample的意思是点击一下按钮,启动一个AsyncTask,然后旋转屏幕,由于屏幕旋转会导致activity重新加载,这样AsyncTask如果还在后台运行就会导致内存泄露。这时候再等一会就会有一个请求权限的弹框,允许后再重复操作一下,点击一下下拉状态栏的通知,就会得到如下界面。



1.2 结合sample源码分析内存泄露

AndroidManifest.java

<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright (C) 2015 Square, Inc.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~      http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.leakcanary"
    >
  <application android:name=".ExampleApplication" android:allowBackup="false"
      android:icon="@drawable/ic_launcher"
      >
    <activity
        android:label="@string/app_name"
        android:name=".MainActivity"
        >
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
        <category android:name="android.intent.category.DEFAULT"/>
      </intent-filter>
    </activity>
  </application>
</manifest>

Application

/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.example.leakcanary;

import android.app.Application;
import android.os.StrictMode;
import com.squareup.leakcanary.LeakCanary;

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

  protected void setupLeakCanary() {
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    enabledStrictMode();
    LeakCanary.install(this);
  }

  private static void enabledStrictMode() {
    StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() //
        .detectAll() //
        .penaltyLog() //
        .penaltyDeath() //
        .build());
  }
}

这个类的大致逻辑应该就是将leakCanary后台启动起来。

Demo:

/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.example.leakcanary;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.SystemClock;
import android.view.View;

public class MainActivity extends Activity {

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

    View button = findViewById(R.id.async_task);
    button.setOnClickListener(new View.OnClickListener() {
      @Override public void onClick(View v) {
        startAsyncTask();
      }
    });
  }

  @SuppressLint("StaticFieldLeak")
  void startAsyncTask() {
    // This async task is an anonymous class and therefore has a hidden reference to the outer
    // class MainActivity. If the activity gets destroyed before the task finishes (e.g. rotation),
    // the activity instance will leak.
    new AsyncTask<Void, Void, Void>() {
      @Override protected Void doInBackground(Void... params) {
        // Do some slow work in background
        SystemClock.sleep(20000);
        return null;
      }
    }.execute();
  }
}


这边demo的注释写的很明白,这个异步任务是个匿名内部类,因此持有一个指向外部的引用。如果这个activity被摧毁,比如说旋转,那边这个activity就会发生内存泄露,用SystemClock.sleep(2000)来保证activity被摧毁的时候AsyncTask还没执行完。

然后看了下内存泄露的截图右边有个加号,可以点击进入看详细描述。

从这上面的截图可以看出是

1)AsyncTask内存泄露了

2)匿名内部类AsyncTask引用了MainActivity

3)泄露了MainActivity的引用

大致说明白了是AsyncTask,这个匿名内部类持有了ManiActivity的引用导致了内存泄露。


1.3 普通APP使用leakcanary方式

leakcanary中有个README.md,其中详细讲述了普通app如何使用leakcanary进行内存泄露的检查。

# LeakCanary

A memory leak detection library for Android and Java.

*“A small leak will sink a great ship.”* - Benjamin Franklin

<p align="center">
<img src="https://github.com/square/leakcanary/blob/master/assets/screenshot.png"/>
</p>

## Getting started

In your `build.gradle`:

```groovy
dependencies {
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
  releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
}
```

In your `Application` class:

```java
public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    LeakCanary.install(this);
    // Normal app init code...
  }
}
```

**You're good to go!** LeakCanary will automatically show a notification when an activity memory leak is detected in your debug build.

To disable LeakCanary in unit tests, add the following to your `build.gradle`:

```groovy
// Ensure the no-op dependency is always used in JVM tests.
configurations.all { config ->
  if (config.name.contains('UnitTest')) {
    config.resolutionStrategy.eachDependency { details ->
      if (details.requested.group == 'com.squareup.leakcanary' && details.requested.name == 'leakcanary-android') {
        details.useTarget(group: details.requested.group, name: 'leakcanary-android-no-op', version: details.requested.version)
      }
    }
  }
}
```

If you want to also disable leak detection in instrumentation tests, add `|| config.name.contains('AndroidTest')` to the
`if` check above.

Questions? Check out [the FAQ](https://github.com/square/leakcanary/wiki/FAQ)!

<p align="center">
<img src="https://github.com/square/leakcanary/blob/master/assets/icon_512.png" width="250"/>
</p>

## License

    Copyright 2015 Square, Inc.

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.


一般使用就照抄上面的sample,然后在build.gradle里加上如下依赖就没问题了

dependencies {
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
  releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
}


demo如下:

build.gradle

apply plugin: 'com.android.application'

android {
    compileSdkVersion 26



    defaultConfig {
        applicationId "com.example.demo_7_memoryleaked"
        minSdkVersion 15
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
    releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
}

androidManifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.demo_7_memoryleaked">

    <application
        android:name=".LeakApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

application

package com.example.demo_7_memoryleaked;

import android.app.Application;
import android.os.StrictMode;

import com.squareup.leakcanary.LeakCanary;

/**
 * Created by jiatai on 18-2-27.
 */

public class LeakApplication extends Application {
    @Override public void onCreate() {
        super.onCreate();
        setupLeakCanary();
    }

    protected void setupLeakCanary() {
        if (LeakCanary.isInAnalyzerProcess(this)) {
            // This process is dedicated to LeakCanary for heap analysis.
            // You should not init your app in this process.
            return;
        }
        enabledStrictMode();
        LeakCanary.install(this);
    }

    private static void enabledStrictMode() {
        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() //
                .detectAll() //
                .penaltyLog() //
                .penaltyDeath() //
                .build());
    }
}

demo:

package com.example.demo_7_memoryleaked;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        LeakThread leakThread = new LeakThread();
        leakThread.start();
    }
    class LeakThread extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(6 * 60 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
    }
}

最后内存泄露分析截图:

大概就是说LeakThread泄露了MainActivity的实例引用。


2. 结合leakcanary探讨handler和AsyncTask的内存泄露

其实leakcanary的sample里就是对AsyncTask的内存泄露做了demo演示,我做一个handler的吧,其实没啥区别,修改方式也一样的。

代码见github,附一下内存泄露源码和泄露分析截图:

package com.example.demo_7_handler;

import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final Handler handler = new MyHandler();
        new Thread(){
            @Override
            public void run() {
                super.run();
                SystemClock.sleep(20000);
                handler.sendEmptyMessage(0);
            }
        }.start();
    }

    class MyHandler extends Handler{
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    }
}

内存泄露分析:

我本来是参考下面的博客了解了handler的内存泄露的

浅析Handler引起的内存泄漏及解决方法

但是从上面看其实Thread也是个内部类,不谈handler,Thread也内存泄露了。

先修改handler下看能不能修改好内存泄露。

具体如下:

package com.example.demo_7_handler;

import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

import java.lang.ref.WeakReference;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final MyHandler handler = new MyHandler(this);
        new Thread(){
            @Override
            public void run() {
                super.run();
                SystemClock.sleep(20000);
                handler.sendEmptyMessage(0);
            }
        }.start();
    }

    private static class MyHandler extends Handler{
        private WeakReference<MainActivity> mActivity;

        public  MyHandler(MainActivity activity){
            mActivity = new WeakReference<MainActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity mainActivity = mActivity.get();
            if (mainActivity == null) {
                return;
            }
            super.handleMessage(msg);
        }
    }
}

改了后还是有和上面一样的内存泄露,说明之前想的是对的,在handler之前Thread就内存泄露了。

然后我把代码改成下面这个鬼样子:

package com.example.demo_7_handler;

import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

import java.lang.ref.WeakReference;

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new MyThread(this).start();
    }

    private static class MyHandler extends Handler{
        private WeakReference<MainActivity> mActivity;

        public  MyHandler(MainActivity activity){
            mActivity = new WeakReference<MainActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity mainActivity = mActivity.get();
            if (mainActivity == null) {
                return;
            }
            super.handleMessage(msg);
        }
    }

    private static class MyThread extends Thread{

        private WeakReference<MainActivity> mActivity;

        public  MyThread(MainActivity activity){
            mActivity = new WeakReference<MainActivity>(activity);
        }

        @Override
        public void run() {
            MainActivity mainActivity = mActivity.get();
            if (mainActivity == null) {
                return;
            }
            super.run();
            SystemClock.sleep(20000);
            Looper.prepare();
            MyHandler handler = new MyHandler(mainActivity);
            handler.sendEmptyMessage(0);
            Looper.loop();
        }
    }
}

emmm,我等了好久还是看到了内存泄露的提示,这有毒吧。。。

之后我加了log:

private static class MyThread extends Thread{

        private WeakReference<MainActivity> mActivity;

        public  MyThread(MainActivity activity){
            mActivity = new WeakReference<>(activity);
        }

        @Override
        public void run() {
            MainActivity mainActivity = mActivity.get();
            if (mainActivity == null) {
                return;
            }
            super.run();
            for(int i = 0 ; i < 50 ; i ++){
                Log.d("zjt", "i = " + i);
                if(mainActivity == null){
                    Log.d("zjt", "the weakreference is null");
                    return;
                }
                SystemClock.sleep(2000);
            }

            Looper.prepare();
            MyHandler handler = new MyHandler(mainActivity);
            handler.sendEmptyMessage(0);
            Looper.loop();
        }
    }

当我旋转屏幕以后有两个打印,说明MainActivity完全没有被回收=-=

02-28 21:14:37.394 7568-7626/com.example.demo_7_handler D/zjt: i = 0
02-28 21:14:39.394 7568-7626/com.example.demo_7_handler D/zjt: i = 1
02-28 21:14:41.395 7568-7626/com.example.demo_7_handler D/zjt: i = 2
02-28 21:14:43.397 7568-7626/com.example.demo_7_handler D/zjt: i = 3
02-28 21:14:43.427 7568-7927/com.example.demo_7_handler D/zjt: i = 0
02-28 21:14:45.397 7568-7626/com.example.demo_7_handler D/zjt: i = 4
02-28 21:14:45.427 7568-7927/com.example.demo_7_handler D/zjt: i = 1
02-28 21:14:47.397 7568-7626/com.example.demo_7_handler D/zjt: i = 5
02-28 21:14:47.427 7568-7927/com.example.demo_7_handler D/zjt: i = 2
02-28 21:14:49.397 7568-7626/com.example.demo_7_handler D/zjt: i = 6

哦,绝望了,这大神给的方法不对还是我有毒。。。

我打印了下弱引用activity的值如下所示


待续。。。



3. 总结

leakcanary使用体验讲道理一般,很小的应用找到内存泄露的复现路径后弹出内存泄露通知有时候要好几分钟(我还没看具体实现方法,先吐槽为敬),但是不失为自己本地开发工程中一种检验内存泄露的方法。

阅读更多
换一批

没有更多推荐了,返回首页