最近在解决项目中的Crash问题,发现有个Java Crash排在Top1,TransactionTooLargeException,这个问题通常是由于App在展示需要存储的页面切换到后台时导致,堆栈如下

1
2
3
4
5
6
7
8
9
java.lang.RuntimeException: android.os.TransactionTooLargeException: data parcel size 8310160 bytes
at android.app.servertransaction.PendingTransactionActions$StopInfo.run(PendingTransactionActions.java:160)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:224)
at android.app.ActivityThread.main(ActivityThread.java:7096)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:604)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:928)

其实看这个堆栈也没有太多有效信息,都是FrameWork层的,得进一步分析

分析

通常这个问题有3类原因导致

  • activity或者fragment跳转时,传递的bundle携带了过大的数据,比如传递了bitmap或者size很大的ArrayList

(这种情况跳转直接crash)

  • Activity.onSaveInstanceState()保存了过大的数据 (一般是退后台挂)
  • FragmentStatePagerAdapter的saveState保存了过多的历史Fragment实例的状态数据,可以重写saveState

由于每个Binder IPC的Buffer上限差不多是1M(除去信息头占位,实际buffer传输大小差不多1M-4k),超过就会抛出这种异常,通过查看 异常链接 slardar上 【自定义数据】、【Alog】看 是App在Binder Transaction期间退到后台,可以初步得出结论是上面的第二类:

com.kongming.parent.module.homeworkdetail.correctionv2.CorrectionResultV2Activity#onSaveInstanceState

image.png

onSaveInstanceState 它为Activity/Fragment提供了一种在应用程序进入后台之前将 UI 状态保存到 Android 系统的方法有关,以便在应用程序的进程被 Android 杀死的情况下恢复应用程序的 UI 状态操作系统。深入研究框架代码来检查onSaveInstanceState()调用时会静默存储哪些数据。

原理

首先可以看下源码中处理onStop()的起点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public void handleStopActivity(IBinder token, boolean show, int configChanges,
PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
final ActivityClientRecord r = mActivities.get(token);
r.activity.mConfigChangeFlags |= configChanges;

final StopInfo stopInfo = new StopInfo();
performStopActivityInner(r, stopInfo, show, true /* saveState */, finalStateRequest,
reason);

// ...ignore unnecessary code

stopInfo.setActivity(r);
stopInfo.setState(r.state);
stopInfo.setPersistentState(r.persistentState);
pendingActions.setStopInfo(stopInfo);
// ...ignore unnecessary code
}

see http ://androidxref.com/9.0.0_r3/xref/frameworks/base/core/java/android/app/ActivityThread.java#4190

该函数做了三件事

  • ActivityClientRecord 获取将要停止状态的activities的实例
  • 传递给performStopActivityInner(后面会详细介绍)
  • 将上述ActivityClientRecord设置为stopInfo,通知系统此时activity对用户不可见

注意到上面代码中 ActivityClientRecord中state属性其实是个Bundle对象
(stopInfo.setState(r.state)),因此继续跟进看看StopInfo里面发生了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/** Reports to server about activity stop. */
public static class StopInfo implements Runnable {
private ActivityClientRecord mActivity;
private Bundle mState;
private PersistableBundle mPersistentState;
private CharSequence mDescription;
// ...
@Override
public void run() {
// Tell activity manager we have been stopped.
try {
// ...
ActivityManager.getService().activityStopped(
mActivity.token, mState, mPersistentState, mDescription);
} catch (RemoteException ex) {
// Dump statistics about bundle to help developers debug
final LogWriter writer = new LogWriter(Log.WARN, TAG);
final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
pw.println("Bundle stats:");
Bundle.dumpStats(pw, mState);
pw.println("PersistableBundle stats:");
Bundle.dumpStats(pw, mPersistentState);

if (ex instanceof TransactionTooLargeException
&& mActivity.packageInfo.getTargetSdkVersion() < Build.VERSION_CODES.N) {
Log.e(TAG, "App sent too much data in instance state, so it was ignored", ex);
return;
}
throw ex.rethrowFromSystemServer();
}
}
}

see:http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/java/android/app/servertransaction/PendingTransactionActions.java#139

可见在run方法中,Bundle对象的mState会通过Binder传递到AMS中;如果对象太大,在Android N(7.0)及以上就会抛异常,这个确实跟slardar上可以对应上
image.png

现在我们知道发生异常的Activity确实在我们的应用程序进入后台时保存了太多的数据。
我们再来看看Activity中的onSaveInstanceState默认实现,寻找更多的线索,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Save all appropriate fragment state.
*/
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);

// ...

Parcelable p = mFragments.saveAllState();
if (p != null) {
outState.putParcelable(FRAGMENTS_TAG, p); // FRAGMENTS_TAG -> "android:support:fragments"
}

//...
}

see https ://android.googlesource.com/platform/frameworks/support/+/androidx-1.0-dev/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java?autodive=0%2F#588

继续追踪看看mFraments.saveAllState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Parcelable saveAllState() {
// ...

mStateSaved = true;

// First collect all active fragments.
ArrayList<FragmentState> active = mFragmentStore.saveActiveFragments();

if (active.isEmpty()) {
if (isLoggingEnabled(Log.VERBOSE)) Log.v(TAG, "saveAllState: no fragments!");
return null;
}

// Build list of currently added fragments.
ArrayList<String> added = mFragmentStore.saveAddedFragments();

// ...

FragmentManagerState fms = new FragmentManagerState();
fms.mActive = active;
fms.mAdded = added;
// ...

return fms;
}

see https ://android.googlesource.com/platform/frameworks/support/+/androidx-1.0-dev/fragment/src/main/java/androidx/fragment/app/FragmentManager.java?autodive=0%2F#2938

saveAllState()是Activity中保存Fragment状态的核心逻辑。mFragmentStore是它的成员字段FragmentManager,用于存储添加到Activity的所有Fragment实例。有两个数组列表mFragmentStore用于存储添加的Fragment,称为mActiveand和mAdded,因此上面的流程将通过它们来收集需要从所有添加的Fragment中保留的数据。最后,这些数据将被设置为FragmentManagerState实现Parcelable接口的数据类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
final class FragmentState implements Parcelable {
final String mClassName;
final String mWho;
final boolean mFromLayout;
final int mFragmentId;
final int mContainerId;
final String mTag;
final boolean mRetainInstance;
final boolean mRemoving;
final boolean mDetached;
final Bundle mArguments;
final boolean mHidden;
final int mMaxLifecycleState;

Bundle mSavedFragmentState;

FragmentState(Fragment frag) {
mClassName = frag.getClass().getName();
mWho = frag.mWho;
mFromLayout = frag.mFromLayout;
mFragmentId = frag.mFragmentId;
mContainerId = frag.mContainerId;
mTag = frag.mTag;
mRetainInstance = frag.mRetainInstance;
mRemoving = frag.mRemoving;
mDetached = frag.mDetached;
mArguments = frag.mArguments;
mHidden = frag.mHidden;
mMaxLifecycleState = frag.mMaxState.ordinal();
}
//...
}

see https://android.googlesource.com/platform/frameworks/support/+/androidx-1.0-dev/fragment/src/main/java/androidx/fragment/app/FragmentState.java?autodive=0%2F#27

可以看出 除了mArguments外都是基本类型,因此,Actvity中的Fragment中的Arguments也是需要关注的重点对象,梳理下整个过程
image.png

解决

工欲善其事必先利其器

在网上查找时候发现有个专门的工具来检查是 actvity或者fragment中有大对象 toolargetool,原理也比较简单,就是hook了Actvity和Fragment的生命周期,然后在onSaveInstanceState时候打印debuginfo 输出到logcat上,这样可以帮助我们解决问题。
根据sladar的异常信息可以确定是在拍搜页面出现了问题

CorrectionResultV2Activity是拍搜页面
PageResultfragment 是拍搜结果页被上面的CorrectionResultV2Activity持有

image.png

review了这里的代码,发现确实会有些问题,数据有不可控性,拍搜大的试卷确实数据量会比较大,然后我们在启动页面和结果页时候都是直接传参
image.png

image.png

因此通过可以通过

  • 中间数据中转或viewmodel方式,或者传输文件的引用避免大对象

  • fragment 使用传参数据后 清除bundle

    1
    2
    3
    arguments?.also {
    it.clear()
    }
  • 如果无需恢复数据直接 重写 onSaveInstanceState(outState: Bundle) { //不保存 }