Monday, February 28, 2011

記憶體超出OutOfMemoryError

最近在開發上常常遇到
java.lang.OutOfMemoryError: bitmap size exceeds VM budget,
甚至有時候程式執行到一半時,
程式會突然重開,甚至關閉電源。

後來發現會出現這些狀況,大部份原因是因為記憶體用太兇了。
在Android裡,每new出一個實體時、做了太多的指派(Reference),
在heap裡都會占用記憶體。

據我目前所知,大部份的Android硬體裝置,
native heap都分配了16MB左右的空間,
但是隨著每臺硬體配備的不同,真正能用的記憶體大小,
又會低於16MB。
一占超出了這個使用空間,就會很容易丟出OutOfMemoryError這個錯誤訊息。

我目前遇到最常發生的時候就是在new一個圖檔Bitmap時,
Android在載入Bitmap時,每個圖點的換算是很耗記憶體的,
圖檔尺寸越大,所占的記憶體也越大
Android在繪圖時,每一個圖檔的算法是︰

圖檔的長*寬*圖檔類型

假設我今天有一張圖

該圖長︰177像素
寬︰111像素
圖檔類型︰RGB8888(每個8代表8bit,共有32bits=4Bytes)



那麼,這張圖記憶體的占用大小就會是
(177*111*4)/1024 = 76KB

我真實去試過,把原圖縮小後,記憶體占用量也真的降低了,
所以,去對圖檔做壓縮或者事先調整尺寸、甚至靈活的運用Bitmap類裡的recycle()這個回收函式,都是不錯的解決辦法。
Bitmap的使用要小心。

另外,我們常常會看到很多範例書裡,在寫ListView的Adapter時,
Adapter裡的getView()函式中,
常會有類似的以下片斷

@Override
public View getView(int position,View convertView, ViewGroup parent){
    ViewHolder holder;
    if(convertView == null){
     convertView = mInflater.inflate(R.layout.file_row, null);//告訴系統我們有自己畫的View
     holder = new ViewHolder();
     holder.text = (TextView) convertView.findViewById(R.id.text);
     holder.icon = (ImageView) convertView.findViewById(R.id.icon);

      convertView.setTag(holder);
    }else{
            holder = (ViewHolder) convertView.getTag();
    }

         holder.text.setText("test");
            .
            .
            .
      return convertView;
   }
//用來存放每個格子的資料的實體類別
private class ViewHolder{
         TextView text;
         ImageView icon;
}
      

ListView的每一個格子,都是Android系統呼叫getView()這個函式後,
去畫出來的。
getView()這個函式決定你每一格格子要畫成什麼鬼樣子(View)。

當我們要開始畫ListView時,
系統就先去看看convertView裡有沒有之前畫好的View,
想當然,一開始畫時怎麼可能會有畫好的View呢?
convertView當然會回傳null,
於是程式開始跑convertView == null 這個判斷式
告訴Android我們每一格ListView要畫成我們自訂的View: file_row(第5行)
(file_row裡我畫了ImageView、TextView等等的組合。你要讓每一個格子變成什麼樣子,就寫成什麼樣子)

而holder則是幫我們記住每個格子裡的資訊內容。

像我手上這臺Tablet一次能畫出5個View,
像剛才說的,系統會從convertView裡去找有沒有之前畫好的View,
ok,程式一次抓了5個convertView,都呈現成null,也就是之前並沒有畫好並產生出來的實體View。

於是,程式開始一口氣畫了5個View(並建立了5個holder實體,程式第6行)。
當我們看到系統將ListView畫出了5個格子並呈現給你看後,
使用者此時將手指往上滑動,ok,第1格、第2格、第3格格子陸續消失,
第6格、第7格、第8格也準備要出現了。

於是,Android又回去找convertView,
找到之前畫好的View,也new出來的holder實體,
於是,reuse,然後只是把holder裡面的內容改一改。

我們就看到第6格、第7格、第8格...又是新的資料。
但其實,那些實體是一直被reuse的。

=================================
好,有沒有想過這些View和實體為什麼要reuse?
我曾經沒有寫這個判斷式
if(convertView == null){
       .
       .
       .
      }else{
       .
       }
而把每一個getView,都寫成新的View
convertView = mInflater.inflate(R.layout.file_row, null);
     holder = new ViewHolder();
     holder.text = (TextView) convertView.findViewById(R.id.text);
     holder.icon = (ImageView) convertView.findViewById(R.id.icon);

      convertView.setTag(holder);
也就是不斷new出實體和畫新的View。
問題來了,
這就是為什麼我會遇到記憶體超出的原因。

原來Android系統,擁有著這種reuse機制。
利用有限的資源,去做無限的事。

相關文章︰
1.使用MAT(記憶體分析)工具檢查Memory Leak
2.Memory Leak經驗分享-Drawable篇
3.何謂Memory Leak(記憶體洩漏)

1 comment:

婊爆俠 said...

可是 資源reuse 不會有錯亂的危險嗎?

以這個例子來說

那個不停的被reuse的view

其中一格如果忽然有了什麼變動

例如 臨時要增加一個view進去 或是變化背景

會不會變成修改view (進而影響到所有格子) 而不是修改單一格子?





或許是程式碼不夠嚴謹才會發生這種事

或許是reuse真的有這種疑慮

想請教版主的經驗跟看法