使用火山引擎 APMPlus 解决抖音Top 1 Java 崩溃的通用优化方案

背景

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

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

初步分析

多维特征

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

  • 机型方面:比较分散,有聚集部分samsung sm-s9180 占比 top1

  • 渠道方面:比较分散,华为、小米、三星占比 top3。

  • ROM方面:比较分散。

  • OS 版本方面:排除古老的 5.1.1 系统,只有 10 以上版本,12/13/10 占比 top3

  • App小版本方面,排除古老的版本,基本跟日活排序一致。

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

aae682e22056224cc65d240e23e93f44.png

崩溃堆栈

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

  • 这是由 Binder 传输数据量超过阈值触发的崩溃。

  • 排除其中无关的动态代理 ProxyInvocationHandler 的业务堆栈,其它都在系统堆栈中,无法直接定位到具体业务方解决。

60f73e51013dd202ff81985408d2d540.png

崩溃日志

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

e6766c8aa8c288022243aee5af3edaf6.png

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

7b7ba11551843fdb6da3eb577320a2c7.png

通过对聚合 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 占比情况。

47915aaeb06442fbadb0d19fee0ca3bf.png

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

初步结论

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

  • 问题是 Activity stop 时给 AMS 传输 Bundle 超过 Binder 驱动限制的大小,触发了崩溃。

  • 调用栈和日志无明显业务聚合,无法简单确定是哪个业务引入的问题。

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

深入分析

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

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

bf1c399f8542c9d836a609994d5a4d8b.png

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

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

21ba548e930cf6cbbc512472bc5a0066.png
  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 中读取出来的命令值。

e26291e9cffefd5fb7dc8c8c4b4fb7f9.png

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

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

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

fe67f356c99ffd3ec9b91b26e15e97ac.png

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

3504266f1ef98758e246bed2b5421487.png

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

1652a86d1757d35f62066e21e50a1983.png
  • 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 的分析。

04d6b099302dbee15b685d21bad7f19a.png

通过下图源码,我们发现 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 异常。所以推测系统在这种情况下其实不崩溃。

88899e9716a37a0c3f4ac4b5cdc3947c.png

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

b3ab2acfaa164086e5a8c88df760f342.png

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

6ec0dd18a86028b35ee3519550ba462c.png
  • ActivityThread.handleStopActivity 过程中保存 callActivityOnSaveInstanceState 产生的 Bundle 数据。

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

Activity.onRestoreInstanceState 流程

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

7df709238a27333b9c88f8479e7cf03b.png
  • 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 机器分析崩溃场景日志:

de4d2cf07b729585c1267d765d87004c.png
// 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 执行流程是这样:

d05d50ef05c5638fcf51b97ad155804c.png
  • 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 接收缓冲区,如果分配失败,则无法完成数据传输。

b480a20d4ab76db5087960bdb63f8dee.png

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

Binder 传输数据最大值

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

  • 是否 Binder mmap 接收缓冲区大小就是小于 500K 呢?

  • 是否最大缓冲区其实超过 100M,只是被其它 Binder 调用分配用完了呢?

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

ecb04de448c0bb821511ae5283c5bdd0.png

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

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

ea4ee9bfc6807de49e2e8d46a361a418.png

因此,一个进程 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 过大流程:

758e85ca2d7ec6882c9af7e9a70976bb.png
  • 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 参数

5440bded9327a005ed747221e80e3080.png
  • 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 数组长度一致,说明方案可行。

05020c63fc64b7d1653aaeac6bbf3819.png

最终方案

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

  • 启动之后通过 Application#registerActivityLifecycleCallbacks 注册 Hook 点函数,从而拿到 save 和 restore 执行时机和参数。

  • 在 ActivityLifecycleCallbacks#onActivityPostSaveInstanceState 时把 Bundle 序列化为二进制,当大小超过 1024 Bytes 时创建 UUID 并在内存记录 UUID 和 Bundle 映射关系,替换 Bundle 数据为 ID,从而减小 Binder 看到的数据量。

  • 在 ActivityLifecycleCallbacks#onActivityPreCreated 时从 Bundle 中读取出 UUID,并从内存 Map 中查找出映射的 Bundle,还原 ID 为 Bundle 数据。

3c999f78926ddc987270e3a7353cc9de.png

上线效果

单个 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、德邦等众多外部业务方。

45e5e6644baaf208c131a8729ef9195d.png

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

方案亮点

  • Java OOM 监控提供全流程自动分析能力,准确定位Java内存问题,泄漏链、泄漏大小、大对象一目了然。

  • ANR 使用基于信号的捕获方案,更节省系统资源,准确度高,提供现场消息调度图,高度还原现场主线程阻塞情况。

  • 真正解决 Native(C/C++)崩溃的现场还原能力,提供了最有价值的Tombstone,精细还原现场。完整展示崩溃线程的进程信息、信号信息、寄存器信息,还原崩溃现场汇编指令,详细的maps、fd和内存信息。

  • 高性能日志库,做到数据稳定性强、性能好,保障了现场业务信息的高度还原。

804db56a42a804657c26a60e5f2b154a.png46c44e25b6c29c6fd58c56e29810b924.png

扫码申请免费试用

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

相关推荐

2024 抖音欢笑中国年(五):Wasm、WebGL 在互动技术中的创新应用

前言随着 Web 前端技术的不断发展,越来越多的新兴技术方案被引入到 Web 开发中,其中 Wasm 和 WebGL 作为前端领域的两大利器,为开发者带来了更多的可能性。本文将结合2024 年抖音欢笑中国年的部分项目,重点介绍如何利用 Wasm 和 WebGL 对目前流行的一些前端互动技术(比如 Lottie、渲染引擎、动画图片等)进行创新和实践,利用 Wasm 和 WebGL 等新技术方案的特性...

字节跳动技术团队官方博客 2万+

2024 抖音欢笑中国年(四):渲染技术实践与探索

作者:陈瑞、欧阳浩铸、王武俊、倪梵云前言抖音在2024年春节期间推出了欢笑中国年系列活动,为用户带来了全新的体验和乐趣。而SAR Creator则为该项目研发工作提供了重要的技术支持。SAR Creator是一款基于 Typescript 的高性能、轻量化的互动解决方案,目前支持了浏览器和跨端框架平台,服务于字节内部的各种互动业务。这些绚烂多彩的互动场景当然也离不开实时渲染技术的支持,因此本文将专...

字节跳动技术团队官方博客 2万+

2024 抖音欢笑中国年(三):编辑器技巧与实践

前言本次春节活动中,我们大部分场景使用内部的 SAR Creator互动方案来实现。SAR Creator 是一款基于 TypeScript 的高性能、轻量化的互动解决方案,目前支持了Web和字节内部跨端框架平台,服务于字节内部的各种互动业务,包括但不限于抖音春节、抖音直播礼物、抖音UG活动等。SAR Creator 编辑器支持了图形化界面,提供了各类完善的系统(光照、动画、脚本等)供用户快速便捷...

字节跳动技术团队官方博客 1万+

2024 抖音欢笑中国年(二):AnnieX互动容器创新玩法解析

本文基于24年抖音春节活动业务背景,介绍了字节跨端容器AnnieX在游戏互动套件上的探索,致力于提升容器在游戏互动场景的优化能力。业务背景AnnieX作为字节一方游戏统一容器,服务字节内部电商、直播、UG等跨端场景业务。在字节一方游戏互动场景,有大量的一方游戏业务对容器有特定的流量、端能力和游戏优化的诉求。因此我们不断深入互动游戏业务特点,为字节游戏提供完善游戏端能力和流量运营能力,同时提供游戏互...

字节跳动技术团队官方博客 1万+

2024 抖音欢笑中国年(一):招财神龙互动技术揭秘

字节跳动旗下的抖音等 App 在 2024 年春节期间推出了欢笑中国年系列活动,在实现增长业务目标的同时,为用户带来了全新的体验和乐趣。「招财神龙」是其中的一个重要玩法。前言本次春节活动,使用到了字节内的主要前端、跨端、互动技术产品。主要涉及:跨端框架提供了首屏直出的方案使其具有较短的首屏时间,能够大大提升业务加载成功率。跨端框架也提供了 Canvas 作为 SAR Creator 等渲染引擎的...

字节跳动技术团队官方博客 1万+

Monorepo 解决方案 — 基于 Bazel 的 Xcode 性能优化实践

背景介绍书接上回《Monorepo 解决方案 — Bazel 在头条 iOS 的实践》,在头条工程切换至 Bazel 构建系统后,为了支持用户使用 Xcode 开发的习惯,我们使用了开源项目 Tulsi 作为生成工具,用于将 Bazel 工程转换为 Xcode 工程。但是在使用的过程中,我们发现了一些问题,其中影响较大的是,Xcode 工程卡顿:对于头条这种大型项目来说,Xcode 卡顿一直是本地...

字节跳动技术团队官方博客 1万+

抖音 ANR 自动归因平台建设实践

背景介绍本文在 2024 年初最新一期『抖音客户端基础技术大揭秘』技术沙龙活动中已做过专题分享,本次将内容重新整理文章进行分享。公众号后台回复技术沙龙可查看沙龙回放及 PPT~抖音作为一个超大型的应用,我们在 ANR 问题治理上面临着很大的挑战。首先对于存量问题的优化,由于缺少有效的归因手段,一些长期的疑难问题一直难以突破解决,例如长期位于 Top 1 的 nativePollOnce 问题。同时...

字节跳动技术团队官方博客 2万+

CVPR 2024 | CAMixerSR 动态注意力分配的超分辨率加速框架

背景随着相关技术和应用的发展,比如超高清屏幕、虚拟现实(VR)等沉浸式体验的增加,用户对超高分辨率图像和视频的需求变得越来越强烈。在这些场景中,图像的质量和清晰度对于提供最佳的用户体验至关重要。超高分辨率不仅能提供更清晰、更真实的视觉效果,还能在一定程度上增强人们的互动和沉浸感,在一些VR场景中我们需要8K甚至16K的才可以满足需求。然而要生成或者处理这些超高分辨率的内容,对算力的要求也是与日增长...

字节跳动技术团队官方博客 1万+

CVPR 2024 | Modular Blind Video Quality Assessment:模块化无参视频质量评估

无参视频质量评估 (Blind Video Quality Assessment,BVQA) 在评估和改善各种视频平台并服务用户的观看体验方面发挥着关键作用。当前基于深度学习的模型主要以下采样/局部块采样的形式分析视频内容,而忽视了实际空域分辨率和时域帧率对视频质量的影响,随着高分辨率和高帧率视频投稿逐渐普及,特别是跨分辨率/帧率视频转码档位画质评估场景中,这种影响变得更加不可忽视。在本文中,我们...

字节跳动技术团队官方博客 1万+

2024 AI & 前端:回首展望,光芒未至,破晓之前!

前言回望 2023 年,ChatGPT 的突然爆火,让 AI 无疑成为最为值得注目的新兴领域之一,我们也一起见证了生成式 AI 的寒武纪大爆发。这一年来,国内外的生成式 AI 、大模型和相关产品以令人眼花缭乱的速度更新迭代,新的创业浪潮风起云涌。在这 AI 浪潮下,也让我们有了新的开发思考,探索着在各个环节中“前端 & AI”的应用场景。勇于探索的前端开发者们已经开始挥舞着 AI 的“魔法...

字节跳动技术团队官方博客 1万+

Kotlin 云端差分缓存技术

本文由字节跳动 Buildinfra 团队出品。在我们的工程上线 Monorepo 全源码后,Kotlin 编译成了整个编译中最耗时的步骤,全源码过程中大量的 BuildCache Miss 导致我们的编译数据落后原来多仓二进制时代很多,且业界没有相关的解决方案。本篇文章我们来具体阐述下 BuildInfra 团队自研的解决方案 - Kotlin 云端差分方案的原理和技术实现。一、Monorepo...

字节跳动技术团队官方博客 1万+

字节跳动基础架构SRE-Copilot获得2023 CCF国际AIOps挑战赛冠军

近日,2023 CCF国际AIOps挑战赛决赛暨“大模型时代的AIOps”研讨会在北京成功举办,活动吸引了来自互联网、运营商、科研院所、高校、软硬件厂商等领域多名专家学者参与,为智能运维的前沿学术研究、落地生产实践打开了新思路。决赛中,从初赛两百多支队伍中脱颖而出的十支入围队伍分别展示了各自的方案,并进行了现场答辩,评审专家从选题方向、创新性、实用性、完整度和实验复现结果等多角度进行了综合评定,最...

字节跳动技术团队官方博客 2万+

字节跳动百万级Metrics Agent性能优化的探索与实践

背景metricserver2 (以下简称Agent)是与字节内场时序数据库 ByteTSD 配套使用的用户指标打点 Agent,用于在物理机粒度收集用户的指标打点数据,在字节内几乎所有的服务节点上均有部署集成,装机量达到百万以上。此外Agent需要负责打点数据的解析、聚合、压缩、协议转换和发送,属于CPU和Mem密集的服务。两者结合,使得Agent在监控全链路服务成本中占比达到70%以上,对Ag...

字节跳动技术团队官方博客 1万+

西瓜视频RenderThread引起的闪退问题攻坚历程

背景影响西瓜之前存在过一类RenderThread闪退,从堆栈上看,全部都是系统so调用,给人的第一印象像是一个系统bug,无从下手。闪退集中在Android 5~6上,表现为打开直播间立即闪退。该问题在2022年占据Native Crash Top5,2023年更是上升到到Top1。因此有必要投入时间和精力再重新审视一下这个问题。在历经多周的源码分析和排查后,逐步明确了问题根因并修复,最终取得了...

字节跳动技术团队官方博客 2万+

字节电商双11 大促容量保障是如何做的?

前言Rhino 简介Rhino是字节自研全链路容量评估产品,致力于构建完整的全链路容量评估解决方案(覆盖:容量预估->资源准备->数据准备->容量验证->监控->分析->决策->处理反馈);围绕容量在稳定性、成本、效率 三方面提供业务全方位基础支撑。Rhino 目前已经成为字节各业务容量评估主流解决方案,并且历年来在业务大型活动稳定性保障中(抖音春节项目、...

字节跳动技术团队官方博客 9万+

用 Addon 增强 Node.js 和 Electron 应用的原生能力

前言Node.js Addon 是 Node.js 中为 JavaScript 环境提供 C/C++ 交互能力的机制。其形态十分类似 Java 的 JNI,都是通过提供一套 C/C++ SDK,用于在 C/C++ 中创建函数方法、进行数据转换,以便 JavaScript / Java 等语言进行调用。这样编写的代码通常叫做 Bindings。此外还有基于 C ABI Calling Convent...

字节跳动技术团队官方博客 2万+

火山引擎 ByteHouse 的增强型数据导入技术实践

作为企业数字化建设的必备要素,易用的数据引擎能帮助企业提升数据使用效率,更好提升数据应用价值,夯实数字化建设基础。数据导入是衡量OLAP引擎性能及易用性的重要标准之一,高效的数据导入能力能够加速数据实时处理和分析的效率。作为一款OLAP引擎,火山引擎云原生数据仓库ByteHouse源于开源ClickHouse,在字节跳动多年打磨下,提供更丰富的能力和更强性能,能为用户带来极速分析体验,支撑实时数据...

字节跳动技术团队官方博客 1万+

打造企业级智能问答系统的秘密:如何使用云数据库 PostgreSQL 版实现向量检索

本文就如何利用火山引擎云数据库 PostgreSQL 版和大语言模型技术(Large Language Model,简称 LLM),实现企业级智能交互式问答系统进行介绍。通过本文,你将会了解交互式问答系统的原理,学习 PostgreSQL 的向量化存储和检索技术,以及大语言模型交互技术等。背景在大数据的浪潮下,众多企业建立了自己的知识库,以便于信息检索和知识查询。然而,随着知识库内容的膨胀,传统的...

字节跳动技术团队官方博客 2万+

抖音大型直播的画质优化实践

面临挑战随着抖音内容生态的不断丰富,越来越多的大型赛事在抖音平台进行直播,世界杯/春晚/亚运会等各项赛事节目引来大量用户观看。卡塔尔世界杯期间,抖音提供的稳定高质直播画面为观众带来了完美的观赛体验,决赛的 PCU 高达 3700W+。不同赛事节目涉及链路众多,且不同赛事之间存在差异,如何保障各链路的画质稳定并进一步提升画质,是一个巨大的挑战。如何应对挑战?画质优化链路大型赛事直播涉及链路较长,不同...

字节跳动技术团队官方博客 2万+

抖音直播新一代BVC编码器正式亮相

面临挑战在直播行业发展如火如荼的今天,用户对视频体验的要求也水涨船高。视频基础体验的关键要素包括清晰度、流畅度、低延迟等,而这些要素的“第一性原理”,就是视频本身的编码效率,也就是压缩率。视频编码是整个技术体系的基座,编码效率的显著提升,能够在同等码率下极大提高画质,从而改善用户体验。视频编码效率的重要性不言而喻,但进一步地提升也并非易事,尤其在直播场景中,对编码速度、延迟、码率控制等方面都有很高...

字节跳动技术团队官方博客 7321

Go Metrics SDK Tag 校验性能优化实践

背景Metrics SDK 是与字节内场时序数据库 ByteTSD 配套的用户指标打点 SDK,在字节内数十万服务中集成,应用广泛,因此 SDK 的性能优化是个重要和持续性的话题。本文主要以 Go Metrics SDK 为例,讲述对打点 API 的 hot-path 优化的实践。用户在使用 SDK API 进行打点时,需要传入指标对应的 Tag:tags:=[]m.T{{Name:"foo...

字节跳动技术团队官方博客 1万+

云上智能驾驶三维重建最佳实践

智能驾驶技术的不断发展,正在改变着我们的出行方式和交通系统。作为其中的一个关键技术,三维重建在智能驾驶系统中起着重要的作用。除去车端本身的感知、重建算法,自动驾驶技术的落地与发展需要庞大的云端重建能力支撑,火山引擎多媒体实验室通过行业领先的自研三维重建技术,结合强大的云平台资源与能力,助力相关技术在云端大规模重建、自动标注、真实感仿真等场景的落地与应用。本文重点介绍火山引擎多媒体实验室三维重建技术...

字节跳动技术团队官方博客 6289

火山引擎实时、低延时拥塞控制算法的优化实践

摘要火山引擎智能拥塞控制算法 VICC(Volcano Intelligent Congestion Control)是一种自适应的拥塞控制算法,旨在解决全球不同网络环境下,不同音视频应用对带宽利用率和延时的差异化要求。它结合了传统拥塞控制算法(如 GCC 和 BBR)的优点,并且能够根据不同的网络条件、业务偏好和码率特征进行自适应调整,包括自适应拥塞响应速度、自适应带宽探测幅度、自适应丢包检测策...

字节跳动技术团队官方博客 6708

veImageX 演进之路:Web 图片加载提速50%

背景说明火山引擎veImageX演进之路主要介绍了veImageX在字节内部从2012年随着字节成长过程中逐步演进的过程,演进中包括V1、V2、V3版本并最终面向行业输出;整个演进过程中包括服务端、客户端、网络库、业务场景与优化等多个角度介绍在图像处理压缩、省成本与体验优化的经验与方案;本篇文章重点介绍在web端演进和提供的能力,图片是 Web 站点中的重要元素,图片体积、格式、分辨率以及渲染方式...

字节跳动技术团队官方博客 6564

自研多模态追踪算法 PICO 为「手柄小型化」找到新思路

作者:张韬、林泽一 、闻超 、赵洋研发背景作为头戴的追踪配件,VR手柄可以通过HMD(头戴显示设备)的inside-out光学追踪定位原理,计算出手柄的空间运动轨迹,同时结合6轴传感器实现6DoF空间定位。与此同时,结合手柄控制器的物理按键、马达反馈、摇杆等,用户还能获得逼真、细腻的触觉反馈,进一步增强虚拟现实人机交互的能力以及沉浸感,这也是目前无手柄方案所难以实现的。目前主流VR手柄的追踪技术方...

字节跳动技术团队官方博客 7692

如何利用播放器节省20%点播成本

点播成本节省的点其实涉及诸多部分,例如:CDN、转码、存储等,而利用播放器降本却是很多客户比较陌生的部分。火山引擎基于内部支撑抖音集团相关业务的实践,播放器恰恰是成本优化中最重要和最为依赖的部分。火山引擎的视频团队做了份数据统计,在一个很经典的视频业务中,我们在2022年至2023年大约1年半的时间里,针对这个业务进行了33次成本优化点,其中13次是播放器主导的优化,其余的有12次也是需要播放器强...

字节跳动技术团队官方博客 6158

火山引擎 ByteHouse:ClickHouse 如何保证海量数据一致性

背景ClickHouse是一个开源的OLAP引擎,不仅被全球开发者广泛使用,在字节各个应用场景中也可以看到它的身影。基于高性能、分布式特点,ClickHouse可以满足大规模数据的分析和查询需求,因此字节研发团队以开源ClickHouse为基础,推出火山引擎云原生数据仓库ByteHouse。在日常工作中,研发人员经常会遇到业务链路过长,导致流程稳定性和数据一致性难保障的问题,这在分布式、跨服务的场...

字节跳动技术团队官方博客 6882

抖音集团都在用的画质评估工具,确定不试试吗?

导读本文从抖音集团内部画质评估体系的建设历程着笔,主要分享了画质评测对于业务的重要性、主要应用场景和内部产品的一些典型实践案例。通过分享业务视角遇到的一些问题和我们的解决思路,希望能抛砖引玉,为遇到类似困扰的伙伴们提供有价值的参考。画质评估体系建设历程为何评测画质如此重要?我们通过线上业务大量实验发现,图片画质优劣对点击率、 停留时长等消费类指标有正相关影响,间接影响用户收益指标。因此,建设一套行...

字节跳动技术团队官方博客 7900

VLDB 2023 | CDSBen: 字节跳动 veDB 数据库存储系统性能测试模型

背景随着业务爆炸式增长与云原生技术的日渐成熟,大量云原生分布式数据库产品如雨后春笋般涌现,其中一部分主打 OLTP 场景的分布式数据库强调的是从计算-存储分离架构获得弹性收益;对于业界各种计算-存储分离架构的数据库而言,怎么用真实的端到端数据库 workload 去 benchmark 其底层存储系统一直存在以下难题:对于数据库专用存储系统,不存在如 fio 一样的“事实标准” benchmark...

字节跳动技术团队官方博客 5882

Interspeech 2023 | 火山引擎流媒体音频技术之语音增强和AI音频编码

背景介绍为了应对处理各类复杂音视频通信场景,如多设备、多人、多噪音场景,流媒体通信技术渐渐成为人们生活中不可或缺的技术。为达到更好的主观体验,使用户听得清、听得真,流媒体音频技术方案融合了传统机器学习和基于AI的语音增强方案,利用深度神经网络技术方案,在语音降噪、回声消除、干扰人声消除和音频编解码等方向,为实时通信中的音频质量保驾护航。作为语音信号处理研究领域的旗舰国际会议,Interspeech...

字节跳动技术团队官方博客 6166
上一篇: 用 Addon 增强 Node.js 和 Electron 应用的原生能力
下一篇: 字节电商双11 大促容量保障是如何做的?
 字节跳动技术团队
字节跳动技术团队 企业官方账号 企业官方账号
博客等级 码龄7年 8122粉丝 316原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值