安卓使用LeakCanary检测代码内存泄漏和BlockCanary优化代码结构

使用LeakCanary检测代码的内层泄漏

首先我们看下面的代码
public class MainActivity extends AppCompatActivity {    
     private Button btn_load;   
     private Handler mHandler = new Handler() {        
            @Override        
            public void handleMessage(Message msg) {    
                   if(msg.what == 0) {                                                    
                      Log.i("handleMessage", "got datas");    
                   }  
             }  
      };    
      @Override    
      protected void onCreate(Bundle savedInstanceState) {                               
                super.onCreate(savedInstanceState); 
                setContentView(R.layout.activity_main);        
                btn_load = (Button)findViewById(R.id.btn_load);
                btn_load.setOnClickListener(new View.OnClickListener() {    
                         Override    
                         public void onClick(View v) { 
                                Log.i("btn_load", "loading datas"); 
                                loadData(); 
                         }
                });
       private void loadData() {    
               new Thread(new Runnable() {        
                   @Override        
                  public void run() {            
                         //do sonething            
                         SystemClock.sleep(10000);            
                        //发送消息            
                       mHandler.sendEmptyMessageDelayed(0, 20000);      
                  }    
              }).start();}
  • 开启界面后, 立即关闭,等待一段时间后,出现泄漏,检查LeakCanary,获取以下的结果:
MainActivity泄漏流程
  • 首先在我们的安卓程序中引入LeakCanary:
    • 在对应安卓模块的build.gradle文件中导入以下的语句引入相应的库,并保证leak检测只在代码debug模式下可用,上线后失效
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta2'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
  • 创建一个MyApp 类继承Application,在onCreate()方法中安装LeakCanary,不要忘了在清单文件中注册MYApp
    具体操作如下:
public class MyApp extends Application {   
              @Override   
              public void onCreate() {        
                       super.onCreate();      
                       LeakCanary.install(this);    
              }
}
  • MainActivity泄漏流程分析
  • 触发按钮点击时间后, loadData()开始执行,loadData()方法中开启了一个子线程,创建了一个匿名的线程对象。当我们在该对象的run方法没有执行完之前就关闭了界面(MainActivity),因为线程对象是一个内部类对象,默认持有外部类(MainActivity)对象的引用,从而导致MainActivity关闭后无法被gc回收从而造成泄漏
  • 解决方法
    我们自建一个内部静态类继承Thread,静态内部类不持有外部类的引用,从而可以避免以上问题,代码如下
private static class  MyThread extends  Thread {    
          private WeakReference<MainActivity> weak;        
          public MyThread(MainActivity activity) {        
                 weak = new WeakReference<MainActivity>(activity);   
          }    
          @Override    
          public void run() {        
                 //do sonething        
                 SystemClock.sleep(100);        
                //发送消息       
                if(null != weak && null != weak.get()) { 
                    weak.get().mHandler.sendEmptyMessageDelayed(0, 20000);       
                }
          }                
} 

loadData()中修改如下

new MyThread(this).start();

开启界面后, 立即关闭,等待一段时间后,又出现泄漏,检查LeakCanary,获取以下的结果:


MainActivity泄漏流程

究其原因是和上述线程是一样的,只不过这次泄漏的是Handler对象。
所以,我们再定义一个Handler的静态内部类,代码如下:

private static class  MyHandler extends Handler {    
          private WeakReference<MainActivity> weak;    
          public MyHandler(MainActivity activity) {        
                 weak = new WeakReference<MainActivity>(activity);   
          }    
          @Override    
          public void handleMessage(Message msg) {       
                 if(msg.what == 0) {            
                    Log.i("handleMessage", "got datas");            
                   if(null != weak && null != weak.get()) {    
                        weak.get().textView.setText("goodbye world");           
                   }       
                 }  
          }
}

再次运行程序将不会产生泄漏问题

  • 进一步优化
  • 但界面不可见时, 我们最好把消息队列中的message清空,代码如下:
@Override
protected void onDestroy() {    
         super.onDestroy();    
         mHandler.removeCallbacksAndMessages(null);
}
  • 当界面关闭时,我们的子线程还在运行,可以通过观察LogCat打印日志看出,实际上,我们在关闭主线程是同时关闭子线程,可以如下操作:
    • 定义一个全局boolbean型变量来控制MyThread的开关,在MyThread提供一个关闭线程的方法close(), 当界面关闭时,调用该方法mt.close(), 代码如下:
      定义的全局变量
private MyThread mt;
private boolean isClose;

提供的方法

public void close() {    
       if(null != weak && null != weak.get()) {      
              weak.get().isClose = true;    
       }
}

修改run() 的逻辑

if(null != weak && null != weak.get()) {  
      if(weak.get().isClose) {        
          //直接返回      
           return;   
       }
}

onDestroy()调用

mt.close();
  • 还想提及的内容
    • 我们在两个自定义的内部类中都有这样的代码段
 private WeakReference<MainActivity> weak; 

其作用是为了然我们的静态内部类可以调用外部类的非静态的字段和方法,从而只有一个外部类对象的引用,但这样做就又回到导致我们的代码泄漏的最初的原因,怎么办呢,于是弱引用横空出世了。弱引用的特点是一旦被gc扫描到就会被立即回收,而不管是否被引用,这也是为什么每次我们使用时都要判断其是否为null的原因。与之对应的还有软引用(SoftReference), 强引用, 虚引用, 相关的详细说明大家自行搜索啊。


使用BlockCanary优化代码的结构

  • 当我们完成我们的app后发现使用起来卡顿特别严重,于是需要对代码进行优化,可是面对动辄几千行、几万行的代码,让人无法下手,于是BlockCanary出现了。接下来,我为大家演示BlockCanary的用法
  • 第一步, 获取对应的库
    在相应的Module的build.gradle中导入如下的语句引入对应的库
compile 'com.github.moduth:blockcanary-android:1.2.1'
// 仅在debug包启用BlockCanary进行卡顿监控和提示的话,可以这么用 
debugCompile 'com.github.moduth:blockcanary-android:1.2.1' 
   releaseCompile 'com.github.moduth:blockcanary-no-op:1.2.1'
  • 新建一个类继承BlockContextCanary
    实现各种上下文,像是卡慢报告阈值,log的保存位置,网络类型等
public class AppBlockCanaryContext extends BlockCanaryContext {    
          // override to provide context like app qualifier, uid,     network type, block threshold, log save path    
          // this is default block threshold, you can set it by phone's performance    
          @Override    
          public int getConfigBlockThreshold() {       
                return 500;   
          }    
          // if set true, notification will be shown, else only write log file 
          @Override   
          public boolean isNeedDisplay() {       
             return BuildConfig.DEBUG;  
          }   
         // path to save log file (在SD卡目录下)   
        @Override   
         public String getLogPath() {     
             return "/blockcanary/performance";   
         }
}
  • MyApp中开启检测, 不要忘了在manifest清单文件中注册MyApp
public class MyApp extends Application {    
      @Override    
      public void onCreate() {        
           super.onCreate();       
           BlockCanary.install(this, new AppBlockCanaryContext()).start();  
      }
}  
  • LeakDemo为例我们来检测代码的卡顿情况,结果如下:
卡顿检测结果图

结果显示第34行有卡顿情况,我们找到这一行:


卡顿的地方

我们还可以查看更详细的信息

卡顿的详细信息

获取到卡顿的代码位置,我们就可以着手修改代码和重构了



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

推荐阅读更多精彩内容