1. 程式人生 > >Android進階:記憶體洩漏

Android進階:記憶體洩漏

Android進階:記憶體洩漏

轉載自:https://github.com/francistao/LearningNotes/blob/master/Part1/Android

一、什麼是Java中的記憶體洩露

在Java中,記憶體洩漏就是存在一些被分配的物件,這些物件有下面兩個特點,首先,這些物件是可達的,即在有向圖中,存在通路可以與其相連;其次,這些物件是無用的,即程式以後不會再使用這些物件。如果物件滿足這兩個條件,這些物件就可以判定為Java中的記憶體洩漏,這些物件不會被GC所回收,然而它卻佔用記憶體。

在C++中,記憶體洩漏的範圍更大一些。有些物件被分配了記憶體空間,然後卻不可達,由於C++中沒有GC

,這些記憶體將永遠收不回來。在Java中,這些不可達的物件都由GC負責回收,因此程式設計師不需要考慮這部分的記憶體洩露。

通過分析,我們得知,對於C++,程式設計師需要自己管理邊和頂點,而對於Java程式設計師只需要管理邊就可以了(不需要管理頂點的釋放)。通過這種方式,Java提高了程式設計的效率。

 因此,通過以上分析,我們知道在Java中也有記憶體洩漏,但範圍比C++要小一些。因為Java從語言上保證,任何物件都是可達的,所有的不可達物件都由GC管理。

同樣給出一個 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。

二、記憶體洩漏具體分類

1、靜態集合類引起記憶體洩漏:

像HashMap、Vector等的使用最容易出現記憶體洩露,這些靜態變數的生命週期和應用程式一致,他們所引用的所有的物件Object也不能被釋放,因為他們也將一直被Vector等引用著。

例如

Static Vector v = new Vector(10);
for (int i = 1; i<100; i++)
{
Object o = new Object();
v.add(o);
o = null;
}

在這個例子中,迴圈申請Object 物件,並將所申請的物件放入一個Vector 中,如果僅僅釋放引用本身(o=null),那麼Vector 仍然引用該物件,所以這個物件對GC 來說是不可回收的。因此,如果物件加入到Vector 後,還必須從Vector 中刪除,最簡單的方法就是將Vector物件設定為null。

2、當集合裡面的物件屬性被修改後,再呼叫remove()方法時不起作用。

例如:

public static void main(String[] args)
{
Set<Person> set = new HashSet<Person>();
Person p1 = new Person("唐僧","pwd1",25);
Person p2 = new Person("孫悟空","pwd2",26);
Person p3 = new Person("豬八戒","pwd3",27);
set.add(p1);
set.add(p2);
set.add(p3);
System.out.println("總共有:"+set.size()+" 個元素!"); //結果:總共有:3 個元素!
p3.setAge(2); //修改p3的年齡,此時p3元素對應的hashcode值發生改變

set.remove(p3); //此時remove不掉,造成記憶體洩漏

set.add(p3); //重新新增,居然新增成功
System.out.println("總共有:"+set.size()+" 個元素!"); //結果:總共有:4 個元素!
for (Person person : set)
{
System.out.println(person);
}
}

3、監聽器

在java 程式設計中,我們都需要和監聽器打交道,通常一個應用當中會用到很多監聽器,我們會呼叫一個控制元件的諸如addXXXListener()等方法來增加監聽器,但往往在釋放物件的時候卻沒有記住去刪除這些監聽器,從而增加了記憶體洩漏的機會。

4、各種連線

比如資料庫連線(dataSourse.getConnection()),網路連線(socket)和io連線,除非其顯式的呼叫了其close()方法將其連線關閉,否則是不會自動被GC 回收的。對於Resultset 和Statement 物件可以不進行顯式回收,但Connection 一定要顯式回收,因為Connection 在任何時候都無法自動回收,而Connection一旦回收,Resultset 和Statement 物件就會立即為NULL。但是如果使用連線池,情況就不一樣了,除了要顯式地關閉連線,還必須顯式地關閉Resultset Statement 物件(關閉其中一個,另外一個也會關閉),否則就會造成大量的Statement 物件無法釋放,從而引起記憶體洩漏。這種情況下一般都會在try裡面去的連線,在finally裡面釋放連線。

5、內部類和外部模組的引用

內部類的引用是比較容易遺忘的一種,而且一旦沒釋放可能導致一系列的後繼類物件沒有釋放。此外程式設計師還要小心外部模組不經意的引用,例如程式設計師A 負責A 模組,呼叫了B 模組的一個方法如: public void registerMsg(Object b); 這種呼叫就要非常小心了,傳入了一個物件,很可能模組B就保持了對該物件的引用,這時候就需要注意模組B 是否提供相應的操作去除引用。

6、單例模式

不正確使用單例模式是引起記憶體洩漏的一個常見問題,單例物件在初始化後將在JVM的整個生命週期中存在(以靜態變數的方式),如果單例物件持有外部的引用,那麼這個物件將不能被JVM正常回收,導致記憶體洩漏,考慮下面的例子:

class A{
public A(){
B.getInstance().setA(this);
}
....
}
//B類採用單例模式
class B{
private A a;
private static B instance=new B();
public B(){}
public static B getInstance(){
return instance;
}
public void setA(A a){
this.a=a;
}
//getter...
} 

顯然B採用singleton模式,它持有一個A物件的引用,而這個A類的物件將不能被回收。想象下如果A是個比較複雜的物件或者集合型別會發生什麼情況

三、Android中常見的記憶體洩漏彙總

1.集合類洩漏

集合類如果僅僅有新增元素的方法,而沒有相應的刪除機制,導致記憶體被佔用。如果這個集合類是全域性性的變數 (比如類中的靜態屬性,全域性性的 map 等即有靜態引用或 final 一直指向它),那麼沒有相應的刪除機制,很可能導致集合所佔用的記憶體只增不減。比如上面的典型例子就是其中一種情況,當然實際上我們在專案中肯定不會寫這麼 2B 的程式碼,但稍不注意還是很容易出現這種情況,比如我們都喜歡通過 HashMap 做一些快取之類的事,這種情況就要多留一些心眼。

2.單例造成的記憶體洩漏

由於單例的靜態特性使得其生命週期跟應用的生命週期一樣長,所以如果使用不恰當的話,很容易造成記憶體洩漏。比如下面一個典型的例子,

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的生命週期的長短至關重要:

1、如果此時傳入的是 Application 的 Context,因為 Application 的生命週期就是整個應用的生命週期,所以這將沒有任何問題。

2、如果此時傳入的是 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 都不用傳進來了:

3.匿名內部類/非靜態內部類和非同步執行緒

非靜態內部類建立靜態例項造成的記憶體洩漏

有的時候我們可能會在啟動頻繁的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 中才能建立

4.匿名內部類

android開發經常會繼承實現Activity/Fragment/View,此時如果你使用了匿名類,並被非同步執行緒持有了,那要小心了,如果沒有任何措施這樣一定會導致洩露

ref1和ref2的區別是,ref2使用了匿名內部類。我們來看看執行時這兩個引用的記憶體:

可以看到,ref1沒什麼特別的。

但ref2這個匿名類的實現物件裡面多了一個引用:

this$0這個引用指向MainActivity.this,也就是說當前的MainActivity例項會被ref2持有,如果將這個引用再傳入一個非同步執行緒,此執行緒和此Acitivity生命週期不一致的時候,就造成了Activity的洩露。

5.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 傳進去,見下面程式碼:

綜述,即推薦使用靜態內部類 + WeakReference 這種方式。每次使用前注意判空。

前面提到了 WeakReference,所以這裡就簡單的說一下 Java 物件的幾種引用型別。

Java對引用的分類有 Strong reference, SoftReference, WeakReference, PhatomReference 四種。

在Android應用的開發中,為了防止記憶體溢位,在處理一些佔用記憶體大而且宣告週期較長的物件時候,可以儘量應用軟引用和弱引用技術。

軟/弱引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果軟引用所引用的物件被垃圾回收器回收,Java虛擬機器就會把這個軟引用加入到與之關聯的引用佇列中。利用這個佇列可以得知被回收的軟/弱引用的物件列表,從而為緩衝器清除已失效的軟/弱引用。

假設我們的應用會用到大量的預設圖片,比如應用中有預設的頭像,預設遊戲圖示等等,這些圖片很多地方會用到。如果每次都去讀取圖片,由於讀取檔案需要硬體操作,速度較慢,會導致效能較低。所以我們考慮將圖片快取起來,需要的時候直接從記憶體中讀取。但是,由於圖片佔用記憶體空間比較大,快取很多圖片需要很多的記憶體,就可能比較容易發生OutOfMemory異常。這時,我們可以考慮使用軟/弱引用技術來避免這個問題發生。以下就是高速緩衝器的雛形:

首先定義一個HashMap,儲存軟引用物件。

再來定義一個方法,儲存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()

1、finalize 方法被執行的時間不確定,不能依賴與它來釋放緊缺的資源。時間不確定的原因是: 虛擬機器呼叫GC的時間不確定 Finalize daemon執行緒被排程到的時間不確定

2、finalize 方法只會被執行一次,即使物件被複活,如果已經執行過了 finalize 方法,再次被 GC 時也不會再執行了,原因是:

含有 finalize 方法的 object 是在 new 的時候由虛擬機器生成了一個 finalize reference 在來引用到該Object的,而在 finalize 方法執行的時候,該 object 所對應的 finalize Reference 會被釋放掉,即使在這個時候把該 object 復活(即用強引用引用住該 object ),再第二次被 GC 的時候由於沒有了 finalize reference 與之對應,所以 finalize 方法不會再執行。

3、含有Finalize方法的object需要至少經過兩輪GC才有可能被釋放。

資源未關閉造成的記憶體洩漏

對於使用了BraodcastReceiver,ContentObserver,File,遊標 Cursor,Stream,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或者登出,否則這些資源將不會被回收,造成記憶體洩漏。

一些不良程式碼造成的記憶體壓力

有些程式碼並不造成記憶體洩露,但是它們,或是對沒使用的記憶體沒進行有效及時的釋放,或是沒有有效的利用已有的物件而是頻繁的申請新記憶體。

比如: Bitmap 沒呼叫 recycle()方法,對於 Bitmap 物件在不使用時,我們應該先呼叫 recycle() 釋放記憶體,然後才它設定為 null. 因為載入 Bitmap 物件的記憶體空間,一部分是 java 的,一部分 C 的(因為 Bitmap 分配的底層是通過 JNI 呼叫的 )。 而這個 recyle() 就是針對 C 部分的記憶體釋放。 構造 Adapter 時,沒有使用快取的 convertView ,每次都在建立新的 converView。這裡推薦使用 ViewHolder。

總結

對 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銷燬時及時關閉或者登出。

保持對物件生命週期的敏感,特別注意單例、靜態物件、全域性性集合等的生命週期。

(PS:第一次寫部落格,如有侵犯版權或者內容有誤情況,還望批評指正!)