美文网首页梦想祭Android软件工程
使用Leak Canary检测应用的内存泄露

使用Leak Canary检测应用的内存泄露

作者: SpikeKing | 来源:发表于2016-01-25 15:41 被阅读13022次

    欢迎Follow我的GitHub, 关注我的简书. 其余参考Android目录.

    LeakCanary是检测App内存泄露的工具, 内存泄露是Android开发中常见的问题, 使用程序的稳定性下降.

    注意: 集成到低版本应用, 会报出Bug.
    Error:(2) Error retrieving parent for item: No resource found that matches the given name 'android:Theme.Material'.
    修改编译版本: compileSdkVersion 21 即可.

    本文示例的Github下载地址.

    内存泄露

    主要原因:
    生命周期较长的类使用Activity的Context, 导致Activity被引用, 无法被及时回收. 除了需要Activity页面支持的控件类, 如Dialog等, 其他全部使用应用的Context替换Activity的Context, 即Context.getApplicationContext(). 还有就是单例不要持有页面的控件, 单例持有控件, 控件附属页面, 最终页面得不到释放, 单例可以使用回调修改页面, 内部仅仅保留处理数据部分.

    LeakCanary可以检查出页面的泄露问题, 并提供具体位置.

    泄露

    表明: LeakSingle的静态单例sInstance引用LeakSingle的mContext, 从而导致MainActivity的instance泄露.

    内存泄露的原因已经理解了, 那么我来讲解一下如何检测应用吧.


    1. 依赖和引入

    build.gradle的依赖.

        debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1' // or 1.4-beta1
        releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // or 1.4-beta1
        testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // or 1.4-beta1
    

    引入应用

    public class DemoApplication extends Application {
        @Override public void onCreate() {
            super.onCreate();
            LeakCanary.install(this);
        }
    }
    

    配置非常简单, 会增加一个附属应用, 去掉Application的引用, 就可以移除LeakCanary.


    2. 泄露单例

    泄露单例, 引入页面的TextView, 强制保留的父Activity, 会导致内存泄露, 可以在onDestroy时, 解引用避免.

    /**
     * 泄露单例, 设计混乱, 单例只应该做事务性的工作, 页面操作应该使用回调.
     * 本示例仅做为反面示例, 切勿学习.
     * <p/>
     * Created by wangchenlong on 16/1/25.
     */
    public class LeakSingle {
        private Context mContext;
        private TextView mTextView;
    
        private static LeakSingle sInstance;
    
        private LeakSingle(Context context) {
            mContext = context;
        }
    
        public static LeakSingle getInstance(Context context) {
            if (sInstance == null) {
                sInstance = new LeakSingle(context);
            }
            return sInstance;
        }
    
        // 内存泄露
        public void setRetainedTextView(TextView tv) {
            mTextView = tv;
            mTextView.setText(mContext.getString(R.string.app_name));
        }
    
        // 删除引用, 防止泄露
        public void removeRetainedTextView() {
            mTextView = null;
        }
    }
    

    单例只应该做事务性的工作, 页面操作应该使用回调, 不是引入控件. 本示例仅做为反面示例, 切勿学习.

    3. 泄露内存

    调用单例, 两种引用都会导致内存泄露, 第一种是Context引用泄露, 第二种是子控件引用泄露. 避免方式是在onDestroy中, 清除引用.

    最优方式:
    在单例中只执行事务性工作, 不执行具体页面操作, 可以使用接口回调, 异步处理.

    public class MainActivity extends AppCompatActivity {
    
        @Bind(R.id.main_tv_text) TextView mTvText;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            ButterKnife.bind(this);
    
            /**
             * me.chunyu.spike.wcl_leakcanary_demo.MainActivity has leaked:
             * GC ROOT static me.chunyu.spike.wcl_leakcanary_demo.LeakSingle.sInstance
             * references me.chunyu.spike.wcl_leakcanary_demo.LeakSingle.mContext
             * leaks me.chunyu.spike.wcl_leakcanary_demo.MainActivity instance
             */
    //        LeakSingle.getInstance(this).setRetainedTextView(mTvText);
    
            /**
             * me.chunyu.spike.wcl_leakcanary_demo.MainActivity has leaked:
             * GC ROOT static me.chunyu.spike.wcl_leakcanary_demo.LeakSingle.sInstance
             * references me.chunyu.spike.wcl_leakcanary_demo.LeakSingle.mTextView
             * references android.support.v7.widget.AppCompatTextView.mContext
             * leaks me.chunyu.spike.wcl_leakcanary_demo.MainActivity instance
             */
            LeakSingle.getInstance(this.getApplication()).setRetainedTextView(mTvText);
        }
    
        @Override protected void onDestroy() {
            super.onDestroy();
            // 防止内泄露
            LeakSingle.getInstance(this.getApplication()).removeRetainedTextView();
        }
    }
    

    根据LeakCanary中的检测结果, 修改内存泄露的地方, 就可以完美的解决问题.


    4. 解析泄露位置

    新手程序员经常会问我, 如何理解LeakCanary的错误信息, 我来举个例子讲解一些.
    如图:

    泄露

    LeakCanary的内存泄露提示一般会包含三个部分:
    第一部分(LeakSingle类的sInstance变量)引用第二部分(LeakSingle类的mContext变量), 导致第三部分(MainActivity类的实例instance)泄露.

    应用最常见的泄露位置就是Activity的实例, 要手动或使用shell命令, 启动所有的Activity. LeakCanary判断时机是Activity启动到结束, 检测这一过程是否会发生内存泄露.

    启动Activity的shell命令是

    adb shell am start  -n [包名]/[Activity名]
    

    详细内容参考第六点.


    内存泄露的问题对于应用的用户体验至关重要, 感谢Square的产品, 让这件事变得如此简单.

    OK, that's all! Enjoy it!

    相关文章

      网友评论

      • 漪滟:请问lz,报这个泄露该怎么解决 UnsupportedOperationException: Could not find char array in java.lang.String@319207032 (0x1306b678)
        at com.squareup.leakcanary.HahaHelper.asString(HahaHelper.java:108)
        at com.squareup.leakcanary.HeapAnalyzer.findLeakingReference(HeapAnalyzer.java:161)
        at com.squareup.leakcanary.HeapAnalyzer.checkForLeak(HeapAnalyzer.java:115)
        at com.squareup.leakcanary.internal.HeapAnalyzerService.onHandleIntent(HeapAnalyzerService.java:58)
        at android.app.IntentService$ServiceHandler.handleMessage(IntentService.java:76)
        at android.os.Handler.dispatchMessage(Handler.java:109)

        at android.app.IntentService$ServiceHandler.handleMessage(IntentService.java:76)
        at android.os.Handler.dispatchMessage(Handler.java:109)
        at android.os.Looper.loop(Looper.java:166)
        at android.os.HandlerThread.run(HandlerThread.java:65)
      • 相互交流:LZ只要引入相应的依赖包,在项目中,有地方存在内存泄漏的话,都会跳出页面提示,哪里哪里存在内存泄漏的风险吗??
      • Aberstin:好东西
      • 任家辉:您好,请问能否将您的文章,用在梦想祭的一期杂志上,我们会
        任家辉:@CLWang 好的
        SpikeKing:@任家辉 OK, 到时候发个链接给我哈, 哈哈!
      • ningso::sweat:有的时候会遇到第三方sdkl里面发生泄漏,譬如说Umeng里面各种啊反馈啊升级啊。表示遇到这种改如何?我改成传applicationcontent进去也都会。无奈了
        SpikeKing:@ningso 仔细点避免内存泄露的几个点就好了, 其实挺简单的. 现在通过LeakCanary还能检查, 比我以前检测简单多了.
        ningso:@CLWang :persevere: 嗯,有的库我也是再封装一层。
        SpikeKing:@ningso 把Context换成ApplicationContext就行, 如果还泄露的话, 也没有办法. 不过UMeng是没有泄露的, 不要冤枉这个常用库, 哈哈, 我管理的几个项目也用到这个了, 这种工具类的写法一定要注意, 防止泄露可以封装一层, 如
        // 打点函数
        public static void event(Context context, String string) {
        MobclickAgent.onEvent(context.getApplicationContext(), string);
        }
        一般这种常用库都需要封装的, 一方面自己检测过的比较放心, 一方面也易于扩展.
      • e19752dfa551:这些东西我都懂,网上也有很多。但是就是看不懂内存泄漏之后显示的信息,网上也没有讲,你能不能也分享一下这个报错信息界面怎么看啊?
        SpikeKing:@r17171709 恩, 是的! 通过这个三个提示, 读读泄露位置的代码, 都会找到错误的位置, 非常简单.
        皮球二二:@CLWang 你的意思是找到act之后然后直接查该变量引用的地方,来解决泄露的问题吧
        SpikeKing:@程序猴 读的很仔细, 非常好的问题! 赞一个, 我司的初级程序员也经常问我这个问题. 这次统一写一下吧.
        LeakCanary的内存泄露提示一般会包含三个部分:
        第一部分(LeakSingle类的sInstance变量)引用第二部分(LeakSingle类的mContext变量), 导致第三部分(MainActivity类的实例instance)泄露.
        其余参考本文第四节-解析泄露位置吧. 还有什么问题也可以提出来.
      • 774796282c0c:好东西,必须顶
        SpikeKing:@774796282c0c 加油!

      本文标题:使用Leak Canary检测应用的内存泄露

      本文链接:https://www.haomeiwen.com/subject/yixdkttx.html