最近看了下MonitoSDK的代码,试用了下里面的Sample App,然后在使用”测试数据库”时,发现存在部分内存泄漏的情况,leakcanary直接弹出提示,可以看到MonitorDBActivityinstance导致泄漏。

一.Leak Canary报错效果图

mem-leak

二.过程分析

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

        ButterKnife.bind(this);
    }

    @OnClick(R.id.inputData)
    public void inputData() {

        EditText input = (EditText) findViewById(R.id.inputTestData);
        int num = Integer.valueOf(input.getEditableText().toString());
        ErrorReporter reporter = ACRA.getErrorReporter();
        CrashReportDataFactory factory = null;
        try {
            factory = Reflect.on(reporter).get("crashReportDataFactory");
        } catch (ReflectException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < num; i++) {
            final ReportBuilder builder = new ReportBuilder();
            builder.exception(new Exception(i + ""));
            final CrashReportData crashReportData = factory.createCrashData(builder);
            MLog log = null;
            try {
                log = new MLog(crashReportData.toJSON().toString());
            } catch (JSONReportBuilder.JSONReportException e) {
                e.printStackTrace();
            }
            MLogStoreMgr.getInstance(this).add(log);
        }
    }

log相关操作只有MLogStoreMgr.getInstance(this).add(log);这一行代码,继续debug,可以看到最后底层执行的是

if(!UploadTask.isRunning()) {
            TaskExecutor.getInstance().postDelayed(1, new UploadTask() {
                public void onUploadExcuted() {
                    if(UploadEngine.this.bRunning) {
                        UploadEngine.this.calNextInterval();
                        if(TaskExecutor.getInstance().hasCallbacks(1)) {
                            TaskExecutor.getInstance().removeCallbacks(1);
                        }

                        if(!UploadTask.isRunning()) {
                            TaskExecutor.getInstance().postDelayed(1, this, UploadEngine.this.mPeriod);
                        }
                    }

                }

                public void deleteError() {
                }
            }, this.mPeriod);
        }

没错,就是这个postDelayed导致的。因为这个方法,导致主线程MainActivity的引用无法被释放,直到消息被looper处理掉。要解决这种问题,思路就是避免使用非静态内部类,继承Handler时,要么是放在单独的类文件中,要么就是使用静态内部类。因为静态的内部类不会持有外部类的引用,所以不会导致外部类实例的内存泄露。当你需要在静态内部类中调用外部的Activity时,我们可以使用弱引用来处理。

假如把耗时操作直接放在子线程中用Handler处理呢?这样也是不行的,如果在Handler中设置了延时操作,则调用线程也会堵塞。每个Handler对象都会绑定一个Looper对象,每个Looper对象对应一个消息队列(MessageQueue)。如果在创建Handler时不指定与其绑定的Looper对象,系统默认会将当前线程的Looper绑定到该Handler上。 在主线程中,可以直接使用new Handler()创建Handler对象,其将自动与主线程的Looper对象绑定;在非主线程中直接这样创建Handler则会报错,因为Android系统默认情况下非主线程中没有开启Looper,而Handler对象必须绑定Looper对象。 所以修改后的代码如下:

    @OnClick(R.id.inputData)
    public void inputData() {
        ...
        for (int i = 0; i < num; i++) {
            ...
            logs.add(log);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Looper.prepare();
                    new Handler(Looper.getMainLooper()).post(new Runnable() {
                        @Override
                        public void run() {
                            for (MLog log: logs){
                                MLogStoreMgr.getInstance(MonitorDBActivity.this).add(log);
                            }
                            Toast.makeText(MonitorDBActivity.this,"写入数据",Toast.LENGTH_SHORT).show();
                        }
                    });
                    Looper.loop();
                }
            }).start();
        }
        Toast.makeText(this,"开始发送消息",Toast.LENGTH_SHORT).show();

后续:这种问题算是出现的比较多的问题,一定要注意两点:

  • 尽量不在主线程里做耗时操作;
  • 不用忽视Android Lint的提示,Handler的内存泄漏用静态代码检查是可以查出来的。