面試官:Android系統中GC什麼情況下會出現OOM

這個問題可以這樣回答:

(1)數據庫的cursor沒有關閉。

(2)Bitmap對象不使用時沒有采用recycle()釋放內存。

(3)Activity中的對象的生命週期大於Activity。

(4)忘記註銷監聽器或者觀察者。

(5)由非靜態內部類導致。

(6)構造Adapter時,沒有使用緩存contentview。

(7)Handler使用不當也可以造成內存泄漏的發生。

解釋一下GC:

GC是垃圾收集的意思(Gabage Collection),內存處理是編程人員容易出現問題的地方,忘記或者錯誤的內存回收會導致程序或系統的不穩定甚至崩潰,Java 提供的GC功能可以自動監測對象是否超過作用域從而達到自動回收內存的目的,Java語言沒有提供釋放已分配內存的顯示操作方法。

舉例說一下吧:

1、集合類泄漏

集合類如果僅僅有添加元素的方法,而沒有相應的刪除機制,導致內存被佔用。如果這個集合類是全局性的變量 (比如類中的靜態屬性,全局性的 map 等即有靜態引用或 final 一直指向它),那麼沒有相應的刪除機制,很可能導致集合所佔用的內存只增不減。例如下面的例子:

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

在這個例子中,我們循環申請Object對象,並將所申請的對象放入一個 Vector 中,如果我們僅僅釋放引用本身,那麼 Vector 仍然引用該對象,所以這個對象對 GC 來說是不可回收的。因此,如果對象加入到Vector 後,還必須從 Vector 中刪除,最簡單的方法就是將 Vector 對象設置爲 null。

當然實際上我們在項目中肯定不會寫這麼 2B 的代碼,但稍不注意還是很容易出現這種情況,比如我們都喜歡通過 HashMap 做一些緩存之類的事,這種情況就要多留一些心眼。

2、單例造成的內存泄漏

public class AppManager {
    private static AppManager mAppManager;
    private Context mContext;
    private AppManager(Context pContext) {
        mContext = pContext;
    }
    public static AppManager getInstance(Context pContext) {
        if (mAppManager == null) {
            mAppManager = new AppManager(pContext);
        }
        return mAppManager;
    }
}

這是一個普通的單例模式,當創建這個單例的時候,由於需要傳入一個Context,所以這個Context的生命週期的長短至關重要:

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

2、如果此時傳入的是 Activity 的 Context,當這個 Context 所對應的 Activity 退出時,由於該 Context 的引用被單例對象所持有,其生命週期等於整個應用程序的生命週期,所以當前 Activity 退出時它的內存並不會被回收,這就造成泄漏了。

正確的方式應該改爲下面這種方式:

public class AppManager {
    private static AppManager mAppManager;
    private Context mContext;
    private AppManager(Context pContext) {
        mContext = pContext.getApplicationContext();
    }
    public static AppManager getInstance(Context pContext) {
        if (mAppManager == null) {
            mAppManager = new AppManager(pContext);
        }
        return mAppManager;
    }
}

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的應用場景如下:

1187237-fb32b0f992da4781

其中: 不推薦表示 Application 和 Service 可以啓動一個 Activity,不過需要創建一個新的 task 任務隊列。而對於 Dialog 而言,只有在 Activity 中才能創建。

 

4、匿名內部類

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

public class MainActivity extends Activity {
...
Runnable ref1 = new MyRunable();
Runnable ref2 = new Runnable() {
    @Override
    public void run() {

    }
};
   ...
}

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

                                                  nimingneibulei

 

可以看到,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 傳進去,見下面代碼:

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 mActivity;

    public MyHandler(SampleActivity activity) {
      mActivity = new WeakReference(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 這種方式。每次使用前注意判空。但是,這樣在回收時也可以回收 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);

上面提到了 WeakReference,所以這裏就簡單的說一下 Java 對象的幾種引用類型,有 Strong reference, SoftReference, WeakReference, PhatomReference 四種。

ruoyinyong

在Android應用的開發中,爲了防止內存溢出,在處理一些佔用內存大而且聲明週期較長的對象時候,可以儘量應用軟引用和弱引用技術。

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

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

首先定義一個HashMap,保存軟引用對象。

private Map<String, SoftReference> mImageCache = new HashMap <String, SoftReference> ();

再來定義一個方法,保存Bitmap的軟引用到HashMap。

public class CacheBySoftRef {
    //首先定義一個HashMap,保存軟引用對象。
    private Map<String, SoftReference>; mImageCache = new HashMap<String, SoftReference>();

    /**
     * 再來定義一個方法,保存Bitmap的軟引用的HashMap
     * @param pPath
     */
    public void addBitMapToCache(String pPath){
        //強引用的Bitmap對象
        Bitmap _bitmap = BitmapFactory.decodeFile(pPath);
        //軟引用的Bitmap對象
        SoftReference _SoftBitmap = new SoftReference<>(_bitmap);
        //添加該對象到Map中進行緩存
        mImageCache.put(pPath, _SoftBitmap);
    }

    /**
     * 獲取的時候,可以通過SoftReference的get方法得到Bitmap對象。
     *
     * @param pPath
     */
    public Bitmap getBitMapToCache(String pPath){
        //從緩存中取軟引用的Bitmap對象
        SoftReference _SoftBitmap = mImageCache.get(pPath);
        //判斷是否存在軟引用
        if (null == _SoftBitmap){
            return null;
        }
        //通過軟引用取出Bitmap對象,如果由於內容不足Bitmap被回收,將取得Null,如果沒有被回收,則可重複使用提高速度。
        Bitmap _Bitmap = _SoftBitmap.get();
        return _Bitmap;
    }
}

使用軟引用以後,在OutOfMemory異常發生之前,這些緩存的圖片資源的內存空間可以被釋放掉的,從而避免內存達到上限,避免Crash發生。

如果只是想避免OutOfMemory異常的發生,則可以使用軟引用。如果對於應用的性能更在意,想盡快回收一些佔用內存比較大的對象,則可以使用弱引用。

另外可以根據對象是否經常使用來判斷選擇軟引用還是弱引用。如果該對象可能會經常使用的,就儘量用軟引用。如果該對象不被使用的可能性更大些,就可以用弱引用。

5、儘量避免使用 static 成員變量

如果成員變量被聲明爲 static,那我們都知道其生命週期將與整個app進程生命週期一樣。

這會導致一系列問題,如果你的app進程設計上是長駐內存的,那即使app切到後臺,這部分內存也不會被釋放。按照現在手機app內存管理機制,佔內存較大的後臺進程將優先回收,如果此app做過進程互保保活,那會造成app在後臺頻繁重啓。當手機安裝了你參與開發的app以後一夜時間手機被消耗空了電量、流量,你的app不得不被用戶卸載或者靜默。

這裏修復的方法是:

不要在類初始時初始化靜態成員。可以考慮lazy初始化。架構設計上要思考是否真的有必要這樣做,儘量避免。如果架構需要這麼設計,那麼此對象的生命週期你有責任管理起來。

6、避免 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纔有可能被釋放。

6、資源未關閉造成的內存泄漏

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

7、一些不良代碼造成的內存壓力

有些代碼並不造成內存泄露,但是它們,或是對沒使用的內存沒進行有效及時的釋放,或是沒有有效的利用已有的對象而是頻繁的申請新內存。

比如:

(1)Bitmap 沒調用 recycle()方法,對於 Bitmap 對象在不使用時,我們應該先調用 recycle() 釋放內存,然後才它設置爲 null. 因爲加載 Bitmap 對象的內存空間,一部分是 java 的,一部分 C 的(因爲 Bitmap 分配的底層是通過 JNI 調用的 )。 而這個 recyle() 就是針對 C 部分的內存釋放。

(2)構造 Adapter 時,沒有使用緩存的 convertView ,每次都在創建新的 converView。這裏推薦使用 ViewHolder。

8、謹慎使用依賴注入框架

使用類似Guice或者RoboGuice等框架注入代碼,在某種程度上可以簡化你的代碼。然而,那些注入框架會通過掃描你的代碼執行許多初始化的操作,這會導致你的代碼需要大量的內存空間來mapping代碼,而且mapped pages會長時間的被保留在內存中。除非真的很有必要,建議謹慎使用這種技術。

9、優化佈局層次,減少內存消耗

越扁平化的視圖佈局,佔用的內存就越少,效率越高。我們需要儘量保證佈局足夠扁平化,當使用系統提供的View無法實現足夠扁平的時候考慮使用自定義View來達到目的。

10、Try catch某些大內存分配的操作

在某些情況下,我們需要事先評估那些可能發生OOM的代碼,對於這些可能發生OOM的代碼,加入catch機制,可以考慮在catch裏面嘗試一次降級的內存分配操作。例如decode bitmap的時候,catch到OOM,可以嘗試把採樣比例再增加一倍之後,再次嘗試decode。

11、避免在Android裏面使用Enum

在StackOverFlow等問答社區常常出現關於在Android系統裏面使用枚舉類型的性能討論,關於這一點,Android官方的Training課程裏面有下面這樣一句話:

For example, enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.

 

enums

 

關於enum的效率,請看下面的討論。假設我們有這樣一份代碼,編譯之後的dex大小是2556 bytes,在此基礎之上,添加一些如下代碼,這些代碼使用普通static常量相關作爲判斷值:

enums1

增加上面那段代碼之後,編譯成dex的大小是2680 bytes,相比起之前的2556 bytes只增加124 bytes。假如換做使用enum,情況如下:

enums2

 

使用enum之後的dex大小是4188 bytes,相比起2556增加了1632 bytes,增長量是使用static int的13倍。不僅僅如此,使用enum,運行時還會產生額外的內存佔用,如下圖所示:

enums3

Android官方強烈建議不要在Android程序裏面使用到enum。

粉絲交流會:

       

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章