在分析Android內存泄漏之前,先了解一下JAVA的一些知識
1. JAVA中的對象的創建
- 使用new指令生成對象時,堆內存將會為此開辟一份空間存放該對象
垃圾回收器回收非存活的對象,並釋放對應的內存空間
2.Java如何判定對象存活或死亡?
-
引用計數法
1給對象中添加一個引用計數,假如為count
2當引用這個對象時:count++
3當count==0時:對象處於,也就是說沒有其它地方在引用這個對象了,對象就處於“死亡”狀態,回收對象 -
可達性分析算法
舉個例子:像找人一樣,A認識B,B認識C,C認識D,那么A就要吧通過這樣的關系認識D,如果能找到D,說明D對象是存活的,不能回收,如果通過所有的關系都找不到D,說明D是“死亡”的,回收D對象。
可達性分析算法的定義:通過一系列的稱為 GC
Roots 的對象作為起點,從這些節點開始向下搜索,搜索把走過的路徑稱為引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連(就是從GC Roots 到這個對象不可達)時,則證明此對象是不可用的。如下圖,Object5,Object6,Object7就是不可達對象,是要被回收的對象
問:哪些對象可以作為GC Roots對象呢?
- 1虛擬機棧中引用的對象
- 2方法區中類靜態屬性引用的對象
- 3方法區中常量引用的對象
- 4本地方法棧中JNI引用的對象
3.引用分類
- 強引用:只要強引用還存在,垃圾回收器永遠不回收強引用的對象.如下
Object obj = new Object() //強引用
- 軟引用:在內存溢出異常之前,回收對象
String str=new String("123"); // 強引用
SoftReference softRef=new SoftReference(str); // 軟引用
- 弱引用 : 在下一次 GC 時,無論當前內存是否足夠,都會回收被引用的對象
String str=new String("abc");
WeakReference abcWeakRef = new WeakReference(str);
str=null;
- 虛引用:虛引用 : 沒用,形同虛設,唯一的用處是在對象回收時,會收到一個系統通知
注:JAVA中這4種引用的級別由高到低依次為: 強引用 > 軟引用 > 弱引用 > 虛引用
** 4.JAVA中內存分配 **
- 靜態儲存區:編譯時就分配好,在程序整個運行期間都存在。它主要存放靜態數據和常量
- 棧區:當方法執行時,會在棧區內存中創建方法體內部的局部變量,方法結束后自動釋放內存
- 堆區:通常存放 new 出來的對象。由 Java 垃圾回收器回收。
上面的是JAVA的一些預備知識,下面分析Android內存泄漏相關
** 1 內存泄漏與內存溢出**
- 內存泄漏:Memory Leak , 無用的對象應該被回收的沒有被回收
- 內存溢出:常說的OOM,沒有足夠的內存供分配了
** 2 Android內存泄漏分類 **
-
長期持有(Activity)Context導致的
(1) 單例類持有Activity引用
(2) 長生命周期引用短生命周期 -
由非靜態內部類或者匿名內部類導致的
(1) Handler泄漏 -
資源使用完忘記釋放
(1) Cursor,InputStream/OutputStream 忘記調用close -
使用某些系統服務不當
(1) 在6.0系統,獲取ConnectivityManager服務,如果第一次使用的是Activity對應的Context去獲取這個服務,就會導致內存泄漏 -
延遲的任務也可能導致內存泄漏
(1) Handler 的消息未處理完,這時如果Handler是在Activity內存類實現的,消息引用Handler,Handler又引用了Activity,這時如果關閉Activity,就會造成內存泄漏 -
忘記注銷監聽器或者觀察者
(1) 比如 EventBus.unregister() 忘記調用
注:非靜態內部類和匿名內部類都會潛在的引用它們所屬的外部類,但是靜態內部類卻不會
** 3 Android內存泄漏分析工具 **
- MAT
- LeakCanary
- Strictmode
- Android Memory Monitors
推薦使用LeakCanary,•LeakCanary是一個檢測Java和Android內存泄漏的庫,集成LeakCanary之后,只需要等待內存泄漏出現就可以了無需認為進行主動檢測
** 4 LeakCanary的添加 **
- 第一步:在build.gradle中添加依賴
compile 'com.squareup.leakcanary:leakcanary-android:1.5.1' - 第二步:在Application的onCreate()方法中添加 LeakCanary.install(this);
完成以上兩步,就添加了LeakCanary,接下來就正常開發測試就行了,如果有內存泄漏,就會在通知欄中會有相應的通知,點開看就可以了,找到對應的內存泄漏的地方,解決
下面是演示的內存泄漏的幾張圖,可以看一下:
5 Android內存泄漏的案例
- 案例一:單例造成的內存泄漏
典型比如context的使用不當造成內存泄漏
public class ToastUtils {
private static String oldMsg;
protected static Toast toast = null;
private static long oneTime = 0;
private static long twoTime = 0;
private static long gapTime = 3 * 1000;//3s只顯示一次
public static void show(Context context, String s) {
if (context != null && !TextUtils.isEmpty(s)) {
if (toast == null) {
toast = Toast.makeText(context, s, Toast.LENGTH_SHORT);
toast.show();
oneTime = System.currentTimeMillis();
} else {
twoTime = System.currentTimeMillis();
if (s.equals(oldMsg)) {
if (twoTime - oneTime > gapTime) {
toast.show();
}
} else {
oldMsg = s;
toast.setText(s);
toast.show();
}
}
oneTime = twoTime;
}
}
}
在Activity 中使用:
ToastUtils.show(this, "登錄成功");
上面的代碼就會出現內存泄漏,因為在activity中使用ToastUtils.show(this, "登錄成功")的時候,傳的第一個參數 this 代表當時的activity,而ToashTuils中的toast變量是一個靜態變量,
代碼如下
protected static Toast toast = null;
創建toast對象如下代碼
toast = Toast.makeText(context, s, Toast.LENGTH_SHORT);
Toast.makeText的第一參數就是上面傳的activity,Toast類中有一個變量mContext會保存這個activity,就是強引用,但是toast又是一個靜態的變量,靜態變量的生命同期是和當前的APP的進程一樣長的,所以這時我們如果關閉這個Activity,就會導致Activity被靜態變量強引用,垃圾回收永遠不會回收這個Activity,所以就會出現內存泄漏。
我們看一下Toast.makeText的源碼
上面圖中,new一個Toast,把context傳給了Toast的構造方法。
所以調用 ToastUtils.show(this, "登錄成功");就會導致 activity 被靜態的toast變量強引用了,導致內存泄漏。
解決方法
用ApplicationContext替代Activity,如下代碼
public static void show(Context context, String s) {
//在這里獲取applicationContext,applicationContext的生命周期是和進程一樣長
//這樣就不會出現內存泄漏了
context = context.getApplicationContext();
if (context != null && !TextUtils.isEmpty(s)) {
if (toast == null) {
toast = Toast.makeText(context, s, Toast.LENGTH_SHORT);
toast.show();
......
}
}
- 案例二 內部類或者匿名內部類造成的內存泄漏
比如在Activity中使用Handler不當造成的內存泄漏
如下圖
上圖:在MainActivity中有一個匿名內部類Handler,並且有一個此類的對象 uiHandler。
這時我們如果在MainAcitity 中調用下面代碼,就會出現內存泄漏
uiHandle.sendMessageDelayed(uiHandle.obtainMessage(),60 * 1000);
uiHandler.obtainMessage獲取的msg 中有一個成員變量 target,target保存的就是uiHandle,而uiHandler又是內部類創建的對象,所以uiHandler隱式的會對當前的外部類,也就是MainActivity會有一個強引用,如下
msg -> uiHandler -> MainActivity
msg 引用了uiHandler,uiHandler引用了MainActivity,然后這個msg需要60s后才被處理完,在處理過程中,如果退出MainActivity,這時候就會導致內存泄漏,MainActivity回收不了。應該被回收的對象沒有被回收掉,就是內存泄漏。
注:handler機制不明白的可以先看下handler機制,message,handler,loop的關系
解決方法
- 可以在MainActivity的onDestroy()方法調用下面代碼:
uiHandle.removeCallbacksAndMessages(null);
- Handler不要用內部類,用靜態的內部類,因為靜態的內部類不會引用外部類,需要外部類的地方,用弱引用,代碼如下:
使用弱引用的時候,需要作一下判斷是否為null。
-
案例三:Activity context的不正確使用
上面的兩個案例中其實也是context的使用場景不當造成的內存泄漏,這里不再舉例,我們通常使用的兩種context是 Acitivty和 Application,只需要注意對context的使用不要超過它的生命同期。部分情況下可以使用applicationContext代替activity的context,因為applicatoinContext會隨着應用程序的存在而存在,而不依賴於activity的生命周期。還有要慎重對context使用static關鍵字。 -
案例四:一些資源使用完后沒有關閉
如數據庫的游標 Cursor,輸入輸出流 InputStream/OutputStream沒有close -
案例五:注冊的監聽器沒有反注冊
如EventBus.register,ButterKnife等沒有在activity的onDestroy中反注冊或者其它地方反注冊 -
案例六:系統服務的泄漏
在實際項目中發現的,在6.0系統上在activity中第一次如果用的是activity對應的context獲取ConnectivityManager服務會造成內存泄漏。
代碼對下:
/**
* 判斷是否有網絡連接
* @param context
* @return
*/
public static boolean isNetworkConnected(Context context) {
if (context != null) {
ConnectivityManager cm = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo mNetworkInfo = cm.getActiveNetworkInfo();
if (mNetworkInfo != null) {
return mNetworkInfo.isAvailable();
}
}
return false;
}
如果是第一次在activity中調用如下代碼,會發現內存泄漏
- 注:是第一次在activity中調用,如果第一次是在application中調用不會出現內存泄漏,原因請參考下面這篇文章: http://www.jianshu.com/p/7d4b55f7ed9f
//注意,這個this 是代表的是當前的activity
isNetworkConnected(this)
簡單介紹下:先從Context的getSystemService方法開始,我們知道Activity是從ContextWrapper繼承而來的,ContextWrapper中持有一個mBase實例,這個實例指向一個ContextImpl對象,同時ContextImpl對象持有一個OuterContext對象,對於Activity來說,這個OuterContext就是Activity對象。所以調用getSystemService最終會調用到ContextImpl的getSystemService方法。
在6.0上,在6.0上,ConnectivityManager實現為單例,創建這個單例對象的時候,把相應的OuterContext就是Activity對象,保存到了ConnectivityManager中,就造成了一個單例對象強引用了activity對象,從而造成了內存泄漏,如果是第一次用的是application,則保存的不是activity而是application,反而不會出現內存泄漏了。
使用LeakCanary檢測 ConnectivityManager 內存泄漏圖如下:
解決方法
使用applicationContext去獲取服務,不要使用activityContext去獲取服務
上面的就是對Android內存泄漏的一些總結,如果有不正確的或者需要補充的地方,請指出,一塊學習進步