借助 Firebase Crashlytics,让您的应用保持精简、高效且无段错误。本讲座将探讨 Firebase 控制台和 Android Studio 中提供的全套内存问题排查工具。首先,我们将介绍如何利用 ANR 数据分析和 GWP-ASan 报告来诊断难以捉摸的内存 bug,然后深入探讨针对内存不足 (OOM) 和内存限制器终止 (MLK) 事件的最新工具,这些工具专为帮助您了解和适应 Android 17 中引入的新内存限制而设计。
ApplicationExitInfo,以显示多线程跟踪记录并找出导致阻塞的根源线程。ProfilingManager 集成,可收集去混淆的堆快照,这些快照可在 Perfetto 中或通过 Android CLI 进行检查。转写文稿
Polina Govorkova
大家好
欢迎使用 Firebase Crashlytics 排查内存问题。
等等,这个职位名称太正式了。
我们来做点不一样的。
借助 Firebase Crashlytics 保持应用精简高效。
哦,听起来好多了。
我叫 Polina,是 Firebase Crashlytics 团队的高级软件开发者,也是今天的主讲人。
在开发周期的某个阶段,您会觉得已经解决了所有 bug。
那些 JSON 解析拼写错误、被遗忘的空指针检查,统统消失了!
您可能会问自己:“我完成了吗?”
好吧,我有个坏消息要告诉你,因为你不是。
因为剩下的实际上都是难题。
今天,我敢说,您将面临的两类最棘手的问题是响应能力和内存。
好消息是,我们是您的后盾。
今天,我们将介绍 Android 和 Crashlytics 中现有和全新的工具,帮助您找出导致应用无响应错误、内存不足异常、内存泄漏和原生内存损坏的响应能力问题的根本原因。
下面我们就来深入探讨一下
为什么响应能力和内存问题如此臭名昭著?
以一个经典 bug 为例:您声明了一个字符串,将其设置为 null,然后在某个时刻尝试读取其长度。
您能获得什么?
一个常见的 NullPointerException。
但实际上,这里的情况很简单,因为异常堆栈轨迹指向导致应用崩溃的确切代码行。
看看左侧的堆栈轨迹。
java.lang.NullPointerException
at MainActivity.onCreate(MainActivity.kt:42)
at android.app.Activity.performCreate
at android.os.Handler.dispatchMessage
at android.os.Looper.loopOnce
at android.app.ActivityThread.main我们可以立即看出,问题就出在 MainActivity.kt 的第 42 行。
对于大多数微不足道的 bug,这种方法都适用。
现在,当出现响应速度或内存问题时,例如右侧的“应用无响应”问题,堆栈轨迹几乎永远不会指向真正的罪魁祸首。
"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 flags=1
| state=S schedstat=( 1450239 48192 145 )
at DataManager.getData(DataManager.kt:25)
- waiting to lock <0x0a1b> held by thread 12
at MainActivity.onResume(MainActivity.kt:50)
at android.app.Activity.performResume
at android.os.Handler.dispatchMessage
at android.os.Looper.loopOnce
at android.app.ActivityThread.main在最好的情况下,它只会显示影响,即在应用冻结或内存不足时恰好在运行的某个无害线程或分配。
而真正的根本原因可能发生在更早的时候,或者可能完全在其他地方。
幸运的是,遇到这些问题时,您不必盲目猜测,因为 Firebase Crashlytics 会为您提供答案。
不过,在开始排查问题之前,请举手示意一下,看看有多少人熟悉 Crashlytics。
噢,太棒了。
很高兴看到这么多人举手。
对于新用户,Firebase Crashlytics 是一款实时错误报告解决方案,可提供现场数据、情境化信息、AI 驱动的分析洞见、问题排查建议,以及关于新版本的明确提示,助力您更快修复问题。
Crashlytics 深受开发者喜爱,是移动端最受欢迎的崩溃报告解决方案,为超过 37 万个原生应用提供支持,其中包括 57% 的热门 Android 应用。
为了了解 Crashlytics 如何帮助我们跟踪实际运行中的 bug,我们来看一个具体示例。
今天,我想和大家分享 ChromaClip 的故事。
这是我一直在构建的视频调色板提取器,我想告诉您,在开发过程中,我是如何解决响应能力和内存错误的。
ChromaClip 的理念很简单:用户选择一个视频,比如一段绚丽的日落片段或一个调色很棒的场景,应用就会分析这些帧,生成一个自定义调色板,然后用户就可以将其用于设计、数字艺术、情绪板等任何用途。
我们的故事要从 1.0 版本发布前说起。
我想在发布前确保它万无一失,所以我在本地的几台设备上进行了最后的测试,选择视频,点按按钮提取调色板,结果,真是太幸运了,每个 Android 开发者最喜欢的对话框,不是吗?
ChromaClip 无响应。
这就是“应用无响应”错误,也称为 ANR。
Polina Govorkova
那么,在这种情况下,除了惊慌失措,我们还能做些什么?
根据我们的第一直觉,由于测试手机直接插入了笔记本电脑,因此我们打开 Android Studio 并查看 Logcat。
我们看到了主线程的堆栈轨迹。
14:32:15.105 1205-1250/system_process E/ActivityManager: ANR in com.chromaclip.app (com.chromaclip.app/.MainActivity)
PID: 14205
Reason: Input dispatching timed out
Dumped ANR trace to /data/anr/traces.txt
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x72a8c3d0 self=0x759fb92000
| sysTid=14205 nice=-10 cgrp=default sched=0/0 handle=0x7623910548
| state=S schedstat=( 1450239120 4819203 1453 ) utm=132 stm=13 core=4 HZ=100
| stack=0x7fed2a0000-0x7fed2a2000 stackSize=8MB
at sun.misc.Unsafe.park(Native method)
- waiting on an unknown object
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source:1)
at com.chromaclip.app.extractor.PaletteExtractor.extractColors(PaletteExtractor.kt:42)这到底说明了什么?
首先,在最上方,我们确认了这是一个 ANR,发生在 MainActivity 中。
还有什么?
我们看到了原因,即输入分发超时。
这意味着用户点按了屏幕。
是的,我们做到了。
但主线程太忙,无法在 Android 的 5 秒看门狗超时内处理该事件。
然后,我们还会看到系统将文件转储到了 traces.txt 中,紧接着,logcat 会输出主线程的堆栈轨迹。
请注意,这里的 state 等于 S。
我们的主线程甚至没有主动消耗 CPU。
S 代表睡眠。
为什么主线程在等待时处于休眠状态?
我们来仔细看看堆栈帧。
在跟踪记录中间,我们可以看到主线程停在 Unsafe.park 内部,等待未知对象。
为什么我们不知道它在等待什么?
确实,对于某些 ANR(例如当线程被经典 Java 同步锁阻塞时),主线程堆栈轨迹会非常贴心地告诉我们它实际上在等待什么,以及是什么在持有锁。
但如果您在后台使用更高级别的并发原语,Logcat 只会告诉我们,我们正在等待一个未知对象。
因此,我们不知道正在等待哪个对象、具体在等待哪个后台工作器线程,或者为什么需要 5 秒以上的时间。
由于我们是在本地进行测试,因此可以从设备中提取该 traces.txt,手动滚动浏览数十个休眠线程,并可能找出罪魁祸首,或者使用 AI 询问。
在某些情况下,这种方法确实有效,事实上,对于本例,它也有效。
但我不仅不想手动读取原始线程转储(说实话,这种做法太落伍了),而且我知道,一旦进入生产环境,这种方法根本行不通。
我们只是无法直接访问用户手机中的 traces.txt 文件,对吧?
幸好我们集成了 Firebase Crashlytics。
因为这样就无需搜索,即使在生产环境中也能查明 ANR 的根本原因。
要怎么做呢?
这时,Android 的 ApplicationExitInfo API 就可以派上用场了。
从 Android 11 开始,每当系统标记 ANR 或终止应用进程时,操作系统都会记录丰富的退出元数据,包括崩溃的确切原因、内存状态以及当时在进程中运行的每个线程的完整系统跟踪记录。
最棒的是,您无需安装任何额外的插件,也无需编写任何代码。
只要您使用的是 Crashlytics SDK 18.3.5 或更高版本,该功能就可以开箱即用。
因此,应用一重启,Crashlytics SDK 就会自动查询 ApplicationExitInfo,捕获完整的多线程转储以及设备遥测数据,并将其发送到 Crashlytics 后端。
我们打开 Crashlytics 控制台,看看它会告诉我们什么。
这是 Firebase Crashlytics 控制台中的 ANR 问题详情视图,我们可以在“堆栈轨迹”标签页下立即看到 DefaultDispatcher worker one。
DefaultDispatcher-worker-1 (Root blocking)
dalvik.system.VMRuntime.allocateNode (Native method)
android.graphics.Bitmap.createBitmap (Bitmap.java:1124)
android.graphics.Bitmap.createBitmap (Bitmap.java:1091)
com.chromaclip.app.extractor.FrameDecoder.getUncompressedFrame (FrameDecoder.kt:112)
com.chromaclip.app.extractor.PaletteExtractor$processVideo$2.invokeSuspend (PaletteExtractor.kt:65)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt:106)
java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1167)
java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:641)
java.lang.Thread.run (Thread.java:923)事实上,Crashlytics 在最顶部显示的这个线程就是主线程的瓶颈,Crashlytics 会用一个名为“Root blocking”(根阻塞)的标记来标识它。
正如该标记上的提示所解释的那样,此工作器线程确实是阻止触发 ANR 的线程的线程。
顺便提一下,Crashlytics 也会为其他罪魁祸首提供智能标记。
举例来说,如果线程在读取大文件或执行非常慢的数据库查询时卡住,那么如果它触发了 ANR,该线程就会被标记为“I/O 根阻止”。
如果两个线程因相互等待而卡住,Crashlytics 会将其标记为死锁。
现在,我们来看看根阻塞线程中的堆栈帧。
因此,我们的 PaletteExtractor processVideo 调用了 FrameDecoder getUncompressedFrame(以蓝色突出显示),而后者完全卡在 createBitmap 中,只是试图为单个帧分配内存。
既然我们知道它不是在等待另一个锁,也不是在进行网络调用,那么我们的主要怀疑对象一定是内存,我们实际上可以通过在 Crashlytics 控制台中查看设备的可用内存来立即确认这一点。
请查看 Crashlytics 显示此信息的两个位置。
在特定事件的“数据”标签页中,就在我们刚刚探索的“堆栈轨迹”标签页旁边,以及右上角显示的所有事件的平均值中。
在这两种情况下,哎呀,看起来几乎没有任何内容,甚至连 100 KB 都不到。
我们来思考一下。
这里发生的情况是,由于进程内存不足,Android 运行时花费了所有时间疯狂地运行垃圾回收,试图为下一个位图找到足够的空间,但始终没有找到。
冻结工作器线程,并使界面处于饥饿状态超过 5 秒。
因此,我们为您提供了一个小技巧。
在您遇到 ANR 之前,只要您遇到 ANR 且可用 RAM 较低,这就是 ANR 实际上是内存泄漏或膨胀问题的确凿证据。
现在,问题是我们的应用是受害者,还是它也是占用内存的应用?
现在,让我们回到项目的问题列表,看看是否有其他相关问题。
Polina Govorkova
看这里。
在底部,我们可以看到问题列表,在包含 8 个事件的 ANR 上方,有一个 OutOfMemory 异常。
所以,我们可能才是罪魁祸首。
这其实是一个非常重要的线索。
因为在 Android 上,Java OutOfMemoryError(简称 OOM)不仅仅意味着手机 RAM 不足。
这意味着我们自己的应用进程超出了其 Android 运行时 Java 堆限制。
因此,目前我的工作理论是,ANR 和 OOM 实际上是同一枚硬币的两面。
根据测试运行期间的时机,ChromaClip 要么会长时间卡在垃圾回收中,从而触发 ANR,要么会完全耗尽堆,从而导致 OOM。
我们来调查一下 OutOfMemory 异常,以确定具体原因。
首先是堆栈轨迹。
java.lang.OutOfMemoryError: Failed to allocate a 256 byte allocation
at android.graphics.Bitmap.createBitmap
at android.view.LayoutInflater.inflate(LayoutInflater.java:532)
at com.chromaclip.app.SettingsActivity.onCreate(SettingsActivity.kt:21)等一下。
Bitmap.createBitmap,这个名字很熟悉。
我们之前不是见过那个吗?
不过,这次要看看是从哪里调用的。
打开设置 activity 时未能分配 256 字节。
我们又掉进了同一个陷阱。
如果您的应用因无法扩大小小的设置图标而崩溃,那么设置 activity 并不是罪魁祸首。
只是因为调用方运气不好,在没有内存时请求了一点内存。
因此,为了找到真正的罪魁祸首,我们需要放大视野,查看崩溃发生时内存中的所有内容。
换句话说,我们需要 Java 堆转储。
在办公桌前,如果您知道如何触发 OOM,可以使用 Android Studio 内存分析器手动获取 Java 堆转储。
但是,当 OOM 无法预测地发生时,无论是在测试运行期间还是在实际使用中,这种方法都无法奏效。
Polina Govorkova
因此,这是 Firebase Crashlytics 的又一个高光时刻。
除了应用退出信息之外,Crashlytics 现在还与 Android 的 ProfilingManager API 集成。
Android 17 中的 ProfilingManager API 经过了强化,支持系统触发的内存性能分析,这正是我们现在所需要的。
这意味着,当您的应用遇到 OutOfMemory 错误或超出系统内存限制时,操作系统会在后台捕获堆快照,然后,只要您使用的是 Crashlytics SDK 20.1.0 或更高版本,Crashlytics 就会自动触发收集。
然后,Crashlytics 会自动将 OOM 和内存限制器终止事件分组,我们稍后会在信息中心内详细讨论这些事件。
当您通过 Firebase 控制台或 CLI 选择启用堆转储收集功能后,Crashlytics 就会开始自动收集这些捕获的堆快照,并为您进行去混淆处理。基本上,您将获得崩溃发生时应用 Java 堆中所有内容的完整 X 射线透视图。
下面是捕获堆后,ChromaClip OOM 在 Crashlytics 控制台中的显示效果。
在顶部,您可以看到 Java 耗尽堆的原因以及发生这种情况的原因。
然后,您还会看到实用的问题排查提示和外部资源。
在堆栈轨迹标签页中,就在那个误导性的设置 activity 上方,Crashlytics 实际上会标记 OOM 堆栈轨迹不确定,这样用户就不必像我们刚才那样浪费时间查看堆栈轨迹了。
然后,Crashlytics 会附加该会话的去混淆 Perfetto 文件(即堆转储)。
在这里,我们可以下载该文件。
我们可以在 Google Cloud Storage 中浏览所有转储,也可以在 Perfetto 界面中打开转储。
因此,在 Perfetto 界面中点击“Analyze”会打开 Perfetto 堆转储浏览器。
现在,如果您已经看过这个,请举手。
好的,有几个人举手了。
对我来说,这个视图非常吓人,但在这里,问题其实很简单。
查看几乎贯穿整个 Java 堆的一组巨大条形。
因此,在大多数情况下,它是一个由 Pair 对象组成的数组,每个 Pair 对象都封装了一种颜色和某个字符串实例。
哦!
我想,我其实知道发生了什么。
我们的代码会为每一帧中的每个像素分配一种颜色和一些支持数据,并将数百万个像素保存在一个列表中。
因此,难怪“设置”activity 无法分配这 256 个字节。
好吧,至少解开了一个谜团。
我们来解决这个问题。
此时,我们有几种选择。
由于 Perfetto 已经指出了 PaletteExtractor,我们可以打开 IDE 并手动重写循环,也可以使用 AI 编码智能体。
或者,我们也可以完全绕过手动堆检查,并利用 Android CLI。
# 1. Install Android CLI & Perfetto skills
$ android skills add android-cli perfetto
# 2. List recent heap dumps from Crashlytics
$ android signals heaps list --kind oom --timeframe=90d
# 3. Download a heap dump as a Perfetto trace
$ android signals heaps download <heap-dump-id> \
--output ./heaps
# 4. Query top retained objects in the heap dump
$ trace_processor query ./heaps/oom.perfetto-trace ...
# 5. Fetch matching Crashlytics events
$ android signals heaps describe <heap-dump-id>因此,安装 Android CLI 和 Perfetto 技能后,您就可以列出并下载去混淆的 Crashlytics 堆转储,通过 trace_processor 查询这些堆转储中的主要保留对象,并提取匹配的 Crashlytics 问题数据,例如自定义键、日志、面包屑导航、关联的发布版本等,所有这些操作都可以在终端中完成。
更棒的是,看看幻灯片底部。
请勿自行运行此命令。
同样,使用自然语言让您常用的 AI 编码智能体通过 Android 命令检查最新的 OOM 堆转储,并提供修复方案。
# Or ask your favorite LLM in natural language
> Using `android` command, inspect the latest heap dump
for my OOM issue and propose a fix
● Ran android signals heaps list · download · describe
● Analyzed ./heaps/oom.perfetto-trace (perfetto)
● Updated PaletteExtractor.kt (+6 -4 lines)它将有足够的上下文来为您完成整个流程。
因此,它会列出 OOM、下载跟踪记录、运行分析,并直接在项目中生成代码补丁。
请查看 PaletteExtractor 文件中的此补丁。
// PaletteExtractor.kt
fun extractPalette(framePixels: IntArray): SparseIntArray {
- val pixelColors = mutableListOf<Pair<Color, String>>() // 8.3M objects per 4K frame!
- for (px in framePixels) {
- pixelColors.add(Color.valueOf(px) to "#%06X".format(px))
- }
+ val colorHistogram = SparseIntArray() // Downsampled & packed ARGB ints!
+ for (i in framePixels.indices step 16) {
+ val argb = framePixels[i]
+ colorHistogram.put(argb, colorHistogram.get(argb) + 1)
+ }
+ return colorHistogram
}现在,我们不再分配数百万个对象,而是直接对帧进行降采样并统计原始整数。
因此,我们来应用修复、关闭问题、升级版本并继续测试。
祝我们好运,不仅内存不足错误应该会消失,而且由于我们不会再在垃圾回收中发生抖动,希望那个讨厌的 ANR 也会消失。
因此,我们在测试设备上部署了补丁 build,选择了一个新片段,并开始提取调色板。
界面再次卡顿。
又一个 ANR。
有趣的是,在进行更多测试后,我们发现,在某些设备上,我们仍然会看到 ANR,但在其他设备上,应用现在只是随机崩溃,而且非常频繁,比那些 OOM 更频繁。
但我们已经修复了 OOM 问题,那么现在是什么情况?
Polina Govorkova
好了,我们回到 Crashlytics。
先说好消息。
查看我们的问题列表。
正如我们所希望的那样,内存不足异常完全消失了。
坏消息是,
现在,ChromaClip 的存活时间稍微长了一些,它有时间去发现下面还隐藏着什么。
因此,ANR 问题并未消失。
现在,我们在底部看到了两个原生代码崩溃,幸好只有几个事件。
最顶端是内存限制器终止事件,共 11 次,谢天谢地,它已经标记为有堆转储。
等等。
什么是内存限制器终止?
请问在座的各位,有谁听说过这个概念?
好了
少数罕见牌型。
让我们一起学习。
如果您之前没有见过内存限制器终止进程,那是因为这是 Android 17 中的全新功能。
Java OOM 仅在您超出受管理的 Java 堆限制时触发,但当应用的总内存占用还包括原生 RAM(例如图形缓冲区和 C++ 分配)时,之前并没有针对每个应用的上限。
因此,我们会有全局低内存终止守护程序。
当某个应用不断消耗系统 RAM 直至整个手机卡顿时,它就会介入。
现在,从 Android 17 开始,内存限制器会限制每个应用的内存占用。
因此,当进程消耗的 RAM 比例过高时,操作系统会终止该特定应用。
Crashlytics 会捕获所有这些详细信息,并在信息中心内将终止事件分组。
因此,在 Crashlytics 中点击进入内存限制器终止事件后,您会看到操作系统介入的原因说明,就像 OOM 一样,所以您无需记住我刚才说的话。
您还会获得有关后续处理方式的提示,以及指向 Android 内存强制执行文档的链接。
与 OOM 一样,当操作系统终止应用时,堆栈轨迹会将问题归咎于无辜的帧,而 Crashlytics 信息中心会突出显示该帧。
现在,内存限制器终止主要发生在原生代码中,因为原生环境缺少处理内存安全和分配的层。
否则,就会发生 OOM。
因此,我们的第一个问题是,Kotlin 对象是否会保留大量原生支持的分配?
还是原始 C++ 代码本身存在泄漏?
我们点击“在 Perfetto 界面中分析”来看看。
查看 Perfetto 堆转储浏览器中的“Overview”标签页。
在常规信息下,我们看到匿名 RSS 加上交换空间显示进程总内存达到了 4 GB。
好的。
不过,在下方,我们看到在“堆保留的字节数”下,整个托管堆(包括与 Kotlin 对象 [如位图] 关联的原生内存)只有 42 MB。
好的。
因此,在 10 秒内,我们排除了所有 Kotlin 和 Java 代码。
因此,罪魁祸首一定是原生 C++ 层。
由于 ChromaClip 有一个用于帧处理的薄 C++ 层,因此我们不必费心寻找。
将我们的编码智能体或 Android Studio 的原生性能分析器指向 NativePaletteEngine.cpp processFrame,就会发现 bug 就在那里。
// NativePaletteEngine.cpp
void NativePaletteEngine::processFrame(const uint8_t* rgba, size_t size) {
uint8_t* scratch = new uint8_t[size]; // Temporary frame buffer
transformColorSpace(rgba, scratch, size);
// BUG: Allocated every frame, but never freed!
+ delete[] scratch; // Free native buffer when done
}因此,在每个视频帧上,我们的 C++ 代码都会分配一个临时暂存缓冲区,如果您不释放它,它就不会那么临时,这真是一个新手错误。
所以,是时候把这些点连起来了。
这是怎么回事?
修复 Java 泄漏后,C++ 泄漏将有时间累积并失控。
在 Android 17 上,内存限制器会捕获此问题并终止应用。
但在尚未推出内存限制器的旧设备上,它实际上表现为 ANR。
所以,最后,一切都说得通了。
我们在这里添加 delete[] scratch,升级版本,然后发布更新。
随着内存限制器终止和 ANR 最终消失,我们应该可以发布到 1.0 了。
令人兴奋。
得益于 ChromaClip 现在的快速和稳定,我们的应用大获成功。
每天都有成千上万的人上传和混剪视频。
但随着流量的增加,还记得我们之前在信息中心底部看到的那些神秘的原生代码崩溃吗?
它们也无法无天了。
突然间,它们数量激增。
我们的工作尚未完成。
Polina Govorkova
因此,在 Crashlytics 中打开该原生代码崩溃,查看堆栈轨迹。
其中没有一行代码来自我们自己的应用。
RenderThread 在释放内存时在 libc.so 内部深处崩溃。
这里还有一个提示:如果系统线程在内存分配器内崩溃,而其中没有您的应用代码,这就是典型的原生内存损坏迹象。
因此,我们的 C++ 代码提前损坏或释放了缓冲区,而渲染线程只是在稍后才遇到该问题。
那么,我们如何在损坏发生的确切时刻抓到真正的罪魁祸首呢?
Android 为我们提供了两个强大的工具,而 Crashlytics 对这两个工具都提供了开箱即用的支持。
首先是 GWP-ASan,这是一种专为生产环境构建的轻量级安全网。
它会对一小部分原生堆分配量进行采样,并用保护页将其包围,从而使 CPU 和内存开销几乎为零。
在实际环境中的数千个用户会话中,当 bug 触及采样分配中的已释放内存或越界内存时,GWP-ASan 会立即捕获它。
不过,由于 GWP-ASan 本质上是概率性的,因此在本地测试时,您不希望测试手机运行 16 个小时或更长时间才命中随机样本。
那么,您会得到什么呢?
对于确定性本地调试,您可以使用 ARM MTE(即 Memory Tagging Extension)。
MTE 使用硬件指针和内存标记来检查每次分配,适用于 Pixel 8 及更新型号等 ARM v9 设备。
因此,在生产环境中将其与 GWP-ASan 搭配使用,您就可以兼得两者的优势。
正如您所料,当任一工具捕获到 bug 时,Crashlytics 会自动为问题添加标记,让您能够按 GWP-ASan 或 MTE 进行过滤,并在信息中心内固定显示包含确切损坏原因的信息卡片。
因此,我们来看看当 GWP-ASan 捕获到崩溃时,Crashlytics 控制台中会发生什么情况。
在常规堆栈轨迹标签页旁边,有一个内存堆栈轨迹标签页。
在顶部,我们可以看到确切的损坏原因:释放后使用。
现在,GWP-ASan 不会显示 libc.so 中崩溃的渲染线程,而是会在我们自己的 effect_pipeline.cc 中提供该缓冲区的完整生命周期。
因此,在“Allocated By”下,我们可以看到缓冲区的创建位置。
在“Deallocated By”下方,我们可以看到导致该对象从渲染线程中释放的确切原因。
它隐藏在特效流水线的第 208 行。
因此,我们在 EffectPipeline 中打开原生代码,发现 bug 正在向我们“微笑”。
// effect_pipeline.cc
void EffectPipeline::onFilterSwitched(FilterPresetId preset) {
// BUG: Freed on worker thread while RenderThread is still reading it!
+ std::lock_guard<std::mutex> lock(lut_mutex_);
releaseLutBuffer(); // Frees old color-grading LUT
loadFilterPreset(preset); // Allocates new color-grading LUT
}每当用户在后台线程中切换色彩滤镜时,我们都会在 RenderThread 仍在读取旧缓冲区时将其释放。
好的,我们在缓冲区交换周围添加了互斥量锁,发布了更新,最后发现 GWP-ASan 报告和所有那些神秘的崩溃几乎在一夜之间消失了。
未遇到崩溃问题的用户数指标也飙升至 100%。
棒极了!
还记得我们在演讲开始时问过自己:“我完成了吗?”
Right before getting hit by basically every single responsiveness and memory boss in the book.
现在,ANR、内存不足错误、内存限制器终止和原生内存损坏崩溃问题都已解决。
即使堆栈轨迹不准确,且问题隐藏在 C++ 的线程中,Android 和 Firebase Crashlytics 也能为您提供工具,帮助您追踪问题并自信地发布应用。
Polina Govorkova
我们来回顾一下,您回到电脑前就可以立即使用哪些功能:通过 ApplicationExitInfo 获取 ANR 线程数据分析和其他相关元数据;通过 ProfilingManager 自动收集 OOM 和内存限制器终止触发器;以及针对原生内存损坏提供全面的 GWP-ASan 和 Arm MTE 支持。
此外,我们还推出了非公开预览版,支持在 Android 17 或更高版本上进行现场堆转储。
如果您想率先尝试在 Firebase Crashlytics 上自动收集堆转储,并使用 Perfetto 和 Android CLI 进行分析,请快速拍下幻灯片,然后通过链接注册。
现在,我们已经完成了大部分讲座内容,感谢您今天抽出宝贵时间参加本次讲座。
祝您的主线程始终畅通,堆始终干净,下一个版本成为迄今为止最稳定的版本。
祝您调试愉快!
再次感谢您。
java.lang.NullPointerException
at MainActivity.onCreate(MainActivity.kt:42)
at android.app.Activity.performCreate
at android.os.Handler.dispatchMessage
at android.os.Looper.loopOnce
at android.app.ActivityThread.main"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 flags=1
| state=S schedstat=( 1450239 48192 145 )
at DataManager.getData(DataManager.kt:25)
- waiting to lock <0x0a1b> held by thread 12
at MainActivity.onResume(MainActivity.kt:50)
at android.app.Activity.performResume
at android.os.Handler.dispatchMessage
at android.os.Looper.loopOnce
at android.app.ActivityThread.main14:32:15.105 1205-1250/system_process E/ActivityManager: ANR in com.chromaclip.app (com.chromaclip.app/.MainActivity)
PID: 14205
Reason: Input dispatching timed out
Dumped ANR trace to /data/anr/traces.txt
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x72a8c3d0 self=0x759fb92000
| sysTid=14205 nice=-10 cgrp=default sched=0/0 handle=0x7623910548
| state=S schedstat=( 1450239120 4819203 1453 ) utm=132 stm=13 core=4 HZ=100
| stack=0x7fed2a0000-0x7fed2a2000 stackSize=8MB
at sun.misc.Unsafe.park(Native method)
- waiting on an unknown object
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source:1)
at com.chromaclip.app.extractor.PaletteExtractor.extractColors(PaletteExtractor.kt:42)DefaultDispatcher-worker-1 (Root blocking)
dalvik.system.VMRuntime.allocateNode (Native method)
android.graphics.Bitmap.createBitmap (Bitmap.java:1124)
android.graphics.Bitmap.createBitmap (Bitmap.java:1091)
com.chromaclip.app.extractor.FrameDecoder.getUncompressedFrame (FrameDecoder.kt:112)
com.chromaclip.app.extractor.PaletteExtractor$processVideo$2.invokeSuspend (PaletteExtractor.kt:65)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt:106)
java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1167)
java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:641)
java.lang.Thread.run (Thread.java:923)java.lang.OutOfMemoryError: Failed to allocate a 256 byte allocation
at android.graphics.Bitmap.createBitmap
at android.view.LayoutInflater.inflate(LayoutInflater.java:532)
at com.chromaclip.app.SettingsActivity.onCreate(SettingsActivity.kt:21)# 1. Install Android CLI & Perfetto skills
$ android skills add android-cli perfetto
# 2. List recent heap dumps from Crashlytics
$ android signals heaps list --kind oom --timeframe=90d
# 3. Download a heap dump as a Perfetto trace
$ android signals heaps download <heap-dump-id> \
--output ./heaps
# 4. Query top retained objects in the heap dump
$ trace_processor query ./heaps/oom.perfetto-trace ...
# 5. Fetch matching Crashlytics events
$ android signals heaps describe <heap-dump-id># Or ask your favorite LLM in natural language
> Using `android` command, inspect the latest heap dump
for my OOM issue and propose a fix
● Ran android signals heaps list · download · describe
● Analyzed ./heaps/oom.perfetto-trace (perfetto)
● Updated PaletteExtractor.kt (+6 -4 lines)// PaletteExtractor.kt
fun extractPalette(framePixels: IntArray): SparseIntArray {
- val pixelColors = mutableListOf<Pair<Color, String>>() // 8.3M objects per 4K frame!
- for (px in framePixels) {
- pixelColors.add(Color.valueOf(px) to "#%06X".format(px))
- }
+ val colorHistogram = SparseIntArray() // Downsampled & packed ARGB ints!
+ for (i in framePixels.indices step 16) {
+ val argb = framePixels[i]
+ colorHistogram.put(argb, colorHistogram.get(argb) + 1)
+ }
+ return colorHistogram
}// NativePaletteEngine.cpp
void NativePaletteEngine::processFrame(const uint8_t* rgba, size_t size) {
uint8_t* scratch = new uint8_t[size]; // Temporary frame buffer
transformColorSpace(rgba, scratch, size);
// BUG: Allocated every frame, but never freed!
+ delete[] scratch; // Free native buffer when done
}// effect_pipeline.cc
void EffectPipeline::onFilterSwitched(FilterPresetId preset) {
// BUG: Freed on worker thread while RenderThread is still reading it!
+ std::lock_guard<std::mutex> lock(lut_mutex_);
releaseLutBuffer(); // Frees old color-grading LUT
loadFilterPreset(preset); // Allocates new color-grading LUT
}