目录
从一行 `native` 堆栈追到 InstallReferrer:一次主线程 ANR 的排查全过程

从一行 native 堆栈追到 InstallReferrer:一次主线程 ANR 的排查全过程

文章头图

线上挂了一批长得几乎一样的 ANR,堆栈顶上全是系统库,翻来覆去看不见自己的代码。这篇把当时一步步往下查的过程原样记下来——从一行 native 栈帧,一直追到根因。

文中堆栈做过脱敏:应用包名、自研模块、第三方 SDK 被混淆的内部类,统一换成 com.xxx.sdk.* 这种占位。Android Framework、ART、Binder,还有 com.android.installreferrer.*com.google.android.finsky.* 这些系统和公开组件都留着原样——它们恰好是查问题的关键,换掉就没法看了。

背景:堆栈里全是系统代码的那种 ANR

Crashlytics 上这批 ANR 长得很像,几个共同点:

  • 主线程栈顶是 libc.so __ioctllibbinder.so 这类 native 帧;
  • Java 栈往下捋,几乎全是 android.*,一行自家业务代码都没有;
  • 通篇没有「死锁」字样。

这种栈最容易被一句「系统问题,修不了」打发掉。但只要 dump 是完整的,主线程到底卡在哪一帧,是写死在那儿、跑不掉的。所以先别急着下结论,把整段栈摆出来再说。

先看堆栈

下面是主线程(maintid=1)的完整堆栈,基本就是原始 dump 的样子——#NN pc 0x… 是 native 帧,at 开头是 Java 帧。我只动了两处:内部 SDK 的混淆包名换成了 com.xxx.sdk.*,每帧末尾设备相关的 BuildId 删掉了,其余照贴。先扫一眼,不用逐帧看懂,后面会一段段拆:

text
复制代码
main (native):tid=1 systid=17704
#00 pc 0x0f522c  libc.so (__ioctl + 12)
#01 pc 0x09c270  libc.so (ioctl + 156)
#02 pc 0x077dd4  libbinder.so (android::IPCThreadState::transact + 960)
#03 pc 0x067eec  libbinder.so (android::BpBinder::transact + 312)
#04 pc 0x1ad680  libandroid_runtime.so (android_os_BinderProxy_transact + 400)
       at android.os.BinderProxy.transactNative(Native method)
       at android.os.BinderProxy.transact(BinderProxy.java:679)
       at com.google.android.a.a.b(BaseProxy.java:2)
       at com.google.android.finsky.externalreferrer.IGetInstallReferrerService$Stub$Proxy.c(IGetInstallReferrerService.java:3)
       at com.android.installreferrer.api.InstallReferrerClientImpl.getInstallReferrer(InstallReferrerClientImpl.java:5)
       at com.xxx.sdk.Attribution.handleAttributionResult(Attribution.kt:122)
       at com.xxx.sdk.Attribution.access$handleAttributionResult(Attribution.kt:24)
       at com.xxx.sdk.Attribution$Setup.handleSetupOk(Attribution.kt:88)
       at com.xxx.sdk.Attribution$Setup.access$handleSetupOk(Attribution.kt:44)
       at com.xxx.sdk.Attribution$Setup$start$1.onInstallReferrerSetupFinished(Attribution.kt:58)
       at com.android.installreferrer.api.InstallReferrerClientImpl$InstallReferrerServiceConnection.onServiceConnected(InstallReferrerClientImpl.java:4)
       at android.app.LoadedApk$ServiceDispatcher.doConnected(LoadedApk.java:2424)
       at android.app.LoadedApk$ServiceDispatcher$RunConnection.run(LoadedApk.java:2457)
       at android.os.Handler.handleCallback(Handler.java:959)
       at android.os.Handler.dispatchMessage(Handler.java:100)
       at android.os.Looper.loopOnce(Looper.java:282)
       at android.os.Looper.loop(Looper.java:387)
       at android.app.ActivityThread.main(ActivityThread.java:9500)
       at java.lang.reflect.Method.invoke(Native method)
       at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:600)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1005)

第一步:先盯住主线程

ANR 说白了就是主线程在规定时间内没把消息处理完。所以 dump 里挂着几百个线程都不要紧,先找 maintid=1,看它卡在哪。

栈的第一行(线程头)写得很清楚:

text
复制代码
main (native):tid=1 systid=17704

tid=1 坐实了这是主线程。但更该留意的是括号里那个状态——native。第一个信号就在这。

第二步:native 状态先把「Java 死锁」排掉

native 这个状态告诉我们两件事:

  • 线程没卡在 Java 的 synchronized / ReentrantLock 上——真要是锁,状态会写 blocked / waiting,还跟一句 held by thread X
  • 它是卡在 native 层的某个系统调用里。

顺手在整份 dump 里搜 held bywaiting to lock,一条没有,死锁这条路堵死。

这一步其实是在定方向:看到 blocked / waiting 就去查锁,看到 native 就去查系统调用、IPC、JNI。方向选错,后面全是白费力气。

第三步:顺着栈往下读,看它在等谁

回到那段栈,从栈顶往栈底一段段看:

  • 栈顶(native):__ioctl → ioctl → IPCThreadState::transact,这是 Binder 在跑一次同步跨进程调用,正卡着等对端回话。
  • 中段(在叫谁):IGetInstallReferrerService$Stub$Proxy 加上 InstallReferrerClientImpl.getInstallReferrer——对端是 Google Play 商店,调的是 Install Referrer(安装来源)服务。
  • 再往下(谁发起的):onInstallReferrerSetupFinished 这个回调里,调了 getInstallReferrer()
  • 栈底(跑在哪):onServiceConnected → Handler.handleCallback → Looper.loop → ActivityThread.main,整段是投到主线程的消息循环里跑的。

连起来就一句话:主线程在「安装来源服务连上了」的回调里,同步向 Play 商店发了一次 Binder 调用,然后就一直等它返回。

顺带一提,「主线程卡在慢 Binder 调用上」这种 ANR,Android 官方的 ANR 排查文档是单独列成一类的(见文末「查找无响应的线程 · 速度缓慢的 Binder 调用」)——所以这条线索的方向没跑偏。

第四步:把 getInstallReferrer() 拆开,看那次 transact 从哪来

第三步是顺着栈读出来的结论。光读栈还不够踏实,我们换个方向再核一遍。项目里这库的依赖就一行:

kotlin
复制代码
implementation("com.android.installreferrer:installreferrer:2.2")

直接在 Android Studio 里点进 getInstallReferrer(),跟着源码往下看,那次 transact 是怎么冒出来的就一目了然。库不大,链子也短,几步就到底。

入口 InstallReferrerClientImpl.getInstallReferrer(),真正干活的就一行:把包名塞进 Bundle,交给 service.c()

java
复制代码
public ReferrerDetails getInstallReferrer() throws RemoteException {
    // 省略 isReady() 检查
    Bundle var1 = new Bundle();
    var1.putString("package_name", this.mApplicationContext.getPackageName());
    return new ReferrerDetails(this.service.c(var1)); // 关键:this.service.c(var1)
}

this.serviceIGetInstallReferrerServiceStub.Proxy。它的 c()Bundle 写进 Parcel,再甩给父类的 b()

java
复制代码
public final Bundle c(Bundle var1) throws RemoteException {
    Parcel var2 = ((a) this).a();     // 取 Parcel,写 InterfaceToken
    c.b(var2, var1);                  // 入参 Bundle 写进去
    Parcel var3 = ((a) this).b(var2); // 关键:交给父类 b() 发起调用
    // 省略:从 var3 读出返回的 Bundle
}

父类 b() 才是真正落到 Binder 的地方。this.a 就是一个 IBindertransact(1, …) 一调下去,当前线程当场同步阻塞,等对端返回:

java
复制代码
protected final Parcel b(Parcel var1) throws RemoteException {
    Parcel var2 = Parcel.obtain();
    try {
        this.a.transact(1, var1, var2, 0); // 关键:同步 Binder transact,阻塞在此
        var2.readException();
    } finally {
        var1.recycle();
    }
    return var2;
}

getInstallReferrer() → service.c() → b() → IBinder.transact() 这条链,对上的正是栈里这几帧(从栈顶往下):

text
复制代码
at android.os.BinderProxy.transactNative(Native method)
at android.os.BinderProxy.transact(BinderProxy.java:679)
at com.google.android.a.a.b(BaseProxy.java:2)
at com.google.android.finsky.externalreferrer.IGetInstallReferrerService$Stub$Proxy.c(IGetInstallReferrerService.java:3)
at com.android.installreferrer.api.InstallReferrerClientImpl.getInstallReferrer(InstallReferrerClientImpl.java:5)

到这儿源码和栈就接上了。栈里那行看着人畜无害的 getInstallReferrer(),底下其实是一次没有超时、没有回调的同步 transact——它快不快,全看 Play 商店那边的脸色。

第五步:是谁在主线程上干这事——归因 SDK

栈里 com.xxx.sdk.* 那几行(handleAttributionResulthandleSetupOkonInstallReferrerSetupFinished)才是真正的肇事方。这里为脱敏换成了 com.xxx.sdk 占位;真实堆栈里它是一串被 R8 重命名、毫无意义的随机包名——这种「乱码一样的包名」本身就是某些 SDK 强混淆留下的指纹。拿它跟项目依赖一对,归属就出来了:归因 SDK,这例子里是 AppsFlyer。

  • 项目用 Gradle 引了 AppsFlyer SDK;
  • 它在 App 启动初始化时,会去绑 Google Play 的 Install Referrer 服务,拉安装来源做归因;
  • 绑定、回调、取值这一套,全封在 SDK 内部。

也就是说,「在主线程同步调 getInstallReferrer()」这个动作是闭源 SDK 自己干的,业务代码一个字都没写过它——可它确实是被我们的启动初始化链路顺手拉起来的。

发起方清楚了:归因 SDK 的安装来源回调,在主线程上发了一次到 Play 商店的同步 Binder 调用。那还剩一个问题——这一下凭什么能卡到触发 ANR?

第六步:为什么会卡——三个条件凑一块了

卡死不是哪一条单独作妖,是三件单看都挺正常的事撞一起了。

一,getInstallReferrer() 底下是一次同步 Binder IPC。第四步拆源码已经看到那行 transact 了,Android 官方文档讲 IBinder.transact() 也是这个说法——默认同步,调用线程一路阻塞到对端处理完返回。换句话说,调用方快不快,全攥在对端手里。而对端是 Play 商店(com.android.vending),它什么时候回,我们说了不算:冷启动、被系统压着、设备内存紧张,都可能慢。

二,onServiceConnected() 默认在主线程回调。bindService 时没给后台 HandlerExecutorServiceConnection 的回调就默认投到主线程跑。栈底那串 LoadedApk$ServiceDispatcher → Handler.handleCallback → Looper.loop 就是「投到主线程」的实锤。

三,回调里直接就把值取了。SDK 在 onInstallReferrerSetupFinished 里没切线程,抬手就调了同步的 getInstallReferrer()

三件事叠一块:一个本该躲到后台去做的同步 IPC,被搁在主线程的服务回调里跑,Play 商店那头只要稍微一磨蹭,主线程就被拖过 ANR 线了。

第七步:回头看现场,系统当时是在「集体抢资源」

主因定了,我又把整份 dump 翻了一遍,想看看这是「Play 服务恰好卡了一下」的偶发,还是底下另有原因。一看,进程当时挂着差不多 300 个线程,整体就是 IPC 和内存双高压:

  • 8 个线程卡在 Binder transact 上(含主线程);
  • 6 个线程在等广告 ID(AdvertisingId);
  • 3 个线程在等数据库连接池;
  • GC 线程正好在做一次 MarkCompact 回收。

这些线程绝大多数是广告、分析、归因这类 SDK 的,全挤在冷启动那一下初始化。说白了,App 一起来就同时点着一大把 SDK,Binder、广告 ID、数据库、GC 全在抢,那次 InstallReferrer 的同步 IPC 自然等得更久。所以主因还是「主线程同步 IPC」,冷启动这场 SDK 风暴只是把它压过了 ANR 阈值。

第八步:横向对一下,这不是我们一家的事

到这儿结论基本立住了。保险起见,再拿业界同类问题对一对。拿 InstallReferrer + ANR + onServiceConnected 一搜,发现这坑被人反复踩过:

  • Facebook Android SDK · Issue #1039:标题直接就是《com.android.installreferrer is called in the main thread which leads to ANR》。报告人说得很明白,InstallReferrer 会长时间阻塞调用线程,贴的栈也是 onServiceConnected → getInstallReferrer → BinderProxy.transactNative,结论是放后台线程调才安全。issue 已关闭。
    https://github.com/facebook/facebook-android-sdk/issues/1039
  • TikTok Business Android SDK · Issue #14:标题《ANR triggered by com.tiktok.util.SystemInfoUtil >>> com.android.installreferrer due to slow binder transactions》。现象跟我们这例几乎一模一样——onInstallReferrerSetupFinished() 在主线程同步调 getInstallReferrer(),Binder 事务一慢就把主线程拖垮,在 Android 14+(API 34/35)上能复现,文里还直接引了 Facebook #1039 当同类。issue 已关闭。
    https://github.com/tiktok/tiktok-business-android-sdk/issues/14
  • Android 官方文档 · android.os.IBindertransact() 同步阻塞的语义就写在这儿,也是「别在主线程做可能耗时的 Binder 调用」这句话的出处。
    https://developer.android.com/reference/android/os/IBinder

Facebook、TikTok 两家都公开认过、也修过这同一类问题,跟我们查到的根因是一回事。

第九步:怎么修

分两类看——能自己马上动手的,和得等 SDK 配合的。

1)把归因/分析 SDK 的初始化挪出冷启动这段(马上能做,性价比也最高)。让它在首帧渲染之后、或主线程闲下来再初始化。错峰一拉,「主线程同步 IPC」正好撞上「Play 商店慢」的概率就低多了,顺带也把冷启动那场 SDK 风暴缓一缓。

2)把 SDK 升到已修复的版本(治本)。这类问题新版 SDK 基本都改成「在后台线程取 InstallReferrer」了。升归因 SDK 的同时,顺手确认它依赖的 com.android.installreferrer:installreferrer 也是新一点的版本。

3)推动「绑定回调走后台」(这条得 SDK 方动手)。getInstallReferrer() 同步这点改不了,但只要 bindService 时把后台线程的 Handler 传进去,让 onServiceConnected 别在主线程回调,或者在回调里先切个线程再取值,就不会堵主线程。这也正是 Facebook #1039 给的修法,可以当作反馈丢给 SDK 方。

4)管住冷启动期同时初始化的 SDK 数量。少几个同一刻一起初始化的广告/分析/归因 SDK,Binder、广告 ID、数据库、GC 的争抢就轻一些,主线程也能多喘口气。

最后,把这次的查法顺一遍

下次再碰上这种栈,照着走就行:

  1. 先看主线程。ANR 先找 main / tid=1,别一上来就跟其他线程较劲。
  2. 先看状态,再读栈。native 往系统调用 / IPC / JNI 查,blocked / waiting 往锁查——方向先定对。
  3. 分段读栈。栈顶看「在等什么」,中段看「在叫谁、哪个进程」,栈底看「跑在哪个线程、被谁拉起来的」。中段那几帧库内部代码看不懂,就在 AS 里点进对应的库、跟着源码顺一遍,黑盒调用能落成具体机制(这次就是那行同步 transact)。
  4. 混淆包名也能反查来源。一串乱码似的随机包名,多半是某个 SDK 强混淆的特征,对一下依赖清单就知道是谁。
  5. 主线程 + 同步跨进程调用,基本就是高危信号。凡是在主线程等一个自己控制不了耗时的 IPC(Play 商店、系统服务、别的 App),都是 ANR 温床;而 ServiceConnection 回调默认就在主线程,是高频踩点。
  6. 最后横向对一下。业界同类 issue 一翻,根因和修法都能再交叉验证一遍。

参考资料

  1. Facebook Android SDK · Issue #1039 — com.android.installreferrer is called in the main thread which leads to ANR
    https://github.com/facebook/facebook-android-sdk/issues/1039
  2. TikTok Business Android SDK · Issue #14 — ANR triggered by com.tiktok.util.SystemInfoUtil >>> com.android.installreferrer due to slow binder transactions
    https://github.com/tiktok/tiktok-business-android-sdk/issues/14
  3. Android Developers — android.os.IBinder
    https://developer.android.com/reference/android/os/IBinder
  4. Android 开发者文档 — 诊断和修复 ANR
    https://developer.android.com/topic/performance/anrs/diagnose-and-fix-anrs?hl=zh-cn
  5. Android 开发者文档 — 查找无响应的线程 · 速度缓慢的 Binder 调用
    https://developer.android.com/topic/performance/anrs/find-unresponsive-thread?hl=zh-cn#slow-binder
本文由 A lonely cat 原创发布于 阳光沙滩 , 未经作者授权,禁止转载
评论
0 / 1024
推荐文章
浅析JS预编译-函数篇
本文详细讲解了JavaScript中函数的预编译过程,包括AO对象的创建、变量和函数声明的提升,以及执行阶段的变化。适合初学者了解JS执行机制,帮助深入理解代码运行逻辑。
访问Github的另一种姿势
本文介绍了如何通过Watt ToolKit等工具加速访问Github,适合对网络技术感兴趣的读者。内容涵盖工具介绍、下载和使用方法,以及一些实用技巧,值得一看。
使用Gulp压缩JS
本文详细介绍了如何使用Gulp压缩JavaScript文件,特别是支持ES6语法的处理方法。通过实际操作和效果对比,帮助开发者提升代码性能,适合对前端自动化工具感兴趣的读者。
使用Gulp压缩CSS
本文详细介绍了如何使用Gulp工具对CSS文件进行压缩,提升网页加载速度。适合需要优化静态资源的开发者,提供清晰的步骤和示例,帮助快速上手。
从0部署Nuxtjs项目
本文详细介绍了如何将Nuxt.js项目部署到公网,涵盖Node.js安装、环境配置、源码上传及使用PM2启动服务的全过程,适合开发者学习和实践。
Linux 命令行美化利器:tree 命令完全指南(安装 + 全部参数详解)
掌握 Linux 下的 `tree` 命令,快速直观地查看目录结构。本文详细讲解安装方法、参数含义及实战场景,适合开发者和系统管理员提升工作效率。
Linux常用命令(一)
本文详细介绍了Linux基础命令,包括文件操作、系统显示、网络状态、软件包管理等,适合初学者学习和查阅,是掌握Linux系统的实用指南。
大模型推理参数解码:温度、Top-p 与核采样如何塑造生成文本的灵魂
探索大语言模型的采样参数如何塑造生成内容,从温度到Top-p,从惩罚机制到熵控制,揭示背后的信息论原理与实践策略。了解如何通过参数调优,让AI既保持知识的严谨,又拥有创造力的自由。
记从0使用Claude code写代码
本文介绍了如何使用Claude Code搭配DeepSeek进行编程,详细讲解了安装Node.js、全局安装Claude、配置模型以及使用方法。对于开发者来说,这是一份实用的技术教程,尤其适合对AI编程工具感兴趣的读者。
Android-Studio-Gradle同步失败-Library为null的通用排查指南
遇到 Android Studio Gradle Sync 失败时,不要轻易删除全局缓存。本文详细解析了错误原因,并提供了一系列精准的排查步骤和解决方案,帮助开发者快速定位并解决问题,提升开发效率。
PHP实现密码加密
本文详细介绍了Bcrypt密码加密技术及其在PHP中的应用,帮助开发者提升用户密码的安全性。通过实际代码示例,展示了如何使用password_hash和password_verify函数进行密码加密与验证,是学习网络安全知识的实用指南。
WordCloud效果,滚动标签
本文详细介绍了如何在Vue项目中集成WordCloud组件,包括依赖安装、属性配置和使用方法。适合开发者学习如何实现数据可视化功能,提升项目交互体验。
从0使用WordPress搭建一个优美的网站
本文详细介绍了如何使用WordPress搭建一个美观的网站,从安装到主题配置和功能拓展,为读者提供了实用的操作指南。无论是初学者还是有一定经验的开发者,都能从中获得有价值的参考。
JS实现在网站底部添加运行时间
想知道如何在网站底部显示运行时间?本文详细讲解了通过JavaScript实现这一功能的方法,包括时间计算逻辑和代码实现。适合前端开发者学习参考,轻松为网站添加实用功能。
在Vercel上部署Hexo博客
本文详细介绍了如何在Vercel上快速部署Hexo博客,无需后端服务即可实现高效发布。相比传统方式,省去了手动生成和上传静态文件的步骤,更加便捷。适合想要搭建个人博客的开发者参考。
从0搭建一个Hexo博客
本文详细介绍了Hexo博客框架的使用方法,从安装到部署全流程讲解,适合想快速搭建个人博客的技术爱好者。内容清晰易懂,是入门Hexo的理想指南。
离线设备激活方案:古老的 Windows 光盘激活,离线算法授权等
本文深入解析了离线激活的核心原理与实现方式,从历史案例到现代技术,全面剖析了如何在无网络环境下确保软件授权的安全性。通过设备指纹、授权文件校验等手段,为开发者提供了可复用的架构设计思路,适用于工业、医疗等对网络依赖较低的场景。
Hexo实现生成站点地图
想为你的Hexo博客添加站点地图功能吗?本文详细介绍了如何通过安装插件和配置文件来实现,适合没有内置该功能的新主题或自定义主题的用户。简单步骤助你提升搜索引擎优化效果。
Hexo实现代码压缩
本文分享了如何通过Hexo插件优化博客性能,详细介绍了安装和配置过程,帮助提升网页加载速度。适合对网站优化感兴趣的开发者阅读。
记一次 GitHub 幽灵协作者大清洗:强制重写 Git 历史与穿透 CDN 缓存实践
本文详细讲解了如何解决GitHub上出现的‘幽灵协作者’问题,通过重写Git历史和穿透CDN缓存,彻底清理错误提交记录。适合开发者学习如何高效管理项目历史与优化仓库信息。
从一行 `native` 堆栈追到 InstallReferrer:一次主线程 ANR 的排查全过程
本文详细记录了一次主线程ANR的排查过程,从一行native堆栈追踪到InstallReferrer服务调用。通过分析堆栈、源码和系统调用,揭示了归因SDK在主线程同步调用Play商店服务导致的ANR问题,并提供了多维度的解决方案。对于Android开发者来说,是一篇深入浅出的技术实践指南。
Linux从 HelloWorld 到数据库服务注册
本文详细解析了Linux系统中服务注册的概念与实现,通过一个简单的Hello World示例,帮助读者理解如何将程序注册为systemd服务,并掌握相关操作命令。无论是数据库还是其他应用,了解服务注册机制都是系统管理的重要基础。
学习虚拟机的笔记
linux ps 命令详解,跟着敲一次就掌握了
深入了解 Linux 中最常用的进程查看命令 `ps`,掌握其各种用法和参数,适用于系统管理和故障排查。从基础到高级,全面解析 `ps` 的使用技巧,帮助您提升 Linux 运维技能。
ObjectMapper 入门:Java 对象与 JSON 之间的「翻译官」
了解ObjectMapper在Java中如何实现对象与JSON的转换,掌握其在Spring Boot项目中的应用及常见使用场景。本文详细解析了序列化/反序列化过程、API用法、与Spring MVC的关系以及与其他JSON库的对比,适合开发者快速上手和深入理解。
服务器一次中病毒的记录
本文详细描述了一次服务器异常流量的排查过程,发现大量外部IP与内部服务建立连接,疑似存在恶意程序。通过分析日志和图片,确认为恶意程序导致带宽占用过高,最终通过备份和删除操作解决问题。文章提供了技术排查思路和解决方案,对系统维护具有参考价值。
JavaWeb微服务脚手架搭建
本文介绍了构建微服务架构时常用的开发模板和核心组件,涵盖技术选型、依赖配置及版本差异分析。通过合理选择 Java 和 Spring Boot 版本,可以显著提升开发效率和系统性能,是开发者不可错过的实践指南。
面向 Java 程序员的 MinIO 入门教程
本文为Java程序员提供了一份详细的MinIO入门教程,涵盖MinIO的部署方法和Java SDK的集成使用。通过本文,您将学习如何在Java项目中高效管理桶和对象,快速上手MinIO这一高性能对象存储服务。
你知道:气和汽的区别吗?
了解‘气’和‘汽’的区别,掌握它们在不同语境下的含义与用法,帮助你更准确地使用中文。无论是日常交流还是写作,这对提升语言能力都大有裨益。
wsl update 下载不下来怎么办呀?
遇到Docker Desktop提示需要更新但无法解决?本文教你如何通过GitHub下载并安装WSL,轻松解决更新问题,适合使用x64芯片的用户。