背景
在过去的几年里,为了检查 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.
-
首先,我尝试了上述代码的替代方法,以获取运行时的内存统计信息:
@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 异常,而是……什么也没有!它只是关闭应用程序。甚至没有一个对话框说它已经崩溃了。
如果我只是创建一个新的位图,而不是对其进行解码(上面提供了代码,只是在注释中),我会得到一个奇怪的日志,说我使用了大量的 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图标(垃圾桶)。我认为它可能会进行一些内存压缩。
- 如果我进行内存转储(使用分析器),与以前版本的 Android 不同,我将无法再看到位图的预览。
问题
这种新行为引发了很多问题。它可以减少 OOM 崩溃的次数,但也可能使检测它们、查找内存泄漏并修复它们变得非常困难。也许我看到的一些只是错误,但仍然......
Android O 上的内存使用到底发生了什么变化?为什么?
如何处理位图?
是否仍然可以预览内存转储报告中的位图?
获取应用程序允许使用的最大本机内存并将其打印在日志上并将其用作决定 max 的正确方法是什么?
有关于这个主题的视频/文章吗?我不是在谈论添加的内存优化,而是更多关于现在如何分配位图、现在如何处理 OOM 等等......
我想这个新行为可能会影响一些缓存库,对吧?那是因为它们可能取决于堆内存大小。
我怎么能创建这么多位图,每个尺寸为 20,000x20,000(意味着 ~1.6 GB),但我只能从尺寸为 7,680x7,680(意味着 ~236MB)的真实图像创建其中一些位图?它真的像我猜测的那样进行内存压缩吗?
在创建位图的情况下,本机内存函数如何返回如此巨大的值,而在解码位图时却返回更合理的值?他们的意思是什么?
位图创建案例中奇怪的分析器图表是怎么回事?它的内存使用量几乎没有增加,但最终达到了无法创建更多内存的程度(在插入大量项目之后)。
奇怪的异常行为是怎么回事?为什么在位图解码时我没有遇到异常,甚至作为应用程序的一部分出现错误日志,而当我创建它们时,我得到了 NPE ?
Play 商店是否会检测到 OOM 并仍然报告它们,以防应用程序因此崩溃?它会在所有情况下检测到它吗? Crashlytics 可以检测到它吗?有没有办法让用户或在办公室的开发过程中获悉这样的事情?