【调试篇5】TransactionTooLargeException 原理解析

1 核心问题

为什么在Activity的onSaveInstanceState中捕获了异常,仍然会发生TransactionTooLargeException崩溃?

答案:因为崩溃不是发生在onSaveInstanceState()方法内部,而是发生在之后的Binder事务提交阶段!


2 Android状态保存完整流程

2.1 整体流程图

scss 复制代码
Activity生命周期变化(如按Home键)
    ↓
[步骤1] Activity.onPause()
    ↓
[步骤2] Activity.onStop()  ← 我们在这里
    ↓
[步骤3] Activity.onSaveInstanceState(Bundle outState)
    ├─ super.onSaveInstanceState(outState)
    │   ├─ 保存View hierarchy状态
    │   ├─ FragmentManager.saveAllState()  ← Fragment状态保存
    │   │   ├─ Fragment1.onSaveInstanceState()
    │   │   ├─ Fragment2.onSaveInstanceState()
    │   │   └─ 将所有Fragment状态序列化到Bundle
    │   └─ SavedStateRegistry保存状态
    └─ 应用自定义保存逻辑
    ↓
[步骤4] Bundle序列化完成,数据已在内存中
    ↓
[步骤5] PendingTransactionActions$StopInfo.run()  ← ⚠️ 崩溃发生在这里!
    ↓
[步骤6] ActivityClient.activityStopped(token, state, persistentState)
    ↓
[步骤7] IActivityClientController.activityStopped()  ← Binder调用
    ↓
[步骤8] BinderProxy.transact()  ← 准备跨进程传输
    ↓
[步骤9] BinderProxy.transactNative()  ← Native方法,检查数据大小
    ↓
    ❌ 如果数据 > 1MB,抛出TransactionTooLargeException
    ↓
[步骤10] SystemServer进程接收状态数据

3 关键源码分析

3.1 Activity.onSaveInstanceState() - 步骤3

less 复制代码
// frameworks/base/core/java/android/app/Activity.java
protected void onSaveInstanceState(@NonNull Bundle outState) {
    // 保存View hierarchy状态
    outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
    
    // 保存Fragment状态 ← 大数据的主要来源
    Parcelable p = mFragments.saveAllState();
    if (p != null) {
        outState.putParcelable(FRAGMENTS_TAG, p);
    }
    
    // 保存SavedStateRegistry状态
    mActivityLifecycleCallbacks.onActivitySaveInstanceState(this, outState);
    
    // ⚠️ 注意:这里只是把数据放到Bundle中,还没有传输!
}

关键点

  • ✅ 这个方法中的try-catch可以捕获这个方法内部的异常
  • ❌ 但无法捕获方法返回后的异常
  • Bundle数据此时还在应用进程内存中,还没有跨进程传输

3.2 FragmentManager.saveAllState() - Fragment状态序列化

ini 复制代码
// androidx/fragment/app/FragmentManager.java
Parcelable saveAllState() {
    // 收集所有Fragment的状态
    ArrayList<FragmentState> active = new ArrayList<>();
    for (Fragment f : mActive.values()) {
        if (f != null) {
            // ⚠️ 调用每个Fragment的onSaveInstanceState()
            FragmentState fs = new FragmentState(f);
            active.add(fs);
            
            // 保存Fragment的Arguments(可能很大)
            fs.mArguments = f.mArguments;
            
            // 保存Fragment的SavedState(可能很大)
            if (f.mSavedFragmentState != null) {
                fs.mSavedFragmentState = f.mSavedFragmentState;
            } else {
                Bundle savedState = new Bundle();
                f.onSaveInstanceState(savedState);  // ← 调用Fragment的方法
                fs.mSavedFragmentState = savedState;
            }
        }
    }
    
    // 创建FragmentManagerState,包含所有Fragment数据
    FragmentManagerState fms = new FragmentManagerState();
    fms.mActive = active;  // ← 这里可能包含大量数据
    return fms;
}

关键点

  • 所有Fragment的状态都会被收集
  • Fragment的netData、mAdapter等如果被序列化,会非常大
  • 这些数据最终都打包到Activity的outState中

3.3 PendingTransactionActions$StopInfo.run() - 步骤5 ⚠️

崩溃堆栈

php 复制代码
at android.app.servertransaction.PendingTransactionActions$StopInfo.run(PendingTransactionActions.java:146)

源码分析

typescript 复制代码
// frameworks/base/core/java/android/app/servertransaction/PendingTransactionActions.java
public class PendingTransactionActions {
    
    static class StopInfo implements Runnable {
        private ActivityClientRecord mActivityClientRecord;
        private Bundle mState;  // ← 这里存储了Activity的状态
        private PersistableBundle mPersistentState;
        
        @Override
        public void run() {
            try {
                // ⚠️ 这里调用ActivityClient.activityStopped()
                // 将状态数据通过Binder传输到SystemServer
                ActivityClient.getInstance().activityStopped(
                    mActivityClientRecord.token,
                    mState,  // ← 包含1.4MB的数据
                    mPersistentState,
                    mActivityClientRecord.getDescription()
                );
            } catch (RemoteException e) {
                // ❌ 这个catch无法捕获TransactionTooLargeException
                // 因为异常是在更深层的transactNative()中抛出的
            }
        }
    }
}

关键点

  • 这个方法在onSaveInstanceState()返回之后执行
  • 它负责将Bundle通过Binder传输到SystemServer进程
  • 这里的异常无法被应用代码的try-catch捕获

3.4 BinderProxy.transact() - 步骤8-9

java 复制代码
// frameworks/base/core/java/android/os/BinderProxy.java
public boolean transact(int code, Parcel data, Parcel reply, int flags) {
    // 将Bundle序列化到Parcel
    data.writeStrongBinder(token);
    data.writeBundle(mState);  // ← 1.4MB数据写入Parcel
    
    // 调用Native方法进行跨进程传输
    return transactNative(code, data, reply, flags);
    // ↑ 如果Parcel大小超过限制,这里会抛出异常!
}
​
// Native方法(C++实现)
private native boolean transactNative(int code, Parcel data, Parcel reply, int flags);

Native层检查(C++)

arduino 复制代码
// frameworks/native/libs/binder/Parcel.cpp
status_t Parcel::writeToParcel(const Parcel* parcel) const {
    size_t dataSize = parcel->dataSize();
    
    // ⚠️ 检查数据大小,限制约1MB
    if (dataSize > IBinder::MAX_IPC_SIZE) {
        // 抛出TransactionTooLargeException
        return BAD_VALUE;  // ← 这里!
    }
    
    // 继续传输...
}
​
// IBinder::MAX_IPC_SIZE 通常是 1MB - 8KB

关键点

  • 异常是在Native层抛出的
  • 发生在Binder事务提交时,不是在Java代码中
  • 应用代码无法捕获这个异常

4 为什么Try-Catch无效?

4.1 原始代码分析

kotlin 复制代码
// 修复前的代码
override fun onSaveInstanceState(outState: Bundle) {
    try {
        super.onSaveInstanceState(outState)  // ← 步骤3:保存状态到Bundle
        // 此时数据还在内存中,没有传输,不会抛异常
        
    } catch (e: Throwable) {
        // ❌ 这个catch只能捕获super.onSaveInstanceState()内部的异常
        // ❌ 无法捕获之后Binder传输时的异常
        PictorialLog.e(TAG, "[onSaveInstanceState] error: ${e.message}")
    }
}
// ← onSaveInstanceState()方法返回,Bundle数据已准备好
​
// ⏬ 之后的流程(应用代码无法控制)
// PendingTransactionActions$StopInfo.run()
//   → ActivityClient.activityStopped(mState)  // mState包含1.4MB数据
//   → BinderProxy.transact()
//   → transactNative()  ← ⚠️ 异常在这里抛出!应用代码无法捕获

4.2 异常传播路径

scss 复制代码
[应用进程 - Java层]
    onSaveInstanceState(outState) {
        try {
            super.onSaveInstanceState(outState)  ← 步骤1:数据放入Bundle
            ✅ 成功返回(数据还在内存)
        } catch (e) {
            ❌ 不会执行到这里
        }
    } ← 方法返回,try-catch作用域结束
    
    ↓ [方法返回后,应用代码失去控制]
    
    PendingTransactionActions$StopInfo.run() {  ← 步骤2:准备传输
        ActivityClient.activityStopped(mState)  ← 步骤3:调用Binder
    }
    
[应用进程 - Native层]
    BinderProxy.transactNative(data) {  ← 步骤4:Native方法
        Parcel.writeToParcel() {
            if (size > 1MB) {
                ❌ 抛出TransactionTooLargeException  ← 步骤5:异常!
                // 应用的try-catch已经不在调用栈上,无法捕获
            }
        }
    }
    
    ↓ 异常向上传播
    
[Android Framework]
    异常未被捕获,传播到UncaughtExceptionHandler
    → 应用崩溃!

5 正确的解决方案

5.1 方案对比

方案 代码 效果 原理
❌ 捕获异常 try { super.onSaveInstanceState() } catch {} 无效 异常发生在方法返回后
✅ 清空数据 outState.remove("android:support:fragments") 有效 从源头减少数据量
✅ 禁止保存 Fragment.onSaveInstanceState() { outState.clear() } 有效 阻止Fragment保存大数据

5.2 修复后的代码

kotlin 复制代码
override fun onSaveInstanceState(outState: Bundle) {
    try {
        super.onSaveInstanceState(outState)  // ← 步骤1:保存状态
        
        // ✅ 步骤2:立即清空大数据,在传输之前!
        outState.remove("android:support:fragments")
        outState.getBundle("androidx.lifecycle.BundlableSavedStateRegistry.key")?.apply {
            remove("android:support:fragments")
            remove("android:support:activity-result")
        }
        
        // 测量Bundle大小
        measureBundleSize(outState)
        
    } catch (e: Throwable) {
        PictorialLog.e(TAG, "[onSaveInstanceState] error: ${e.message}")
    }
}
// ← 方法返回,Bundle大小已被控制在安全范围
// → 之后的Binder传输不会超过限制,不会抛异常 ✅

关键改进

  1. ✅ 在onSaveInstanceState()内部清空大数据
  2. ✅ 在Bundle传输之前就减少数据量
  3. ✅ 从源头解决问题,而不是捕获异常

6 执行时序对比

6.1 修复前(会崩溃)

css 复制代码
时间轴:
T1: onSaveInstanceState() 开始
    └─ super.onSaveInstanceState(outState)
       └─ Fragment状态保存 (1.4MB) ✅ 成功
T2: onSaveInstanceState() 结束
    └─ outState 包含 1.4MB 数据 ⚠️
    
T3: PendingTransactionActions$StopInfo.run() 开始
    └─ ActivityClient.activityStopped(mState)
       └─ BinderProxy.transact()
          └─ transactNative() 检查数据大小
             └─ 1.4MB > 1MB ❌
                └─ 抛出 TransactionTooLargeException
                   └─ 应用崩溃!

6.2 修复后(不会崩溃)

css 复制代码
时间轴:
T1: onSaveInstanceState() 开始
    └─ super.onSaveInstanceState(outState)
       └─ Fragment状态保存 (1.4MB) ✅ 成功
    └─ outState.remove("android:support:fragments") ✅ 关键!
       └─ Fragment状态被删除
T2: onSaveInstanceState() 结束
    └─ outState 只包含 8KB 数据 ✅
    
T3: PendingTransactionActions$StopInfo.run() 开始
    └─ ActivityClient.activityStopped(mState)
       └─ BinderProxy.transact()
          └─ transactNative() 检查数据大小
             └─ 8KB < 1MB ✅
                └─ 传输成功
                   └─ 应用正常运行 ✅

7 Fragment的onSaveInstanceState()原理

7.1 Fragment修复

kotlin 复制代码
// 修复前:没有重写,使用默认实现
// 默认实现会保存所有成员变量(如果它们是可序列化的)

// 修复后:
override fun onSaveInstanceState(outState: Bundle) {
    // 不调用super,完全跳过状态保存
    // super.onSaveInstanceState(outState)
    
    // 清空可能被父类保存的状态
    outState.clear()
    
    PictorialLog.i(TAG, "[onSaveInstanceState] 跳过状态保存,避免数据过大")
}

执行时机

scss 复制代码
Activity.onSaveInstanceState(outState)
    → super.onSaveInstanceState(outState)
        → FragmentManager.saveAllState()
            → Fragment.onSaveInstanceState(savedState)  ← 这里!
                → savedState包含Fragment状态
            → savedState打包到outState

为什么有效

  1. Fragment.onSaveInstanceState()在Activity.onSaveInstanceState()内部被调用
  2. 如果Fragment不保存状态,savedState就是空的
  3. 最终outState中的Fragment数据就很小

8 Bundle大小计算

8.1 崩溃时的数据分布

python 复制代码
Bundle总大小: 1,419,084 bytes (1.35 MB)
├─ android:viewHierarchyState: 4,780 bytes (0.5%)
│   └─ android:views: 4,732 bytes
└─ androidx.lifecycle.BundlableSavedStateRegistry.key: 1,413,508 bytes (99.5%)
    ├─ android:support:activity-result: 1,680 bytes (0.1%)
    └─ android:support:fragments: 1,411,568 bytes (99.4%) ← 主要问题!
        └─ android:support:fragments: 1,411,496 bytes
            └─ ***Fragment状态
                ├─ netData: List<***ItemInfo> (约800KB)
                ├─ mAdapter数据 (约400KB)
                ├─ slideViewFirstData (约100KB)
                └─ 其他状态变量 (约100KB)

8.2 修复后的数据分布(预期)

python 复制代码
Bundle总大小: 8,192 bytes (8 KB)
├─ android:viewHierarchyState: 4,780 bytes (58%)
│   └─ android:views: 4,732 bytes
└─ androidx.lifecycle.BundlableSavedStateRegistry.key: 3,412 bytes (42%)
    ├─ android:support:activity-result: 1,680 bytes
    └─ android:support:fragments: 0 bytes ← 已清空!
└─ EXTRA_IMAGE_ID: "6564140" (50 bytes)

减少量 : 1,419,084 - 8,192 = 1,410,892 bytes (99.4%减少)


9 总结

9.1 核心要点

  1. 异常发生的真实位置

    • ❌ 不是在Activity.onSaveInstanceState()内部
    • ✅ 是在之后的Binder.transactNative()中
  2. 为什么Try-Catch无效

    • onSaveInstanceState()方法返回后,try-catch作用域结束
    • 异常在Native层抛出,应用代码无法捕获
    • 异常传播到UncaughtExceptionHandler,导致崩溃
  3. 正确的解决思路

    • ❌ 不是捕获异常(无法捕获)
    • ✅ 是从源头减少数据量
    • ✅ 在Bundle传输之前就控制大小
  4. 修复的技术原理

    • 在onSaveInstanceState()内部清空Fragment状态
    • 在Fragment.onSaveInstanceState()中禁止保存大数据
    • 确保Bundle大小 < 1MB,避免触发异常

9.2 类比理解

这就像快递包裹超重:

错误做法(Try-Catch):

复制代码
你:把100kg的包裹交给快递员
快递员:好的(接收包裹)
你:离开现场
快递员:称重发现超重,拒绝发货
你:已经离开,无法处理这个问题
结果:快递失败 ❌

正确做法(清空数据):

复制代码
你:准备100kg的包裹
你:意识到超重,减少到5kg
你:把5kg的包裹交给快递员
快递员:称重通过,发货成功
结果:快递送达 ✅

10 相关源码路径

组件 源码路径
Activity frameworks/base/core/java/android/app/Activity.java
FragmentManager androidx/fragment/app/FragmentManager.java
PendingTransactionActions frameworks/base/core/java/android/app/servertransaction/PendingTransactionActions.java
ActivityClient frameworks/base/core/java/android/app/ActivityClient.java
BinderProxy frameworks/base/core/java/android/os/BinderProxy.java
Parcel (Native) frameworks/native/libs/binder/Parcel.cpp

相关推荐
爱吃水蜜桃的奥特曼5 小时前
玩Android Flutter版本,通过项目了解Flutter项目快速搭建开发
android·flutter
桦说编程5 小时前
CompletableFuture 异常处理常见陷阱——非预期的同步异常
后端·性能优化·函数式编程
太过平凡的小蚂蚁6 小时前
Android 版本特性完全解析:从6.0到16.0的实用指南
android
杨筱毅6 小时前
【底层机制】【Android】深入理解UI体系与绘制机制
android·底层机制
介一安全6 小时前
【Frida Android】基础篇8:Java层Hook基础——调用带对象参数的方法
android·网络安全·逆向·安全性测试·frida
puyaCheer6 小时前
Android 13 启动的时候会显示一下logo,很不友好
android·gitee
long_hai_d7 小时前
Aosp14桌面壁纸和锁屏壁纸的设置和加载分析
android
2501_916007478 小时前
iOS 26 软件性能测试 新版系统下评估全流程 + 多工具辅助方案
android·macos·ios·小程序·uni-app·cocoa·iphone
云霄IT8 小时前
绕过Frida检测反调试的一些办法
android·javascript