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

时间:2011-07-31 22:14:38

标签: android out-of-memory

我目前正在努力应对Android平台的奇怪行为 - Bitmap / Java堆内存限制。根据设备的不同,Android会将应用程序开发人员限制为16,24或32 MiB的Java堆空间(或者您可能会在有根电话上找到任意随机值)。这可以说是相当小的,但相对简单,因为我可以使用以下API测量使用情况:

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

够容易;现在为了扭曲。在Android中,除少数例外的位图存储在本机堆中,不计入Java堆。谷歌的一些眼光炯炯,纯粹的开发人员认为这是“糟糕的”,并允许开发者获得“超过他们的公平份额”。因此,有一个很好的小代码可以计算位图和可能的其他资源所产生的本机内存使用量,并与Java堆相加,如果你去了... java.lang.OutOfMemory。 哎哟

但没什么大不了的。我有很多位图,并且不需要所有这些位图。我可以“分页”一些目前尚未使用的内容:

因此,对于尝试#1,我重构了代码,因此我可以使用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 (caught 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 (this is 4 * #pixels in all loaded bitmaps)
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): Recycling bitmap 'game_word_puzzle_11_aniframe_005'
I/Epic    (23221): BITMAP_RECYCLING: recycled 1 bitmaps worth 1.1M).  age=294

注意totalMemory - freeMemory只有4.7 MiB,但是〜26?由位图占用的本机内存的MiB,我们处于31/32 MiB范围内,我们达到了极限。我在这里仍然有点困惑,因为我所有加载的位图的运行计数是26.6 MiB,但本机alloc大小只有25.2 MiB。所以我算错了。但这一切都在球场上,并且肯定地证明了与mem-limit发生的跨池“总结”。

我知道我修好了。但不,Android不会轻易放弃......

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

I/dalvikvm-heap(17641): Clamp target GC heap from 32.687MB to 32.000MB
D/dalvikvm(17641): GC_FOR_MALLOC freed <1K, 41% free 4684K/7815K, external 24443K/24443K, paused 24ms
D/dalvikvm(17641): GC_EXTERNAL_ALLOC freed <1K, 41% free 4684K/7815K, external 24443K/24443K, paused 29ms
E/dalvikvm-heap(17641): 1111200-byte external allocation too large for this process.
E/dalvikvm(17641): Out of memory: Heap Size=7815KB, Allocated=4684KB, Bitmap Size=24443KB, Limit=32768KB
E/dalvikvm(17641): Trim info: Footprint=7815KB, Allowed Footprint=7815KB, Trimmed=880KB
E/GraphicsJNI(17641): VM won't let us allocate 1111200 bytes
I/dalvikvm-heap(17641): Clamp target GC heap from 32.686MB to 32.000MB
D/dalvikvm(17641): GC_FOR_MALLOC freed <1K, 41% free 4684K/7815K, external 24443K/24443K, paused 17ms
I/DEBUG   ( 1505): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
I/DEBUG   ( 1505): Build fingerprint: 'verizon_wwe/htc_mecha/mecha:2.3.4/GRJ22/98797:user/release-keys'
I/DEBUG   ( 1505): pid: 17641, tid: 17641
I/DEBUG   ( 1505): signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 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 0000000000000000
I/DEBUG   ( 1505):  d26 0000000000000000  d27 0000000000000000
I/DEBUG   ( 1505):  d28 00ff00ff00ff00ff  d29 00ff00ff00ff00ff
I/DEBUG   ( 1505):  d30 0000000000000000  d31 3fe55167807de022
I/DEBUG   ( 1505):  scr 68000012

这是本机崩溃。不低于段错误(sig11)。根据定义,段错误总是一个错误。这绝对是处理GC和/或mem-limit检查的本机代码中的Android错误。但它仍然是我的应用程序崩溃导致糟糕的评论,退货和较低的销售。

所以我必须自己计算限制。 除了我在这里挣扎。我自己尝试将像素加起来(EpicPixels),但我仍然定期点击memcrash,所以我在计算一些东西。我尝试将javaBytes(总计 - 免费)添加到NativeHeapAllocSize,但这偶尔会导致我的应用程序变得“厌食”,释放并释放位图,直到没有任何东西可以清除。

  1. 有没有人知道用于计算内存限制的精确计算并触发java.lang.OutOfMemory?

  2. 还有其他人遇到过这个问题并完成了吗?你有任何智慧珍珠吗?

  3. 有谁知道哪个Google员工想到了这个计划,所以我可以打破他毁掉我生命中的40个小时? J / K

  4. 答案:限制是针对NativeHeapAllocSize&lt; maxMemory();然而,由于内存碎片,Android在实际限制之前崩溃。因此,您必须将自己限制在略低于实际限制的值。这个“安全系数”取决于应用程序,但少数MiB似乎适用于大多数人。 (我可以说,我对这种行为的破坏感到震惊)

3 个答案:

答案 0 :(得分:18)

使用这个snipplet,为我工作

/**
 * 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 :(得分:3)

每个设备的限制会有所不同(如果要按原样加载位图,请使用第3个链接)或者您可以使用一些技巧来避免这个问题:

  • 使用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 :(得分:3)

好的,所以我开始怀疑在 java堆大小+本机使用的内存上强制执行纯模式的限制。

限制基于NativeHeapAllocSize与maxMemory()。你会在下面看到我在22.0 MiB / 24 MiB时崩溃分配~1 MiB。关于可以分配多少内存的限制是一个上限。这就是把我扔了一会儿。在达到限制之前,崩溃发生了很多。因此,在解决方案中需要“memoryPad”值,因为尝试分配23.999 MiB / 24 MiB将导致几乎100%的时间崩溃。所以如果限制是24 MiB,你能安全使用多少?未知。 20 MiB似乎有效。 22 MiB似乎有效。我紧张地推着比这更近的东西。 ammount取决于malloc内存空间在本机进程中的碎片程度。当然,没有办法衡量这一点,所以在安全方面是错误的。

07-31 18:37:19.031: WARN/Epic(3118): MEMORY-USED: 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): LoadedClassCount=0.00M
07-31 18:37:19.081: INFO/Epic(3118): GlobalAllocSize=0.02M
07-31 18:37:19.081: INFO/Epic(3118): GlobalFreedSize=0.05M
07-31 18:37:19.081: INFO/Epic(3118): GlobalExternalAllocSize=0.00M
07-31 18:37:19.081: INFO/Epic(3118): GlobalExternalFreedSize=0.00M
07-31 18:37:19.081: INFO/Epic(3118): EpicPixels=17.9M
07-31 18:37:19.081: INFO/Epic(3118): NativeHeapSize=22.2M
07-31 18:37:19.081: INFO/Epic(3118): NativeHeapFree=0.07M
07-31 18:37:19.081: INFO/Epic(3118): NativeHeapAllocSize=22.0M
07-31 18:37:19.081: INFO/Epic(3118): ThreadAllocSize=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-byte external allocation too large for this process.
07-31 18:37:19.081: ERROR/dalvikvm(3118): Out of memory: Heap Size=6535KB, Allocated=4247KB, Bitmap Size=17767KB
07-31 18:37:19.081: ERROR/GraphicsJNI(3118): VM won't let us allocate 1111200 bytes

打印所有内容的代码:


    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);
    }