文章都為原創(chuàng),轉(zhuǎn)載請(qǐng)注明出處,未經(jīng)允許而盜用者追究法律責(zé)任。 很久之前寫的了,留著有點(diǎn)浪費(fèi),共享之。 編寫者:李文棟 2.1.1 什么是內(nèi)存溢出
2.1.2 為什么會(huì)有內(nèi)存溢出Android 主要應(yīng)用在嵌入式設(shè)備當(dāng)中,而嵌入式設(shè)備由于一些眾所周知的條件限制,通常都不會(huì)有很高的配置,特別是內(nèi)存比較有限。如果我們編寫的代碼當(dāng)中有太多的對(duì)內(nèi)存使用不當(dāng)?shù)牡胤?,難免會(huì)使得我們的設(shè)備運(yùn)行緩慢,甚至是死機(jī)。為了能夠使系統(tǒng)安全且快速的運(yùn)行,Android 的每個(gè)應(yīng)用程序都運(yùn)行在單獨(dú)的進(jìn)程中,這個(gè)進(jìn)程是由 Zygote 進(jìn)程孵化出來的,每個(gè)應(yīng)用進(jìn)程中都有且僅有一個(gè)虛擬機(jī)實(shí)例。如果程序在運(yùn)行過程中出現(xiàn)了內(nèi)存泄漏的問題,只會(huì)影響自己的進(jìn)程,不會(huì)直接影響其他進(jìn)程。 Java雖然有自己的垃圾回收機(jī)制,但并不是說用Java編寫的程序就不會(huì)內(nèi)存溢出了。Java程序運(yùn)行在虛擬機(jī)中,虛擬機(jī)初始化時(shí)會(huì)設(shè)定它的堆內(nèi)存的上限值,在Android中這個(gè)上限值默認(rèn)是“16m”,而你可以根據(jù)實(shí)際的硬件配置來調(diào)整這個(gè)上限值,調(diào)整的方法是在系統(tǒng)啟動(dòng)時(shí)加載的某個(gè)配置文件中設(shè)置一個(gè)系統(tǒng)屬性: dalvik.vm.heapsize=24m 當(dāng)然也可以設(shè)置成更大的值(例如“32m”)。這樣Android中每個(gè)應(yīng)用進(jìn)程的DalvikVM實(shí)例的堆內(nèi)存上限值就變成了24MB,也就是說一個(gè)應(yīng)用進(jìn)程中可以同時(shí)存在更多的Java數(shù)據(jù)對(duì)象了。有一些大型的應(yīng)用程序(例如游戲)運(yùn)行時(shí)需要比較多的內(nèi)存,heapsize太小的話根本無法運(yùn)行,此時(shí)就需要考慮調(diào)整heapsize的大小了。heapsize的大小是同時(shí)對(duì)整個(gè)系統(tǒng)生效的,原生代碼中無法單獨(dú)的調(diào)整某一個(gè)Java進(jìn)程的heapsize(除非我們自己修改源碼,不過我們從來沒這么做過)。 當(dāng)代碼中的缺陷造成內(nèi)存泄漏時(shí),泄漏的內(nèi)存無法在虛擬機(jī)GC的時(shí)候被釋放,因?yàn)檫@些內(nèi)存被一些數(shù)據(jù)對(duì)象占用著,而這些數(shù)據(jù)對(duì)象之所以沒有被釋放,可以歸結(jié)為兩類情況: a) 被強(qiáng)引用著 例如被一個(gè)正在運(yùn)行的線程、一個(gè)類中的static變量強(qiáng)引用著,或者當(dāng)前對(duì)象被注冊(cè)進(jìn)了framework中的一些接口中。 b) 被JNI中的指針引用著 Framework中的一些類經(jīng)常會(huì)在Java層創(chuàng)建一個(gè)對(duì)象,同時(shí)也在C++層創(chuàng)建一個(gè)對(duì)象,然后通過JNI讓這兩個(gè)對(duì)象相互引用(保存對(duì)方的地址),BinderProxy對(duì)象就是一個(gè)很典型的例子,在這種情況下,Java層的對(duì)象同樣不會(huì)被釋放。 當(dāng)泄漏的內(nèi)存隨著程序的運(yùn)行越來越多時(shí),最終就會(huì)達(dá)到heapsize設(shè)定的上限值,此時(shí)虛擬機(jī)就會(huì)拋出OutOfMemoryError錯(cuò)誤,內(nèi)存溢出了。 2.2 容易引起內(nèi)存泄漏的常見問題2.2.1 Cursor對(duì)象未正確關(guān)閉關(guān)于此類問題其實(shí)已經(jīng)是老生常談了,但是由于Android應(yīng)用源碼中的缺陷和使用的場合比較復(fù)雜,所以還是會(huì)時(shí)常出現(xiàn)這類問題。 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. 引申內(nèi)容 (1) 實(shí)際在使用的時(shí)候代碼的邏輯通常會(huì)比上述示例要復(fù)雜的多,但總的原則是一定要在使用完畢Cursor以后正確的關(guān)閉。 (2) 如果你的Cursor需要在Activity的不同的生命周期方法中打開和關(guān)閉,那么一般可以這樣做: 在onCreate()中打開,在onDestroy()中關(guān)閉; 在onStart() 中打開,在onStop() 中關(guān)閉; 在onResume()中打開,在onPause() 中關(guān)閉; 即要在成對(duì)的生命周期方法中打開/關(guān)閉。 (3) 如果程序中使用了CursorAdapter(例如Music),那么可以使用它的changeCursor(Cursor cursor)方法同時(shí)完成關(guān)閉舊Cursor使用新Cursor的操作。 (4) 至于在cursor.close時(shí)需不需要try...catch(cursor非空時(shí)),其實(shí)在close時(shí)做的工作就是釋放資源,包括通過Binder跨進(jìn)程注銷ContentObserver時(shí)已經(jīng)捕獲了RemoteException異常,所以其實(shí)可以不用try...catch。 (5) 關(guān)于deactive和close,deactive不等同于close,看他們的API comments就能知道,如果deactive了一個(gè)Cursor,說明以后還是會(huì)用到它(利用requery方法),這個(gè)Cursor會(huì)釋放一部分資源,但是并沒有完全釋放;如果確認(rèn)不再使用這個(gè)Cursor了,一定要close。 (6)除了Cursor有時(shí)我們也會(huì)對(duì)Database對(duì)象做操作,例如要修正MediaProvider中的一個(gè)attachVolume方法,在每次檢測到attach的是一個(gè)external的volume時(shí)就重新建立一個(gè)數(shù)據(jù)庫,而不是采用以前的,那么在remove舊的數(shù)據(jù)庫對(duì)象的時(shí)候不要忘記關(guān)閉它。<!-- 第6點(diǎn)關(guān)于Database是否考慮去掉 --> 4. 影響范圍 如果沒有關(guān)閉Cursor,在測試次數(shù)足夠多的情況下,就會(huì)出現(xiàn): (1) 內(nèi)存泄漏 我們先簡單的看一下Cursor的結(jié)構(gòu),這樣會(huì)更好理解。數(shù)據(jù)庫操作涉及到服務(wù)端的ContentProvider和客戶端程序,客戶端通常會(huì)通過ContentResolver.query函數(shù)查詢并獲取一個(gè)結(jié)果集的Cursor對(duì)象。而這個(gè)Cursor對(duì)象實(shí)際上也只是一個(gè)代理,因?yàn)橐紤]到客戶端和服務(wù)端在不同進(jìn)程的情況,所以Cursor的使用本身也是利用了Binder機(jī)制的,而客戶端和服務(wù)端的數(shù)據(jù)共享是利用共享內(nèi)存來實(shí)現(xiàn)的,如下圖所示。
客戶端和服務(wù)端使用的Cursor經(jīng)過了層層封裝,顯得十分臃腫,但它們的工作其實(shí)可以簡單的從控制流和數(shù)據(jù)流兩個(gè)方面來看。在控制流方面,客戶端為了能和遠(yuǎn)端的服務(wù)端通信,使用實(shí)現(xiàn)了IBulkCursor接口的BulkCursorProxy和CusorToBulkCursorAdapter對(duì)象,例如要獲取結(jié)果集數(shù)據(jù)時(shí),客戶端通過BulkCursoryProxy.onMove函數(shù)調(diào)用到CursorToBulkCursorAdapter.onMove函數(shù),然后再調(diào)用到SQLiteCursor.onMove函數(shù)來填充數(shù)據(jù)的。在數(shù)據(jù)流方面,服務(wù)端的SQLiteCursor將從數(shù)據(jù)庫中查詢到的結(jié)果集寫入到共享內(nèi)存中,然后Binder調(diào)用返回到客戶端,客戶端就可以從共享內(nèi)存中獲取到想要的數(shù)據(jù)了??蛻舳说目刂屏骱蛿?shù)據(jù)流的訪問由BulkCursorToCursorAdapter負(fù)責(zé),服務(wù)端則是分別由CursorToBulkCursorAdapter和SQLiteCursor負(fù)責(zé)。 如果Cursor沒有正常關(guān)閉,那么客戶端和服務(wù)端的CursorWindow對(duì)象和申請(qǐng)的那塊共享內(nèi)存都不會(huì)被回收,盡管其他相關(guān)的Java對(duì)象可能由于沒有強(qiáng)引用而被回收,但是真正占用內(nèi)存的通常是存放結(jié)果集數(shù)據(jù)的共享內(nèi)存。大量的Cursor沒有關(guān)閉的話,你可能會(huì)看到以下類型的異常信息:
(2) 文件描述符泄漏 當(dāng)然有可能很幸運(yùn),每次查詢的結(jié)果集都很小,做幾千次查詢都不會(huì)內(nèi)存溢出,但是Android的Linux內(nèi)核還有另外一個(gè)限制,就是文件描述符的上限,這個(gè)上限默認(rèn)是1024。 文件描述符本身是一個(gè)整數(shù),用來表示每一個(gè)被進(jìn)程所打開的文件和Socket,第一個(gè)打開的文件是0,第二個(gè)是1,依此類推。而Linux給每個(gè)進(jìn)程能打開的文件數(shù)量設(shè)置了一個(gè)上限,可以使用命令“ulimit -n”查看。另外,操作系統(tǒng)還有一個(gè)系統(tǒng)級(jí)的限制。 每次創(chuàng)建一個(gè)Cursor對(duì)象,都會(huì)向內(nèi)核申請(qǐng)創(chuàng)建一塊共享內(nèi)存,這塊內(nèi)存以文件形式提供給應(yīng)用進(jìn)程,應(yīng)用進(jìn)程會(huì)獲得這個(gè)文件的描述符,并將其映射到自己的進(jìn)程空間中。如果有大量的Cursor對(duì)象沒有正常關(guān)閉,可想而知就會(huì)有大量的共享內(nèi)存的文件描述符無法關(guān)閉,同時(shí)再加上應(yīng)用進(jìn)程中的其他文件描述符,就很容易達(dá)到1024這個(gè)上限,一旦達(dá)到,進(jìn)程就掛掉了。 提示:可以到系統(tǒng)的“/proc/進(jìn)程號(hào)/fd”目錄中查看進(jìn)程所有的文件描述符。
(3) GREF has increased to 2001 先說明一下“死亡代理”的概念。利用Binder做進(jìn)程間通信時(shí),允許對(duì)Binder的客戶端代理設(shè)置一個(gè)DeathRecipient對(duì)象,它只有一個(gè)名為binderDied的函數(shù)。當(dāng)Binder的服務(wù)端進(jìn)程死掉了,binder驅(qū)動(dòng)會(huì)通知客戶端進(jìn)程,最終回調(diào)DeathRecipient對(duì)象的binderDied函數(shù),客戶端進(jìn)程可以借此做一些清理工作。 需要注意的是,“死亡代理”的概念只對(duì)進(jìn)程間通信有效,對(duì)進(jìn)程內(nèi)通信沒有意義;另外,Binder的客戶端和服務(wù)端的概念是相對(duì)的,例如BulkCursorProxy是CursorToBulkCursorAdapter的客戶端,而后者又有一個(gè)IContentObserver的客戶端,其對(duì)應(yīng)的服務(wù)端在BulkCursorToCursorAdapter的getObserver函數(shù)中創(chuàng)建。這里需要關(guān)注的就是在CursorToBulkCursorAdapter對(duì)象被創(chuàng)建時(shí),會(huì)同時(shí)將該對(duì)象注冊(cè)為IContentObserver的客戶端對(duì)象的“死亡代理”,代碼如下: CursorToBulkCursorAdaptor的內(nèi)部類ContentObserverProxy的構(gòu)造函數(shù)中 public ContentObserverProxy(IContentObserver remoteObserver, DeathRecipient recipient) { super(null); mRemote = remoteObserver; try { //此處的recipient就是CursorToBulkCursorAdapter對(duì)象 remoteObserver.asBinder().linkToDeath(recipient, 0); } catch (RemoteException e) { } }
“死亡代理”對(duì)象的引用會(huì)被Native層的Binder代理對(duì)象的mObituaries集合引用,所以“死亡代理”對(duì)象及其關(guān)聯(lián)對(duì)象由于被強(qiáng)引用而不會(huì)被垃圾回收掉,同時(shí)JNI在實(shí)現(xiàn)linkToDeath函數(shù)的過程中也創(chuàng)建了一些具有全局性的引用,被稱作“Global Reference(簡寫為GREF)”,每一個(gè)GREF都會(huì)被記錄到虛擬機(jī)中維護(hù)的一個(gè)“全局引用表”中。 eng模式下,JNI全局引用計(jì)數(shù)(GREF)有一個(gè)上限值為2000,如果大量Cursor對(duì)象沒有被正常關(guān)閉,服務(wù)端進(jìn)程就會(huì)因?yàn)椤八劳龃怼睂?duì)象的創(chuàng)建使得虛擬機(jī)中的全局引用計(jì)數(shù)增多,當(dāng)超過2000時(shí),虛擬機(jī)就會(huì)拋出異常,導(dǎo)致進(jìn)程掛掉,典型的異常信息就是“GREF has increased to 2001”。 提示:全局引用計(jì)數(shù)的上限2000已經(jīng)是一個(gè)比較大的值,正常情況下很難達(dá)到。Android在eng模式下開啟這項(xiàng)檢查,就是為了能夠在開發(fā)階段發(fā)現(xiàn)Native層的內(nèi)存泄漏問題。在usr模式下這項(xiàng)檢查會(huì)被禁用,此時(shí)如果有內(nèi)存泄漏就只有等到拋出內(nèi)存溢出錯(cuò)誤或者文件描述符超出上限等其他異常時(shí)才能發(fā)現(xiàn)了。 Cursor未正常關(guān)閉是導(dǎo)致GREF越界的原因之一,后續(xù)會(huì)在其他章節(jié)中詳細(xì)討論。 2.2.2 釋放對(duì)象的引用內(nèi)存的問題是Bugzilla中的???,經(jīng)常會(huì)在不經(jīng)意間遺留一些對(duì)象沒有釋放或銷毀。 1. 靜態(tài)成員變量 有時(shí)因?yàn)橐恍┰?/span>(比如希望節(jié)省Activity初始化時(shí)間等),將一些對(duì)象設(shè)置為static的,比如: private static TextView mTv; ... ... mTv = (TextView) findViewById(...); 而且沒有在Activity退出時(shí)釋放mTv的引用,那么此時(shí)mTv本身,和與mTv相關(guān)的那個(gè)Activity的對(duì)象也不會(huì)在GC時(shí)被釋放掉,Activity強(qiáng)引用的其他對(duì)象也無法被釋放掉,這樣就造成了內(nèi)存泄漏。如果沒有充分的理由,或者不能夠清楚的控制這樣做帶來的影響,請(qǐng)不要這樣寫代碼。 2. 正確注冊(cè)/注銷監(jiān)聽器對(duì)象 經(jīng)常要用到一些XxxListener對(duì)象,或者是XxxObserver、XxxReceiver對(duì)象,然后用registerXxx方法注冊(cè),用unregisterXxx方法注銷。本身用法也很簡單,但是從一些實(shí)際開發(fā)中的代碼來看,仍然會(huì)有一些問題: (1) registerXxx和unregisterXxx方法的調(diào)用通常也和Cursor的打開/關(guān)閉類似,在Activity的生命周期中成對(duì)的出現(xiàn)即可: 在 onCreate() 中 register,在 onDestroy() 中 unregitster; 在 onStart() 中 register,在 onStop() 中 unregitster; 在 onResume() 中 register,在 onPause() 中 unregitster; (2) 忘記unregister 以前看到過一段代碼,在Activity中定義了一個(gè)PhoneStateListener的對(duì)象,將其注冊(cè)到TelephonyManager中: TelephonyManager.listen(l,PhoneStateListener.LISTEN_SERVICE_STATE); 但是在Activity退出的時(shí)候注銷掉這個(gè)監(jiān)聽,即沒有調(diào)用以下方法: TelephonyManager.listen(l,PhoneStateListener.LISTEN_NONE); 因?yàn)?/span>PhoneStateListener的成員變量callback,被注冊(cè)到了TelephonyRegistry中,TelephonyRegistry是后臺(tái)的一個(gè)服務(wù)會(huì)一直運(yùn)行著。所以如果不注銷,則callback對(duì)象無法被釋放,PhoneStateListener對(duì)象也就無法被釋放,最終導(dǎo)致Activity對(duì)象無法被釋放。 3. 適當(dāng)?shù)氖褂?/strong>SoftReference、WeakReference 如果要寫一個(gè)緩存之類的類(例如圖片緩存),建議使用SoftReference,而不要直接用強(qiáng)引用,例如: private final ConcurrentHashMap<Long, SoftReference<Bitmap>> mBitmapCache = new ConcurrentHashMap<Long, SoftReference<Bitmap>>(); 當(dāng)加載的圖片過多,應(yīng)用可用堆內(nèi)存不足的時(shí)候,就可以自動(dòng)的釋放這些緩存的Bitmap對(duì)象。 關(guān)于Java中的強(qiáng)引用、軟引用、弱引用和虛引用是一些比較重要的概念,在Android開發(fā)中經(jīng)常會(huì)用到。 2.2.3 構(gòu)造 Adapter 時(shí),沒有使用緩存的 convertView以構(gòu)造 ListView 的 BaseAdapter 為例,在 BaseAdapter 中提供了以下方法: public View getView(int position,View convertView,ViewGroup parent) 來向 ListView 提供每一個(gè) item 所需要的 view 對(duì)象。初始時(shí) ListView 會(huì)從 BaseAdapter 中根據(jù)當(dāng)前的屏幕布局實(shí)例化一定數(shù)量的 view 對(duì)象,同時(shí) ListView 會(huì)將這些 view 對(duì)象緩存起來 。當(dāng)向上滾動(dòng)ListView 時(shí),原先位于最上面的 list item 的 view 對(duì)象會(huì)被回收,然后被用來構(gòu)造新出現(xiàn)的最下面的 listitem。這個(gè)構(gòu)造過程就是由 getView()方法完成的,getView()的第二個(gè)形參 View convertView 就是被緩存起來的 list item 的 view 對(duì)象(初始化時(shí)緩存中沒有 view對(duì)象則 convertView 是 null)。由此可以看出,如果我們不去使用 convertView,而是每次都在 getView()中重新實(shí)例化一個(gè) View 對(duì)象的話,即浪費(fèi)資源也浪費(fèi)時(shí)間,也會(huì)使得內(nèi)存占用越來越大ListView 回收listitem 的 view 對(duì)象的過程可以查看:android.widget.AbsListView類中的addScrapView(View scrap) 方法。 示例代碼: public View getView(int position,View convertView,ViewGroup parent) { View view = new Xxx(...); ... ... return view; } 修正示例代碼: public View getView(int position,View convertView,ViewGroup parent) { View view = null; if (convertView != null) { view = convertView; populate(view,getItem(position)); ... } else { view = new Xxx(...); ... } return view; } 2.2.4 Bitmap 對(duì)象不再使用時(shí)調(diào)用 recycle()釋放內(nèi)存有時(shí)我們會(huì)自己操作 Bitmap 對(duì)象,如果一個(gè) Bitmap 對(duì)象比較占內(nèi)存,當(dāng)它不再被使用的時(shí)候,可以調(diào)用 Bitmap.recycle()方法回收此對(duì)象的像素所占用的內(nèi)存,但這不是必須的 ,視情況而定??梢钥匆幌麓a中的注釋: /** * Free up the memory associated with this bitmap's pixels,and mark the * bitmap as "dead",meaning it will throw an exception if getPixels() or * setPixels() is called,and will draw nothing. This operation cannot be * reversed,so it should only be called if you are sure there are no * further uses for the bitmap. This is an advanced call,and normally need * not be called,since the normal GC process will free up this memory when * there are no more references to this bitmap. */
|
|