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

2024-04-19

背景

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

这意味着,您使用的内存越多,特别是通过将位图存储到内存中,您就越接近允许应用程序使用的最大堆内存。当您达到最大值时,您的应用程序将因内存不足异常(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 https://issuetracker.google.com/issues/71564968.

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

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

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

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

另外,与解码位图时相反,我确实在这里遇到了崩溃(包括对话框),但这不是 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图标(垃圾桶)。我认为它可能会进行一些内存压缩。

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

问题

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

  1. Android O 上的内存使用到底发生了什么变化?为什么?

  2. 如何处理位图?

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

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

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

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

  7. 我怎么能创建这么多位图,每个尺寸为 20,000x20,000(意味着 ~1.6 GB),但我只能从尺寸为 7,680x7,680(意味着 ~236MB)的真实图像创建其中一些位图?它真的像我猜测的那样进行内存压缩吗?

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

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

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

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


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

启用内核过度使用并解除对位图分配的基于堆的限制可能会导致您看到的结果。你可以阅读一些关于过度使用的内容here https://landley.net/writing/memory-faq.txt.

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


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

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

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

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

  1. 我怎么能创建这么多位图,每个尺寸为 20,000x20,000

Magic.

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

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

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

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

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

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

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

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

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


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

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

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

随机推荐

  • 如何确定两个泛型类型值是否相等?

    Update 我很抱歉 我的示例代码包含一个错误 导致很多我不理解的答案 代替 Console WriteLine 3 this Equals go1 Equals go2 我本来想写 Console WriteLine 3 this Eq
  • 更改 Android 操作栏菜单项的点击图标

    我正在使用 Sherlock 操作栏 我的操作栏上有 2 个项目 当选择该项目 活动 时 我想更改图标的图像 这是我的Java代码 Override public boolean onPrepareOptionsMenu Menu menu
  • Jquery - DataTables [tableTools]:仅导出可见行

    我刚刚开始使用 jQuery DataTables 使用DataTables的tableTools 是否可以仅导出可见行而不是所有行 例如 如果分页设置为 10 我预计只会导出 10 行 搜索结果也是如此 这是代码的一部分 document
  • BMP280 ServiceSpecificException:I/O 错误(代码 5)

    我尝试使用 AndroidThings 来测量 Raspberry Pi 3 和 BMP280 的温度 我选择3 3V是因为BMP280规格 https learn adafruit com adafruit bmp280 barometr
  • 如何将可以为 null 的值或数组隐式包装到 Scala 选项中

    我在 Jar 文件中包含这个 Java 类 作为 Scala 程序的依赖项 如 Axis jar class MyClass private String someStrings public String getSomeStrings r
  • 将 OnItemSelectedListener 添加到 Spinner

    我有一个按钮和一个微调器 最初是隐藏的 当用户按下按钮时 微调器会填充项目并变得可见 现在我想将 OnItemSelectedListener 添加到微调器中 我尝试了很多教程但没有运气 这是我的 OnCreate 函数 public vo
  • Seaborn 直方图与大数据

    我正在尝试绘制 300 万行大数据集的漂亮直方图 我有 2CPU 16GB RAM 尽管我提供了垃圾箱 但我从未得到过任何线索 有没有更有效的方法来绘制直方图 请参阅下面的代码 df0 dd read csv filename sep he
  • 使用正则表达式清除工作区中的变量(例外)

    我想清除工作区中的所有变量 但有一些由正则表达式定义的例外 功能clear有一个附加选项 regexp clear regexp expr1 exprN清除与任何一个匹配的所有变量 列出的正则表达式 该选项仅清除变量 所以我正在寻找相反的东
  • 快速应用图像的 uibutton 动画

    我想通过依次更改按钮上的两个图像来应用动画效果 我已经应用了以下代码 但无法看到动画效果 我已经正确应用了 IBOutlets var image1 UIImage UIImage named img mic off var image2
  • Google应用程序脚本复制文档页面

    我在 Google 文档中有一个包含一页的模板文档 我想创建一个新文档 其中 N 页每页都与模板文档中的一页相同 我怎样才能做到这一点 请看一下这个帖子 https stackoverflow com questions 10692669
  • 错误:系列'对象没有属性'包含'”

    我搜索 DataFrame 的所有列 第一列除外 并添加一个带有匹配列名称的新列 Matching Columns 当我尝试在测试我的模式是否包含在一行中之前删除所有点时 我收到错误 这有效 keyword 123456789 f lamb
  • iPhone - 可以不显示键盘但仍然在 UITextField 中显示光标吗?

    我有一个自定义键盘 我想在用户点击 UITextField 时显示 但同时我想在文本字段中显示光标 如果为 canBecomeFirstResponder 返回 NO 则它不会显示默认键盘 但也不显示光标 有人可以帮我吗 Thanks 您问
  • 如何去掉 CR (^M) 并保留 LF (^J) 字符?

    我正在尝试使用 Hexl 模式手动从文本文件中删除一些特殊字符 但不知道如何在 Hexl 模式下删除任何内容 我真正想要的是删除回车符并保留换行符 Hexl 模式是执行此操作的正确方法吗 无需寻找替代 就用吧 M x delete trai
  • 该文件无法打开,因为您无权查看它(真实设备)

    我知道有人问过类似的问题 但我找不到任何可以解决我的问题的东西 就我而言 我开发了一个 iOS 应用程序 Xcode 13 4 用于从我使用选择的 CSV 文件导入和解析数据UIDocumentPickerViewController 在模
  • 是否应该尝试防范 MVC 视图中的空引用异常/索引越界异常?

    我想发表评论这个问题的 https stackoverflow com questions 1005819 can we unit test view v of mvc接受的答案 现在视图不是已经放弃了后面的代码了吗 那么你要测试什么 指出
  • 在tensorboard中创建日志目录

    我正在尝试学习如何使用张量板 我希望它在我的程序中运行 我不明白如何创建日志目录 这些是我运行张量板的线路 summary writer tf train SummaryWriter tensorflow logdir sess graph
  • 为什么使用“new DelegateType(Delegate)”?

    好吧 假设您在某个类中定义了一个委托 public delegate void StringDelegate string s 另一个类实现了一个方法 public static void StringWriter string s 在我正
  • Flutter:使构建时环境变量可用于代码

    如何让构建时环境变量可用于 Flutter 应用程序中的代码 我的具体用例是将应用程序版本号注入并将哈希提交到调试屏幕中 此信息在构建时可用 但在运行时不可用 我本来希望能够做这样的事情 flutter run dart define AP
  • 当不使用默认构造函数声明 POD 结构时会实例化什么?

    当我们的团队在 C 代码中处理有关 POD 统一成员的 valgrind 警告时 我发现了这个有趣的答案 https stackoverflow com a 5914697 629530 https stackoverflow com a
  • 位图分配在 Oreo 上如何工作,以及如何调查它们的内存?

    背景 在过去的几年里 为了检查 Android 上有多少堆内存以及使用了多少内存 您可以使用如下命令 JvmStatic fun getHeapMemStats context Context String val runtime Runt