自己编写的一个Activity内存泄漏检测工具

自己编写的一个Activity内存泄漏检测工具

看完LeakCanary的源码之后,有点想自己动手写一个内存泄漏的工具了,正所谓自己动手,丰衣足食。

真正做起来,其实会出现各种问题,最大的问题就是误判,经常由于GC没有及时把销毁的Activity回收而误判未内存泄漏,即便把检测时间延长也是如此:
tu
(这是在两次gc后等待10秒仍然存活的activity,此时仅被虚引用指向)

最后的解决办法就是,尽可能多地调用GC,让误判率降低,这样导致的一个结果时,当频繁的活动销毁时会带来性能下降。

使用

配置

首先在Application中安装:

public class MyApplication extends Application{
    @Override
    public void onCreate() {
        super.onCreate();
        new LeakMonitor().install(this);
    }
}

当发生内存泄漏时会弹出通知:
这里写图片描述

创建一个模拟内存泄漏的activity:
这里写图片描述

定位内存泄漏

打开目录:

/storage/emulated/0/Android/data/com.newsapp/files/Documents/

把文件导出到桌面:
这里写图片描述

用以下命令转换为标准的hprof文件:

hprof-conv C:\Users\mingC\Desktop\com.newsapp.main.LeakActivity@a4c55ea heapdump.hprof

用MAT工具打开heapdump.hprof,进入Histogram页面:
这里写图片描述

在regex中输入疑似内存泄漏的类名,回车:
这里写图片描述

可以看到LeakActivity虽然已经走完了生命周期,但是仍然存在它是一个实例,LeakActivity$1表示内部类Thread,接下来查看该对象的强引用链(exclude掉弱引用、软应用、虚引用):
这里写图片描述

最终我们定位到了引用着LeakActivity的是内部类Thread:
这里写图片描述

Java会将内部类表示为”外部类 1 " " t h i s 0”。

工具架构

这里写图片描述

  1. LeakInfo 内存泄漏数据实体
  2. LeakMonitor 主要的类,用来开启检测线程,传递LeakInfo给LeakHandler进行处理
  3. LeakHandler 处理器接口,用户可以实现这个接口来处理LeakInfo
  4. DefaultLeakHandler 默认的处理器,弹出通知告知用户发生内存泄漏

工具实现

LeakInfo

定义了泄漏数据的结构:

/**
 * 对象内存泄漏的信息
 * @author mingC
 * @date 2018/8/8
 */
public class LeakInfo {

    /**
     * 泄漏的对象类名
     */
    public String objectName = "null";

    /**
     *  堆栈信息文件,可能为null
     */
    public File heapdumpFile = new File("NoneFile");

    @Override
    public String toString() {
        return "对象名:" + objectName + ";heapdump:" + (heapdumpFile == null ? "null" : heapdumpFile.getAbsolutePath());
    }
}

LeakHandler

public interface LeakHandler {
    /**
     * 处理内存泄漏,此方法在子线程运行
     * @param leakInfo 泄漏信息
     */
    void handleLeak(Context context, @NonNull LeakInfo leakInfo);
}

默认的实现类,适配了8.0以上通知栏行为变更:

/**
 * 默认的泄漏处理器
 * @author mingC
 * @date 2018/8/8
 */
public class DefaultLeakHandler implements LeakHandler{
    private static final String CHANNEL_ID = "Leak";
    private LeakInfo mLeakInfo;
    private Context mContext;
    private static AtomicInteger id = new AtomicInteger(0); //发布通知的id,避免后来的通知覆盖上面之前的通知

    private String mContentText;
    private String mTitleText;

    @Override
    public void handleLeak(Context context, LeakInfo leakInfo) {
        //在通知栏告知用户
        mContext = context;
        mLeakInfo = leakInfo;
        mTitleText = "疑似发生内存泄漏";
        mContentText = "泄漏对象:" + mLeakInfo.objectName;
        Log.d("DefaultLeakHandler", "mLeakInfo:" + mLeakInfo);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            showNotificationV26();
        } else {
            showNotification();
        }
    }

    //Android 8.0以下
    private void showNotification() {
        Uri sound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
        NotificationManager manager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
        Notification notification = new NotificationCompat.Builder(mContext)
                .setWhen(System.currentTimeMillis())
                .setContentTitle(mTitleText)
                .setContentText(mContentText)
                .setSound(sound)
                .setVibrate(new long[]{1000,2000,1000})
                .setSmallIcon(R.mipmap.ic_launcher_round)
                .build();
        manager.notify(id.getAndAdd(1), notification);
    }

    //Android 8.0以上
    @RequiresApi(api = Build.VERSION_CODES.O)
    private void showNotificationV26() {
        NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "Leak",
                NotificationManagerCompat.IMPORTANCE_HIGH);
        channel.enableVibration(true);
        NotificationManager manager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
        manager.createNotificationChannel(channel);
        Notification notification = new Notification.Builder(mContext, CHANNEL_ID)
                .setWhen(System.currentTimeMillis())
                .setContentTitle(mTitleText)
                .setContentText(mContentText)
                .setAutoCancel(false)
                .setSmallIcon(R.mipmap.ic_launcher_round)
                .build();
        manager.notify(id.getAndAdd(1), notification);
    }
}

LeakMonitor

主要流程:

  1. 通过Application注册活动生命周期的回调
  2. 当活动执行onDestory时,添加一个IdelHandler到主线程Looper,当主线程闲置时提交检测任务DetectorRunnable到mExecutor执行器中执行
  3. 创建待检测activity的虚引用mRef,循环若干秒一次gc,如果超过一段时间虚引用还没有添加到关联队列mQueue中,则怀疑发生内存泄漏
  4. 执行heapdump,交给LeakHandler处理
/**
 * 1、内存泄漏检测工具:只能检测Activity的内存泄漏
 * 2、GC的问题:GC是不保证一定立即调用,不保证清理效果,不保证什么时候会调用的
 * 3、活动销毁之后,并不一定马上就没有强引用了,所以最好在主线程空闲的时候才检测
 * 4、被检测出内存泄漏的对象还需要手动分析hprof文件,查看gc root上是否还有强引用链
 * @author mingC
 * @date 2018/8/8
 */
public class LeakMonitor implements Application.ActivityLifecycleCallbacks{

    public static final String TAG = "LeakMonitor";

    private Application mContext;

    private File mDumpDictory;

    private final ReferenceQueue<Object> mQueue;

    private final Handler mainHandler;

    //检测内存泄漏的线程执行器(单线程执行器)
    private final ExecutorService mExecutor;

    //内存泄漏的处理器
    private final LeakHandler mLeakHandler;

    public LeakMonitor() {
        //使用默认的处理器
        this(new DefaultLeakHandler());
    }

    public LeakMonitor(LeakHandler leakHandler) {
        this.mLeakHandler = leakHandler;
        mQueue = new ReferenceQueue<>();
        mExecutor = Executors.newSingleThreadExecutor();
        mainHandler = new Handler(Looper.getMainLooper());
    }

    /**
     * 安装使用
     * @param application 需要检测活动内存泄漏的应用
     */
    public void install(final Application application) {
        mContext = application;
        mDumpDictory = application.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);
        //注册生命周期监听
        application.registerActivityLifecycleCallbacks(this);
    }

    /**
     * 取消内存泄漏监测
     */
    public void uninstall() {
        mContext.unregisterActivityLifecycleCallbacks(this);
        mExecutor.shutdownNow();
    }

    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}

    @Override
    public void onActivityStarted(Activity activity) {}

    @Override
    public void onActivityResumed(Activity activity) {}

    @Override
    public void onActivityPaused(Activity activity) {}

    @Override
    public void onActivityStopped(Activity activity) {}

    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}

    @Override
    public void onActivityDestroyed(final Activity activity) {
        //在主线程空闲时才执行
        mainHandler.post(new Runnable() {
            @Override
            public void run() {
                Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
                    @Override
                    public boolean queueIdle() {
                        Log.d(TAG, "主线程idle");
                        mExecutor.execute(new DetectorRunnable(activity));
                        return false;
                    }
                });
            }
        });
    }

    /**
     * 检测弱引用指向的对象是否已经是线程不可达(GC回收了)
     */
    class DetectorRunnable implements Runnable{
        //使用虚引用
        PhantomReference<Object> mRef;
        //对象类名+地址
        String mObjectName;

        public DetectorRunnable(Object object) {
            mObjectName = object.toString();
            mRef = new PhantomReference<>(object, mQueue);
        }

        @Override
        public void run() {
            //等待gc的时间
            long waitTime = 10000;
            Log.d(TAG, Thread.currentThread().getId() + "开始内存泄漏检测");
            //第一次判断
            if (mRef.isEnqueued()) {
                Log.d(TAG, mObjectName + "第一次判断:已被回收");
                return;
            }
            //这里不等待的话会出现正常活动不回收的情况
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //两次gc,增加不使用对象被回收的概率
            Runtime.getRuntime().gc();
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(waitTime / 5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Runtime.getRuntime().gc();
                //第二次判断
                if (mRef.isEnqueued()) {
                    Log.d(TAG, mObjectName + ",第" + (i+1) + "次判断:已被回收" );
                    return;
                }
            }

            //发生内存泄漏
            Log.d(TAG, mObjectName + "发生内存泄漏");
            try {
                //dump出的文件以objectName命名
                String file = new File(mDumpDictory, mObjectName).getAbsolutePath();
                Debug.dumpHprofData(file);
                LeakInfo info = new LeakInfo();
                info.objectName = mObjectName;
                info.heapdumpFile = new File(file);
                //回调处理
                mLeakHandler.handleLeak(mContext, info);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

正常销毁的activity在第二次gc后被回收:

08-09 14:15:30.725 3231-3350/com.newsapp I/zygote: Explicit concurrent copying GC freed 880(163KB) AllocSpace objects, 0(0B) LOS objects, 61% free, 950KB/2MB, paused 932us total 12.050ms
08-09 14:15:32.734 3231-3350/com.newsapp I/zygote: Explicit concurrent copying GC freed 1229(116KB) AllocSpace objects, 0(0B) LOS objects, 63% free, 866KB/2MB, paused 592us total 5.558ms
08-09 14:15:34.736 3231-3350/com.newsapp D/LeakMonitor: com.newsapp.main.UnexportedActivity@624e33d,第2次判断:已被回收

猜你喜欢

转载自blog.csdn.net/mingC0758/article/details/81545119