原來OOM的罪魁禍首是C程式碼---android out of memory(OOM)

風的王子發表於2013-09-02
 一.

1.什麼是 OutOfMemoryError:

官方引用: Thrown when a request for memory is made that can not be satisfied using the available platform resources. Such a request may be made by both the running application or by an internal function of the VM.
通俗的講:就是在請求一塊記憶體的時候,當前可用資源不夠用來請求時丟擲的一種錯誤。我們知道,每個 android 程式就是一個獨立 dalvik vm 例項,每個例項限制了最大記憶體佔用,如果超過了這個限制,系統就會丟擲這個錯誤。所以跟整個裝置的剩餘記憶體沒太大關係,當然如果裝置剩餘記憶體都不足以再執行一個程式時,系統就會選擇 kill 部分程式以確保有足夠記憶體執行其他程式。

2.android 記憶體組成:

android 記憶體由 dalvik 和 native 2部分組成,dalvik 也就是 java 堆,建立的物件就是在這裡分配的,而 native 是通過 c/c++ 方式申請的記憶體,Bitmap 就是以一種方式分配的(android3.0 以後,系統預設是通過 dalvik 分配的)。當然無論以何種方式分配,2部分加起來不能超過 android 對單個程式的記憶體限制。

3.記憶體限制大小:

1 ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);2 activityManager.getMemoryClass();

以上方法會返回以 M 為單位的數字,可能在不同的平臺或者裝置上值都不太一樣,比如:HTC G7 預設 24M,Galaxy 36M,emulator-2.3 24M,等等。

4.程式實際佔用:

以一個簡單的 android 程式為例,該程式是用 eclipse adt 自動生成的最簡單的一個 android 專案,只有1個 activity 和 adt 自動生成的 res 目錄,測試環境:emulator-2.3.3
啟動該程式,命令列執行:

adb shell dumpsys meminfo com.mem.demo

執行結果:

複製程式碼

Applications Memory Usage (kB):

Uptime: 1195344 Realtime: 1195344 ** MEMINFO in pid 333 [com.mem.demo] **

                    native   dalvik    other    total
    --------------------------------------------------------
    |       size:     3968     5379      N/A     9347      |
    |                                                      |
    |   allocated:    3964     2649      N/A     6613      |
    --------------------------------------------------------
            free:        3     2730      N/A     2733

           (Pss):      553      449     2516     3518

  (shared dirty):     2272     1868     6648    10788

    (priv dirty):      420       32     1140     1592  

 Objects

           Views:        0        ViewRoots:        0      AppContexts:        0       Activities:        0           Assets:        2    AssetManagers:        2    Local Binders:        5    Proxy Binders:       10 Death Recipients:        0  OpenSSL Sockets:        0  

 SQL

               heap:        0         MEMORY_USED:        0  PAGECACHE_OVERFLOW:        0         MALLOC_SIZE:        0  

 

 Asset Allocations

    zip:/data/app/com.mem.demo-1.apk:/resources.arsc: 1K

複製程式碼

從上面被框出來的部分可以看出,一個最簡單的 android 程式在啟動後都有 6m 左右記憶體的佔用(上面是 6613kb)。那這 6m 的記憶體除了該 android 自己的資源和類之外,其他的還有什麼呢:

簡單說:在初始化的時候會 preload 一些東西,這些就包括 classes 和系統資源,就是系統的一些佈局啊,圖片啊,等等,在 android 完成啟動以後,這部分就通過記憶體共享的方式共享給其他程式,可以讓其他程式可以呼叫這部分資源,程式碼可以參考:http://goo.gl/EKvCV,android 整個啟動流程可以參考:http://goo.gl/K36Lr 。

5.發生 OOM :

為了製造 OOM,我們對上面最簡單的程式進行了改寫:

    
package com.mem.demo;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
public class DemoActivity extends Activity {
Bitmap map1, map2, map3, map4;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super .onCreate(savedInstanceState);
setContentView(R.layout.main);
map1 = BitmapFactory.decodeResource(getResources(), R.drawable.big1);
map2 = BitmapFactory.decodeResource(getResources(), R.drawable.big2);
map3 = BitmapFactory.decodeResource(getResources(), R.drawable.big3);
map4 = BitmapFactory.decodeResource(getResources(), R.drawable.big4);
}
}

其中:big1 到 big4 都是 1600*900 解析度的圖片,放在 drawbable-hdpi 資料夾下面,啟動程式,發生了 OOM:

複製程式碼

05-08 07:44:44.372: E/dalvikvm-heap(386): 5760000-byte external allocation too large for this process.05-08 07:44:44.412: I/dalvikvm-heap(386): Clamp target GC heap from 25.099MB to 24.000MB05-08 07:44:44.412: E/GraphicsJNI(386): VM won't let us allocate 5760000 bytes 05-08 07:44:44.412: D/dalvikvm(386): GC_FOR_MALLOC freed 0K, 53% free 2548K/5379K, external 18500K/20548K, paused 36ms05-08 07:44:44.422: D/skia(386): --- decoder->decode returned false 05-08 07:44:44.422: D/AndroidRuntime(386): Shutting down VM05-08 07:44:44.432: W/dalvikvm(386): threadid=1: thread exiting with uncaught exception (group=0x40015560)05-08 07:44:44.442: E/AndroidRuntime(386): FATAL EXCEPTION: main05-08 07:44:44.442: E/AndroidRuntime(386): java.lang.OutOfMemoryError: bitmap size exceeds VM budget05-08 07:44:44.442: E/AndroidRuntime(386):     at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)05-08 07:44:44.442: E/AndroidRuntime(386):     at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:460)05-08 07:44:44.442: E/AndroidRuntime(386):     at android.graphics.BitmapFactory.decodeResourceStream(BitmapFactory.java:336)05-08 07:44:44.442: E/AndroidRuntime(386):     at android.graphics.BitmapFactory.decodeResource(BitmapFactory.java:359)05-08 07:44:44.442: E/AndroidRuntime(386):     at android.graphics.BitmapFactory.decodeResource(BitmapFactory.java:385)05-08 07:44:44.442: E/AndroidRuntime(386):     at com.mem.demo.DemoActivity.onCreate(DemoActivity.java:20)05-08 07:44:44.442: E/AndroidRuntime(386):     at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)05-08 07:44:44.442: E/AndroidRuntime(386):     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1611)05-08 07:44:44.442: E/AndroidRuntime(386):     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1663)05-08 07:44:44.442: E/AndroidRuntime(386):     at android.app.ActivityThread.access$1500(ActivityThread.java:117)05-08 07:44:44.442: E/AndroidRuntime(386):     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:931)05-08 07:44:44.442: E/AndroidRuntime(386):     at android.os.Handler.dispatchMessage(Handler.java:99)05-08 07:44:44.442: E/AndroidRuntime(386):     at android.os.Looper.loop(Looper.java:123)05-08 07:44:44.442: E/AndroidRuntime(386):     at android.app.ActivityThread.main(ActivityThread.java:3683)05-08 07:44:44.442: E/AndroidRuntime(386):     at java.lang.reflect.Method.invokeNative(Native Method)05-08 07:44:44.442: E/AndroidRuntime(386):     at java.lang.reflect.Method.invoke(Method.java:507)05-08 07:44:44.442: E/AndroidRuntime(386):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:839)05-08 07:44:44.442: E/AndroidRuntime(386):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:597)05-08 07:44:44.442: E/AndroidRuntime(386):     at dalvik.system.NativeStart.main(Native Method)

複製程式碼

從日誌上看,記憶體不足發生在 decode big4 這個圖片的時候,我們對記憶體情況進行列印:

複製程式碼

Applications Memory Usage (kB):
Uptime: 958383 Realtime: 958383 ** MEMINFO in pid 386 [com.mem.demo] **                     native   dalvik    other    total
            size:    25144     5379      N/A    30523        allocated:    20799     2614      N/A    23413             free:       60     2765      N/A     2825            (Pss):      494      426    18494    19414   (shared dirty):     2288     1876     5292     9456     (priv dirty):      360       28    17836    18224

複製程式碼

從記憶體列印情況看出,當前已經分配了 23m 左右記憶體,同時結合 logcat 錯誤日誌,big4 這個圖片需要申請 5760000byte,即:5.76m 的記憶體,加起來就是 29m 左右記憶體,我們測試環境是 emulator2.3.3 預設是
24m,記憶體不足以申請這麼多,所以丟擲了OOM。

那為什麼區區3,4張圖片就會讓 android 程式記憶體不足?
裝置限制是一方面,像上面第3點說的,每個 android 裝置的記憶體限制不一樣,這個程式在模擬器上會有問題,在其他裝置上,比如:galaxy 就不會有問題。最主要的還是跟圖片所佔記憶體有關係,那麼一張圖片到底
佔用多少記憶體呢,java 沒有 c 的 sizeof() 函式,無法準確去量化這個數值,但是可以有粗略的計算方法:

寬 * 高 * 每個畫素所佔的 bytes

寬度和高度這個很容易獲得,那每個畫素所佔的 bytes 呢,這個主要取決於 decode 圖片的方式:

Bitmap.Config     ALPHA_8        Each pixel is stored as a single translucency (alpha) channel.
Bitmap.Config     ARGB_4444      This field is deprecated. Because of the poor quality of this configuration, it is advised to use ARGB_8888 instead.  
Bitmap.Config     ARGB_8888      Each pixel is stored on 4 bytes.
Bitmap.Config     RGB_565        Each pixel is stored on 2 bytes and only the RGB channels are encoded: red is stored with 5 bits of precision (32 possible values), green is store                                 d with 6 bits of precision (64 possible values) and blue is stored with 5 bits of precision.

複製程式碼

以上是官方文件對 Bitmap.Config 類的描述,所以,如果以 ARGB_8888 的方式 decode,那個每個畫素佔用4個 bytes,而如果用 RGB_565 就佔用2個 bytes。
我們計算一下,在 2.3 以後,程式自帶的圖片資源,都預設以 ARGB_8888 的方式,而在此以之前是以 RGB_565 的方式(不確定,待驗證),所以顏色會有損耗,典型的就是如果有漸變色的話,會出現光圈。
所以,計算如下:

1600 * 900 * 4 = 5760000

這個 5760000 也就是上面 logcat 錯誤日誌裡面所提到的申請數字,當然在實際用命令列印出的記憶體情況上看,比這個數字要大,是因為這只是圖片畫素的記憶體,還有一些屬性,變數和類本身沒有計算在內。
二.基於Android開發多媒體和遊戲應用時,可能會挺經常出現Out Of Memory 異常 ,顧名思義這個異常是說你的記憶體不夠用或者耗盡了。
在Android中,一個Process 只能使用16M記憶體,如果超過了這個限制就會跳出這個異常。這樣就要求我們要時刻想著釋放資源。Java的回收工作是交給GC的,如何讓GC能及時的回收已經不是用的物件,這個裡面有很多技巧,大家可以google一下。
因為總記憶體的使用超過16M而導致OOM的情況,非常簡單,我就不繼續展開說。值得注意的是Bitmap在不用時,一定要recycle,不然OOM是非常容易出現的。
本文想跟大家一起討論的是另一種情況:明明還有很多記憶體,但是發生OOM了。
這種情況經常出現在生成Bitmap的時候。有興趣的可以試一下,在一個函式裡生成一個13m 的int陣列。
再該函式結束後,按理說這個int陣列應該已經被釋放了,或者說可以釋放,這個13M的空間應該可以空出來,
這個時候如果你繼續生成一個10M的int陣列是沒有問題的,反而生成一個4M的Bitmap就會跳出OOM。這個就奇怪了,為什麼10M的int夠空間,反而4M的Bitmap不夠呢?
這個問題困擾很久,在網上,國外各大論壇搜尋了很久,一般關於OOM的解釋和解決方法都是,如何讓GC儘快回收的程式碼風格之類,並沒有實際的支出上述情況的根源。
直到昨天在一個老外的blog上終於看到了這方面的解釋,我理解後歸納如下:
在Android中:
1.一個程式的記憶體可以由2個部分組成:java 使用記憶體 ,C 使用記憶體 ,這兩個記憶體的和必須小於16M,不然就會出現大家熟悉的OOM,這個就是第一種OOM的情況。
2.更加奇怪的是這個:一旦記憶體分配給Java後,以後這塊記憶體即使釋放後,也只能給Java的使用,這個估計跟java虛擬機器裡把記憶體分成好幾塊進行快取的原因有關,反正C就別想用到這塊的記憶體了,所以如果Java突然佔用了一個大塊記憶體,即使很快釋放了:
C能使用的記憶體 = 16M - Java某一瞬間佔用的最大記憶體。
而Bitmap的生成是通過malloc進行記憶體分配的,佔用的是C的記憶體,這個也就說明了,上述的4MBitmap無法生成的原因,因為在13M被Java用過後,剩下C能用的只有3M了。

相關文章