Skip to content

常见问题怎么查 ​

下面是以前真实查过的问题,每条写现象、怎么查、查出来什么、结果。新问题多半能在这里找到相似的查法。卡顿、掉帧另见卡顿、掉帧怎么查。

常用的取证手段 ​

  • 线程在干什么:
    • top -H -p <pid> 看各线程的 CPU;
    • /proc/<pid>/task/<tid>/stat、wchan 看线程状态和停在哪个内核函数;
    • /proc/<pid>/status 里的 TracerPid 看有没有被别人 ptrace;
    • /proc/<pid>/syscall 看卡在哪个系统调用、参数是什么。
  • 线程栈:shell su 0 debuggerd -b <pid>,不会杀进程。
  • 采样和跟踪:
    • simpleperf record -g -t <线程 id>,也能跟指定的系统调用;
    • 内核 tracefs(/sys/kernel/tracing)跟信号;
    • uprobe / uretprobe 看某个库函数的返回值。
  • 网络:
    • 模拟器控制台 emu network capture start <文件> / stop 抓客体全部流量;
    • ss -tni 看每条连接的接收积压(Recv-Q)。
  • 磁盘:/proc/diskstats;/proc/<pid>/io 的 wchar、syscw;宿主上 qemu 进程的读写次数。
  • 宿主:Process Explorer 看 qemu 各线程的 CPU;模拟器崩溃时拿崩溃转储,配上对应版本的符号看栈。
  • 对带保护的游戏,优先被动读 /proc,不要随手 ptrace 游戏进程。

卡住、冻住 ​

启动时好时坏,坏的时候约 20 秒后 ANR(阴阳师) ​

  • 怎么查:ANR 时整机 CPU 只有 4.6%,主线程不是忙,是被停住了。被动读 /proc:主线程状态是 t(被跟踪停住),wchan 是 ptrace_stop,TracerPid 不是 0。顺着找到停住它的是游戏的一个子进程:WebView 的崩溃处理程序,命令行带 --trace-parent-with-exception,说明它认定父进程崩了。
  • 查出来:执行桥靠一次故意的段错误进入。WebView 的崩溃处理是原生代码,它装的信号处理函数抢到了最前面,把执行桥的那次段错误当成崩溃,于是去 ptrace 父进程,挂在那里。会不会碰上全看时序,所以时好时坏。
  • 结果:改用 native bridge 接口本来就有的「第一处理权」(getSignalHandler),它排在所有 sigaction 处理函数之前,别人装得再晚也插不到前面。

卡在「正在获取音频列表...」不动(阴阳师,约五次冷启动有一次) ​

  • 怎么查:游戏 139 个线程全在睡,抓包 60 秒只有一个包,不是网络。主逻辑线程在读管道,等自己 fork 出来的子进程。子进程停在 futex 上,锁的地址落在转译层的内部数据里;同一把锁在父进程里是空闲的,在子进程那份里是锁着的。
  • 查出来:转译层不是 fork 安全的。fork 那一瞬间,这把锁正被父进程的别的线程拿着,子进程里永远没人放。这是经典的多线程 fork 死锁。
  • 结果:在伴生库里用 pthread_atfork 把这几把锁带过 fork。修后连续 15 次冷启动 0 次卡住。

跑二十分钟左右画面冻住(星铁) ​

  • 怎么查:debuggerd -b 看到渲染线程在 dequeueBuffer 一直等空闲缓冲;logcat 里有 Too many open files。游戏进程的 fd 数随帧数线性增长,每帧一个,约 3.2 万帧用尽,时间和冻住对得上。
  • 查出来:镜像自带的客体 Vulkan 驱动每帧漏一个 fd。fd 用尽后,回送缓冲的回调在应用这边解包失败,缓冲再也不归还。
  • 结果:开机后设属性 debug.angle.feature_overrides_disabled=supportsSwapchainMaintenance1 规避。不用改模拟器。

加载时偶发卡死(终末地,修下面那个崩溃时第一版补丁引入的) ​

  • 怎么查:debuggerd -b 看到渲染线程在等一把锁,几个别的线程在等宿主回包,其中一个正拿着那把锁。
  • 查出来:开了 VulkanQueueSubmitWithCommands 时,宿主按全局序号逐条执行。渲染线程先领了序号再去等锁,拿锁的线程又在等一个序号更靠后的回包,宿主得先执行渲染线程那条,就互相等住了。
  • 结果:规则是拿着锁不能跨宿主往返。第二版补丁只在锁里查表,出了锁再发给宿主。

闪退、崩溃 ​

一进游戏 1 秒必崩(阴阳师,执行桥就是为它做的) ​

  • 查出来:游戏的保护库把原生 x86 代码里的一些 GOT 槽改成跳进转译后的 arm64 代码。真机上这些都是 arm64,没问题;转译层只支持转译代码调原生,不支持反过来,所以必崩。
  • 结果:执行桥用转译层自己导出的接口,补上「原生调转译」这条路。
    • 要钩转译层自己的 import 表(实际起作用的是它的 mprotect),因为 arm64 应用的库加载走转译层自己的加载器,不经过 libnativeloader;
    • 全程不改游戏和它的保护库。

不定时闪退,tombstone 是 SIGSEGV、pc 为 0、线程名每次不同(终末地) ​

  • 怎么查:这是二次现场。用 tracefs 跟信号,链条是:游戏自己的断言 → SIGTRAP → 崩溃上报 SDK → 转译层 → 上报 SDK 在宿主线程上页错 → 转成 SIGSEGV。也顺带确认了反作弊没发信号。断言内容是 ret == VK_SUCCESS,再用 uretprobe 抓到 vkBindImageMemory 返回了 -2。
  • 查出来:镜像自带的客体 Vulkan 驱动里,BindImageMemory 不加锁就查句柄表。别的线程恰好让表扩容时会查漏,返回 -2。这是数据竞争,上游代码也是同一段。
  • 结果:给客体驱动打「先加锁再查表」的补丁。冷启动 10 次断言 0 次,长跑 3 小时没有新 tombstone。

模拟器自己崩溃(0.36 发版回归时) ​

  • 怎么查:升级测试里实例重启、adb 重连时 qemu 崩了。拿崩溃转储,配上那一版的符号看栈,再反汇编,定位到一条从空指针附近读数据的指令。
  • 查出来:注销网络监听时,没先判断监听节点在不在。
  • 结果:先判空再注销。修好以后整套回归重跑。

画面 ​

闪屏、截图偶发全黑(NVIDIA 显卡,2%–5% 的画面) ​

  • 怎么查:在窗口、共享内存、adb 截图、录屏四条路径上都做探针,逐帧数全黑帧,确认几条路径都受影响,问题在宿主读回这一步。
  • 查出来:gfxstream 在 NVIDIA 驱动要求「专属分配」时没给专属分配,这部分画面缓冲读回来全是 0。
  • 结果:修好以后各条路径全黑帧都是 0,已提交上游。

网络、下载、数据 ​

提示「网络请求超时」,其实网络是好的(星铁) ​

  • 怎么查:在游戏数据目录里找 0 字节的文件;用模拟器控制台抓包,确认热更新的请求和下载都正常。
  • 查出来:之前一次关机没先 sync,刚写完的热更新清单变成了 0 字节。游戏解析空文件抛异常,界面上却显示成网络错误。
  • 结果:游戏停着时,用同目录下 _cache 的内容写回正式文件;以后关机一律先 sync。

下载很慢,不到 8 MB/s(明日方舟) ​

  • 怎么查:
    • 分三层比:宿主直连 CDN 约 100 MB/s,模拟器网卡单连接 80 多 MB/s,游戏实际只有 5.9 MB/s。
    • ss 看到每条连接的接收积压有几 MB,说明数据已经到了客体,是游戏读得慢。
    • /proc/<pid>/io 和 /proc/diskstats:写到磁盘的量是下载量的约 8 倍,都是小块写。下载线程停在 jbd2_log_wait_commit,也就是边下边 fsync。
    • qemu 没有一条线程跑满。
  • 查出来:瓶颈是数据盘每个写请求的往返成本。每次客体写在宿主上变成约 4 次写。游戏这种写法在真机闪存上没问题,到了模拟器就只剩 6 MB/s。
  • 结果:还没修完。方向是降低每次写的往返成本。这类改动动的是磁盘 IO 路径,先要跑硬杀回归。

硬杀以后文件变成 0 字节或退回旧版 ​

  • 怎么查:几种写文件的方式(先删后改名、改名覆盖、直接覆盖写),在不同回写参数下写完就杀,统计多久内被杀会丢。
  • 结果:就是「关机先 sync」这条规矩的来源。模拟器开机时还把客体回写周期调短,缩短丢数据的窗口,但消除不了。

系统层面 ​

客体热重启以后卡死(WHPX) ​

  • 怎么查:客体内核日志里有 soft lockup,一个虚拟 CPU 不响应核间中断,别的核卡在等它。
  • 结果:所有需要重启客体的地方都改成冷启动(关机再开),不用 adb reboot。

内存占用比别的模拟器高 ​

  • 怎么查:分三块量,再和别的模拟器同条件对比:客体实际在用多少,宿主上的客体内存块多大,模拟器自身另占多少(约 0.65–0.75 GB)。
  • 结果:
    • 打开内存气球的空闲页回报,客体里释放的内存还给 Windows;
    • 图形暂存缓冲改成按需增长,每台实例静置时少占约 140 MB。

冷启动慢,加核、提频都不见效(终末地) ​

  • 怎么查:
    • 先看宿主电源计划:「最大处理器状态」如果是 99%,睿频就是关着的。
    • 然后把几种钉核方式交替各跑几轮,看组间差别是不是大过组内波动。
  • 查出来:钉哪组核都分不出差别。睿频打开后主频高了约 20%,冷启动只快约 2%。主线程看着忙,其实在等(访存、转译缓存或别的等待),不是算力不够。
  • 结果:钉核不用改;下一步要查主线程在等什么。

几条经验 ​

  • 先拿现场,再下结论。 以前把 fork 死锁当成网络慢,把硬杀弄坏的文件当成 CDN 连不上,把混着菜单的帧率当成战斗帧率,都是没取证就猜。
  • 对比要交替多轮跑,看组间差别是不是大过组内波动,只跑一次比一次不算数。
  • 问题在客体镜像自带的组件里时(客体 Vulkan 驱动、ANGLE),先找属性或客体侧的规避,不一定要改模拟器。
  • 改到磁盘 IO 或关机流程的,先跑硬杀回归。

Android 是 Google LLC 的商标。