Bitmap分配如何在Oreo上运行,以及如何调查它们的内存?

and*_*per 17 memory android bitmap android-8.0-oreo

背景

在过去几年中,为了检查Android上的堆内存量以及使用量,可以使用以下内容:

@JvmStatic
fun getHeapMemStats(context: Context): String {
    val runtime = Runtime.getRuntime()
    val maxMemInBytes = runtime.maxMemory()
    val availableMemInBytes = runtime.maxMemory() - (runtime.totalMemory() - runtime.freeMemory())
    val usedMemInBytes = maxMemInBytes - availableMemInBytes
    val usedMemInPercentage = usedMemInBytes * 100 / maxMemInBytes
    return "used: " + Formatter.formatShortFileSize(context, usedMemInBytes) + " / " +
            Formatter.formatShortFileSize(context, maxMemInBytes) + " (" + usedMemInPercentage + "%)"
}
Run Code Online (Sandbox Code Playgroud)

这意味着,您使用的内存越多,特别是通过将位图存储到内存中,您就可以越接近允许应用程序使用的最大堆内存.当您达到最大值时,您的应用程序将因OutOfMemory异常(OOM)而崩溃.

问题

我注意到在Android O上(在我的情况下是8.1,但它也可能在8.0上),上面的代码不受Bitmap分配的影响.

进一步深入,我注意到在Android分析器中你使用的内存越多(在我的POC中保存大位图),使用的本机内存就越多.

为了测试它是如何工作的,我创建了一个简单的循环:

    val list = ArrayList<Bitmap>()
    Log.d("AppLog", "memStats:" + MemHelper.getHeapMemStats(this))
    useMoreMemoryButton.setOnClickListener {
        AsyncTask.execute {
            for (i in 0..1000) {
                // list.add(Bitmap.createBitmap(20000, 20000, Bitmap.Config.ARGB_8888))
                list.add(BitmapFactory.decodeResource(resources, R.drawable.huge_image))
                Log.d("AppLog", "heapMemStats:" + MemHelper.getHeapMemStats(this) + " nativeMemStats:" + MemHelper.getNativeMemStats(this))
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

在某些情况下,我已经在一次迭代中完成了它,而在某些情况下,我只在列表中创建了一个位图,而不是解码它(注释中的代码).稍后会详细介绍......

这是运行上面的结果:

在此输入图像描述

从图中可以看出,应用程序达到了巨大的内存使用量,远高于报告给我的允许的最大堆内存(即201MB).

我发现了什么

我发现了很多奇怪的行为.因此,我决定在这里报告它们.

  1. 首先,我尝试了上述代码的替代方法,以便在运行时获取内存统计信息:

     @JvmStatic
     fun getNativeMemStats(context: Context): String {
         val nativeHeapSize = Debug.getNativeHeapSize()
         val nativeHeapFreeSize = Debug.getNativeHeapFreeSize()
         val usedMemInBytes = nativeHeapSize - nativeHeapFreeSize
         val usedMemInPercentage = usedMemInBytes * 100 / nativeHeapSize
         return "used: " + Formatter.formatShortFileSize(context, usedMemInBytes) + " / " +
                 Formatter.formatShortFileSize(context, nativeHeapSize) + " (" + usedMemInPercentage + "%)"
     }
    
    Run Code Online (Sandbox Code Playgroud)

但是,与堆内存检查相反,似乎最大本机内存会随着时间的推移而改变其值,这意味着我无法知道它的真正最大值是什么,所以我不能在实际应用中决定什么是内存缓存大小应该是.这是上面代码的结果:

heapMemStats:used: 2.0 MB / 201 MB (0%) nativeMemStats:used: 3.6 MB / 6.3 MB (57%)
heapMemStats:used: 1.8 MB / 201 MB (0%) nativeMemStats:used: 290 MB / 310 MB (93%)
heapMemStats:used: 1.8 MB / 201 MB (0%) nativeMemStats:used: 553 MB / 579 MB (95%)
heapMemStats:used: 1.8 MB / 201 MB (0%) nativeMemStats:used: 821 MB / 847 MB (96%)
Run Code Online (Sandbox Code Playgroud)
  1. 当我到达设备无法存储更多位图(在我的Nexus 5x上停止在1.1GB或~850MB上)而不是OutOfMemory异常时,我得到......没有!它只是关闭应用程序.甚至没有对话说它已经崩溃了.

  2. 如果我只是创建一个新的Bitmap,而不是解码它(上面提供的代码,只是在注释中),我得到一个奇怪的日志,说我使用大量的GB并且有大量GB的本机内存可用:

在此输入图像描述

另外,与我解码位图相反,我确实在这里遇到了崩溃(包括对话框),但它不是OOM.相反,它是...... NPE!

01-04 10:12:36.936 30598-31301/com.example.user.myapplication E/AndroidRuntime:FATAL EXCEPTION:AsyncTask#1进程:com.example.user.myapplication,PID:30598 java.lang.NullPointerException:Attempt to在android.graphics.Bitmap.createBitmap(Bitmap.java:980)的android.graphics.Bitmap.createBitmap(Bitmap.java:1046)上的空对象引用上调用虚方法'void android.graphics.Bitmap.setHasAlpha(boolean)' )在android.graphics.Bitmap.createBitmap(Bitmap.java:930)的android.graphics.Bitmap.createBitmap(Bitmap.java:891)at com.example.user.myapplication.MainActivity $ onCreate $ 1 $ 1.run(MainActivity. kt:21)在android.os.AsyncTask $ SerialExecutor $ 1.run(AsyncTask.java:245)java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162)java.util.concurrent.ThreadPoolExecutor $ Worker.在java.lang.Thread.run上运行(ThreadPoolExecutor.java:636)(Thread.java:764)

查看分析器图表,它甚至更奇怪.内存使用量似乎没有太大增长,在崩溃点,它只是下降:

在此输入图像描述

如果你看图表,你会看到很多GC图标(垃圾桶).我认为它可能正在做一些内存压缩.

  1. 如果我进行内存转储(使用分析器),而不是以前版本的Android,我再也看不到Bitmaps的预览了.

在此输入图像描述

问题

这种新行为引发了很多问题.它可以减少OOM的崩溃次数,但也可能使它很难检测到它们,发现内存泄漏并修复它们.也许我见过的一些只是错误,但仍然......

  1. Android O的内存使用情况究竟发生了什么变化?为什么?

  2. 如何处理位图?

  3. 是否仍可以在内存转储报告中预览位图?

  4. 获取应用程序允许使用的最大本机内存的正确方法是什么,并将其打印在日志中,并将其用作决定最大值的内容?

  5. 是否有关于此主题的视频/文章?我不是在谈论添加的内存优化,而是更多关于如何分配Bitmaps,如何处理OOM等等...

  6. 我想这个新行为可能会影响一些缓存库,对吧?那是因为它们可能取决于堆内存大小.

  7. 怎么可能创建这么多位图,每个大小为20,000x20,000(意思是~1.6 GB),但是当我只能从7,680x7,680(意思是~236MB)的真实图像中创建一些位图时?正如我猜测的那样,它真的会进行内存压缩吗?

  8. 在创建位图的情况下,本机内存函数如何能够为我返回如此巨大的值,而在解码位图时更合理呢?他们的意思是什么?

  9. Bitmap创建案例中奇怪的探查器图表是什么?它的内存使用量几乎没有增加,但它最终达到了无法创建它们的程度(在插入大量项目之后).

  10. 奇怪的异常行为是什么?为什么在位图解码中我没有异常甚至错误日志作为应用程序的一部分,当我创建它们时,我得到了NPE?

  11. Play商店会检测OOM并仍然报告它们,以防应用程序崩溃吗?它会在所有情况下检测到它吗?Crashlytics可以检测到它吗?有无法通过用户或在办公室开发过程中了解此类事情的方法吗?

use*_*723 6

看起来你的应用程序被 Linux OOM 杀手杀死了。积极使用本机内存的游戏开发人员和其他人一直看到这种情况发生。

启用内核过量使用以及解除对位图分配的基于堆的限制可能会导致您看到的图片。你可以在这里阅读一些关于过度使用的内容。

就我个人而言,我很想看到一个用于了解应用程序死亡的 OS API,但我不会屏住呼吸。


  1. 获取允许应用程序使用的最大本机内存并将其打印在日志上并将其用作决定 max 的正确方法是什么?

选择一些任意值(例如,四分之一的堆大小)并坚持下去。如果您收到调用onTrimMemory(这与 OOM 杀手和本机内存压力直接相关),请尝试减少消耗。

  1. 我猜这个新行为可能会影响一些缓存库,对吧?那是因为它们可能取决于堆内存大小。

没关系——Android 堆大小总是小于总物理内存。任何使用堆大小作为准则的缓存库都应该继续工作。

  1. 我怎么能创建这么多位图,每个大小都是20,000x20,000

魔法。

我认为,当前版本的 Android Oreo 允许内存过量使用:实际上并未从硬件请求未触及的内存,因此您可以拥有操作系统可寻址内存限制所允许的尽可能多的内存(在 x86 上小于 2 GB,几 TB在 x64 上)。所有虚拟内存都由页面组成(通常每个页面 4Kb)。当你尝试使用一个页面时,它会被调入。如果内核没有足够的物理内存来为你的进程映射一个页面,应用程序将收到一个信号,杀死它。在实践中,该应用程序会在此之前被 Linux OOM 杀手杀死。

  1. 在位图创建的情况下,本机内存函数如何为我返回如此巨大的值,而在我解码位图时更合理?他们的意思是什么?

  2. Bitmap 创建案例上奇怪的分析器图是怎么回事?它的内存使用量几乎没有增加,但最终(在插入了大量项目之后)达到了无法创建更多内存的程度。

分析器图显示堆内存使用情况。如果位图不计入堆,则该图自然不会显示它们。

本机内存函数似乎按(最初)预期工作——它们正确跟踪虚拟分配,但没有意识到内核为每个虚拟分配保留了多少物理内存(对用户空间不透明)。

此外,与解码位图时相反,我确实在这里崩溃(包括对话框),但这不是 OOM。相反,它是... NPE !

您没有使用过任何这些页面,因此它们没有映射到物理内存,因此 OOM 杀手不会杀死您(还)。分配失败可能是因为虚拟内存用完了,这比物理内存用完更无害,或者因为达到了其他类型的内存限制(例如基于 cgroups 的限制),甚至更多无害。

  1. ... Crashlytics 可以检测到它吗?有没有办法让用户知道这样的事情,或者在办公室的开发过程中?

OOM 杀手使用 SIGKILL 破坏您的应用程序(与您的进程在进入后台后终止时相同)。您的过程无法对其做出反应。理论上可以从子进程观察到进程死亡,但确切的原因可能很难了解。看看谁“杀死”了我的进程,为什么?. 一个编写良好的库可能能够定期检查内存使用情况并做出有根据的猜测。一个编写得非常好的库可能能够通过挂钩到本机malloc函数(例如,通过热修补应用程序导入表或类似的东西)来检测内存分配。


为了更好地演示虚拟内存管理的工作原理,让我们想象一下分配 1000 个 1Gb 的位图,然后更改每个中的一个像素。操作系统最初不会为这些位图分配物理内存,因此它们总共占用大约 0 字节的物理内存。在触摸 Bitmap 的单个四字节 RGBA 像素后,内核将分配一个页面来存储该像素。

操作系统对 Java 对象和位图一无所知——它只是将所有进程内存视为连续的页面列表。

常用的内存页大小为4Kb。触摸 1000 个像素(每个 1Gb 位图中一个)后,您仍将使用不到 4Mb 的实际内存。