Android 系統穩定性 - OOM(一)

2.1.1 什麼是內存溢出

 

2.1.2 爲什麼會有內存溢出

Android 主要應用在嵌入式設備當中,而嵌入式設備由於一些衆所周知的條件限制,通常都不會有很高的配置,特別是內存比較有限。如果我們編寫的代碼當中有太多的對內存使用不當的地方,難免會使得我們的設備運行緩慢,甚至是死機。爲了能夠使系統安全且快速的運行,Android 的每個應用程序都運行在單獨的進程中,這個進程是由 Zygote 進程孵化出來的,每個應用進程中都有且僅有一個虛擬機實例。如果程序在運行過程中出現了內存泄漏的問題,只會影響自己的進程,不會直接影響其他進程。

Java雖然有自己的垃圾回收機制,但並不是說用Java編寫的程序就不會內存溢出了。Java程序運行在虛擬機中,虛擬機初始化時會設定它的堆內存的上限值,在Android中這個上限值默認是“16m”,而你可以根據實際的硬件配置來調整這個上限值,調整的方法是在系統啓動時加載的某個配置文件中設置一個系統屬性:

dalvik.vm.heapsize=24m

當然也可以設置成更大的值(例如“32m”)。這樣Android中每個應用進程的DalvikVM實例的堆內存上限值就變成了24MB,也就是說一個應用進程中可以同時存在更多的Java數據對象了。有一些大型的應用程序(例如遊戲)運行時需要比較多的內存,heapsize太小的話根本無法運行,此時就需要考慮調整heapsize的大小了。heapsize的大小是同時對整個系統生效的,原生代碼中無法單獨的調整某一個Java進程的heapsize(除非我們自己修改源碼,不過我們從來沒這麼做過)。

當代碼中的缺陷造成內存泄漏時,泄漏的內存無法在虛擬機GC的時候被釋放,因爲這些內存被一些數據對象佔用着,而這些數據對象之所以沒有被釋放,可以歸結爲兩類情況:

a) 被強引用着

例如被一個正在運行的線程、一個類中的static變量強引用着,或者當前對象被註冊進了framework中的一些接口中。

b) JNI中的指針引用着

Framework中的一些類經常會在Java層創建一個對象,同時也在C++層創建一個對象,然後通過JNI讓這兩個對象相互引用(保存對方的地址),BinderProxy對象就是一個很典型的例子,在這種情況下,Java層的對象同樣不會被釋放。

當泄漏的內存隨着程序的運行越來越多時,最終就會達到heapsize設定的上限值,此時虛擬機就會拋出OutOfMemoryError錯誤,內存溢出了。

2.2 容易引起內存泄漏的常見問題

2.2.1 Cursor對象未正確關閉

關於此類問題其實已經是老生常談了,但是由於Android應用源碼中的缺陷和使用的場合比較複雜,所以還是會時常出現這類問題。

1. 問題舉例

Cursor cursor = getContentResolver().query(...);

        if (cursor.moveToNext()) {

        ... ...

}

2. 問題修正

Cursor cursor = null;

try {

        cursor = getContentResolver().query(...);

        if (cursor != null && cursor.moveToNext()) {

        ... ...

        }

} catch (Exception e) {

        ... ...

} finally {

        if (cursor != null) {

                cursor.close();

        }

}

3. 引申內容

(1) 實際在使用的時候代碼的邏輯通常會比上述示例要複雜的多,但總的原則是一定要在使用完畢Cursor以後正確的關閉。

(2) 如果你的Cursor需要在Activity的不同的生命週期方法中打開和關閉,那麼一般可以這樣做:

onCreate()中打開,在onDestroy()中關閉;

onStart() 中打開,在onStop() 中關閉;

onResume()中打開,在onPause() 中關閉;

即要在成對的生命週期方法中打開/關閉。

(3) 如果程序中使用了CursorAdapter(例如Music),那麼可以使用它的changeCursor(Cursor cursor)方法同時完成關閉舊Cursor使用新Cursor的操作。

(4) 至於在cursor.close時需不需要try...catchcursor非空時),其實在close時做的工作就是釋放資源,包括通過Binder跨進程註銷ContentObserver時已經捕獲了RemoteException異常,所以其實可以不用try...catch

(5) 關於deactiveclosedeactive不等同於close,看他們的API comments就能知道,如果deactive了一個Cursor,說明以後還是會用到它(利用requery方法),這個Cursor會釋放一部分資源,但是並沒有完全釋放;如果確認不再使用這個Cursor了,一定要close

(6)除了Cursor有時我們也會對Database對象做操作,例如要修正MediaProvider中的一個attachVolume方法,在每次檢測到attach的是一個externalvolume時就重新建立一個數據庫,而不是採用以前的,那麼在remove舊的數據庫對象的時候不要忘記關閉它。<!-- 第6點關於Database是否考慮去掉 -->

4. 影響範圍

如果沒有關閉Cursor,在測試次數足夠多的情況下,就會出現:

(1) 內存泄漏

我們先簡單的看一下Cursor的結構,這樣會更好理解。數據庫操作涉及到服務端的ContentProvider和客戶端程序,客戶端通常會通過ContentResolver.query函數查詢並獲取一個結果集的Cursor對象。而這個Cursor對象實際上也只是一個代理,因爲要考慮到客戶端和服務端在不同進程的情況,所以Cursor的使用本身也是利用了Binder機制的,而客戶端和服務端的數據共享是利用共享內存來實現的,如下圖所示。



 

客戶端和服務端使用的Cursor經過了層層封裝,顯得十分臃腫,但它們的工作其實可以簡單的從控制流和數據流兩個方面來看。在控制流方面,客戶端爲了能和遠端的服務端通信,使用實現了IBulkCursor接口的BulkCursorProxyCusorToBulkCursorAdapter對象,例如要獲取結果集數據時,客戶端通過BulkCursoryProxy.onMove函數調用到CursorToBulkCursorAdapter.onMove函數,然後再調用到SQLiteCursor.onMove函數來填充數據的。在數據流方面,服務端的SQLiteCursor將從數據庫中查詢到的結果集寫入到共享內存中,然後Binder調用返回到客戶端,客戶端就可以從共享內存中獲取到想要的數據了。客戶端的控制流和數據流的訪問由BulkCursorToCursorAdapter負責,服務端則是分別由CursorToBulkCursorAdapterSQLiteCursor負責。

如果Cursor沒有正常關閉,那麼客戶端和服務端的CursorWindow對象和申請的那塊共享內存都不會被回收,儘管其他相關的Java對象可能由於沒有強引用而被回收,但是真正佔用內存的通常是存放結果集數據的共享內存。大量的Cursor沒有關閉的話,你可能會看到以下類型的異常信息:

  • 創建新的Java對象時發現沒有足夠的內存,拋出內存溢出錯誤:OutOfMemoryError

  • 創建新的CursorWindow時無法申請到足夠的內存,可能的異常信息有:
    RuntimeException: No memory for native window object
    IllegalStateException: Couldn't init cursor window
    CursorWindow heap allocation failed 
    failed to create the CursorWindow heap

(2) 文件描述符泄漏

當然有可能很幸運,每次查詢的結果集都很小,做幾千次查詢都不會內存溢出,但是AndroidLinux內核還有另外一個限制,就是文件描述符的上限,這個上限默認是1024

文件描述符本身是一個整數,用來表示每一個被進程所打開的文件和Socket,第一個打開的文件是0,第二個是1,依此類推。而Linux給每個進程能打開的文件數量設置了一個上限,可以使用命令“ulimit -n”查看。另外,操作系統還有一個系統級的限制。

每次創建一個Cursor對象,都會向內核申請創建一塊共享內存,這塊內存以文件形式提供給應用進程,應用進程會獲得這個文件的描述符,並將其映射到自己的進程空間中。如果有大量的Cursor對象沒有正常關閉,可想而知就會有大量的共享內存的文件描述符無法關閉,同時再加上應用進程中的其他文件描述符,就很容易達到1024這個上限,一旦達到,進程就掛掉了。



提示:可以到系統的“/proc/進程號/fd”目錄中查看進程所有的文件描述符。

 

(3) GREF has increased to 2001

先說明一下“死亡代理”的概念。利用Binder做進程間通信時,允許對Binder的客戶端代理設置一個DeathRecipient對象,它只有一個名爲binderDied的函數。當Binder的服務端進程死掉了,binder驅動會通知客戶端進程,最終回調DeathRecipient對象的binderDied函數,客戶端進程可以藉此做一些清理工作。

需要注意的是,“死亡代理”的概念只對進程間通信有效,對進程內通信沒有意義;另外,Binder的客戶端和服務端的概念是相對的,例如BulkCursorProxyCursorToBulkCursorAdapter的客戶端,而後者又有一個IContentObserver的客戶端,其對應的服務端在BulkCursorToCursorAdaptergetObserver函數中創建。這裏需要關注的就是在CursorToBulkCursorAdapter對象被創建時,會同時將該對象註冊爲IContentObserver的客戶端對象的“死亡代理”,代碼如下:

CursorToBulkCursorAdaptor的內部類ContentObserverProxy的構造函數中

public ContentObserverProxy(IContentObserver remoteObserver, DeathRecipient recipient) {

        super(null);

        mRemote = remoteObserver;

        try {

                //此處的recipient就是CursorToBulkCursorAdapter對象

                remoteObserver.asBinder().linkToDeath(recipient, 0);

        catch (RemoteException e) {

        }

}

 

死亡代理”對象的引用會被Native層的Binder代理對象的mObituaries集合引用,所以“死亡代理”對象及其關聯對象由於被強引用而不會被垃圾回收掉,同時JNI在實現linkToDeath函數的過程中也創建了一些具有全局性的引用,被稱作“Global Reference(簡寫爲GREF)”,每一個GREF都會被記錄到虛擬機中維護的一個“全局引用表”中。

eng模式下,JNI全局引用計數(GREF)有一個上限值爲2000,如果大量Cursor對象沒有被正常關閉,服務端進程就會因爲“死亡代理”對象的創建使得虛擬機中的全局引用計數增多,當超過2000時,虛擬機就會拋出異常,導致進程掛掉,典型的異常信息就是“GREF has increased to 2001”



提示:全局引用計數的上限2000已經是一個比較大的值,正常情況下很難達到。Androideng模式下開啓這項檢查,就是爲了能夠在開發階段發現Native層的內存泄漏問題。在usr模式下這項檢查會被禁用,此時如果有內存泄漏就只有等到拋出內存溢出錯誤或者文件描述符超出上限等其他異常時才能發現了。

Cursor未正常關閉是導致GREF越界的原因之一,後續會在其他章節中詳細討論。

2.2.2 釋放對象的引用

內存的問題是Bugzilla中的常客,經常會在不經意間遺留一些對象沒有釋放或銷燬

1. 靜態成員變量

有時因爲一些原因(比如希望節省Activity初始化時間等),將一些對象設置爲static的,比如:

private static TextView mTv;

... ...

mTv = (TextView) findViewById(...);

而且沒有在Activity退出時釋放mTv的引用,那麼此時mTv本身,和與mTv相關的那個Activity的對象也不會在GC時被釋放掉,Activity強引用的其他對象也無法被釋放掉,這樣就造成了內存泄漏。如果沒有充分的理由,或者不能夠清楚的控制這樣做帶來的影響,請不要這樣寫代碼。

2. 正確註冊/註銷監聽器對象

經常要用到一些XxxListener對象,或者是XxxObserverXxxReceiver對象,然後用registerXxx方法註冊,用unregisterXxx方法註銷。本身用法也很簡單,但是從一些實際開發中的代碼來看,仍然會有一些問題:

(1) registerXxxunregisterXxx方法的調用通常也和Cursor的打開/關閉類似,在Activity的生命週期中成對的出現即可:

在 onCreate() 中 register,在 onDestroy() 中 unregitster

在 onStart() 中 register,在 onStop() 中 unregitster

在 onResume() 中 register,在 onPause() 中 unregitster

(2) 忘記unregister

以前看到過一段代碼,在Activity中定義了一個PhoneStateListener的對象,將其註冊到TelephonyManager中:

TelephonyManager.listen(lPhoneStateListener.LISTEN_SERVICE_STATE);

但是在Activity退出的時候註銷掉這個監聽,即沒有調用以下方法:

TelephonyManager.listen(lPhoneStateListener.LISTEN_NONE);

因爲PhoneStateListener的成員變量callback,被註冊到了TelephonyRegistry中,TelephonyRegistry是後臺的一個服務會一直運行着。所以如果不註銷,則callback對象無法被釋放,PhoneStateListener對象也就無法被釋放,最終導致Activity對象無法被釋放。

3. 適當的使用SoftReferenceWeakReference

如果要寫一個緩存之類的類(例如圖片緩存),建議使用SoftReference,而不要直接用強引用,例如:

private final ConcurrentHashMap<Long, SoftReference<Bitmap>> mBitmapCache = new ConcurrentHashMap<LongSoftReference<Bitmap>>();

當加載的圖片過多,應用可用堆內存不足的時候,就可以自動的釋放這些緩存的Bitmap對象。

關於Java中的強引用、軟引用、弱引用和虛引用是一些比較重要的概念,在Android開發中經常會用到。

2.2.3 構造 Adapter 時,沒有使用緩存的 convertView

以構造 ListView 的 BaseAdapter 爲例,在 BaseAdapter 中提供了以下方法:

public View getView(int positionView convertViewViewGroup parent)

來向 ListView 提供每一個 item 所需要的 view 對象。初始時 ListView 會從 BaseAdapter 中根據當前的屏幕布局實例化一定數量的 view 對象,同時 ListView 會將這些 view 對象緩存起來 。當向上滾動ListView 時,原先位於最上面的 list item 的 view 對象會被回收,然後被用來構造新出現的最下面的listitem。這個構造過程就是由 getView()方法完成的,getView()的第二個形參 View convertView 就是被緩存起來的 list item 的 view 對象(初始化時緩存中沒有 view對象則 convertView 是 null)。由此可以看出,如果我們不去使用 convertView,而是每次都在 getView()中重新實例化一個 View 對象的話,即浪費資源也浪費時間,也會使得內存佔用越來越大ListView 回收listitem 的 view 對象的過程可以查看:android.widget.AbsListView類中的addScrapView(View scrap) 方法。

示例代碼:

public View getView(int positionView convertViewViewGroup parent) {

        View view = new Xxx(...);

        ... ...

        return view;

}

修正示例代碼:

public View getView(int positionView convertViewViewGroup parent) {

        View view = null;

        if (convertView != null) {

                view = convertView;

                populate(viewgetItem(position));

                ...

        } else {

                view = new Xxx(...);

                ...

        }

        return view;

}

2.2.4 Bitmap 對象不再使用時調用 recycle()釋放內存

有時我們會自己操作 Bitmap 對象,如果一個 Bitmap 對象比較佔內存,當它不再被使用的時候,可以調用 Bitmap.recycle()方法回收此對象的像素所佔用的內存,但這不是必須的 ,視情況而定。可以看一下代碼中的註釋:



/**

* Free up the memory associated with this bitmap's pixelsand mark the

* bitmap as "dead"meaning it will throw an exception if getPixels() or

* setPixels() is calledand will draw nothing. This operation cannot be

* reversedso it should only be called if you are sure there are no

* further uses for the bitmap. This is an advanced calland normally need

* not be calledsince the normal GC process will free up this memory when

* there are no more references to this bitmap.

*/

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