使用火山引擎 APMPlus 解决抖音Top 1 Java 崩溃的通用优化方案
2023-11-29 18:40:33 Author: mp.weixin.qq.com(查看原文) 阅读量:3 收藏

背景

近3个月,抖音 Android 版面临一个多次触发线上报警的崩溃问题,全量版本和灰度版本的异常数据激增,该问题不仅容易触发报警,更成为了 Java Top 1 崩溃问题,带来巨大困扰,急需攻坚解决。

本文展现了具体的分析过程、优化思路和解决方案,同时提供了已集成该方案的实用工具。

初步分析

多维特征

我们以某发版期间数据为例进行分析:

  • 机型方面:比较分散,有聚集部分samsung sm-s9180 占比 top1
  • 渠道方面:比较分散,华为、小米、三星占比 top3。
  • ROM方面:比较分散。
  • OS 版本方面:排除古老的 5.1.1 系统,只有 10 以上版本,12/13/10 占比 top3
  • App小版本方面,排除古老的版本,基本跟日活排序一致。

通过对多维特征分析,我们得出结论是:存在 OS 高版本和三星机型聚集特征。

崩溃堆栈

从下图我们看到,崩溃类型 TransactionTooLargeException,崩溃现场在 BinderProxy.transactNative 方法附近,崩溃提示消息 data parcel size xxx bytes,崩溃时 Activity 在向 AMS 传递 stop 信息。初步分析这些信息,我们得出结论:

  • 这是由 Binder 传输数据量超过阈值触发的崩溃。
  • 排除其中无关的动态代理 ProxyInvocationHandler 的业务堆栈,其它都在系统堆栈中,无法直接定位到具体业务方解决。

崩溃日志

除了崩溃堆栈,我们也要分析崩溃日志,这样往往能够从中获得一些有价值的线索。以下,我们分别从聚合 logcat 日志和单次 logcat 日志进行分析。通过 应用性能监控全链路版(APMPlus)App端监控 的 logcat 词云,看到聚合日志 top1 是 JavaBinder: !!! FAILED BINDER TRANSACTION !!! (parcel size = **。这个是系统 Binder 传输数据失败打印的日志。

我们从源码里面找到打印日志的地方,发现是在 Native 层 Binder.cpp 里面,接收到 Binder 驱动的错误码 FAILED_TRANSACTION 之后,先打印错误日志,然后通过 jniThrowException 函数从 JNI 层进入 Java 层抛出异常。

通过对聚合 logcat 日志分析,我们得到结论:只有系统 Binder 传输失败日志,无业务相关日志,不确定是哪个业务不合理导致的崩溃。根据前面聚合 locat 日志关键字,我们翻看了几十个上报日志,发现在 JavaBinder: !!! FAILED BINDER TRANSACTION !!! (parcel size = **的后面紧接着出现 Tag 为 ActivityStopInfoBundle stats: 日志,其中一个如图所示。因此这时我们知道 Binder transaction 传输的 parcel 内容很大可能跟 Bundle 有关。

从打印的 Bundle stats:树形结构看,只打印了系统的 view 和 fragment 占用 size,没有 App 业务相关占用 size,无法细分是哪里不合理。另外在日志 JavaBinder: !!! FAILED BINDER TRANSACTION !!! (parcel size = ** 的前面我们终于发现了跟业务相关的 Activity 是 com.ss.android.ugc.aweme.detail.ui.DetailActivity。翻看了几十个上报日志,并不是每个日志都是这个 com.ss.android.ugc.aweme.detail.ui.DetailActivity ,还有 com.ss.android.ugc.aweme.shortvideo.ui.VideoPublishActivity等。

经过对单次 logcat 日志分析,能够从单次 logcat 日志中知道是哪个 Activity stop 时出错,但是目前分析不出这些处于 stop 状态的 Activity 占比情况。

综合聚合和单次 logcat 日志看,无明显 App 业务逻辑聚合,只有系统日志,无法简单定位到某个业务解决。

初步结论

经过前面的分析,我们得出初步结论:

  • 问题是 Activity stop 时给 AMS 传输 Bundle 超过 Binder 驱动限制的大小,触发了崩溃。
  • 调用栈和日志无明显业务聚合,无法简单确定是哪个业务引入的问题。

因此,需要进行深入分析,一方面要从监控归因着手排查其中不合理的因素,另一方面也要从底层通用解决方案尝试突破限制彻底解决问题。

深入分析

下面将主要从崩溃堆栈进行深入分析,翻了几十个崩溃堆栈,我们发现并不是只有 Activity stop 调用栈,还有 Activity start 调用栈,主要包含两类,一个是 activityStopped 关键字的崩溃,占比约 97.25%;另一个是 handleStartActivity 关键字的崩溃,占比约 2.75%,这些崩溃主要与三星兼容性问题有关。

因此,我们判断在有限的时间和精力下,先解决 top 的 activityStopped 关键字的崩溃,以下主要是对 activityStopped 分析。如图所示,我们接下来从底层向上层对核心逻辑崩溃堆栈进行逐步分析。

Caused by: android.os.TransactionTooLargeException: data parcel size 541868 bytes

根据关键字 data parcel size 和相关源码分析,我们得出 data parcel size 附近的数据处理流程:

  1. JNI 层 BinderProxy_transact 函数先调用 IBinder->transact 函数进行 Binder 传输数据。
  2. IBinder->transact 函数的返回值 err 用于识别 Binder 传输数据错误码。
  3. 当 err != NO_ERROR && err != UNKNOWN_TRANSACTION 情况下,说明 Binder 传输数据出现异常。
  4. signalExceptionForError 函数对异常情况多种错误码进行分发处理。
  5. 当错误码 err = FAILED_TRANSACTION 时,说明是在传输的过程中失败,接下来主要根据 parcelSize 和 200*1024 的大小关系处理。
  6. 当 parcelSize > 200*1024 时,后续通过 jniThrowException 函数在 Java 层抛出 TransactionTooLargeException,并提示消息 "data parcel size %d bytes"
  7. 当 parcelSize <= 200*1024 时,后续通过 jniThrowException 函数在 Java 层抛出 DeadObjectException 或者 RuntimeException,并提示消息 "Transaction failed on small parcel; remote process probably died, but this could also be caused by running out of binder buffer space",意思是 Binder 传输小的数据量情况下也可能失败,原因有两个:第一是远程进程可能已经死亡,Binder 驱动找不到远程进程对应的 mmap 虚拟内存,也就无法完成 Binder 数据传输;第二是远程进程还存活,但是它的 Binder mmap 接收缓冲区已经被用完,无法分配空闲的 mmap 虚拟内存,也就无法完成 Binder 数据传输。

通过对错误消息分析,我们得出结论:

  • IBinder transact 函数返回 FAILED_TRANSACTION 错误码且 parcelSize > 200*1024 情况下会抛出 TransactionTooLargeException。

BinderProxy.transactNative 流程

前面我们在 signalExceptionForError 函数中读取这个 FAILED_TRANSACTION 错误码,接下来我们要知道其写入的地方在哪里,以便理顺数据处理流程。通过关键字查找,我们发现这个 FAILED_TRANSACTION 错误码只在 IPCThreadState::waitForResponse 函数中写入。对应有两种情况会写入:BR_FAILED_REPLY 回复失败,BR_FROZEN_REPLY 冻结回复。

这两个 cmd 对应的值是从 mIn.readInt32读取出来,也就是 Parcel 中读取出来的命令值。

我们依然循着错误码传递路径来到 Binder 驱动层。

BR_FAILED_REPLY 错误码在 binder 驱动中有 40 个引用,过于宽泛,没有唯一性,导致继续使用这个排查方向的 ROI 不高,因此暂停这个排查方向。

http://aospxref.com/kernel-android13-5.10-lts/more/drivers/android/binder.c?full=BR_FAILED_REPLY

BR_FROZEN_REPLY 错误码在 binder 驱动中有 1 个引用,指进程/线程被冻结情况,主要用于区分进程死亡时 Binder 无法回复 BR_DEAD_REPLY。我们这个 Activity stop 场景用户大概率还在前台,App 和 System Server 不太可能出现进程被冻结,因此暂停这个排查方向。

http://aospxref.com/kernel-android13-5.10-lts/xref/drivers/android/binder.c#6298

除了错误码之外,我们结合源码得出 BinderProxy.transactNative 执行流程如下:

  • Java 层 BinderProxy.transactNative 调用 Native 层 android_os_BinderProxy_transact。
  • 数据经过 IBinder::transact、BpBinder::transact 函数调用到达 IPCThreadState::transact。
  • IPCThreadState::writeTransactionData 先把待传输数据写入 Parcel 。
  • IPCThreadState::waitForResponse 负责和 Binder 驱动通讯进行数据发送和接收,并处理接收的命令。
  • IPCThreadState::talkWithDriver 负责和和 Binder 驱动通讯,关键是通过 ioctl 系统调用进入内核处理。

PendingTransactionActions$StopInfo.run 流程

前面我们分析了崩溃类型 TransactionTooLargeException、崩溃消息 data parcel size xx bytes、和堆栈最底层函数 BinderProxy.transactNative 数据处理流程之后,得出的结论是:该崩溃是Binder 传输数据超过 200 * 1024 时出现的崩溃。

由于 BinderProxy.transactNative 是底层通用函数,它的调用方有很多,我们需要快速确定这个崩溃场景最大的 Context 是在哪个里面,所以我们直接跳到分析主线程消息处理里面业务根消息PendingTransactionActions$StopInfo.run 的分析。

通过下图源码,我们发现 App 进程通过 Binder IPC 通知 System Server 进程的 AMS Activity Stop 信息时,有个 catch RemoteException 异常捕获逻辑。

通过先打印 Bundle 统计数据,后进行异化逻辑处理,我们看到 targetSdk < 24 情况下,程序打印了一行日志 App sent too much data in instance state, so it was ignored,然后 return 返回,并没有执行 throw 异常。所以推测系统在这种情况下其实不崩溃。

使用 App 监控统计分析到,6/7/10系统没有崩溃,这个和源码逻辑可以匹配上。在 targetSdk >= 24 之后,把捕获的异常重新通过 throw 关键字抛出,这种情况下会出现崩溃。经过回溯 StopInfo 中的 Bundle 来自Activity.onSaveInstanceState :

因此,我们得出崩溃场景是:

  • ActivityThread.handleStopActivity 过程中保存 callActivityOnSaveInstanceState 产生的 Bundle 数据。
  • 在主线程异步消息执行 StopInfo.run 告诉 AMS activityStopped 信息过程中,传递 Bundle 数据用于未来恢复 Activity 状态。

Activity.onRestoreInstanceState 流程

前面我们分析崩溃场景 Binder 传输数据和 Activity.onSaveInstance 的 Bundle 数据有关,接下来我们分析这个 Bundle 数据恢复处理流程。

  • Activity 被销毁重建场景,在 ActivityThread.performLaunchActivity 函数中,Bundle 会跟随新 Activity 实例创建从 AMS 传递到 App 进程内。
  • 由于 ActivityThread.performLaunchActivity 中 Bundle 是从 AMS 进程序列化传递到 App 进程,所以其中的类需要在 App 进程进行反序列化,重新进程类加载和类初始化,所以 Bundle.setClassLoader 用于 App 进程内 Bundle 数据反序列化。
  • 接下来通过 Instrumentation 传递 Bundle 到 Activity.onCreate 函数,在这里是 Activity 接收到 Bundle 最早的地方,Activity 可以从这里开始访问被恢复的 Bundle 数据。
  • 在 ActivityThread.handleStartActivity 处理 start 时,也会通过 Activity.onRestoreInstanceState 函数用于恢复 Activity 状态。

TransactionTooLargeException 直接原因

经过前面分析,我们知道 Binder 传输数据量超限是 Activity.onSaveInstance 保存的 Bundle 过大导致。但是我们并不知道 Bundle 超过多大会崩溃,比如 Bundle 超过 200K、500K、1M 会崩溃吗?

为了分析崩溃执行过程,在 demo App 中 Activity.onSaveInstanceState 保存大数据,并使用 root 机器分析崩溃场景日志:

// 4692 是发送方 app 进程 pid,1838 是接收方 system server 进程 pid。接收方接收数据时,遇到分配 4198824 虚拟内存失败,没有足够的虚拟地址空间
4692  4692 E binder_alloc: 1838: binder_alloc_buf size 4198824 failed, no address space
4692  4692 E binder_alloc: allocated: 2240 (num: 8 largest: 2080), free: 1038144 (num: 4 largest: 1036672)
4692  4692 I binder  : 4692:4692 transaction failed 29201/-28, size 4198812-8 line 3317
4692  4692 E JavaBinder: !!! FAILED BINDER TRANSACTION !!!  (parcel size = 4198812)

结合源码,我们得出 BinderProxy.transactNative 执行流程是这样:

  • Java 层 BinderProxy.transactNative 调用 Native 层 android_os_BinderProxy_transact 。
  • 数据经过 IBinder::transact、BpBinder::transact 函数调用到达 IPCThreadState::transact 。
  • IPCThreadState::writeTransactionData 先把待传输数据写入 Parcel 。
  • IPCThreadState::waitForResponse 负责和 Binder 驱动通讯进行数据发送和接收,并处理接收的命令。
  • IPCThreadState::talkWithDriver 负责和和 Binder 驱动通讯,关键是通过 ioctl 系统调用进入内核 binder_ioctl 处理。
  • binder_thread_write 负责向目标进程写入 Binder 传输数据,binder_thread_read 负责读取其它进程发送给当前进程的 Binder 传输数据。
  • 进程间传输数据走到公用函数 binder_transaction,通过函数参数 int reply识别区分是否是发送数据还是接收数据。
  • 在往一个进程写入传输数据之前,需要通过 binder_alloc_new_buf_locked 函数先申请合适大小的 Binder 接收缓冲区,如果分配失败,则无法完成数据传输。

因此,TransactionTooLargeException 直接原因是接收方进程 Binder mmap 接收缓冲区无法分配足够大的空闲内存。

Binder 传输数据最大值

前面我们只传输了约 500K 大小的数据就遇到了 TransactionTooLargeException 问题。那么

  • 是否 Binder mmap 接收缓冲区大小就是小于 500K 呢?
  • 是否最大缓冲区其实超过 100M,只是被其它 Binder 调用分配用完了呢?

通过 binder 驱动底层源码分析,单进程 Binder mmap 分析最大值 4MB,但是还受到参数 vma 控制。

而从整个 Binder 架构初始化流程分析,这个参数来自 ProcessState 里面,最大值是 1M - 8K。

这两个值里面取最小值作为 Binder 传输数据最大值。

因此,一个进程 Binder 接收缓冲区最大是 1M-8K。

问题总结

经过前面的深入分析,我们对这个问题得出更加清晰的结论:

  • 问题是在 App 进程向 system_server 进程发送 Activity.onSaveInstanceState 保存的 Bundle 数据时,驱动层在 system_server 的 Binder 接收缓冲区(大小上限 1M-8K)中分配不出用于接收 Bundle 大小的内存,导致 Binder 发送失败
  • Bundle 数据用于 Activity 页面销毁重建时恢复页面状态,其来源包含 framework 和 App 层保存的数据。当页面过于复杂或者很重场景下可能会出现超限问题
  • 从日志和调用栈无法定位/拆解出 Bundle 中什么数据过大导致的崩溃,需要有归因和解决工具

解决方案

优化思路

前面我们分析得知崩溃场景是 Activity stop 和 start 时和 AMS 传递的 Bundle 过大流程:

  • App 进程内通过 Activity.onSaveInstance 方法把数据放到 Bundle 中
  • 通过 Binder Driver 提供的接口 App 把 Bundle 写入 System Server 进程的 mmap 内存
  • 接下来 Activity 重建/启动时,System Server 再把保存的 Bundle 数据转发给 App 进程内 Activity

按照这个数据处理流程,一旦 Bundle 超过进程 Binder 接收缓冲区大小,就会触发 Binder 传输失败。

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    // 需要 hook 拦截处理 outState
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    // 需要 hook 拦截处理 savedInstanceState
    super.onRestoreInstanceState(savedInstanceState);
}

因此,结合问题场景和现状,我们的优化思路是:

  • 由于目前没有太好的办法在运行时应用态直接修改 Binder 驱动层内核态的执行指令,因此主要从上层 App 应用态入手解决问题
  • 在 onSaveInstanceState 之后把 outState 缩小到小于底层 Binder 传输的阈值
  • 在 onRestoreInstanceState 之前把 savedInstanceState 扩大到原始大小给业务层使用

Hook 点

通过阅读源码和断点调试,我们发现一个合适的 hook 类 ActivityLifecycleCallbacks,其能满足我们的诉求:

  • onActivityPostSaveInstanceState 正好是在 onSaveInstanceState 之后执行,而且能拿到 outState 参数
  • onActivityPreCreated 正好是在 onRestoreInstanceState 之前执行,而且能拿到 savedInstanceState 参数

所以我们通过 Application.registerActivityLifecycleCallbacks 能够拿到合适的执行时机。

缩小 Binder 传输 Bundle

为了让 Binder 读取到的 Bundle 数据量小从而避免崩溃,我们需要思考小到什么程度?比如:

  • 完全不执行 onSaveInstanceState 函数,这样也就不会超过 Binder 阈值
  • onSaveInstanceState 中主动调用 clear 函数清理 outState 数据,这样也不会超过 Binder 阈值
  • 把 outState 数据替换为一个小的 KEY 数据,而且建立 KEY-outState 一一对应的 Map 结构,方便查找还原 Bundle

为了尽量不影响系统本身的 save 和 restore 逻辑,我们选择传输 KEY 数据缩小 Bundle 数据。其中依赖的 KEY 我们使用 UUID 生成方式。

public void onActivityPostSaveInstanceState(Activity activity, Bundle outState) 
    outState.clear();
    String uuid = UUID.randomUUID().toString();
    sKey2ContentMap.put(uuid, outState);
    outState.putString("TransactionTooLargeOptKey", uuid);
}

还原 Binder 传输 Bundle

前面我们有了 KEY 和 Map 结构,现在我们只需要根据 KEY 查找对应的 Bundle 数据进行还原即可:

public void onActivityPreCreated(Activity activity, Bundle savedInstanceState) {
    if (savedInstanceState != null) {
        Object uuid = savedInstanceState.get("TransactionTooLargeOptKey");
        Bundle bundle = sKey2ContentMap.get(uuid);
        if (bundle != null) {
            savedInstanceState.clear();
            savedInstanceState.putAll(bundle);
        }
    }
}

功能、性能和稳定性折中

有了整体稳定性优化数据处理流程之后,我们发现维护的 Map 结构需要一定的空间开销,可能包括内存或者外存。

一方面,假如我们需要跟系统 save 和 restore 行为保持一致,即冷启动之后还可以恢复 Activity 状态,那么需要把 Bundle 信息保存在外存。而外存一般只能保存文本/二进制数据,这就需要把 Bundle 进行转换成二进制,并且涉及到一些 io 操作,很可能影响性能。

另一方面,我们对抖音主要 Activity 进行销毁重建,发现 Activity 并不能很完美的支持恢复能力。

所以,基于以上两点,我们决定 Map 结构不涉及的 io,只做内存级别。

前面我们的流程中针对所有 Bundle 都进行了优化,可能它就没有超过 Binder 的阈值。因此,为了精准识别问题场景,我们希望对超过一定大小的 Bundle 才进行优化。所以我们使用 Parcel 对 Bundle 进行了序列化量化:

private static byte[] bundle2Bytes(Bundle outState) {
    Parcel parcel = Parcel.obtain();
    try {
        parcel.writeBundle(outState);
        return parcel.marshall();
    } catch (Throwable t) {
        t.printStackTrace();
    } finally {
        parcel.recycle();
    }
    return null;
}

把 Bundle 序列化为 byte[] 之后,我们限制大小超过 1024 Bytes 才进行优化。

假如用户不断触发 save 操作,那么很可能会挤爆 Map 内存。所以我们需要有个 Map 容量上限和清理逻辑。这里初步设置容量 64,超限之后清理掉最久不用的记录:

if (sKey2ContentMap.size() > 64) {
    Set<Map.Entry<String, byte[]>> entries = sKey2ContentMap.entrySet();
    List<String> removeKeys = new ArrayList<>();
    // 清理掉前 20%,留下后面 80%
    final int removeCount = (int) (64 * 0.2);
    for (Map.Entry<String, byte[]> entry : entries) {
        removeKeys.add(entry.getKey());
        if (removeKeys.size() >= removeCount) {
            break;
        }
    }
    for (String firstKey : removeKeys) {
        sKey2ContentMap.remove(firstKey);
    }
}

Demo 验证

在 demo App 中,我们尝试往 Bundle 中写入超过 Binder 传输阈值的 16 MB 数据,预期在 restore 中能读取到相同数据:


@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    byte[] bytes = new byte[16 * 1024 * 1024]; // 16MB
    outState.putByteArray("key", bytes);
    System.out.println("onSaveInstanceState bytes.length = " + bytes.length);
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    byte[] bytes = savedInstanceState.getByteArray("key");
    System.out.println("onRestoreInstanceState bytes.length = " + bytes.length);
}

在机器中开启开发者选项中不保留活动选项,测试结果中两行日志 bytes 数组长度一致,说明方案可行。

最终方案

经过前面层层分析和逐步优化,我们的最终方案是:

  • 启动之后通过 Application#registerActivityLifecycleCallbacks 注册 Hook 点函数,从而拿到 save 和 restore 执行时机和参数。
  • 在 ActivityLifecycleCallbacks#onActivityPostSaveInstanceState 时把 Bundle 序列化为二进制,当大小超过 1024 Bytes 时创建 UUID 并在内存记录 UUID 和 Bundle 映射关系,替换 Bundle 数据为 ID,从而减小 Binder 看到的数据量。
  • 在 ActivityLifecycleCallbacks#onActivityPreCreated 时从 Bundle 中读取出 UUID,并从内存 Map 中查找出映射的 Bundle,还原 ID 为 Bundle 数据。

上线效果

单个 issue 修复效果:优化方案已经解决 97% 的问题。

剩余三星 startActivity 兼容性问题,已经不是 Top 问题。由于涉及启动 Activity 多进程数据共享问题,故此次未做优化

接入方式

目前该方案已经在抖音全量上线,并集成到火山引擎 APMPlus ,供各产品快速接入(由于不同产品的复杂度不同,建议接入时做好观察验证)。接入方式如下:

MonitorCrash.Config config = MonitorCrash.Config.app(APM_AID)
        .enableOptimizer(true);//打开稳定性优化开关
        .build();

只需要初始化时打开优化开关即可,欢迎各产品接入优化异常问题,该能力开箱即用,具体接入方式详见文档(https://www.volcengine.com/docs/6431/68852)。

总结和思考

  • 系统层:对于系统服务端 system_server 进程,其会接收众多 App 进程发送的 Binder 数据,Binder 传输数据量上限限制和 App 进程一样,可能是不太合理的。随着手机内存发展,这个数值在内存充足的机器上或存在优化空间,例如扩容到 2M 之后,传输失败率下降,App 使用时长增加。
  • 业务层:跨进程场景 Bundle 和 Intent 中不适合传递大数据,可以经过外存中转,通过维护外存 Map 和传递 Key 解决;另外 Bundle 中不建议放受到后台 Server 活动影响较大的序列化数据结构,这种情况平时没有问题,一到线上活动就容易崩溃,在活动上线前需要多加演练。

关于APMPlus

APMPlus 是火山引擎下的应用性能监控产品,通过先进的数据采集与监控技术,为企业提供全链路的应用性能监控服务,助力企业提升异常问题排查与解决的效率。基于海量数据的聚合分析,平台可帮助客户发现多类异常问题,并及时报警,做分配处理。目前,APMPlus 已服务了抖音、今日头条等多个大规模移动 App,以及作业帮、Keep、德邦等众多外部业务方。

APMPlus APP端监控方案提供了 Java 崩溃、Native 崩溃、ANR 等不同的异常类别监控,以及启动、页面、卡顿等流畅性监控,还包括内存、CPU、电量、流量、磁盘等资源消耗问题的监控。此外,APMPlus 提供网络耗时和异常监控,拥有强大的单点分析和日志回捞能力。在数据采集方面,提供采样和开关配置,以满足客户对数据量和成本控制的需求,并支持实时报警能力。针对跨平台方案,其提供了 WebView 页面的监控。丰富的能力满足客户对 App 全面性能监控的诉求。

方案亮点

  • Java OOM 监控提供全流程自动分析能力,准确定位Java内存问题,泄漏链、泄漏大小、大对象一目了然。
  • ANR 使用基于信号的捕获方案,更节省系统资源,准确度高,提供现场消息调度图,高度还原现场主线程阻塞情况。
  • 真正解决 Native(C/C++)崩溃的现场还原能力,提供了最有价值的Tombstone,精细还原现场。完整展示崩溃线程的进程信息、信号信息、寄存器信息,还原崩溃现场汇编指令,详细的maps、fd和内存信息。
  • 高性能日志库,做到数据稳定性强、性能好,保障了现场业务信息的高度还原。

扫码申请免费试用

👇 点击阅读原文,了解更多


文章来源: https://mp.weixin.qq.com/s?__biz=MzI1MzYzMjE0MQ==&mid=2247504929&idx=1&sn=30af202393f347f0cbb3be868b76a40c&chksm=e9d31fc3dea496d5d3bf3479be6f79103800383aac5d93981345f54ea9a7f0ccad996054e57b&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh