Android位图限制 - 防止java.lang.OutOfMemory异常

27

我目前正在苦恼于Android平台的一个奇怪行为 -- 位图/Java堆内存限制。根据设备不同,Android限制应用程序开发者只能使用16、24或32 MiB的Java堆空间(或在已Root的手机上可能找到任意随机值)。这可能相对较小,但可以使用以下API测量使用情况:

Runtime rt = Runtime.getRuntime();
long javaBytes = rt.totalMemory() - rt.freeMemory();
long javaLimit = rt.maxMemory();

很容易;现在有个小变化。在Android中,除了极少数例外,位图存储在native堆中,不计入Java堆。Google的某位热心净化开发者认为这是“不好的”,允许开发者“获得超过他们应得的份额”。因此,有一个不错的小代码可以计算由位图和其他资源造成的native内存使用量,并将其与Java堆相加。如果超出限制,就会抛出java.lang.OutOfMemory异常。 痛苦

但没关系。我有很多位图,并不需要所有的都一直存在。我可以"分页"一些暂时不使用的位图:

所以,在第一次尝试中,我进行了代码重构,以便能够在每次加载位图时包装try/catch:

while(true) {
    try {
        return BitmapFactory.decodeResource(context.getResources(), android_id, bitmapFactoryOptions);
    } catch (java.lang.OutOfMemory e) {
        // Do some logging

        // Now free some space (the code below is a simplified version of the real thing)
        Bitmap victim = selectVictim();
        victim.recycle();
        System.gc(); // REQUIRED; else, weird behavior ensues
    }
}

看这里,这是一个漂亮的日志片段,显示我的代码捕获了异常,并回收了一些位图:

E/Epic (23221): OUT_OF_MEMORY(捕获java.lang.OutOfMemory)
I/Epic (23221): ArchPlatform [android] .logStats()-
I/Epic (23221): LoadedClassCount = 0.00M
I/Epic (23221): GlobalAllocSize = 0.00M
I/Epic (23221): GlobalFreedSize = 0.02M
I/Epic (23221): GlobalExternalAllocSize = 0.00M
I/Epic (23221): GlobalExternalFreedSize = 0.00M
I/Epic (23221): EpicPixels = 26.6M(这是所有加载的位图中像素数目的4倍)
I/Epic (23221): NativeHeapSize = 29.4M
I/Epic (23221): NativeHeapAllocSize = 25.2M
I/Epic (23221): ThreadAllocSize = 0.00M
I/Epic (23221): totalMemory()= 9.1M
I/Epic (23221): maxMemory()= 32.0M
I/Epic (23221): freeMemory()= 4.4M
W/Epic (23221): 回收位图'game_word_puzzle_11_aniframe_005'
I/Epic (23221): BITMAP_RECYCLING:回收1个位图,价值1.1M)。 年龄= 294

注意totalMemory-freeMemory只有4.7 MiB,但是由位图占用的近26? MiB的本机内存,我们处于31/32 MiB的范围内,超出了限制。 我仍然有点困惑,因为我所有已加载的位图的运行总和为26.6 MiB,但本机分配大小只有25.2 MiB。所以我数错了什么。 但是这都在大约的范围内,绝对演示了跨池“总和”发生的mem-limit。

我以为我修复了它。 但不,Android并没有这么容易放弃...

以下是我从四台测试设备中的两台设备中获取的信息:

I/dalvikvm-heap(17641): 将目标GC堆从32.687MB压缩到32.000MB D/dalvikvm(17641): GC_FOR_MALLOC释放<1K,空闲41%,占用4684K / 7815K,外部24443K / 24443K,暂停24ms D/dalvikvm(17641): GC_EXTERNAL_ALLOC释放<1K,空闲41% ,占用4684K / 7815K,外部24443K / 24443K,暂停29ms E/dalvikvm-heap(17641): 这个进程的1111200字节的外部分配过大。 E/dalvikvm(17641): 内存不足:堆大小= 7815KB,已分配= 4684KB,位图大小= 24443KB,限制= 32768KB E/dalvikvm(17641): 裁剪信息:占用内存= 7815KB,允许占用内存= 7815KB,裁剪880KB E/GraphicsJNI(17641): VM不允许我们分配1111200字节 I/dalvikvm-heap(17641): 将目标GC堆从32.686MB压缩到32.000MB D/dalvikvm(17641): GC_FOR_MALLOC释放<1K,空闲41%,占用4684K / 7815K,外部24443K / 24443K,暂停17ms I/DEBUG(1505):******** ******** ******** ******** ******** ******** ******** ******** ******** ******** I/DEBUG(1505):构建指纹:'verizon_wwe / htc_mecha / mecha:2.3.4 / GRJ22 / 98797:user / release-keys' I/DEBUG(1505):pid:17641,tid:17641 I/DEBUG(1505):信号11(SIGSEGV),代码1(SEGV_MAPERR),故障地址00000000 I/DEBUG(1505):r0 0055dab8 r1 00000000 r2 00000000 r3 0055dadc I/DEBUG(1505):r4 0055dab8 r5 00000000 r6 00000000 r7 00000000 I/DEBUG(1505):r8 000002b7 r9 00000000 10 00000000 fp 00000384 I/DEBUG(1505):ip 0055dab8 sp befdb0c0 lr 00000000 pc ab14f11c cpsr 60000010 I/DEBUG(1505):d0 414000003f800000 d1 2073646565637834 I/DEBUG(1505):d2 4de4b8bc426fb934 d3 42c80000007a1f34 I/DEBUG(1505):d4 00000008004930e0 d5 0000000000000000 I/DEBUG(1505):d6 0000000000000000 d7 4080000080000000 I/DEBUG(1505):d8 0000025843e7c000 d9 c0c0000040c00000 I/DEBUG(1505):d10 40c0000040c00000 d11 0000000000000000 I/DEBUG(1505):d12 0000000000000000 d13 0000000000000000 I/DEBUG(1505):d14 0000000000000000 d15 0000000000000000 I/DEBUG(1505):d16 afd4242840704ab8 d17 0000000000000000 I/DEBUG(1505):d18 0000000000000000 d19 0000000000000000 I/DEBUG(1505):d20 0000000000000000 d21 0000000000000000 I/DEBUG(1505):d22 0000000000000000 d23 0000000000000000 I/DEBUG(1505):d24 0000000000000000 d25 000000000000本地崩溃,segfault错误(sig11)。根据定义,segfault错误始终是一个bug。这显然是 Android 在本地代码中处理GC和/或内存限制检查时的一个bug。但是,我的应用程序仍会崩溃,导致差评、退货和销售额下降。
因此,我必须自己计算限制。除了我在这里挣扎。我尝试自己添加像素(EpicPixels),但我仍然偶尔遇到内存崩溃,所以我在低估某些东西。我试图将javaBytes(总数-空闲)添加到NativeHeapAllocSize中,但这会偶尔导致我的应用程序变得“厌食”,不断释放位图,直到没有任何东西可以清除。
1.有人知道计算内存限制并触发 java.lang.OutOfMemory 的确切方法吗? 2.是否有其他人遇到过这个问题并解决了它?你有什么经验? 3.有人知道是哪个 Google 员工想出了这个方案,以便我可以为破坏了我 ~40 小时的生活而打他一拳吗?开个玩笑。
答案:限制为 NativeHeapAllocSize < maxMemory();但是,由于内存碎片化,Android在实际限制之前就会崩溃。因此,您必须将自己限制在实际限制的略低值之下。这个“安全因子”取决于应用程序,但对大多数人来说,几个MiB似乎都可以工作。(我只能说,我被这种行为的破坏深深震惊了)
4个回答

18

使用这个片段,对我有效

/**
 * Checks if a bitmap with the specified size fits in memory
 * @param bmpwidth Bitmap width
 * @param bmpheight Bitmap height
 * @param bmpdensity Bitmap bpp (use 2 as default)
 * @return true if the bitmap fits in memory false otherwise
 */
public static boolean checkBitmapFitsInMemory(long bmpwidth,long bmpheight, int bmpdensity ){
    long reqsize=bmpwidth*bmpheight*bmpdensity;
    long allocNativeHeap = Debug.getNativeHeapAllocatedSize();


    final long heapPad=(long) Math.max(4*1024*1024,Runtime.getRuntime().maxMemory()*0.1);
    if ((reqsize + allocNativeHeap + heapPad) >= Runtime.getRuntime().maxMemory())
    {
        return false;
    }
    return true;

}

以下是一个使用示例

        BitmapFactory.Options bmpFactoryOptions = new BitmapFactory.Options();
        bmpFactoryOptions.inJustDecodeBounds=true;
        BitmapFactory.decodeFile(path,bmpFactoryOptions);
        if ( (runInSafeMemoryMode()) && (!Manager.checkBitmapFitsInMemory(bmpFactoryOptions.outWidth, bmpFactoryOptions.outHeight, 2)) ){
            Log.w(TAG,"Aborting bitmap load for avoiding memory crash");
            return null;        
        }

1
这与我最终使用的代码非常相似。我使用bitmap.recycle()来释放空间,而不是返回true/false。 - Dave Dopson
1
我尝试了一下...但是如果我使用checkBitmapFitsInMemory(),它会返回false,如果我不使用checkBitmapFitsInMemory(),应用程序会加载位图而没有任何崩溃。我的日志值:bmpwidth=93,bmpheight=131,bmpdensity=2,reqsize=24366,heapPad=4194304,allocNativeHeap=33150304,maxMemory=33554432。有什么想法可以编写一个类似的方法,但效果更好吗? - Geltrude
2
这段代码在Honeycomb及以上版本上无法工作,因为位图不再存储在本地堆上。 - vomitcuddle
@vomitcuddle,在发布Honeycomb之后,您需要做什么才能更新此代码以使其正常工作?只是调用Debug.getNativeHeapAllocatedSize()并将其替换为其他内容吗? - Jay Snayder
Addev,你上面定义的常量是什么?1024个组件是否代表纹理大小,4代表每像素字节数的配置号,0.1只是一个任意数字,以便我们不会加载超过可用内存的十分之一? - Jay Snayder

4
每个设备的限制都不同(如果要按原样加载位图,请使用第三个链接),或者您可以使用以下一些技巧来避免这个问题,例如:
  • 使用Application类的onLowMemory()方法释放一些内存,以避免崩溃。
  • 在解码之前指定位图的所需大小。请查看以下链接了解更多信息:

http://davidjhinson.wordpress.com/2010/05/19/scarce-commodities-google-android-memory-and-bitmaps/

Strange out of memory issue while loading an image to a Bitmap object

此链接显示如何检查堆栈

BitmapFactory OOM driving me nuts

  • 当然也要释放旧位图的内存。

2
我尝试了Application.onLowMemory()。不幸的是,在我的segfault崩溃重现中,它从未被调用。 - Dave Dopson

4

好的,我开始怀疑本地模式下的限制是基于总Java堆大小和本机已使用内存。

限制是基于NativeHeapAllocSize与maxMemory()之间的比较。如下所示,当我在22.0 MiB / 24 MiB时分配约1 MiB时会崩溃。该限制是您可以分配的内存量的上限。这是让我困惑了一段时间的原因。崩溃发生在接近限制之前。因此,在解决方案中需要“memoryPad”值,因为尝试分配23.999 MiB / 24 MiB将几乎100%导致崩溃。那么,如果限制是24 MiB,您可以安全使用多少??未知。20 MiB似乎可以工作。22 MiB似乎可以工作。我不敢再靠近了。数量取决于本机进程中的malloc内存空间有多少碎片化。当然,没有办法测量任何这些内容,因此要保守。

07-31 18:37:19.031: WARN/Epic(3118): 内存使用:27.3M = 4.2M + 23.0M。 jf=1.7M,nhs=23.3M,nhf=0.0M 07-31 18:37:19.081: INFO/Epic(3118): ArchPlatform[android].logStats() - 07-31 18:37:19.081: INFO/Epic(3118): 已加载类数量=0.00M 07-31 18:37:19.081: INFO/Epic(3118): 全局分配大小=0.02M 07-31 18:37:19.081: INFO/Epic(3118): 全局释放大小=0.05M 07-31 18:37:19.081: INFO/Epic(3118): 全局外部分配大小=0.00M 07-31 18:37:19.081: INFO/Epic(3118): 全局外部释放大小=0.00M 07-31 18:37:19.081: INFO/Epic(3118): Epic像素=17.9M 07-31 18:37:19.081: INFO/Epic(3118): 原生堆大小=22.2M 07-31 18:37:19.081: INFO/Epic(3118): 原生堆空闲=0.07M 07-31 18:37:19.081: INFO/Epic(3118): 原生堆分配大小=22.0M 07-31 18:37:19.081: INFO/Epic(3118): 线程分配大小=0.12M 07-31 18:37:19.081: INFO/Epic(3118): totalMemory()=5.7M 07-31 18:37:19.081: INFO/Epic(3118): maxMemory()=24.0M 07-31 18:37:19.081: INFO/Epic(3118): freeMemory()=1.6M 07-31 18:37:19.081: INFO/Epic(3118): app.mi.availMem=126.5M 07-31 18:37:19.081: INFO/Epic(3118): app.mi.threshold=16.0M 07-31 18:37:19.081: INFO/Epic(3118): app.mi.lowMemory=false 07-31 18:37:19.081: INFO/Epic(3118): dbg.mi.dalvikPrivateDirty=0.00M 07-31 18:37:19.081: INFO/Epic(3118): dbg.mi.dalvikPss=0.00M 07-31 18:37:19.081: INFO/Epic(3118): dbg.mi.dalvikSharedDirty=0.00M 07-31 18:37:19.081: INFO/Epic(3118): dbg.mi.nativePrivateDirty=0.00M 07-31 18:37:19.081: INFO/Epic(3118): dbg.mi.nativePss=0.00M 07-31 18:37:19.081: INFO/Epic(3118): dbg.mi.nativeSharedDirty=0.00M 07-31 18:37:19.081: INFO/Epic(3118): dbg.mi.otherPrivateDirty=0.02M 07-31 18:37:19.081: INFO/Epic(3118): dbg.mi.otherPss0.02M 07-31 18:37:19.081: INFO/Epic(3118): dbg.mi.otherSharedDirty=0.00M 07-31 18:37:19.081: ERROR/dalvikvm-heap(3118): 1111200字节的外部分配对于此进程过大。 07-31 18:37:19.081: ERROR/dalvikvm(3118): 内存不足:堆大小=6535KB,已分配=4247KB,位图大小=17767KB 07-31 18:37:19.081: ERROR/GraphicsJNI(3118): 虚拟机不允许我们分配1111200字节。
打印所有内容的代码:

代码如下:


    public static void logMemoryStats() {
        String text = "";
        text += "\nLoadedClassCount="               + toMib(android.os.Debug.getLoadedClassCount());
        text += "\nGlobalAllocSize="                + toMib(android.os.Debug.getGlobalAllocSize());
        text += "\nGlobalFreedSize="                + toMib(android.os.Debug.getGlobalFreedSize());
        text += "\nGlobalExternalAllocSize="        + toMib(android.os.Debug.getGlobalExternalAllocSize());
        text += "\nGlobalExternalFreedSize="        + toMib(android.os.Debug.getGlobalExternalFreedSize());
        text += "\nEpicPixels="                     + toMib(EpicBitmap.getGlobalPixelCount()*4);
        text += "\nNativeHeapSize="                 + toMib(android.os.Debug.getNativeHeapSize());
        text += "\nNativeHeapFree="                 + toMib(android.os.Debug.getNativeHeapFreeSize());
        text += "\nNativeHeapAllocSize="            + toMib(android.os.Debug.getNativeHeapAllocatedSize());
        text += "\nThreadAllocSize="                + toMib(android.os.Debug.getThreadAllocSize());

        text += "\ntotalMemory()="                  + toMib(Runtime.getRuntime().totalMemory());
        text += "\nmaxMemory()="                    + toMib(Runtime.getRuntime().maxMemory());
        text += "\nfreeMemory()="                   + toMib(Runtime.getRuntime().freeMemory());

        android.app.ActivityManager.MemoryInfo mi1 = new android.app.ActivityManager.MemoryInfo();
        ActivityManager am = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE);
        am.getMemoryInfo(mi1);
        text += "\napp.mi.availMem="                + toMib(mi1.availMem);
        text += "\napp.mi.threshold="               + toMib(mi1.threshold);
        text += "\napp.mi.lowMemory="               + mi1.lowMemory;

        android.os.Debug.MemoryInfo mi2 = new android.os.Debug.MemoryInfo();        
        Debug.getMemoryInfo(mi2);
        text += "\ndbg.mi.dalvikPrivateDirty="      + toMib(mi2.dalvikPrivateDirty);
        text += "\ndbg.mi.dalvikPss="               + toMib(mi2.dalvikPss);
        text += "\ndbg.mi.dalvikSharedDirty="       + toMib(mi2.dalvikSharedDirty);
        text += "\ndbg.mi.nativePrivateDirty="      + toMib(mi2.nativePrivateDirty);
        text += "\ndbg.mi.nativePss="               + toMib(mi2.nativePss);
        text += "\ndbg.mi.nativeSharedDirty="       + toMib(mi2.nativeSharedDirty);
        text += "\ndbg.mi.otherPrivateDirty="       + toMib(mi2.otherPrivateDirty);
        text += "\ndbg.mi.otherPss"                 + toMib(mi2.otherPss);
        text += "\ndbg.mi.otherSharedDirty="        + toMib(mi2.otherSharedDirty);

        EpicLog.i("ArchPlatform[android].logStats() - " + text);
    }

@dpk - toMib 是一个帮助程序,将大字节计数格式化为更友好的“12.3 MiB”... MiB 是 MB 的更严谨形式,它意味着基于1024的SI单位而不是基于1000的。我可能没有意识到我使用了它并且没有粘贴它。您可以删除它或者将其变成您自己喜欢的格式,而不会损失代码的意图。 - Dave Dopson

0
如果你捕获到了“OutOfMemory”异常,那么你可以像这样计算位图内存:
WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
Point size = new Point();
display.getRealSize(size);
// Cap memory usage at 1.5 times the size of the display
// 1.5 * 4 bytes/pixel * w * h ==> 6 * w * h
int mMaxWidgetBitmapMemory = 6 * size.x * size.y;
Log.i(TAG,"mMaxWidgetBitmapMemory result: " + mMaxWidgetBitmapMemory);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接