安卓專案實戰之記憶體洩漏檢測神器LeakCanary
為什麼會產生記憶體洩漏?
Java記憶體洩漏指的是程序中某些物件(垃圾物件)已經沒有使用價值了,但有另外一個正在使用的物件持有它的引用,從而導致它不能回收停留在堆記憶體中,這就產生了記憶體洩漏。無用的物件佔據著記憶體空間,使得實際可使用記憶體變小,形象地說法就是記憶體洩漏了。
記憶體洩露對程式產生的影響?
記憶體洩漏是造成應用程式OOM的主要原因之一。Android系統為每個應用程式分配有限的記憶體,當應用中記憶體洩漏較多時,就難免會導致應用所需要的記憶體超出系統分配限額,從而導致OOM應用Crash;
Android常見的記憶體洩露
1,單例造成:由於單例靜態特性使得單例的生命週期和應用的生命週期一樣長,如果一個物件(如Context)已經不使用了,而單例物件還持有物件的引用造成這個物件不能正常被回收;
2,非靜態內部類建立靜態例項造成
3,Handler造成:子執行緒執行網路任務,使用Handler處理子執行緒傳送訊息。由於handler物件是非靜態匿名內部類的物件,持有外部類(Activity)的引用。在Handler-Message中Looper執行緒不斷輪詢處理訊息,當Activity退出還有未處理或者正在處理的訊息時,訊息佇列中的訊息持有handler物件引用,handler又持有Activity,導致Activity的記憶體和資源不能及時回收;
4,執行緒造成
5,資源未關閉造成的記憶體洩露:對於使用了BroadcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或者登出,否則這些資源將不會被回收,造成記憶體洩露;
記憶體洩漏與記憶體溢位的區別是什麼?
記憶體洩漏: 垃圾物件依舊佔據記憶體,如水龍頭的洩漏,水本來是屬於水源的, 但是水龍頭沒關緊,那麼洩漏到了水池;再來看記憶體,記憶體本來應 該被回收,但是依舊在記憶體堆中;總結一下就是記憶體存在於不該存在的地方(沒用的地方)
記憶體溢位: 記憶體佔用達到最大值,當需要分配記憶體時,已經沒有記憶體可以分配了,就是溢位;依舊以水池為例, 水池的水如果滿了,那麼如果繼 續需要從水龍頭流水的話,水就會溢位。總結一下就是,記憶體的分配超出最大閥值,導致了一種異常
明白了兩者的概念,那麼兩者有什麼關係呢?
記憶體的溢位是記憶體分配達到了最大值,而記憶體洩漏是無用記憶體充斥了記憶體堆;因此記憶體洩漏是導致記憶體溢位的元凶之一,而且是很大的元凶;因為記憶體分配完後,哪怕佔用再大,也會回收,而洩漏的記憶體則不然;當清理掉無用記憶體後,記憶體溢位的閥值也會相應降低。
LeakCanary簡介
LeakCanary是Square公司開源的一個檢測記憶體的洩露的函式庫,可以方便地和你的專案進行整合,在Debug版本中監控Activity、Fragment等的記憶體洩露;
LeakCanary整合到專案中之後,在檢測到記憶體洩露時,會發送訊息到系統通知欄。點選後開啟名稱DisplayLeakActivity的頁面,並顯示洩露的跟蹤資訊,Logcat上面也會有對應的日誌輸出。同時如果跟蹤資訊不足以定位時,DisplayLeakActivity還為開發者預設儲存了最近7個dump檔案到App的目錄中,可以使用MAT等工具對dump檔案進行進一步的分析;
如何使用leakcanary檢測記憶體洩露
1,在android studio的build.gradle中引用leakcanary
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.4'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
2,自定義Application,並在onCreate方法中執行以下程式碼:
public class QAplication extends Application{
@Override
public void onCreate() {
super.onCreate();
... ...
//初始化LeakCanary
if (LeakCanary.isInAnalyzerProcess(this)) {
return;
}
LeakCanary.install(this);
}
}
3,AndroidManifest.xml中新增許可權
<!--SDCard中建立與刪除檔案許可權-->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
<!--向SDCard寫入資料許可權-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
已經結束了!通過以上配置,你就可以輕鬆使用LeakCanary檢測記憶體洩漏了,當然這種簡單的配置僅限於在Activity中進行檢測,當然還存在其他類的記憶體洩漏,如Fragment等,這時我們就需要使用RefWatcher來進行監控了,具體見下面在非Activity的其它類中使用RefWatcher來進行記憶體洩漏的監控的講解。
單例記憶體洩露模擬
1.單例如下:
public class TestManager {
//單例靜態特性使得單例的生命週期和應用的生命週期一樣長
private static TestManager instance;
private Context context;
/**
* 傳入的Context的生命週期很重要:
* 如果傳入的是Application的Context,則生命週期和單例生命週期一樣長;
* 如果傳入的是Activity的Context,由於該Context和Activity的生命週期一樣長,當Activity退出的時候它的記憶體不會被回收,因為單例物件持有它的引用;
*/
private TestManager(Context context) {
this.context = context;
}
public static TestManager getInstance(Context context) {
if (instance == null) {
instance = new TestManager(context);
}
return instance;
}
}
2,在activity中建立該單例物件:
public class LeakCanaryActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leakcanary);
//獲取單例物件,退出Activity即可模擬出記憶體洩露
TestManager testManager = TestManager.getInstance(this);
}
}
執行App到LeakCanaryActivit頁面並退出,在檢測到記憶體洩露的時候,會發送訊息到系統通知欄;
點選通知訊息,開啟名為DisplayLeakActivity的頁面,並顯示洩漏的跟蹤資訊;
在非Activity的其它類中使用RefWatcher來進行記憶體洩漏的監控
第二節的例子程式碼只能夠檢測Activity的記憶體洩漏,當然還存在其他類的記憶體洩漏,這時我們就需要使用RefWatcher來進行監控。改寫Application,如下所示。
public class LeakApplication extends Application {
private RefWatcher refWatcher;
@Override
public void onCreate() {
super.onCreate();
refWatcher= setupLeakCanary();
}
private RefWatcher setupLeakCanary() {
if (LeakCanary.isInAnalyzerProcess(this)) {
return RefWatcher.DISABLED;
}
return LeakCanary.install(this);
}
public static RefWatcher getRefWatcher(Context context) {
LeakApplication leakApplication = (LeakApplication) context.getApplicationContext();
return leakApplication.refWatcher;
}
}
install方法會返回RefWatcher用來監控物件,LeakApplication中還要提供getRefWatcher靜態方法來返回全域性RefWatcher。
最後為了舉例,我們在一段存在記憶體洩漏的程式碼中引入LeakCanary監控,如下所示。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LeakThread leakThread = new LeakThread();
leakThread.start();
}
class LeakThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(6 * 60 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = LeakApplication.getRefWatcher(this); //1
refWatcher.watch(this);
}
}
MainActivity存在記憶體洩漏,原因就是非靜態內部類LeakThread持有外部類MainActivity的引用,LeakThread中做了耗時操作,導致MainActivity無法被釋放。
在註釋1處得到RefWatcher,並呼叫它的watch方法,watch方法的引數就是要監控的物件。當然,在這個例子中onDestroy方法是多餘的,因為LeakCanary在呼叫install方法時會啟動一個ActivityRefWatcher類,它用於自動監控Activity執行onDestroy方法之後是否發生記憶體洩露。這裡只是為了方便舉例,如果想要監控Fragment,在Fragment中新增如上的onDestroy方法是有用的。
執行程式,這時會在介面生成一個名為Leaks的應用圖示。接下來不斷的切換橫豎屏,這時會閃出一個提示框,提示內容為:“Dumping memory app will freeze.Brrrr.”。再稍等片刻,記憶體洩漏資訊就會通過Notification展示出來,比如三星S8的通知欄如下所示。
Notification中提示了MainActivity發生了記憶體洩漏, 洩漏的記憶體為787B。點選Notification就可以進入記憶體洩漏詳細頁,除此之外也可以通過Leaks應用的列表介面進入,列表介面如下圖所示。
記憶體洩漏詳細頁如下圖所示。
點選加號就可以檢視具體類所在的包名稱。整個詳情就是一個引用鏈:MainActiviy的內部類LeakThread引用了LeakThread的this$0,this$0的含義就是內部類自動保留的一個指向所在外部類的引用,而這個外部類就是詳情最後一行所給出的MainActiviy的例項,這將會導致MainActivity無法被GC,從而產生記憶體洩漏。