使用LeakCanary檢測(cè)代碼的內(nèi)層泄漏
首先我們看下面的代碼
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);
//發(fā)送消息
mHandler.sendEmptyMessageDelayed(0, 20000);
}
}).start();}
- 開(kāi)啟界面后, 立即關(guān)閉舀奶,等待一段時(shí)間后暑竟,出現(xiàn)泄漏,檢查L(zhǎng)eakCanary,獲取以下的結(jié)果:
- 首先在我們的安卓程序中引入
LeakCanary
:- 在對(duì)應(yīng)安卓模塊的
build.gradle
文件中導(dǎo)入以下的語(yǔ)句引入相應(yīng)的庫(kù)伪节,并保證leak
檢測(cè)只在代碼debug
模式下可用光羞,上線后失效
- 在對(duì)應(yīng)安卓模塊的
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'
- 創(chuàng)建一個(gè)
MyApp
類繼承Application
,在onCreate()
方法中安裝LeakCanary
绩鸣,不要忘了在清單文件中注冊(cè)MYApp
具體操作如下:
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
LeakCanary.install(this);
}
}
- MainActivity泄漏流程分析
- 觸發(fā)按鈕點(diǎn)擊時(shí)間后怀大,
loadData()
開(kāi)始執(zhí)行,loadData()
方法中開(kāi)啟了一個(gè)子線程呀闻,創(chuàng)建了一個(gè)匿名的線程對(duì)象化借。當(dāng)我們?cè)谠搶?duì)象的run
方法沒(méi)有執(zhí)行完之前就關(guān)閉了界面(MainActivity
),因?yàn)榫€程對(duì)象是一個(gè)內(nèi)部類對(duì)象,默認(rèn)持有外部類(MainActivity
)對(duì)象的引用捡多,從而導(dǎo)致MainActivity
關(guān)閉后無(wú)法被gc
回收從而造成泄漏 - 解決方法
我們自建一個(gè)內(nèi)部靜態(tài)類繼承Thread
蓖康,靜態(tài)內(nèi)部類不持有外部類的引用铐炫,從而可以避免以上問(wèn)題,代碼如下
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);
//發(fā)送消息
if(null != weak && null != weak.get()) {
weak.get().mHandler.sendEmptyMessageDelayed(0, 20000);
}
}
}
loadData()
中修改如下
new MyThread(this).start();
開(kāi)啟界面后蒜焊, 立即關(guān)閉倒信,等待一段時(shí)間后,又出現(xiàn)泄漏泳梆,檢查L(zhǎng)eakCanary,獲取以下的結(jié)果:
究其原因是和上述線程是一樣的鳖悠,只不過(guò)這次泄漏的是Handler對(duì)象。
所以优妙,我們?cè)俣x一個(gè)Handler的靜態(tài)內(nèi)部類乘综,代碼如下:
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");
}
}
}
}
再次運(yùn)行程序?qū)⒉粫?huì)產(chǎn)生泄漏問(wèn)題
- 進(jìn)一步優(yōu)化
- 但界面不可見(jiàn)時(shí), 我們最好把消息隊(duì)列中的
message
清空套硼,代碼如下:
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
- 當(dāng)界面關(guān)閉時(shí)卡辰,我們的子線程還在運(yùn)行,可以通過(guò)觀察
LogCat
打印日志看出邪意,實(shí)際上九妈,我們?cè)陉P(guān)閉主線程是同時(shí)關(guān)閉子線程,可以如下操作:- 定義一個(gè)全局
boolbean
型變量來(lái)控制MyThread
的開(kāi)關(guān)抄罕,在MyThread
提供一個(gè)關(guān)閉線程的方法close()
, 當(dāng)界面關(guān)閉時(shí)允蚣,調(diào)用該方法mt.close()
, 代碼如下:
定義的全局變量
- 定義一個(gè)全局
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()調(diào)用
mt.close();
- 還想提及的內(nèi)容
- 我們?cè)趦蓚€(gè)自定義的內(nèi)部類中都有這樣的代碼段
private WeakReference<MainActivity> weak;
其作用是為了然我們的靜態(tài)內(nèi)部類可以調(diào)用外部類的非靜態(tài)的字段和方法,從而只有一個(gè)外部類對(duì)象的引用呆贿,但這樣做就又回到導(dǎo)致我們的代碼泄漏的最初的原因嚷兔,怎么辦呢,于是弱引用橫空出世了做入。弱引用的特點(diǎn)是一旦被gc
掃描到就會(huì)被立即回收冒晰,而不管是否被引用,這也是為什么每次我們使用時(shí)都要判斷其是否為null
的原因竟块。與之對(duì)應(yīng)的還有軟引用(SoftReference
), 強(qiáng)引用壶运, 虛引用, 相關(guān)的詳細(xì)說(shuō)明大家自行搜索啊浪秘。
- 最后蒋情, 這里就泄漏的問(wèn)題就舉了一個(gè)例子, 大家想要了解更多可以參考這篇博文:
http://www.reibang.com/p/4a45f3ecc288
使用BlockCanary優(yōu)化代碼的結(jié)構(gòu)
- 當(dāng)我們完成我們的app后發(fā)現(xiàn)使用起來(lái)卡頓特別嚴(yán)重耸携,于是需要對(duì)代碼進(jìn)行優(yōu)化棵癣,可是面對(duì)動(dòng)輒幾千行、幾萬(wàn)行的代碼夺衍,讓人無(wú)法下手狈谊,于是BlockCanary出現(xiàn)了。接下來(lái),我為大家演示BlockCanary的用法
- 第一步河劝, 獲取對(duì)應(yīng)的庫(kù)
在相應(yīng)的Module的build.gradle中導(dǎo)入如下的語(yǔ)句引入對(duì)應(yīng)的庫(kù)
compile 'com.github.moduth:blockcanary-android:1.2.1'
// 僅在debug包啟用BlockCanary進(jìn)行卡頓監(jiān)控和提示的話壁榕,可以這么用
debugCompile 'com.github.moduth:blockcanary-android:1.2.1'
releaseCompile 'com.github.moduth:blockcanary-no-op:1.2.1'
- 新建一個(gè)類繼承
BlockContextCanary
實(shí)現(xiàn)各種上下文,像是卡慢報(bào)告閾值赎瞎,log的保存位置牌里,網(wǎng)絡(luò)類型等
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
中開(kāi)啟檢測(cè), 不要忘了在manifest
清單文件中注冊(cè)MyApp
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
BlockCanary.install(this, new AppBlockCanaryContext()).start();
}
}
- 以
LeakDemo
為例我們來(lái)檢測(cè)代碼的卡頓情況,結(jié)果如下:
結(jié)果顯示第34行有卡頓情況务甥,我們找到這一行:
我們還可以查看更詳細(xì)的信息
獲取到卡頓的代碼位置二庵,我們就可以著手修改代碼和重構(gòu)了
- 最后附上
- LeakCanary源碼:
https://github.com/square/leakcanary - BlockCanary源碼
https://github.com/markzhai/AndroidPerformanceMonitor - 筆者的源碼
https://github.com/CwugsChen18/leakdemo
- 后話
筆者花了一晚上終于完成了自己的第一篇博文,希望大家多多支持缓呛,筆者還會(huì)繼續(xù)努力催享,為大家奉上更多有趣,有用的文章的哟绊,下次再見(jiàn)咯R蛎睢!