Android 關於記憶體洩露,你必須瞭解的東西

developerHaoz發表於2018-02-05

前言

記憶體管理的目的就是讓我們在開發過程中有效避免我們的應用程式出現記憶體洩露的問題。記憶體洩露相信大家都不陌生,我們可以這樣理解:「沒有用的物件無法回收的現象就是記憶體洩露」。

如果程式發生了記憶體洩露,則會帶來以下這些問題

  • 應用可用的記憶體減少,增加了堆記憶體的壓力

  • 降低了應用的效能,比如會觸發更頻繁的 GC

  • 嚴重的時候可能會導致記憶體溢位錯誤,即 OOM Error

OOM 發生在,當我們嘗試進行建立物件,但是堆記憶體無法通過 GC 釋放足夠的空間,堆記憶體也無法再繼續增長,從而完成物件建立請求的時候,OOM 發生很有可能是記憶體洩露導致的,但並非所有的 OOM 都是由記憶體洩露引起的,記憶體洩露也並不一定引起 OOM。

一、基礎準備


如果真的想比較清楚的瞭解記憶體洩露的話,對於 Java 的記憶體管理以及引用型別有一個清晰的認識是必不可少的。

  • 理解 Java 的記憶體管理能讓我們更深一層地瞭解 Java 虛擬機器是怎樣使用記憶體的,一旦出現記憶體洩露,我們也能更加從容地排查問題。

  • 瞭解 Java 的引用型別,能讓我們更加理解記憶體洩露出現的原因,以及常見的解決方法。

具體的內容,可以看下這篇文章 你真的懂 Java 的記憶體管理和引用型別嗎?

二、Android 中記憶體洩露的常見場景 &
解決方案


1、單例造成的記憶體洩露

單例模式是非常常用的設計模式,使用單例模式的類,只會產生一個物件,這個物件看起來像是一直佔用著記憶體,但這並不意味著就是浪費了記憶體,記憶體本來就是拿來裝東西的,只要這個物件一直都被高效的利用就不能叫做洩露。

但是過多的單例會讓記憶體佔用過多,而且單例模式由於其 靜態特性,其生命週期 = 應用程式的生命週期,不正確地使用單例模式也會造成記憶體洩露。

舉個例子:

public class SingleInstanceTest { 
private static SingleInstanceTest sInstance;
private Context mContext;
private SingleInstanceTest(Context context){
this.mContext = context;

} public static SingleInstanceTest newInstance(Context context){
if(sInstance == null){
sInstance = new SingleInstanceTest(context);

} return sInstance;

}
}複製程式碼

上面是一個比較簡單的單例模式用法,需要外部傳入一個 Context 來獲取該類的例項,如果此時傳入的 Context 是 Activity 的話,此時單例就有持有該 Activity 的強引用(直到整個應用生命週期結束)。這樣的話,即使該 Activity 退出,該 Activity 的記憶體也不會被回收,這樣就造成了記憶體洩露,特別是一些比較大的 Activity,甚至還會導致 OOM(Out Of Memory)。

解決方法: 單例模式引用的物件的生命週期 = 應用生命週期

public class SingleInstanceTest { 
private static SingleInstanceTest sInstance;
private Context mContext;
private SingleInstanceTest(Context context){
this.mContext = context.getApplicationContext();

} public static SingleInstanceTest newInstance(Context context){
if(sInstance == null){
sInstance = new SingleInstanceTest(context);

} return sInstance;

}
}複製程式碼

可以看到在 SingleInstanceTest 的建構函式中,將 context.getApplicationContext() 賦值給 mContext,此時單例引用的物件是 Application,而 Application 的生命週期本來就跟應用程式是一樣的,也就不存在記憶體洩露。

這裡再擴充一點,很多時候我們在需要用到 Activity 或者 Context 的地方,會直接將 Activity 的例項作為引數傳給對應的類,就像這樣:

public class Sample { 
private Context mContext;
public Sample(Context context){
this.mContext = context;

} public Context getContext() {
return mContext;

}
}// 外部呼叫Sample sample = new Sample(MainActivity.this);
複製程式碼

這種情況如果不注意的話,很容易就會造成記憶體洩露,比較好的寫法是使用弱引用(WeakReference)來進行改進。

public class Sample { 
private WeakReference<
Context>
mWeakReference;
public Sample(Context context){
this.mWeakReference = new WeakReference<
>
(context);

} public Context getContext() {
if(mWeakReference.get() != null){
return mWeakReference.get();

} return null;

}
}// 外部呼叫Sample sample = new Sample(MainActivity.this);
複製程式碼

被弱引用關聯的物件只能存活到下一次垃圾回收之前,也就是說即使 Sample 持有 Activity 的引用,但由於 GC 會幫我們回收相關的引用,被銷燬的 Activity 也會被回收記憶體,這樣我們就不用擔心會發生記憶體洩露了。

2、非靜態內部類 / 匿名類

我們先來看看非靜態內部類(non static inner class)和 靜態內部類(static inner class)之間的區別。

class 對比 static inner class non static inner class
與外部 class 引用關係 如果沒有傳入引數,就沒有引用關係 自動獲得強引用
被呼叫時需要外部例項 不需要 需要
能否呼叫外部 class 中的變數和方法 不能
生命週期 自主的生命週期 依賴於外部類,甚至比外部類更長

可以看到非靜態內部類自動獲得外部類的強引用,而且它的生命週期甚至比外部類更長,這便埋下了記憶體洩露的隱患。如果一個 Activity 的非靜態內部類的生命週期比 Activity 更長,那麼 Activity 的記憶體便無法被回收,也就是發生了記憶體洩露,而且還有可能發生難以預防的空指標問題。

舉個例子:

public class MainActivity extends AppCompatActivity { 
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new MyAscnyTask().execute();

} class MyAscnyTask extends AsyncTask<
Void, Integer, String>
{
@Override protected String doInBackground(Void... params) {
try {
Thread.sleep(5000);

} catch (InterruptedException e) {
e.printStackTrace();

} return "";

}
}
}複製程式碼

可以看到我們在 Activity 中繼承 AsyncTask 自定義了一個非靜態內部類,在 doInbackground() 方法中做了耗時的操作,然後在 onCreate() 中啟動 MyAsyncTask。如果在耗時操作結束之前,Activity 被銷燬了,這時候因為 MyAsyncTask 持有 Activity 的強引用,便會導致 Activity 的記憶體無法被回收,這時候便會產生記憶體洩露。

解決方法: 將 MyAsyncTask 變成非靜態內部類

public class MainActivity extends AppCompatActivity { 
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new MyAscnyTask().execute();

} static class MyAscnyTask extends AsyncTask<
Void, Integer, String>
{
@Override protected String doInBackground(Void... params) {
try {
Thread.sleep(50000);

} catch (InterruptedException e) {
e.printStackTrace();

} return "";

}
}
}複製程式碼

這時候 MyAsyncTask 不再持有 Activity 的強引用,即使 AsyncTask 的耗時操作還在繼續,Activity 的記憶體也能順利地被回收。

匿名類和非靜態內部類最大的共同點就是 都持有外部類的引用,因此,匿名類造成記憶體洩露的原因也跟靜態內部類基本是一樣的,下面舉個幾個比較常見的例子:

public class MainActivity extends AppCompatActivity { 
private Handler mHandler = new Handler(){
@Override public void handleMessage(Message msg) {
super.handleMessage(msg);

}
};
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// ① 匿名執行緒持有 Activity 的引用,進行耗時操作 new Thread(new Runnable() {
@Override public void run() {
try {
Thread.sleep(50000);

} catch (InterruptedException e) {
e.printStackTrace();

}
}
}).start();
// ② 使用匿名 Handler 傳送耗時訊息 Message message = Message.obtain();
mHandler.sendMessageDelayed(message, 60000);

}複製程式碼

上面舉出了兩個比較常見的例子

  • new 出一個匿名的 Thread,進行耗時的操作,如果 MainActivity 被銷燬而 Thread 中的耗時操作沒有結束的話,便會產生記憶體洩露

  • new 出一個匿名的 Handler,這裡我採用了 sendMessageDelayed() 方法來傳送訊息,這時如果 MainActivity 被銷燬,而 Handler 裡面的訊息還沒傳送完畢的話,Activity 的記憶體也不會被回收

解決方法:

  • 繼承 Thread 實現靜態內部類

  • 繼承 Handler 實現靜態內部類,以及在 Activity 的 onDestroy() 方法中,移除所有的訊息 mHandler.removeCallbacksAndMessages(null);

3、集合類

集合類新增元素後,仍引用著集合元素物件,導致該集合中的元素物件無法被回收,從而導致記憶體洩露,舉個例子:

   static List<
Object>
objectList = new ArrayList<
>
();
for (int i = 0;
i <
10;
i++) {
Object obj = new Object();
objectList.add(obj);
obj = null;

}複製程式碼

在這個例子中,迴圈多次將 new 出來的物件放入一個靜態的集合中,因為靜態變數的生命週期和應用程式一致,而且他們所引用的物件 Object 也不能釋放,這樣便造成了記憶體洩露。

解決方法: 在集合元素使用之後從集合中刪除,等所有元素都使用完之後,將集合置空。

    objectList.clear();
objectList = null;
複製程式碼

4、其他的情況

除了上述 3 種常見情況外,還有其他的一些情況

  • 1、需要手動關閉的物件沒有關閉

    • 網路、檔案等流忘記關閉
    • 手動註冊廣播時,退出時忘記 unregisterReceiver()
    • Service 執行完後忘記 stopSelf()
    • EventBus 等觀察者模式的框架忘記手動解除註冊
  • 2、static 關鍵字修飾的成員變數

  • 3、ListView 的 Item 洩露

三、利用工具進行記憶體洩露的排查


除了必須瞭解常見的記憶體洩露場景以及相應的解決方法之外,掌握一些好用的工具,能讓我們更有效率地解決記憶體洩露的問題。

1、Android Lint

Lint 是 Android Studio 提供的 程式碼掃描分析工具,它可以幫助我們發現程式碼機構 / 質量問題,同時提供一些解決方案,檢測記憶體洩露當然也不在話下,使用也是非常的簡單,可以參考下這篇文章:Android 效能優化:使用 Lint 優化程式碼、去除多餘資源

2、leakcanary

LeakCanary 是 Square 公司開源的「Android 和 Java 的記憶體洩漏檢測庫」,Square 出品,必屬精品,功能很強大,使用也很簡單。建議直接看 Github 上的說明:leakcanary,也可以參考這篇文章:Android記憶體優化(六)LeakCanary使用詳解


參考資料

來源:https://juejin.im/post/5a77bcd65188257a59119dc1

相關文章