TransactionTooLargeException 是什么?

TransactionTooLargeException 是 Android 在 Binder IPC 事务数据过大时抛出的异常。当通过 Intent、Bundle 等方式在组件间传递的数据序列化后超出 Binder 缓冲区限制时,系统就会抛出这个异常。

Binder 是 Android 进程间通信的核心机制,设计初衷是传递轻量级消息(如启动 Activity 的 Intent),而不是搬运大块数据。

大小限制是多少?

Binder 事务缓冲区总大小为 1MB(1,048,576 bytes),这是整个进程(即整个 App)共享的,不是单个事务独占。
实践中,单次事务超过 500KB~800KB 就很容易触发。因为:

  • 进程中其他 Binder 事务也在使用这块缓冲区
  • 系统服务调用(如 AMSWMS)也走 Binder

举例:如果 Intent extras 序列化后有 580KB,加上系统自身的 Binder 开销,就很可能超限。

“整个进程共享”具体指什么?

缓冲区不是”占着不放”的,而是事务级别的。每次 Binder 调用(比如 startActivity())会临时占用缓冲区,传输完成后立即释放。所以不活动的 Activity 不会持续占用空间。
真正的风险是并发——同一时刻多个 Binder 事务同时在传输:

时刻 T:
  事务A: startActivity() 带 580KB intent extras  → 占 580KB
  事务B: 系统回调 AMS                             → 占 50KB
  事务C: WMS 窗口更新                              → 占 30KB
  ─────────────────────────────────────────────
  总计: 660KB / 1MB

⚠️ 实际场景中,绝大部分崩溃都是单次事务太大导致的,而不是多个事务并发挤爆的。

为什么有这个限制?

1MB 的限制是内核层面/dev/binder 驱动 mmap 的缓冲区大小硬编码的,原因包括:

  1. 内核内存效率:Binder 缓冲区在内核空间分配,过大会浪费有限的内核资源
  2. 防止阻塞:Binder 事务是同步的,大数据传输会阻塞调用方线程
  3. 共享缓冲区:1MB 是该进程所有 Binder 事务共享的,一个事务占满,其他 IPC(包括系统服务调用)都会失败

⚠️ 这 1MB 不是”你的事务可以用 1MB”,而是整个进程所有并发 Binder 事务共享 1MB。所以实际可用空间往往远小于 1MB。

常见触发场景

  1. Intent 传递大数据 — 通过 intent.putExtra() 塞入大型 Serializable/Parcelable 对象
  2. onSaveInstanceStateFragmentActivity 默认会把 Intent extras 再次序列化到 Bundle,二次放大问题
  3. 跨进程 AIDL 调用 — 传递大型 List 或 Bitmap

AIDL 是什么?

AIDL(Android Interface Definition Language)是 Android 提供的一种跨进程通信接口定义语言。当你的 app 需要和另一个进程(比如另一个 app、或者自己 app 的远程 Service)通信时,AIDL 帮你定义”双方约定好的接口”,底层走的就是 Binder。

// 定义接口 —— IMyService.aidl
interface IMyService {
    List<String> fetchData(String query);
    void sendBitmap(in Bitmap image);
}

编译后 Android 会自动生成 Binder 的序列化/反序列化代码。调用方像调本地方法一样调用,实际数据通过 Binder 跨进程传输,一样受 1MB 限制。

相关概念:AMS 和 WMS

AMS(ActivityManagerService)

Activity 管理服务,Android 系统中最核心的服务之一,负责管理所有四大组件的生命周期:

  • 启动/销毁 Activity(你调 startActivity() 就是在跟 AMS 通信)
  • 管理 Service 的启动和绑定
  • 发送和接收 Broadcast
  • 管理进程优先级(决定谁该被杀掉回收内存)
  • 管理 Task 和返回栈
你的 App                          系统进程 (system_server)
┌──────────┐    Binder IPC     ┌──────────────────┐
│ startActivity()  ──────────→ │  AMS             │
│                  ←────────── │  "好的,帮你启动"  │
└──────────┘                   └──────────────────┘

每次启动 Activity、绑定 Service,底层都要经过 Binder 和 AMS 通信。580KB 的 Intent 就是在这条路上传输的。

WMS(WindowManagerService)

窗口管理服务,负责所有窗口的显示和管理:

  • 管理窗口的创建、销毁、层级(Z-order)
  • 处理窗口大小和位置
  • 协调动画和转场效果
  • StatusBar、NavigationBar、Dialog、Toast 这些都是”窗口”
你看到的屏幕上每一层都是 WMS 管的:
 
  ┌─────────────────────┐  ← StatusBar 窗口
  │  ┌───────────────┐  │  ← Dialog 窗口
  │  │   Alert!       │  │
  │  └───────────────┘  │
  │                     │  ← Activity 窗口
  │  你的 App 内容       │
  │                     │
  ├─────────────────────┤  ← NavigationBar 窗口
  └─────────────────────┘

AMS 和 WMS 的关系

AMS 管逻辑(组件生命周期),WMS 管显示(窗口绘制)。启动一个 Activity 时两者配合:

startActivity()
  → AMS:创建 Activity 实例、走生命周期
  → WMS:创建窗口、计算布局、显示到屏幕

两者都运行在 system_server 进程中,你的 app 和它们通信全走 Binder,所以都在共享那 1MB 缓冲区。

正确做法

Intent / Bundle 只传小数据(key、ID),大数据通过其他方式传递:

// 方案一:内存缓存(同进程)
AdCommonCache.put(key, data)
intent.putExtra("cache_key", key)
 
// 方案二:文件 / SharedPreferences
// 方案三:ContentProvider(跨进程)
// 方案四:ViewModel(同 Activity 的 Fragment 间)

AndroidParcel 的关系

Intent extras 最终通过 Parcel 序列化后经由 Binder 传输。Parcel 是高效的二进制序列化容器,但它不改变 Binder 缓冲区的大小限制——数据序列化后仍然不能超过 ~1MB。


相关笔记