Android 內存泄漏總結
內存管理的目的就是讓咱們在開發中怎麼有效的避免咱們的應用出現內存泄漏的問題。內存泄漏你們都不陌生了,簡單粗俗的講,就是該被釋放的對象沒有釋放,一直被某個或某些實例所持有卻再也不被使用致使 GC 不能回收。最近本身閱讀了大量相關的文檔資料,打算作個 總結 沉澱下來跟你們一塊兒分享和學習,也給本身一個警示,之後 coding 時怎麼避免這些狀況,提升應用的體驗和質量。android
我會從 java 內存泄漏的基礎知識開始,並經過具體例子來講明 Android 引發內存泄漏的各類緣由,以及如何利用工具來分析應用內存泄漏,最後再作總結。nginx
篇幅有些長,你們能夠分幾節來看!git
Java 內存分配策略
Java 程序運行時的內存分配策略有三種,分別是靜態分配,棧式分配,和堆式分配,對應的,三種存儲策略使用的內存空間主要分別是靜態存儲區(也稱方法區)、棧區和堆區。程序員
靜態存儲區(方法區):主要存放靜態數據、全局 static 數據和常量。這塊內存在程序編譯時就已經分配好,而且在程序整個運行期間都存在。github
棧區 :當方法被執行時,方法體內的局部變量都在棧上建立,並在方法執行結束時這些局部變量所持有的內存將會自動被釋放。由於棧內存分配運算內置於處理器的指令集中,效率很高,可是分配的內存容量有限。算法
堆區 : 又稱動態內存分配,一般就是指在程序運行時直接 new 出來的內存。這部份內存在不使用時將會由 Java 垃圾回收器來負責回收。編程
棧與堆的區別:
在方法體內定義的(局部變量)一些基本類型的變量和對象的引用變量都是在方法的棧內存中分配的。當在一段方法塊中定義一個變量時,Java 就會在棧中爲該變量分配內存空間,當超過該變量的做用域後,該變量也就無效了,分配給它的內存空間也將被釋放掉,該內存空間能夠被從新使用。api
堆內存用來存放全部由 new 建立的對象(包括該對象其中的全部成員變量)和數組。在堆中分配的內存,將由 Java 垃圾回收器來自動管理。在堆中產生了一個數組或者對象後,還能夠在棧中定義一個特殊的變量,這個變量的取值等於數組或者對象在堆內存中的首地址,這個特殊的變量就是咱們上面說的引用變量。咱們能夠經過這個引用變量來訪問堆中的對象或者數組。
舉個例子:
public class Sample() { int s1 = 0; Sample mSample1 = new Sample(); public void method() { int s2 = 1; Sample mSample2 = new Sample(); } } Sample mSample3 = new Sample();
Sample 類的局部變量 s2 和引用變量 mSample2 都是存在於棧中,但 mSample2 指向的對象是存在於堆上的。
mSample3 指向的對象實體存放在堆上,包括這個對象的全部成員變量 s1 和 mSample1,而它本身存在於棧中。
結論:
局部變量的基本數據類型和引用存儲於棧中,引用的對象實體存儲於堆中。—— 由於它們屬於方法中的變量,生命週期隨方法而結束。
成員變量所有存儲與堆中(包括基本數據類型,引用和引用的對象實體)—— 由於它們屬於類,類對象終究是要被new出來使用的。
瞭解了 Java 的內存分配以後,咱們再來看看 Java 是怎麼管理內存的。
Java是如何管理內存
Java的內存管理就是對象的分配和釋放問題。在 Java 中,程序員須要經過關鍵字 new 爲每一個對象申請內存空間 (基本類型除外),全部的對象都在堆 (Heap)中分配空間。另外,對象的釋放是由 GC 決定和執行的。在 Java 中,內存的分配是由程序完成的,而內存的釋放是由 GC 完成的,這種收支兩條線的方法確實簡化了程序員的工做。但同時,它也加劇了JVM的工做。這也是 Java 程序運行速度較慢的緣由之一。由於,GC 爲了可以正確釋放對象,GC 必須監控每個對象的運行狀態,包括對象的申請、引用、被引用、賦值等,GC 都須要進行監控。
監視對象狀態是爲了更加準確地、及時地釋放對象,而釋放對象的根本原則就是該對象再也不被引用。
爲了更好理解 GC 的工做原理,咱們能夠將對象考慮爲有向圖的頂點,將引用關係考慮爲圖的有向邊,有向邊從引用者指向被引對象。另外,每一個線程對象能夠做爲一個圖的起始頂點,例如大多程序從 main 進程開始執行,那麼該圖就是以 main 進程頂點開始的一棵根樹。在這個有向圖中,根頂點可達的對象都是有效對象,GC將不回收這些對象。若是某個對象 (連通子圖)與這個根頂點不可達(注意,該圖爲有向圖),那麼咱們認爲這個(這些)對象再也不被引用,能夠被 GC 回收。
如下,咱們舉一個例子說明如何用有向圖表示內存管理。對於程序的每個時刻,咱們都有一個有向圖表示JVM的內存分配狀況。如下右圖,就是左邊程序運行到第6行的示意圖。
Java使用有向圖的方式進行內存管理,能夠消除引用循環的問題,例若有三個對象,相互引用,只要它們和根進程不可達的,那麼GC也是能夠回收它們的。這種方式的優勢是管理內存的精度很高,可是效率較低。另一種經常使用的內存管理技術是使用計數器,例如COM模型採用計數器方式管理構件,它與有向圖相比,精度行低(很難處理循環引用的問題),但執行效率很高。
什麼是Java中的內存泄露
在Java中,內存泄漏就是存在一些被分配的對象,這些對象有下面兩個特色,首先,這些對象是可達的,即在有向圖中,存在通路能夠與其相連;其次,這些對象是無用的,即程序之後不會再使用這些對象。若是對象知足這兩個條件,這些對象就能夠斷定爲Java中的內存泄漏,這些對象不會被GC所回收,然而它卻佔用內存。
在C++中,內存泄漏的範圍更大一些。有些對象被分配了內存空間,而後卻不可達,因爲C++中沒有GC,這些內存將永遠收不回來。在Java中,這些不可達的對象都由GC負責回收,所以程序員不須要考慮這部分的內存泄露。
經過分析,咱們得知,對於C++,程序員須要本身管理邊和頂點,而對於Java程序員只須要管理邊就能夠了(不須要管理頂點的釋放)。經過這種方式,Java提升了編程的效率。
所以,經過以上分析,咱們知道在Java中也有內存泄漏,但範圍比C++要小一些。由於Java從語言上保證,任何對象都是可達的,全部的不可達對象都由GC管理。
對於程序員來講,GC基本是透明的,不可見的。雖然,咱們只有幾個函數能夠訪問GC,例如運行GC的函數System.gc(),可是根據Java語言規範定義, 該函數不保證JVM的垃圾收集器必定會執行。由於,不一樣的JVM實現者可能使用不一樣的算法管理GC。一般,GC的線程的優先級別較低。JVM調用GC的策略也有不少種,有的是內存使用到達必定程度時,GC纔開始工做,也有定時執行的,有的是平緩執行GC,有的是中斷式執行GC。但一般來講,咱們不須要關心這些。除非在一些特定的場合,GC的執行影響應用程序的性能,例如對於基於Web的實時系統,如網絡遊戲等,用戶不但願GC忽然中斷應用程序執行而進行垃圾回收,那麼咱們須要調整GC的參數,讓GC可以經過平緩的方式釋放內存,例如將垃圾回收分解爲一系列的小步驟執行,Sun提供的HotSpot JVM就支持這一特性。
一樣給出一個 Java 內存泄漏的典型例子,
Vector v = new Vector(10); for (int i = 1; i < 100; i++) { Object o = new Object(); v.add(o); o = null; }
在這個例子中,咱們循環申請Object對象,並將所申請的對象放入一個 Vector 中,若是咱們僅僅釋放引用自己,那麼 Vector 仍然引用該對象,因此這個對象對 GC 來講是不可回收的。所以,若是對象加入到Vector 後,還必須從 Vector 中刪除,最簡單的方法就是將 Vector 對象設置爲 null。
Android中常見的內存泄漏彙總
集合類泄漏
集合類若是僅僅有添加元素的方法,而沒有相應的刪除機制,致使內存被佔用。若是這個集合類是全局性的變量 (好比類中的靜態屬性,全局性的 map 等即有靜態引用或 final 一直指向它),那麼沒有相應的刪除機制,極可能致使集合所佔用的內存只增不減。好比上面的典型例子就是其中一種狀況,固然實際上咱們在項目中確定不會寫這麼 2B 的代碼,但稍不注意仍是很容易出現這種狀況,好比咱們都喜歡經過 HashMap 作一些緩存之類的事,這種狀況就要多留一些心眼。
單例形成的內存泄漏
因爲單例的靜態特性使得其生命週期跟應用的生命週期同樣長,因此若是使用不恰當的話,很容易形成內存泄漏。好比下面一個典型的例子,
public class AppManager { private static AppManager instance; private Context context; private AppManager(Context context) { this.context = context; } public static AppManager getInstance(Context context) { if (instance != null) { instance = new AppManager(context); } return instance; } }
這是一個普通的單例模式,當建立這個單例的時候,因爲須要傳入一個Context,因此這個Context的生命週期的長短相當重要:
一、若是此時傳入的是 Application 的 Context,由於 Application 的生命週期就是整個應用的生命週期,因此這將沒有任何問題。
二、若是此時傳入的是 Activity 的 Context,當這個 Context 所對應的 Activity 退出時,因爲該 Context 的引用被單例對象所持有,其生命週期等於整個應用程序的生命週期,因此當前 Activity 退出時它的內存並不會被回收,這就形成泄漏了。
正確的方式應該改成下面這種方式:
public class AppManager { private static AppManager instance; private Context context; private AppManager(Context context) { this.context = context.getApplicationContext();// 使用Application 的context } public static AppManager getInstance(Context context) { if (instance != null) { instance = new AppManager(context); } return instance; } }
或者這樣寫,連 Context 都不用傳進來了:
在你的 Application 中添加一個靜態方法,getContext() 返回 Application 的 context, ... context = getApplicationContext(); ... /** * 獲取全局的context * @return 返回全局context對象 */ public static Context getContext(){ return context; } public class AppManager { private static AppManager instance; private Context context; private AppManager() { this.context = MyApplication.getContext();// 使用Application 的context } public static AppManager getInstance() { if (instance != null) { instance = new AppManager(); } return instance; } }
匿名內部類/非靜態內部類和異步線程
非靜態內部類建立靜態實例形成的內存泄漏
有的時候咱們可能會在啓動頻繁的Activity中,爲了不重複建立相同的數據資源,可能會出現這種寫法:
public class MainActivity extends AppCompatActivity { private static TestResource mResource = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if(mManager == null){ mManager = new TestResource(); } //... } class TestResource { //... } }
這樣就在Activity內部建立了一個非靜態內部類的單例,每次啓動Activity時都會使用該單例的數據,這樣雖然避免了資源的重複建立,不過這種寫法卻會形成內存泄漏,由於非靜態內部類默認會持有外部類的引用,而該非靜態內部類又建立了一個靜態的實例,該實例的生命週期和應用的同樣長,這就致使了該靜態實例一直會持有該Activity的引用,致使Activity的內存資源不能正常回收。正確的作法爲:
將該內部類設爲靜態內部類或將該內部類抽取出來封裝成一個單例,若是須要使用Context,請按照上面推薦的使用Application 的 Context。固然,Application 的 context 不是萬能的,因此也不能隨便亂用,對於有些地方則必須使用 Activity 的 Context,對於Application,Service,Activity三者的Context的應用場景以下:
其中: NO1表示 Application 和 Service 能夠啓動一個 Activity,不過須要建立一個新的 task 任務隊列。而對於 Dialog 而言,只有在 Activity 中才能建立
匿名內部類
android開發常常會繼承實現Activity/Fragment/View,此時若是你使用了匿名類,並被異步線程持有了,那要當心了,若是沒有任何措施這樣必定會致使泄露
public class MainActivity extends Activity { ... Runnable ref1 = new MyRunable(); Runnable ref2 = new Runnable() { @Override public void run() { } }; ... }
ref1和ref2的區別是,ref2使用了匿名內部類。咱們來看看運行時這兩個引用的內存:
能夠看到,ref1沒什麼特別的。
但ref2這個匿名類的實現對象裏面多了一個引用:
this$0這個引用指向MainActivity.this,也就是說當前的MainActivity實例會被ref2持有,若是將這個引用再傳入一個異步線程,此線程和此Acitivity生命週期不一致的時候,就形成了Activity的泄露。
Handler 形成的內存泄漏
Handler 的使用形成的內存泄漏問題應該說是最爲常見了,不少時候咱們爲了不 ANR 而不在主線程進行耗時操做,在處理網絡任務或者封裝一些請求回調等api都藉助Handler來處理,但 Handler 不是萬能的,對於 Handler 的使用代碼編寫一不規範即有可能形成內存泄漏。另外,咱們知道 Handler、Message 和 MessageQueue 都是相互關聯在一塊兒的,萬一 Handler 發送的 Message 還沒有被處理,則該 Message 及發送它的 Handler 對象將被線程 MessageQueue 一直持有。
因爲 Handler 屬於 TLS(Thread Local Storage) 變量, 生命週期和 Activity 是不一致的。所以這種實現方式通常很難保證跟 View 或者 Activity 的生命週期保持一致,故很容易致使沒法正確釋放。
舉個例子:
public class SampleActivity extends Activity { private final Handler mLeakyHandler = new Handler() { @Override public void handleMessage(Message msg) { // ... } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Post a message and delay its execution for 10 minutes. mLeakyHandler.postDelayed(new Runnable() { @Override public void run() { /* ... */ } }, 1000 * 60 * 10); // Go back to the previous Activity. finish(); } }
在該 SampleActivity 中聲明瞭一個延遲10分鐘執行的消息 Message,mLeakyHandler 將其 push 進了消息隊列 MessageQueue 裏。當該 Activity 被 finish() 掉時,延遲執行任務的 Message 還會繼續存在於主線程中,它持有該 Activity 的 Handler 引用,因此此時 finish() 掉的 Activity 就不會被回收了從而形成內存泄漏(因 Handler 爲非靜態內部類,它會持有外部類的引用,在這裏就是指 SampleActivity)。
修復方法:在 Activity 中避免使用非靜態內部類,好比上面咱們將 Handler 聲明爲靜態的,則其存活期跟 Activity 的生命週期就無關了。同時經過弱引用的方式引入 Activity,避免直接將 Activity 做爲 context 傳進去,見下面代碼:
public class SampleActivity extends Activity { /** * Instances of static inner classes do not hold an implicit * reference to their outer class. */ private static class MyHandler extends Handler { private final WeakReference<SampleActivity> mActivity; public MyHandler(SampleActivity activity) { mActivity = new WeakReference<SampleActivity>(activity); } @Override public void handleMessage(Message msg) { SampleActivity activity = mActivity.get(); if (activity != null) { // ... } } } private final MyHandler mHandler = new MyHandler(this); /** * Instances of anonymous classes do not hold an implicit * reference to their outer class when they are "static". */ private static final Runnable sRunnable = new Runnable() { @Override public void run() { /* ... */ } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Post a message and delay its execution for 10 minutes. mHandler.postDelayed(sRunnable, 1000 * 60 * 10); // Go back to the previous Activity. finish(); } }
綜述,即推薦使用靜態內部類 + WeakReference 這種方式。每次使用前注意判空。
前面提到了 WeakReference,因此這裏就簡單的說一下 Java 對象的幾種引用類型。
Java對引用的分類有 Strong reference, SoftReference, WeakReference, PhatomReference 四種。
在Android應用的開發中,爲了防止內存溢出,在處理一些佔用內存大並且聲明週期較長的對象時候,能夠儘可能應用軟引用和弱引用技術。
軟/弱引用能夠和一個引用隊列(ReferenceQueue)聯合使用,若是軟引用所引用的對象被垃圾回收器回收,Java虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。利用這個隊列能夠得知被回收的軟/弱引用的對象列表,從而爲緩衝器清除已失效的軟/弱引用。
假設咱們的應用會用到大量的默認圖片,好比應用中有默認的頭像,默認遊戲圖標等等,這些圖片不少地方會用到。若是每次都去讀取圖片,因爲讀取文件須要硬件操做,速度較慢,會致使性能較低。因此咱們考慮將圖片緩存起來,須要的時候直接從內存中讀取。可是,因爲圖片佔用內存空間比較大,緩存不少圖片須要不少的內存,就可能比較容易發生OutOfMemory異常。這時,咱們能夠考慮使用軟/弱引用技術來避免這個問題發生。如下就是高速緩衝器的雛形:
首先定義一個HashMap,保存軟引用對象。
private Map <String, SoftReference<Bitmap>> imageCache = new HashMap <String, SoftReference<Bitmap>> ();
再來定義一個方法,保存Bitmap的軟引用到HashMap。
使用軟引用之後,在OutOfMemory異常發生以前,這些緩存的圖片資源的內存空間能夠被釋放掉的,從而避免內存達到上限,避免Crash發生。
若是隻是想避免OutOfMemory異常的發生,則可使用軟引用。若是對於應用的性能更在乎,想盡快回收一些佔用內存比較大的對象,則可使用弱引用。
另外能夠根據對象是否常用來判斷選擇軟引用仍是弱引用。若是該對象可能會常用的,就儘可能用軟引用。若是該對象不被使用的可能性更大些,就能夠用弱引用。
ok,繼續回到主題。前面所說的,建立一個靜態Handler內部類,而後對 Handler 持有的對象使用弱引用,這樣在回收時也能夠回收 Handler 持有的對象,可是這樣作雖然避免了 Activity 泄漏,不過 Looper 線程的消息隊列中仍是可能會有待處理的消息,因此咱們在 Activity 的 Destroy 時或者 Stop 時應該移除消息隊列 MessageQueue 中的消息。
下面幾個方法均可以移除 Message:
public final void removeCallbacks(Runnable r); public final void removeCallbacks(Runnable r, Object token); public final void removeCallbacksAndMessages(Object token); public final void removeMessages(int what); public final void removeMessages(int what, Object object);
儘可能避免使用 static 成員變量
若是成員變量被聲明爲 static,那咱們都知道其生命週期將與整個app進程生命週期同樣。
這會致使一系列問題,若是你的app進程設計上是長駐內存的,那即便app切到後臺,這部份內存也不會被釋放。按照如今手機app內存管理機制,佔內存較大的後臺進程將優先回收,yi'wei若是此app作過進程互保保活,那會形成app在後臺頻繁重啓。當手機安裝了你參與開發的app之後一晚上時間手機被消耗空了電量、流量,你的app不得不被用戶卸載或者靜默。
這裏修復的方法是:
不要在類初始時初始化靜態成員。能夠考慮lazy初始化。
架構設計上要思考是否真的有必要這樣作,儘可能避免。若是架構須要這麼設計,那麼此對象的生命週期你有責任管理起來。
避免 override finalize()
一、finalize 方法被執行的時間不肯定,不能依賴與它來釋放緊缺的資源。時間不肯定的緣由是:
- 虛擬機調用GC的時間不肯定
- Finalize daemon線程被調度到的時間不肯定
二、finalize 方法只會被執行一次,即便對象被複活,若是已經執行過了 finalize 方法,再次被 GC 時也不會再執行了,緣由是:
含有 finalize 方法的 object 是在 new 的時候由虛擬機生成了一個 finalize reference 在來引用到該Object的,而在 finalize 方法執行的時候,該 object 所對應的 finalize Reference 會被釋放掉,即便在這個時候把該 object 復活(即用強引用引用住該 object ),再第二次被 GC 的時候因爲沒有了 finalize reference 與之對應,因此 finalize 方法不會再執行。
三、含有Finalize方法的object須要至少通過兩輪GC纔有可能被釋放。
詳情見這裏 深刻分析過dalvik的代碼
資源未關閉形成的內存泄漏
對於使用了BraodcastReceiver,ContentObserver,File,遊標 Cursor,Stream,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或者註銷,不然這些資源將不會被回收,形成內存泄漏。
一些不良代碼形成的內存壓力
有些代碼並不形成內存泄露,可是它們,或是對沒使用的內存沒進行有效及時的釋放,或是沒有有效的利用已有的對象而是頻繁的申請新內存。
好比:
- Bitmap 沒調用 recycle()方法,對於 Bitmap 對象在不使用時,咱們應該先調用 recycle() 釋放內存,而後才它設置爲 null. 由於加載 Bitmap 對象的內存空間,一部分是 java 的,一部分 C 的(由於 Bitmap 分配的底層是經過 JNI 調用的 )。 而這個 recyle() 就是針對 C 部分的內存釋放。
- 構造 Adapter 時,沒有使用緩存的 convertView ,每次都在建立新的 converView。這裏推薦使用 ViewHolder。
工具分析
Java 內存泄漏的分析工具備不少,但衆所周知的要數 MAT(Memory Analysis Tools) 和 YourKit 了。因爲篇幅問題,我這裏就只對 MAT 的使用作一下介紹。--> MAT 的安裝
MAT分析heap的總內存佔用大小來初步判斷是否存在泄露
打開 DDMS 工具,在左邊 Devices 視圖頁面選中「Update Heap」圖標,而後在右邊切換到 Heap 視圖,點擊 Heap 視圖中的「Cause GC」按鈕,到此爲止需檢測的進程就能夠被監視。
Heap視圖中部有一個Type叫作data object,即數據對象,也就是咱們的程序中大量存在的類類型的對象。在data object一行中有一列是「Total Size」,其值就是當前進程中全部Java數據對象的內存總量,通常狀況下,這個值的大小決定了是否會有內存泄漏。能夠這樣判斷:
進入某應用,不斷的操做該應用,同時注意觀察data object的Total Size值,正常狀況下Total Size值都會穩定在一個有限的範圍內,也就是說因爲程序中的的代碼良好,沒有形成對象不被垃圾回收的狀況。
因此說雖然咱們不斷的操做會不斷的生成不少對象,而在虛擬機不斷的進行GC的過程當中,這些對象都被回收了,內存佔用量會會落到一個穩定的水平;反之若是代碼中存在沒有釋放對象引用的狀況,則data object的Total Size值在每次GC後不會有明顯的回落。隨着操做次數的增多Total Size的值會愈來愈大,直到到達一個上限後致使進程被殺掉。
MAT分析hprof來定位內存泄露的緣由所在
這是出現內存泄露後使用MAT進行問題定位的有效手段。
A)Dump出內存泄露當時的內存鏡像hprof,分析懷疑泄露的類:
B)分析持有此類對象引用的外部對象
C)分析這些持有引用的對象的GC路徑
D)逐個分析每一個對象的GC路徑是否正常
從這個路徑能夠看出是一個antiRadiationUtil工具類對象持有了MainActivity的引用致使MainActivity沒法釋放。此時就要進入代碼分析此時antiRadiationUtil的引用持有是否合理(若是antiRadiationUtil持有了MainActivity的context致使節目退出後MainActivity沒法銷燬,那通常都屬於內存泄露了)。
MAT對比操做先後的hprof來定位內存泄露的根因所在
爲查找內存泄漏,一般須要兩個 Dump結果做對比,打開 Navigator History面板,將兩個表的 Histogram結果都添加到 Compare Basket中去
A) 第一個HPROF 文件(usingFile > Open Heap Dump ).
B)打開Histogram view.
C)在NavigationHistory view裏 (若是看不到就從Window >show view>MAT- Navigation History ), 右擊histogram而後選擇Add to Compare Basket .
D)打開第二個HPROF 文件而後重作步驟2和3.
E)切換到Compare Basket view, 而後點擊Compare the Results (視圖右上角的紅色」!」圖標)。
F)分析對比結果
能夠看出兩個hprof的數據對象對比結果。
經過這種方式能夠快速定位到操做先後所持有的對象增量,從而進一步定位出當前操做致使內存泄露的具體緣由是泄露了什麼數據對象。
注意:
若是是用 MAT Eclipse 插件獲取的 Dump文件,不須要通過轉換則可在MAT中打開,Adt會自動進行轉換。
而手機SDk Dump 出的文件要通過轉換才能被 MAT識別,Android SDK提供了這個工具 hprof-conv (位於 sdk/tools下)
首先,要經過控制檯進入到你的 android sdk tools 目錄下執行如下命令:
./hprof-conv xxx-a.hprof xxx-b.hprof
例如 hprof-conv input.hprof out.hprof
此時才能將out.hprof放在eclipse的MAT中打開。
Ok,下面將給你們介紹一個屌炸天的工具 -- LeakCanary 。
使用 LeakCanary 檢測 Android 的內存泄漏
什麼是 LeakCanary 呢?爲何選擇它來檢測 Android 的內存泄漏呢?
別急,讓我來慢慢告訴你們!
LeakCanary 是國外一位大神 Pierre-Yves Ricau 開發的一個用於檢測內存泄露的開源類庫。通常狀況下,在對戰內存泄露中,咱們都會通過如下幾個關鍵步驟:
一、瞭解 OutOfMemoryError 狀況。
二、重現問題。
三、在發生內存泄露的時候,把內存 Dump 出來。
四、在發生內存泄露的時候,把內存 Dump 出來。
五、計算這個對象到 GC roots 的最短強引用路徑。
六、肯定引用路徑中的哪一個引用是不應有的,而後修復問題。
很複雜對吧?
若是有一個類庫能在發生 OOM 以前把這些事情所有都搞定,而後你只要修復這些問題就行了。LeakCanary 作的就是這件事情。你能夠在 debug 包中輕鬆檢測內存泄露。
一塊兒來看這個例子(摘自 LeakCanary 中文使用說明,下面會附上全部的參考文檔連接):
class Cat { } class Box { Cat hiddenCat; } class Docker { // 靜態變量,將不會被回收,除非加載 Docker 類的 ClassLoader 被回收。 static Box container; } // ... Box box = new Box(); // 薛定諤之貓 Cat schrodingerCat = new Cat(); box.hiddenCat = schrodingerCat; Docker.container = box;
建立一個RefWatcher,監控對象引用狀況。
// 咱們期待薛定諤之貓很快就會消失(或者不消失),咱們監控一下 refWatcher.watch(schrodingerCat);
當發現有內存泄露的時候,你會看到一個很漂亮的 leak trace 報告:
- GC ROOT static Docker.container
- references Box.hiddenCat
- leaks Cat instance
咱們知道,你很忙,天天都有一大堆需求。因此咱們把這個事情弄得很簡單,你只須要添加一行代碼就好了。而後 LeakCanary 就會自動偵測 activity 的內存泄露了。
public class ExampleApplication extends Application { @Override public void onCreate() { super.onCreate(); LeakCanary.install(this); } }
而後你會在通知欄看到這樣很漂亮的一個界面:
以很直白的方式將內存泄露展示在咱們的面前。
Demo
一個很是簡單的 LeakCanary demo: 一個很是簡單的 LeakCanary demo: https://github.com/liaohuqiu/leakcanary-demo
接入
在 build.gradle 中加入引用,不一樣的編譯使用不一樣的引用:
dependencies { debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3' releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3' }
如何使用
使用 RefWatcher 監控那些本該被回收的對象。
RefWatcher refWatcher = {...}; // 監控 refWatcher.watch(schrodingerCat);
LeakCanary.install() 會返回一個預約義的 RefWatcher,同時也會啓用一個 ActivityRefWatcher,用於自動監控調用 Activity.onDestroy() 以後泄露的 activity。
在Application中進行配置 :
public class ExampleApplication extends Application { public static RefWatcher getRefWatcher(Context context) { ExampleApplication application = (ExampleApplication) context.getApplicationContext(); return application.refWatcher; } private RefWatcher refWatcher; @Override public void onCreate() { super.onCreate(); refWatcher = LeakCanary.install(this); } }
使用 RefWatcher 監控 Fragment:
public abstract class BaseFragment extends Fragment { @Override public void onDestroy() { super.onDestroy(); RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity()); refWatcher.watch(this); } }
使用 RefWatcher 監控 Activity:
public class MainActivity extends AppCompatActivity { ...... @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //在本身的應用初始Activity中加入以下兩行代碼 RefWatcher refWatcher = ExampleApplication.getRefWatcher(this); refWatcher.watch(this); textView = (TextView) findViewById(R.id.tv); textView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { startAsyncTask(); } }); } private void async() { startAsyncTask(); } private void startAsyncTask() { // This async task is an anonymous class and therefore has a hidden reference to the outer // class MainActivity. If the activity gets destroyed before the task finishes (e.g. rotation), // the activity instance will leak. new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { // Do some slow work in background SystemClock.sleep(20000); return null; } }.execute(); } }
工做機制
1.RefWatcher.watch() 建立一個 KeyedWeakReference 到要被監控的對象。
2.而後在後臺線程檢查引用是否被清除,若是沒有,調用GC。
3.若是引用仍是未被清除,把 heap 內存 dump 到 APP 對應的文件系統中的一個 .hprof 文件中。
4.在另一個進程中的 HeapAnalyzerService 有一個 HeapAnalyzer 使用HAHA 解析這個文件。
5.得益於惟一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位內存泄露。
6.HeapAnalyzer 計算 到 GC roots 的最短強引用路徑,並肯定是不是泄露。若是是的話,創建致使泄露的引用鏈。
7.引用鏈傳遞到 APP 進程中的 DisplayLeakService, 並以通知的形式展現出來。
ok,這裏就再也不深刻了,想要了解更多就到 做者 github 主頁 這去哈。
總結
對 Activity 等組件的引用應該控制在 Activity 的生命週期以內; 若是不能就考慮使用 getApplicationContext 或者 getApplication,以免 Activity 被外部長生命週期的對象引用而泄露。
儘可能不要在靜態變量或者靜態內部類中使用非靜態外部成員變量(包括context ),即便要使用,也要考慮適時把外部成員變量置空;也能夠在內部類中使用弱引用來引用外部類的變量。
對於生命週期比Activity長的內部類對象,而且內部類中使用了外部類的成員變量,能夠這樣作避免內存泄漏:
- 將內部類改成靜態內部類
- 靜態內部類中使用弱引用來引用外部類的成員變量
Handler 的持有的引用對象最好使用弱引用,資源釋放時也能夠清空 Handler 裏面的消息。好比在 Activity onStop 或者 onDestroy 的時候,取消掉該 Handler 對象的 Message和 Runnable.
在 Java 的實現過程當中,也要考慮其對象釋放,最好的方法是在不使用某對象時,顯式地將此對象賦值爲 null,好比使用完Bitmap 後先調用 recycle(),再賦爲null,清空對圖片等資源有直接引用或者間接引用的數組(使用 array.clear() ; array = null)等,最好遵循誰建立誰釋放的原則。
正確關閉資源,對於使用了BraodcastReceiver,ContentObserver,File,遊標 Cursor,Stream,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或者註銷。
保持對對象生命週期的敏感,特別注意單例、靜態對象、全局性集合等的生命週期。
以上部分圖片、實例代碼和文段都摘自或參考如下文章 :
支付寶:
Android怎樣coding避免內存泄露
支付寶錢包Android內存治理
IBM :
Java的內存泄漏
Android Design Patterns :
How to Leak a Context: Handlers & Inner Classes
伯樂在線團隊:
Android性能優化之常見的內存泄漏
我廠同窗 :
Dalvik虛擬機 Finalize 方法執行分析
騰訊bugly :
內存泄露從入門到精通三部曲之基礎知識篇
內存泄露從入門到精通三部曲之排查方法篇
內存泄露從入門到精通三部曲之常見緣由與用戶實踐
LeakCanary :
LeakCanary 中文使用說明
LeakCanary: 讓內存泄露無所遁形
https://github.com/square/leakcanary
轉載地址:https://yq.aliyun.com/articles/3009?&utm_campaign=sys&utm_medium=market&utm_source=edm_email&msctype=email&mscmsgid=111616020300373231&