安卓使用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行有卡頓情況,我們找到這一行:


卡頓的地方

我們還可以查看更詳細的信息

卡頓的詳細信息

獲取到卡頓的代碼位置,我們就可以著手修改代碼和重構了



  • 后話
    筆者花了一晚上終于完成了自己的第一篇博文,希望大家多多支持,筆者還會繼續努力,為大家奉上更多有趣,有用的文章的,下次再見咯!!
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容