native 堆栈追到 InstallReferrer:一次主线程 ANR 的排查全过程
线上挂了一批长得几乎一样的 ANR,堆栈顶上全是系统库,翻来覆去看不见自己的代码。这篇把当时一步步往下查的过程原样记下来——从一行 native 栈帧,一直追到根因。
文中堆栈做过脱敏:应用包名、自研模块、第三方 SDK 被混淆的内部类,统一换成
com.xxx.sdk.*这种占位。Android Framework、ART、Binder,还有com.android.installreferrer.*、com.google.android.finsky.*这些系统和公开组件都留着原样——它们恰好是查问题的关键,换掉就没法看了。
Crashlytics 上这批 ANR 长得很像,几个共同点:
libc.so __ioctl、libbinder.so 这类 native 帧;android.*,一行自家业务代码都没有;这种栈最容易被一句「系统问题,修不了」打发掉。但只要 dump 是完整的,主线程到底卡在哪一帧,是写死在那儿、跑不掉的。所以先别急着下结论,把整段栈摆出来再说。
下面是主线程(main,tid=1)的完整堆栈,基本就是原始 dump 的样子——#NN pc 0x… 是 native 帧,at 开头是 Java 帧。我只动了两处:内部 SDK 的混淆包名换成了 com.xxx.sdk.*,每帧末尾设备相关的 BuildId 删掉了,其余照贴。先扫一眼,不用逐帧看懂,后面会一段段拆:
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 里挂着几百个线程都不要紧,先找 main、tid=1,看它卡在哪。
栈的第一行(线程头)写得很清楚:
main (native):tid=1 systid=17704
tid=1 坐实了这是主线程。但更该留意的是括号里那个状态——native。第一个信号就在这。
native 状态先把「Java 死锁」排掉native 这个状态告诉我们两件事:
synchronized / ReentrantLock 上——真要是锁,状态会写 blocked / waiting,还跟一句 held by thread X;顺手在整份 dump 里搜 held by 和 waiting to lock,一条没有,死锁这条路堵死。
这一步其实是在定方向:看到 blocked / waiting 就去查锁,看到 native 就去查系统调用、IPC、JNI。方向选错,后面全是白费力气。
回到那段栈,从栈顶往栈底一段段看:
__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 从哪来第三步是顺着栈读出来的结论。光读栈还不够踏实,我们换个方向再核一遍。项目里这库的依赖就一行:
implementation("com.android.installreferrer:installreferrer:2.2")
直接在 Android Studio 里点进 getInstallReferrer(),跟着源码往下看,那次 transact 是怎么冒出来的就一目了然。库不大,链子也短,几步就到底。
入口 InstallReferrerClientImpl.getInstallReferrer(),真正干活的就一行:把包名塞进 Bundle,交给 service.c():
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.service 是 IGetInstallReferrerService 的 Stub.Proxy。它的 c() 把 Bundle 写进 Parcel,再甩给父类的 b():
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 就是一个 IBinder,transact(1, …) 一调下去,当前线程当场同步阻塞,等对端返回:
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() 这条链,对上的正是栈里这几帧(从栈顶往下):
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 商店那边的脸色。
栈里 com.xxx.sdk.* 那几行(handleAttributionResult、handleSetupOk、onInstallReferrerSetupFinished)才是真正的肇事方。这里为脱敏换成了 com.xxx.sdk 占位;真实堆栈里它是一串被 R8 重命名、毫无意义的随机包名——这种「乱码一样的包名」本身就是某些 SDK 强混淆留下的指纹。拿它跟项目依赖一对,归属就出来了:归因 SDK,这例子里是 AppsFlyer。
也就是说,「在主线程同步调 getInstallReferrer()」这个动作是闭源 SDK 自己干的,业务代码一个字都没写过它——可它确实是被我们的启动初始化链路顺手拉起来的。
发起方清楚了:归因 SDK 的安装来源回调,在主线程上发了一次到 Play 商店的同步 Binder 调用。那还剩一个问题——这一下凭什么能卡到触发 ANR?
卡死不是哪一条单独作妖,是三件单看都挺正常的事撞一起了。
一,getInstallReferrer() 底下是一次同步 Binder IPC。第四步拆源码已经看到那行 transact 了,Android 官方文档讲 IBinder.transact() 也是这个说法——默认同步,调用线程一路阻塞到对端处理完返回。换句话说,调用方快不快,全攥在对端手里。而对端是 Play 商店(com.android.vending),它什么时候回,我们说了不算:冷启动、被系统压着、设备内存紧张,都可能慢。
二,onServiceConnected() 默认在主线程回调。bindService 时没给后台 Handler 或 Executor,ServiceConnection 的回调就默认投到主线程跑。栈底那串 LoadedApk$ServiceDispatcher → Handler.handleCallback → Looper.loop 就是「投到主线程」的实锤。
三,回调里直接就把值取了。SDK 在 onInstallReferrerSetupFinished 里没切线程,抬手就调了同步的 getInstallReferrer()。
三件事叠一块:一个本该躲到后台去做的同步 IPC,被搁在主线程的服务回调里跑,Play 商店那头只要稍微一磨蹭,主线程就被拖过 ANR 线了。
主因定了,我又把整份 dump 翻了一遍,想看看这是「Play 服务恰好卡了一下」的偶发,还是底下另有原因。一看,进程当时挂着差不多 300 个线程,整体就是 IPC 和内存双高压:
transact 上(含主线程);AdvertisingId);这些线程绝大多数是广告、分析、归因这类 SDK 的,全挤在冷启动那一下初始化。说白了,App 一起来就同时点着一大把 SDK,Binder、广告 ID、数据库、GC 全在抢,那次 InstallReferrer 的同步 IPC 自然等得更久。所以主因还是「主线程同步 IPC」,冷启动这场 SDK 风暴只是把它压过了 ANR 阈值。
到这儿结论基本立住了。保险起见,再拿业界同类问题对一对。拿 InstallReferrer + ANR + onServiceConnected 一搜,发现这坑被人反复踩过:
com.android.installreferrer is called in the main thread which leads to ANR》。报告人说得很明白,InstallReferrer 会长时间阻塞调用线程,贴的栈也是 onServiceConnected → getInstallReferrer → BinderProxy.transactNative,结论是放后台线程调才安全。issue 已关闭。com.tiktok.util.SystemInfoUtil >>> com.android.installreferrer due to slow binder transactions》。现象跟我们这例几乎一模一样——onInstallReferrerSetupFinished() 在主线程同步调 getInstallReferrer(),Binder 事务一慢就把主线程拖垮,在 Android 14+(API 34/35)上能复现,文里还直接引了 Facebook #1039 当同类。issue 已关闭。android.os.IBinder:transact() 同步阻塞的语义就写在这儿,也是「别在主线程做可能耗时的 Binder 调用」这句话的出处。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 的争抢就轻一些,主线程也能多喘口气。
下次再碰上这种栈,照着走就行:
main / tid=1,别一上来就跟其他线程较劲。native 往系统调用 / IPC / JNI 查,blocked / waiting 往锁查——方向先定对。transact)。ServiceConnection 回调默认就在主线程,是高频踩点。com.android.installreferrer is called in the main thread which leads to ANRcom.tiktok.util.SystemInfoUtil >>> com.android.installreferrer due to slow binder transactionsandroid.os.IBinder





