LeakCanary直面项目中的内存泄露

前言
LeakCanary一个直白的展示Android中内存泄露的工具。它是Square公司开源出来的内存泄露自动探测神器,能够在程序发生内存泄漏的时候在通知栏提示通知,而且学习成本巨低。通过学习本文,了解和如何使用LeakCanary工具,同时了解和解决实际开发中出现的经常遇到的内存泄露案例。

更多详细介绍请参见Github地址:https://github.com/square/leakcanary

好了,在学习如何使用LeakCanary之前,我们先对内存泄露与内存溢出做出概念性的理解。原因是大部分人对这两个的区别总是朦朦胧胧分不清楚。

▲概念要点(什么是内存泄露,内存溢出)

  • 内存泄露(Memory Leak)指你用malloc或new申请了一块内存,但是没有通过free或delete将内存释放,导致这块内存一直处于占用状态内存泄露和硬件没有关系,它是由软件设计缺陷引起的。
  • 内存溢出(Memory Overflow)指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory。比如你申请了10个字节的空间,但是你在这个空间写入11或以上字节的数据,就是溢出

▲内存泄露、溢出的异同(两者之间的区别)
相同点:都会导致应用程序运行出现问题,性能下降或挂起。

不同点:

  1. 内存泄露是导致内存溢出的原因之一;内存泄露积累起来将导致内存溢出。
    2)内存泄露可以通过完善代码来避免;内存溢出可以通过调整配置来减少发生频率,但无法彻底避免。

▲Android中会造成内存泄露的情景无外乎两种

  • 全局进程(process-global)的static变量。这个无视应用的状态,持有Activity的强引用的怪物。
  • 活在Activity生命周期之外的线程。没有清空对Activity的强引用。

了解了内存溢出与内存泄露之后,我们接下来看看如何使用LeakCanary工具减少我们项目中的内存泄露的问题。

▲Android Studio集成LeakCanary
在app的build文件加上:

dependencies {
    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'
}

新建MyApplication 中初始化,同时别忘了在AndroidManifest中配置Application标签中的name

public class MyApplication 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;
        }
        enabledStrictMode();
        LeakCanary.install(this);
    }

    private void enabledStrictMode() {
        if (SDK_INT >= GINGERBREAD) {
            StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() //
                    .detectAll() //
                    .penaltyLog() //
                    .penaltyDeath() //
                    .build());
        }
    }
}

LeakCanary集成完成。接下来通过介绍三个经常会在项目中写错的内存泄露实例很大众的实例!!!,介绍和使用如何LeakCanary。同时给出**解决方法!!! **没错,不是五个,不是六个,只有三个。三个不多,但相比百度上搜索常见的内存泄露,写出了5个同时给出文!字!描!述!的解决方法,却不给demo,那才是在耍流氓。但在给出三种解决方法之前,常见的内存泄露情况,我们还是有必要过目一下。

▲常见的内存泄露

  • 持有Context引用造成的泄漏
  • 线程之间通过Handler通信引起的内存泄漏
  • 将变量的作用域设置为最小
  • 构造Adapter时,没有使用缓存的convertView
  • 资源对象没关闭造成的内存泄露(Cursor、IO 流)
  • 各种注册没取消
  • 集合容器对象没清理造成的内存泄露
  • static关键字的滥用
  • WebView对象没有销毁
  • GridView的滥用
  • Handler的使用
  • 线程的使用
  • Bitmap的回收和置空(对象内存过大)
    (如有纰漏,还望指正)

▲接下来是很大众的内存泄露实例与解决方法
1 . 单例模式造成的内存泄露

//X错误的示范
public class InsUtil {
    private static InsUtil instance;
    private Context mContext;

    private InsUtil(Context context) {
        this.mContext = context;
    }
}

相信很多人看到上述单例的代码,都会感到内心有一股泥石流,是的,没错,因为自己也是这么写的。而上述造成内存泄露的原因是传入Activity的Context,当前Activity退出时它的内存并不会被回收,因为单例对象持有该Activity的引用。Context的引用超过了本身的生命周期,所以不会被回收。正确的写法是使用Application的Context,使得这个Context的生命周期跟Application一样长

//√正确的示范
public class InsUtil {
    private static InsUtil instance;
    private Context mContext;

    private InsUtil(Context context) {
        this.mContext = context.getApplicationContext();
    }

    public static InsUtil getInsUtil(Context context) {
        if (instance == null) {
            instance = new InsUtil(context);
        }
        return instance;
    }
}

2 . handler造成的内存泄露

//X错误的示范
public class HandlerActivity extends Activity {
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // do something you want
        }
    };

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

        mHandler.sendMessageDelayed(Message.obtain(), 10000);
        //just finish this activity
        finish();
    }
}

从上述的HandlerActivity 可以看出,在finish()的时候,该Message还没有被处理,Message持有Handler, Handler持有Activity,这样阻止了GC对Acivity的回收,就发生了内存泄露。正确的写法应该是使用显形的引用,静态内部类与 外部类。使用弱引用WeakReference。 最后在Activity调用onDestroy()的时候要取消掉该Handler对象的Message和Runnable

//√正确的示范
public class HandlerActivity extends Activity {
   private static class MyHandler extends Handler {
        private final  WeakReference<HandlerActivity> mActivityReference;

        public MyHandler(HandlerActivity activity) {
            mActivityReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            HandlerActivity handlerAct = mActivityReference.get();
            if (handlerAct == null) {
                return;
            }
            // Do something  you want
        }
    }

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

        mHandler=new MyHandler(HandlerActivity.this);
        //just finish this activity
        finish();
    }

    @Override
    protected void onDestroy() {
        //  Remove all Runnable and Message.
        mHandler.removeCallbacksAndMessages(null);
        super.onDestroy();
    }
}

3 . Thread造成的内存泄露

//X错误的示范
public class ThreadActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_thread);
        //两种常见线程写法造成的内存泄露
        new MyAsyncTask().execute();
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                SystemClock.sleep(10000);
            }
        }).start();
    }

    private class MyAsyncTask extends AsyncTask<Void,Void,Void>{

        @Override
        protected Void doInBackground(Void... params) {
            SystemClock.sleep(10000);
            return null;
        }
    }
}

从上述ThreadActivity可以看出 以上的异步任务和Runnable都是一个匿名内部类,因此它们对当前Activity都有一个隐式引用。 如果Activity在销毁之前,任务还未完成, 那么将导致Activity的内存资源无法回收,造成内存泄漏。 正确的做法还是使用静态内部类的方式 最后在Activity销毁的时候,相对应的取消异步任务

//√正确的示范
public class ThreadActivity extends Activity {

    private MyAsyncTask myAsyncTask;

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

        myAsyncTask = new MyAsyncTask(ThreadActivity.this);
        myAsyncTask.execute();
        new Thread(new MyRunnable()).start();
    }

    private static class MyAsyncTask extends AsyncTask<Void, Void, Void> {
        private WeakReference<Context> weakReference;

        public MyAsyncTask(Context context) {
            weakReference = new WeakReference<>(context);
        }

        //doInBackground方法内部执行后台任务,不可在此方法内修改UI
        @Override
        protected Void doInBackground(Void... params) {
            SystemClock.sleep(10000);
            return null;
        }

        //onPostExecute方法用于在执行完后台任务后更新UI,显示结果
        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);
            MainActivity activity = (MainActivity) weakReference.get();
            if (activity != null) {
                //...
            }
        }

        //onCancelled方法用于在取消执行中的任务时更改UI
        @Override
        protected void onCancelled() {
            super.onCancelled();
        }
    }

    static class MyRunnable implements Runnable {
        @Override
        public void run() {
            SystemClock.sleep(10000);
        }
    }

    @Override
    protected void onDestroy() {
        //判断异步任务是否存在
        if (myAsyncTask != null && myAsyncTask.getStatus() == AsyncTask.Status.RUNNING) {
            myAsyncTask.cancel(true);
        }
        super.onDestroy();
    }
}

最后下面祭出本案例使用LeakCanary会出现的效果图。


Github下载地址:https://github.com/ChenYXin/LeakCanary_Demo

关于我

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容