使用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
), 強引用, 虛引用, 相關的詳細說明大家自行搜索啊。
- 最后, 這里就泄漏的問題就舉了一個例子, 大家想要了解更多可以參考這篇博文:
http://www.lxweimin.com/p/4a45f3ecc288
使用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行有卡頓情況,我們找到這一行:
卡頓的地方
我們還可以查看更詳細的信息
卡頓的詳細信息
獲取到卡頓的代碼位置,我們就可以著手修改代碼和重構了
- 最后附上
- LeakCanary源碼:
https://github.com/square/leakcanary - BlockCanary源碼
https://github.com/markzhai/AndroidPerformanceMonitor - 筆者的源碼
https://github.com/CwugsChen18/leakdemo
- 后話
筆者花了一晚上終于完成了自己的第一篇博文,希望大家多多支持,筆者還會繼續努力,為大家奉上更多有趣,有用的文章的,下次再見咯!!