引起Android記憶體泄露有很多種原因,下面羅列了一些問題,以後會一一解決
1、構造Adapter時沒有使用緩衝convertView(衍生出ListView最佳化問題)
2、查詢資料庫遊標沒有關閉
3、Activity中生命週期對象大於Activity生命週期(關於Application Context與Activity Context)
4、Bitmap對象不使用時沒有recycle掉(這裡還有其他解決方案)
今天說的是第一種:如何使用緩衝來最佳化ListView
因為如果不使用緩衝convertView的話,調用getView時每次都會重新建立View,這樣之前的View可能還沒有銷毀,加之不斷的建立View勢必會造成記憶體泄露。
使用getView時有3方案:(1)沒有使用convertView,(2)使用convertView,(3)使用convertView+靜態類ViewHolder
我做了一個測試,代碼在下面,建立2000個View,從0拉到最後,計算總共耗,同時顯示GC釋放記憶體的大小,三種測試的結果如下:
註:這裡先說下 GC_EXTERNAL_ALLOC freed 7K, 18% free 11153K/13511K, external 1632K/1672K, paused 89ms 的意思
在Dalvik中,為一個程式分配的記憶體要根據機型的不同而不同,一般為32M,而虛擬機器會把這些記憶體分別分配給,JAVA使用的堆記憶體(heap)和Nativie使用的記憶體(external)(即虛擬機器中通過JNI調用本地Nativie的類中malloc分配的記憶體,如Bitmap,java.nio.ByteBuffers)。不過兩者不同共用,也就是說Native的記憶體不夠用了,而JAVA記憶體夠用時是不能向JAVA申請的,必須向虛擬機器申請才行,當虛擬機器無法分配的時候就會報OOM的錯誤
freed 7k:表示GC已經釋放了7K的記憶體
18% free 11153K/13511K:表示JAVA使用的堆記憶體(對象存在於此),18% free表示當前剩餘18%的堆記憶體(heap memory),11153K表示當前已用的堆記憶體,13511K表示堆記憶體總共大小(網上有些文章這部分弄錯了,很多轉載都是同一個)
external 1632K/1672K:1632K表示已用external memory,總共1672K external memory(注意:這個可能只存在於Android 3.0之前)
paused 89ms:這裡其實包括了兩部分,一個是在調用GC之前暫停時間,一個是調用GC後基本完成時暫停時間
詳細可參考:http://stackoverflow.com/questions/4550757/android-logs-gc-external-alloc-gc-for-malloc
(1)沒有使用convertView
沒有任何處理,不建議這樣寫。如果資料量少可以,但是如果清單項目資料量很大的時候,會每次都重新建立View,設定資源,嚴重影響效能,所以從一開始就不要用這種方式
@Override public View getView(int position, View convertView, ViewGroup parent) { //Get a View that displays the data at the specified position in the data set. //開始計時,效能測試用nanoTime會更精確,因為它是納秒級的 long startTime = System.nanoTime(); View item = mInflater.inflate(R.layout.list_item, null); ImageView img = (ImageView)item.findViewById(R.id.img); TextView title = (TextView)item.findViewById(R.id.title); TextView info = (TextView)item.findViewById(R.id.info); img.setImageResource(R.drawable.ic_launcher); title.setText("loulijun"); info.setText("www.cnblogs.com/loulijun"); //停止計時 long endTime = System.nanoTime(); //耗時 long spendTime = (endTime - startTime); sumTime += spendTime; Log.d("GoogleIO", "position at:"+position+"--sumTime:"+String.valueOf(sumTime)); return item; }
測試結果:
目前VM只為他們分配了5767K+518k的記憶體,而記憶體峰值是32M
剛開始時,而且heap memory只申請了5767K,已用記憶體3353K,注意資料大小的變化:耗時:167633055ns = 0.167633055秒
當拉到1000的時候,堆記憶體總計已經申請了9607K,已用記憶體7245K,明顯已經比剛開始時要大了 ,耗時:3435241667ns=3.435241667秒
當拉到2000的時候,堆記憶體總計13511K,已用記憶體11153K,耗時:6660369835ns = 6.660369835秒
---------------------------我又建立了10000個ListView,測試後直到記憶體泄露,證明峰值卻是是32M,而不使用convertView導致的記憶體泄露,當記憶體泄露時手機會提示force close,並將錯誤寫入/data/anr/traces.txt中,你可以adb pull下來查看具體資訊
(2)使用convertView後的測試資料(最佳化後)
通過緩衝convertView,convertView可以緩衝可視範圍內的convertView,當再次向下滑動時又開始更新View,這種利用緩衝convertView的方式可以判斷如果緩衝中不存在View才建立View,如果已經存在可以利用緩衝中的View,這樣會減少很多View的建立,提升了效能
@Override public View getView(int position, View convertView, ViewGroup parent) { //Get a View that displays the data at the specified position in the data set. if(convertView == null) { convertView = mInflater.inflate(R.layout.list_item, null); } //開始計時,效能測試用nanoTime會更精確,因為它是納秒級的 long startTime = System.nanoTime(); ImageView img = (ImageView)convertView.findViewById(R.id.img); TextView title = (TextView)convertView.findViewById(R.id.title); TextView info = (TextView)convertView.findViewById(R.id.info); img.setImageResource(R.drawable.ic_launcher); title.setText("loulijun"); info.setText("www.cnblogs.com/loulijun"); //停止計時 long endTime = System.nanoTime(); //耗時 long spendTime = (endTime - startTime); sumTime += spendTime; Log.d("GoogleIO", "position at:"+position+"--sumTime:"+String.valueOf(sumTime)); return convertView; }
測試資料我還是用2000吧,10000太大了(一萬年太久,只爭朝夕)
測試結果:
這次一直拉到最後明顯比剛才流暢多了,而且GC釋放記憶體的次數也明顯少了很多,最後用的時間和當前使用的記憶體也小很多,最佳化後的確好多了
當position為1000的時候,附近沒怎麼調用GC,用時:213653551ns=0.213653551秒,額,差距有點大,上面到達1000時用時達到3.43秒之多。
當position為2000的時候,已用記憶體只有3068K,堆總共記憶體6215K,而且external memory是0K,用時:378326396ns = 0.378326396秒,效能差距如此之大,都有點不敢相信。也不知道這種方式對不對,如有不妥的地方,還希望大牛能給出正確回答
(3)使用contentView+靜態類ViewHolder類
通過convertView+ViewHolder來實現,ViewHolder就是一個靜態類,使用 ViewHolder 的關鍵好處是緩衝了顯示資料的視圖(View),加快了 UI 的響應速度。
當我們判斷 convertView == null 的時候,如果為空白,就會根據設計好的List的Item布局(XML),來為convertView賦值,並產生一個viewHolder來綁定converView裡面的各個View控制項(XML布局裡面的那些控制項)。再用convertView的setTag將viewHolder設定到Tag中,以便系統第二次繪製ListView時從Tag中取出。(看下面代碼中)
如果convertView不為空白的時候,就會直接用convertView的getTag(),來獲得一個ViewHolder。
靜態類ViewHolder
//定義靜態類ViewHolder static class ViewHolder { public ImageView img; public TextView title; public TextView info; }
@Override public View getView(int position, View convertView, ViewGroup parent) { //Get a View that displays the data at the specified position in the data set. //開始計時,效能測試用nanoTime會更精確,因為它是納秒級的 long startTime = System.nanoTime(); ViewHolder holder; if(convertView == null) { holder = new ViewHolder(); convertView = mInflater.inflate(R.layout.list_item, null); holder.img = (ImageView)convertView.findViewById(R.id.img); holder.title = (TextView)convertView.findViewById(R.id.title); holder.info = (TextView)convertView.findViewById(R.id.info); convertView.setTag(holder); }else { holder = (ViewHolder)convertView.getTag(); holder.img.setImageResource(R.drawable.ic_launcher); holder.title.setText("loulijun"); holder.info.setText("www.cnblogs.com/loulijun"); } //停止計時 long endTime = System.nanoTime(); //耗時 long spendTime = (endTime - startTime); sumTime += spendTime; Log.d("GoogleIO", "position at:"+position+"--sumTime:"+String.valueOf(sumTime)); return convertView; }
到這裡,可能會有人問ViewHolder靜態類結合緩衝convertView與直接使用convertView有什麼區別嗎,是否重複了
在這裡,官方給出瞭解釋
提升Adapter的兩種方法
To work efficiently the adapter implemented here uses two techniques:
-It reuses the convertView passed to getView() to avoid inflating View when it is not necessary
(譯:重用緩衝convertView傳遞給getView()方法來避免填充不必要的視圖)
-It uses the ViewHolder pattern to avoid calling findViewById() when it is not necessary
(譯:使用ViewHolder模式來避免沒有必要的調用findViewById():因為太多的findViewById也會影響效能)
ViewHolder類的作用
-The ViewHolder pattern consists in storing a data structure in the tag of the view
returned by getView().This data structures contains references to the views we want to bind data to,
thus avoiding calling to findViewById() every time getView() is invoked
(譯:ViewHolder模式通過getView()方法返回的視圖的標籤(Tag)中儲存一個資料結構,這個資料結構包含了指向我們
要綁定資料的視圖的引用,從而避免每次調用getView()的時候調用findViewById())
測試資料:(跟直接使用convertView資料相差不多)
當position為1000時,用時:199188216ns = 0.199188216秒,堆記憶體的時候也沒比沒有使用convertView理想的多
當position為2000時,用時:336669887ns = 0.336669887秒,比直接使用convertView的方式稍微好一點點,不過效能相差不多