1

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

 9 months ago
source link: https://juejin.cn/post/7306388118914973734
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

2023-11-28 8,343 阅读19分钟

作者:李光(抖音Android质量团队)

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

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

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

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

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

f4c234cc87e345ff89b4c19fbe1460a5~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=2824&h=496&s=980190&e=png&b=ffffff

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

  • 这是由 Binder 传输数据量超过阈值触发的崩溃。
  • 排除其中无关的动态代理 ProxyInvocationHandler 的业务堆栈,其它都在系统堆栈中,无法直接定位到具体业务方解决。
2189a2175dac4aeb8d8e0051f29fe8cb~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=3484&h=1362&s=1983987&e=png&b=fcfefe

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

3b13d56014974cb3999216f77545ce9b~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=3406&h=682&s=1875336&e=png&b=ffffff

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

5daf9ec63d0941a2bde08fd602f61984~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1684&h=988&s=429306&e=png&b=fdfdfd

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

6347674eab744f80be2983703f51ed3b~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=2180&h=938&s=1523013&e=png&b=fef8e9

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

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

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

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

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

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

逐步分析.png

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

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

处理流程.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 中读取出来的命令值。

e4e437dce47b472191c3fad1d5f82e6e~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1536&h=1326&s=372079&e=png&b=fefefe

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

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

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

除了错误码之外,我们结合源码得出 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 传输失败。



typescript

@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 进行了序列化量化:



csharp

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

0e270d5f6dd44e1d82897d57ea6c0bb1~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=2178&h=260&s=91220&e=png&b=3e4244

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

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

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

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

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

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

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

总结和思考

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

关于APMPlus

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

25d133aa855347539d8c99b8eb635d29~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1820&h=644&s=218828&e=png&b=fefefe

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

方案亮点

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

28ba97bf5e774881b8102ce084fff1af~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=400&h=400&s=1225&e=png

扫码申请免费试用

👉🏻 点击这里,了解更多


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK