Android記憶體洩露常見問題總結
概念梳理
在介紹記憶體洩漏之前很有必要提及一下Android系統的垃圾回收機制。Java GC(Garbage Collection,垃圾收集,垃圾回收)機制,是Java與C++/C的主要區別之一,作為Java開發者,不需要專門編寫記憶體回收和垃圾清理程式碼,對記憶體洩露和溢位的問題,也不需要像C程式設計師那樣戰戰兢兢。這是因為在Java虛擬機器中,存在自動記憶體管理和垃圾清掃機制。概括地說,該機制對虛擬機器中的記憶體進行標記,並確定哪些記憶體需要回收,根據一定的回收策略,自動的回收記憶體,永不停息(Nerver Stop)的保證虛擬機器中的記憶體空間,防止出現記憶體洩露和溢位問題。Android系統的垃圾回收是基於可達性分析演算法(根搜尋演算法)的。從GC Roots(每種具體實現對GC Roots有不同的定義)作為起點,向下搜尋它們引用的物件,可以生成一棵引用樹,樹的節點視為可達物件,反之視為不可達,不可達物件會被回收。
舉個例子,我們在開發中經常使用單例模式,單例的靜態特性導致其生命週期同應用一樣長。有時建立單例時如果我們需要Context物件,如果傳入的是Application的Context那麼不會有問題。如果傳入的是Activity的Context物件,那麼當Activity生命週期結束時,該Activity的引用依然被單例持有,所以不會被回收,而單例的生命週期又是跟應用一樣長,這個情況就叫做記憶體洩露(Memory Leak)。它指的是當你不再需要某個例項後,但是這個物件卻仍然被引用,防止被垃圾回收(Prevent from being bargage collected)。
public class Util {
private Context mContext;
private static Util sInstance;
private Util(Context context) {
this.mContext = context;
}
public static Util getInstance(Context context) {
if (sInstance == null) {
sInstance = new Util(context);
}
return sInstance;
}
}
本傑明 富蘭克林曾說:A small leak will sink a great ship(小漏不補沉大船)。基於Android系統的裝置一般來說記憶體就不大,特別是早期的Android裝置,記憶體洩漏是很致命的,記憶體洩漏積攢到一定程度會引發記憶體溢位(OOM),如果處理不當直接導致程式崩潰退出。
常見的記憶體洩漏
一般來說在開發中我們經常會犯下下面幾個錯誤,導致記憶體洩漏。這幾個都是前人踩坑總結出來的,非常有參考價值,至少我在排查解決記憶體洩漏的時候是這樣的。
一. 單例造成的記憶體洩漏
Android的單例模式非常受開發者的喜愛,不過使用的不恰當的話也會造成記憶體洩漏。因為單例的靜態特性使得單例的生命週期和應用的生命週期一樣長,這就說明了如果一個物件已經不需要使用了,而單例物件還持有該物件的引用,那麼這個物件將不能被正常回收,這就導致了記憶體洩漏。例子見上面那段程式碼。
二、非靜態內部類建立靜態例項造成的記憶體洩漏
有的時候我們可能會在啟動頻繁的Activity中,為了避免重複建立相同的資料資源,在Activity內部建立了一個非靜態內部類的單例,每次啟動Activity時都會使用該單例的資料,這樣雖然避免了資源的重複建立,不過這種寫法卻會造成記憶體洩漏,因為非靜態內部類預設會持有外部類的引用,而又使用了該非靜態內部類建立了一個靜態的例項,該例項的生命週期和應用的一樣長,這就導致了該靜態例項一直會持有該Activity的引用,導致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 (mResource == null) {
mResource = new TestResource();
}
//......
}
class TestResource {
//......
}
}
三、Handler造成的記憶體洩漏
Handler的使用造成的記憶體洩漏問題應該說最為常見了,平時在處理網路任務或者封裝一些請求回撥等api都應該會藉助Handler來處理,我們經常在Activity裡面這樣定義一個私有的Handler物件並初始化,這種建立Handler的方式會造成記憶體洩漏,由於mHandler是Handler的非靜態匿名內部類的例項,所以它持有外部類Activity的引用,我們知道訊息佇列是在一個Looper執行緒中不斷輪詢處理訊息,那麼當這個Activity退出時訊息佇列中還有未處理的訊息或者正在處理訊息,而訊息佇列中的Message持有mHandler例項的引用,mHandler又持有Activity的引用,所以導致該Activity的記憶體資源無法及時回收,引發記憶體洩漏。
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
//.....
}
};
四、資源未關閉造成的記憶體洩漏
對於使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或者登出,否則這些資源將不會被回收,造成記憶體洩漏。
檢測記憶體洩漏的常見工具
LeakCanary是Square開源了一個記憶體洩露自動探測神器 。對應的github倉庫地址:https://github.com/square/leakcanary 。使用非常簡單,在build.gradle中引入包依賴:
debugCompile 'com.squareup.leakcanary:leakcanary-
android:1.5'
releaseCompile 'com.squareup.leakcanary:leakcanary-
android-no-op:1.5'
testCompile 'com.squareup.leakcanary:leakcanary-
android-no-op:1.5'
在Application中的onCreate方法中增加初始化程式碼:
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for
// heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
整合後什麼都不用做,按照正常測試,當有記憶體洩漏發生後,應用會通過系統通知欄發出通知,點選通知就可以進入檢視記憶體洩漏的具體資訊。其實無論是MAT工具的記憶體分析,還是AndroidStudio中自帶的分析工具亦或是LeakCanary,原理都是一樣的,都是dump java heap出來進行分析,找到洩漏的問題,只是LeakCanary幫我們把分析的工作做了。