位图分配如何在 Oreo 上工作,以及如何调查它们的内存?

Posted

技术标签:

【中文标题】位图分配如何在 Oreo 上工作,以及如何调查它们的内存?【英文标题】:How does Bitmap allocation work on Oreo, and how to investigate their memory? 【发布时间】:2018-06-13 22:56:03 【问题描述】:

背景

在过去的几年中,为了检查您在 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 + "%)"

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

问题

我注意到在 Android O 上(在我的例子中是 8.1,但它可能也在 8.0 上),上面的代码不受位图分配的影响。

进一步挖掘,我在 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))
            
        
    

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

这是上面运行的结果:

正如您从图表中看到的那样,该应用达到了巨大的内存使用量,远高于向我报告的允许的最大堆内存(即 201MB)。

我发现了什么

我发现了许多奇怪的行为。因此,我决定报告他们,here

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

     @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 + "%)"
     
    

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

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%)

    当我达到设备无法存储更多位图的程度(在我的 Nexus 5x 上停止在 1.1GB 或 ~850MB 上)时,而不是 OutOfMemory 异常,我得到......什么都没有!它只是关闭应用程序。甚至没有一个对话框说它已经崩溃了。

    1234563可用内存:

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

01-04 10:12:36.936 30598-31301/com.example.user.myapplication E/AndroidRuntime:致命异常:AsyncTask #1 进程:com.example.user.myapplication,PID:30598 java.lang.NullPointerException:尝试调用虚拟方法'void android.graphics.Bitmap.setHasAlpha(boolean)' 在一个空对象上 参考 在 android.graphics.Bitmap.createBitmap(Bitmap.java:1046) 在 android.graphics.Bitmap.createBitmap(Bitmap.java:980) 在 android.graphics.Bitmap.createBitmap(Bitmap.java:930) 在 android.graphics.Bitmap.createBitmap(Bitmap.java:891) 在 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.run(ThreadPoolExecutor.java:636) 在 java.lang.Thread.run(Thread.java:764)

查看分析器图表,它变得更加奇怪。内存使用量似乎根本没有增加多少,在崩溃点,它只是下降:

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

    如果我执行内存转储(使用分析器),与以前版本的 Android 不同,我将无法再看到位图的预览。

问题

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

    Android O 上的内存使用究竟发生了什么变化?为什么?

    如何处理位图?

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

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

    有没有关于这个主题的视频/文章?我不是在谈论添加的内存优化,而是更多关于现在如何分配位图,现在如何处理 OOM 等等......

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

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

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

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

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

    如果应用程序因此而崩溃,Play 商店是否会检测到 OOM 并仍然报告它们?它会在所有情况下检测到它吗? Crashlytics 可以检测到它吗?有没有办法让用户或在办公室的开发过程中知道这样的事情?

【问题讨论】:

【参考方案1】:

看起来您的应用已被 Linux OOM 杀手杀死。积极使用本机内存的游戏开发人员和其他人经常看到这种情况发生。

启用内核过度使用以及解除基于堆的位图分配限制可能会导致您看到的图片。您可以阅读有关过度使用here 的信息。

就我个人而言,我希望看到一个用于了解应用程序死亡情况的操作系统 API,但我不会屏住呼吸。


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

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

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

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

    我怎么能创建这么多位图,每个位图大小为 20,000x20,000

魔法。

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

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

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

分析器图表显示堆内存使用情况。如果位图不算 对于堆,该图自然不会显示它们。

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

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

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

    ...Crashlytics 可以检测到它吗?有没有办法让用户或在办公室的开发过程中获知这种情况?

OOM 杀手使用 SIGKILL 销毁您的应用程序(与您的进程在进入后台后终止时相同)。你的进程无法对它做出反应。理论上可以从子进程中观察到进程死亡,但确切的原因可能很难学习。见Who “Killed” my process and why?。一个编写良好的库可能能够定期检查内存使用情况并做出有根据的猜测。一个编写得非常好的库可能能够通过挂钩到原生 malloc 函数来检测内存分配(例如,通过热修补应用程序导入表或类似的东西)。


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

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

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

【讨论】:

你能把每个Q&A的编号输入吗?我想你错过了一些,比如发生了什么变化,以及如何处理它,包括检测、通知和调查。例如,在过去,可以使用 try-catch 以防您解码可能导致 OOM 的巨大位图。现在你不能。关于 64 位,我的设备是基于 64 位的:gsmarena.com/lg_nexus_5x-7556.phpqualcomm.com/products/snapdragon/processors/808。使用 onLowMemory ,它不会被调用,但似乎 onTrimMemory 会。 关于“空”位图的存储,我尝试用设置的一些像素填充它们(设置了左上角和右下角像素)。我仍然可以在任何问题发生之前存储大量它们。 关于 SIGSEGV,我实际上在 Play 商店的错误报告中看到了这个词,但不仅仅是在 Android 8.x 上。我想这不仅仅是关于OOM,那么?它可能来自哪些其他可能的情况?我什至在 Android 问题跟踪器上写了一些关于此的错误报告:issuetracker.google.com/issues/62234463issuetracker.google.com/issues/37140362 “关于 SIGSEGV,我实际上在 Play 商店的错误报告中看到了这个词,但不仅仅是在 Android 8.x 上。我想这不仅仅是关于 OOM,那么?” — 不,SIGSEGV 可能发生的原因有很多(取消引用零指针、堆栈空间不足、mmap 故障——基本上都是坏事,与操作系统内存使用有关)。由于本机内存不足而导致的特定 SIGSEGV 实际上无法在 Android 上观察到:只有在启用 OOM 杀手 not (并且 Android 始终启用它)时,该进程才可能获取它。我将编辑答案以使该部分不那么混乱。 “例如,在过去,可以使用 try-catch 来解码可能导致 OOM 的巨大位图”——我猜,这是当前实现的一个缺点。理论上,由于 Bitmap 存储不直接暴露给 Java 代码,因此应该可以在访问 Bitmap 内存的代码中按需执行记帐检查。这些检查可以反过来选择是否抛出 OOM 错误。请注意,过度使用总是会让 JNI 给自己带来麻烦——这些位图更改只会让不使用 JNI 的人更容易做到这一点。【参考方案2】:

如果有人对这个出色的 Kotlin 代码的 Java 版本感兴趣以获取应用程序使用的内存,这里是:

private String getHeapMemStats(Context context) 
    Runtime runtime = Runtime.getRuntime();
    long maxMemInBytes = runtime.maxMemory();
    long availableMemInBytes = runtime.maxMemory() - (runtime.totalMemory() - runtime.freeMemory());
    long usedMemInBytes = maxMemInBytes - availableMemInBytes;
    long usedMemInPercentage = usedMemInBytes * 100 / maxMemInBytes;
    return "used: " + Formatter.formatShortFileSize(context, usedMemInBytes) + " / " +
            Formatter.formatShortFileSize(context, maxMemInBytes) + " (" + usedMemInPercentage + "%)";


【讨论】:

以上是关于位图分配如何在 Oreo 上工作,以及如何调查它们的内存?的主要内容,如果未能解决你的问题,请参考以下文章

在运行 Oreo 和 Pie 的设备中如何在锁定屏幕上覆盖布局

Windows 上 C++ 中的位图操作

如何更改 Android O / Oreo / api 26 应用程序语言

Android Oreo如何知道FCM消息已经到来?

为啥我的所有位图都被上采样了 200%?

仅限 Android 9 (Pie):Context.startForegroundService() 没有调用 Service.startForeground() - 在 Oreo 上工作正常