Nougat上的android.os.TransactionTooLargeException

80

我把Nexus 5X升级到了安卓N系统,现在每次在其中安装应用(无论是debug还是release版本),都会在任何带有Bundle的extras的屏幕转换中遇到TransactionTooLargeException异常。该应用在其他设备上运行正常。与PlayStore上的旧应用程序代码基本相同的旧应用程序在Nexus 5X上也可以正常运行。有人遇到过类似问题吗?

java.lang.RuntimeException: android.os.TransactionTooLargeException: data parcel size 592196 bytes
   at android.app.ActivityThread$StopInfo.run(ActivityThread.java:3752)
   at android.os.Handler.handleCallback(Handler.java:751)
   at android.os.Handler.dispatchMessage(Handler.java:95)
   at android.os.Looper.loop(Looper.java:154)
   at android.app.ActivityThread.main(ActivityThread.java:6077)
   at java.lang.reflect.Method.invoke(Native Method)
   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:865)
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:755)
Caused by: android.os.TransactionTooLargeException: data parcel size 592196 bytes
   at android.os.BinderProxy.transactNative(Native Method)
   at android.os.BinderProxy.transact(Binder.java:615)
   at android.app.ActivityManagerProxy.activityStopped(ActivityManagerNative.java:3606)
   at android.app.ActivityThread$StopInfo.run(ActivityThread.java:3744)
   at android.os.Handler.handleCallback(Handler.java:751) 
   at android.os.Handler.dispatchMessage(Handler.java:95) 
   at android.os.Looper.loop(Looper.java:154) 
   at android.app.ActivityThread.main(ActivityThread.java:6077) 
   at java.lang.reflect.Method.invoke(Native Method) 
   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:865) 
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:755

编写一个带有参数的类,其中包含大量数据。 - Vyacheslav
https://dev59.com/lmgu5IYBdhLWcg3wRlBh - Vyacheslav
1
这是个问题。在大多数情况下,我只发送带有几个字符串的小可序列化对象。我在每次活动转换时都会遇到此错误。 在其他设备上,它可以正常工作。 - Vladimir Jovanović
6
已有报告称此为错误,请参见 https://code.google.com/p/android/issues/detail?id=212316。 - Romain Piel
13个回答

34
每当一个Activity正在停止的过程中出现“TransactionTooLargeException”异常时,这意味着该Activity试图将其保存的状态Bundles发送到系统OS以供稍后恢复(在配置更改或进程死亡后),但它发送的一个或多个Bundles太大了。所有此类事务一次发生的最大限制约为1MB,并且即使没有单个Bundle超过该限制,也可能达到该限制。
主要罪魁祸首通常是在Activity或任何由Activity托管的Fragments的onSaveInstanceState中保存过多的数据。通常情况下,当保存特别大的数据如Bitmap时,就会出现这种情况,但当发送大量较小的数据时,如Parcelable对象列表时,也会出现这种情况。Android团队已经多次明确表示,只应在onSavedInstanceState中保存少量与视图相关的数据。然而,开发者通常保存了大量的网络数据,以便通过不必重新获取相同的数据来使配置更改看起来尽可能平滑。截至2017年Google I/O,Android团队已经明确表示,Android应用程序的首选架构应该将网络数据:
  • 存储在内存中,以便在配置更改时可以轻松重用
  • 存储在磁盘上,以便在进程死亡和应用程序会话后可以轻松恢复
他们的新ViewModel框架和Room持久性库旨在帮助开发人员符合此模式。如果您的问题是在onSaveInstanceState中保存了太多数据,则使用这些工具更新到这样的架构应该会解决您的问题。

个人而言,在更新到新的模式之前,我想先处理现有应用程序中的TransactionTooLargeException。我编写了一个快速的库来解决这个问题:https://github.com/livefront/bridge。它使用相同的常规思路:在配置更改和进程死亡后从内存中恢复状态并从磁盘中恢复状态,而不是通过onSaveInstanceState将所有状态发送到操作系统,但需要对您现有的代码进行非常小的更改才能使用。任何符合这两个目标的策略都应该帮助您避免异常,而不会牺牲保存状态的能力。

最后一点提示:您在Nougat+上看到这个原因仅仅是因为如果超过binder事务限制,将保存的状态发送到操作系统的进程将悄无声息地失败,只会在Logcat中显示此错误:

!!! FAILED BINDER TRANSACTION !!!

在Nougat中,这种无声故障被升级为硬崩溃。值得赞扬的是,这是开发团队在Nougat发布说明中记录的内容:

许多平台API现在开始检查跨Binder事务发送大型有效负载,系统现在将TransactionTooLargeExceptions重新抛出为RuntimeExceptions,而不是默默记录或抑制它们。一个常见的例子是在Activity.onSaveInstanceState()中存储过多的数据,这会导致ActivityThread.StopInfo在您的应用程序针对Android 7.0时抛出RuntimeException。


我尝试实现你的库,但它没有起作用。仍然出现相同的崩溃。 - Hampel Előd
我很想看看你的实现。请随意在库的问题中留下细节。如果正确使用,我没有收到任何报告。 - Brian Yencho
我按照库的Github页面上的说明进行了操作。在代码中还需要添加其他内容吗? - Hampel Előd
这取决于你之前如何保存状态。如果你使用的是类似Icepick的东西,那么就不需要额外的步骤。但如果没有使用,那么你可能需要重新组织代码,首先开始使用这样的库。如果你在Github项目上发布了一些更多信息和代码示例作为问题,我可以看看你的设置是否有问题。 - Brian Yencho
我没有使用任何东西。我只是从GitLab复制了你的代码,就这样。 - Hampel Előd
2
如果你还没有使用Icepick(或类似的东西),那就不够了。正如文档中所述:“Bridge旨在作为注解状态保存库(如Icepick、Android-State和Icekick)的简单包装器。”有一些方法可以让Bridge在不使用其中之一的情况下工作,但最终需要付出比更新应用程序以使用其中之一更多的工作。 - Brian Yencho

26
最终,我的问题出在被保存在 onSaveInstance 中的事物,而不是发送到下一个活动中的事物。我删除了所有保存那些我无法控制对象大小(网络响应)的内容,现在它可以工作了。
更新2: 现在谷歌提供了基于相同技术的 AndroidX ViewModel,但使用起来要容易得多,因此 ViewModel 是首选方法。
更新1: 为了保留大量数据,谷歌建议使用保留实例的 Fragment。其思想是创建一个没有视图但包含所有必要字段的空 Fragment,否则这些字段会保存在 Bundle 中。在 Fragment 的 onCreate 方法中添加 `setRetainInstance(true);`。 然后在 Activity 的 onDestroy 中保存数据,在 onCreate 中加载数据。 以下是 Activity 的示例:
public class MyActivity extends Activity {

    private DataFragment dataFragment;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // find the retained fragment on activity restarts
        FragmentManager fm = getFragmentManager();
        dataFragment = (DataFragment) fm.findFragmentByTag(“data”);

        // create the fragment and data the first time
        if (dataFragment == null) {
            // add the fragment
            dataFragment = new DataFragment();
            fm.beginTransaction().add(dataFragment, “data”).commit();
            // load the data from the web
            dataFragment.setData(loadMyData());
        }

        // the data is available in dataFragment.getData()
        ...
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // store the data in the fragment
        dataFragment.setData(collectMyLoadedData());
    }
}

Fragment示例:

public class DataFragment extends Fragment {

    // data object we want to retain
    private MyDataObject data;

    // this method is only called once for this fragment
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // retain this fragment
        setRetainInstance(true);
    }

    public void setData(MyDataObject data) {
        this.data = data;
    }

    public MyDataObject getData() {
        return data;
    }
}

想了解更多,可以阅读这里


5
回答正确!自targetSdkVersion 24+起,此异常会抛出,而不是“应用程序在实例状态中发送了太多数据,因此被忽略。”想要了解更多细节的人可以访问以下网址:https://code.google.com/p/android/issues/detail?id=212316。 - Yazon2006
2
当应用程序被简单地置于后台并且视图状态引起了这种情况时,这并没有帮助。有人有任何处理此问题的想法吗? - AllDayAmazing
只是好奇,如果您持有对所讨论数据的静态引用会怎样?我的情况是一个POJO对象的ArrayList。 - X09
@X09 这种方法的问题在于,当你转到另一个活动时,你应该清除那些内存,这可能会很棘手(你需要在onDestory上清理它,但不是在旋转屏幕时)。否则,所有的活动都将具有一些你不需要的静态变量和对象。这种方法可以工作,但你的应用程序会变得非常混乱,所以我不建议使用它。 - Vladimir Jovanović

23

我进行了一次试错,最终解决了我的问题。 将此添加到您的 Activity 中。

@Override
protected void onSaveInstanceState(Bundle oldInstanceState) {
    super.onSaveInstanceState(oldInstanceState);
    oldInstanceState.clear();
}

2
谢谢@Raj...你节省了我的时间。 - varotariya vajsi
1
那样做您并未保存任何内容。清除用户在您的“Activity”中所做的任何“进度”都是一个不好的实践。 - N. Park
如果您的实例很重要并且想要存储它们,那么在清除之前可以先保存它们,`//创建实例变量,Bundle oldInstance;@Override protected void onSaveInstanceState(Bundle oldInstanceState) { super.onSaveInstanceState(oldInstanceState); //在此处保存您的实例 oldInstance = oldInstanceStateoldInstanceState.clear();}` - Raj Yadav
你救了我的一生兄弟。@RajYadav - Hitesh Sarsava

18

TransactionTooLargeException问题已经困扰我们约4个月,最终我们解决了这个问题!

发生的情况是我们在ViewPager中使用了一个FragmentStatePagerAdapter。用户会浏览并创建100多个片段(它是一款阅读应用程序)。

尽管我们在destroyItem()中正确地管理了片段,但在Android的FragmentStatePagerAdapter实现中存在一个错误,它会保留对以下列表的引用:

private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();

当 Android 的 FragmentStatePagerAdapter 尝试保存状态时,它会调用该函数

@Override
public Parcelable saveState() {
    Bundle state = null;
    if (mSavedState.size() > 0) {
        state = new Bundle();
        Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
        mSavedState.toArray(fss);
        state.putParcelableArray("states", fss);
    }
    for (int i=0; i<mFragments.size(); i++) {
        Fragment f = mFragments.get(i);
        if (f != null && f.isAdded()) {
            if (state == null) {
                state = new Bundle();
            }
            String key = "f" + i;
            mFragmentManager.putFragment(state, key, f);
        }
    }
    return state;
}

正如您所看到的,即使您在FragmentStatePagerAdapter子类中正确管理片段,基类仍将为每个已创建的片段存储一个Fragment.SavedState。当该数组被转储到parcelableArray并且操作系统不喜欢它超过100个项目时,就会出现TransactionTooLargeException。

因此,我们的修复方法是覆盖saveState()方法,并不存储任何“状态”。

@Override
public Parcelable saveState() {
    Bundle bundle = (Bundle) super.saveState();
    bundle.putParcelableArray("states", null); // Never maintain any states from the base class, just null it out
    return bundle;
}

2
在saveState()中,bundle可能为空,因此如果(bundle!= null),则bundle.putParcelableArray("states", null); - Zbarcea Christian
@IKK828 非常感谢您的回答,您解救了我的一天!功能非常棒 :) - jaumebd

12

我在我的 Nougat 设备上也遇到了这个问题。我的应用程序使用一个带有视图页面的片段,其中包含 4 个片段。我将一些大型构造参数传递给了这 4 个片段,导致了这个问题。

我使用 TooLargeTool 工具追踪导致此问题的 Bundle 的大小。

最后,我使用实现了 Serializable 接口的 POJO 对象上的 putSerializable 解决了这个问题,而不是在片段初始化期间使用 putString 传递大型原始 String。这将 Bundle 的大小减半,并且不会抛出 TransactionTooLargeException。因此,请确保不要将巨大的参数传递给 Fragment

P.S. 相关问题请参考 Google 问题跟踪器:https://issuetracker.google.com/issues/37103380


3
谢谢David。这个工具确实帮助我找到了问题的根本原因。你太棒了:) - Nitin Mesta
欢迎 @NitinMesta, :) - chubao

8

我遇到了类似的问题,但是问题和场景有所不同,我用以下方式解决了它。请检查场景和解决方案。

场景: 在Google Nexus 6P设备(7 OS)上,我的应用程序会在工作4个小时后崩溃,顾客反馈出现了奇怪的错误。后来我发现它抛出了类似的(android.os.TransactionTooLargeException:)异常。

解决方案: 日志没有指向应用程序中的任何特定类,后来我发现这是因为保持片段的后退堆栈。在我的情况下,通过自动屏幕移动动画,重复添加了4个片段到后退堆栈中。所以我重写了onBackstackChanged()如下所示。

 @Override
    public void onBackStackChanged() {
        try {
            int count = mFragmentMngr.getBackStackEntryCount();
            if (count > 0) {
                if (count > 30) {
                    mFragmentMngr.popBackStack(1, FragmentManager.POP_BACK_STACK_INCLUSIVE);
                    count = mFragmentMngr.getBackStackEntryCount();
                }
                FragmentManager.BackStackEntry entry = mFragmentMngr.getBackStackEntryAt(count - 1);
                mCurrentlyLoadedFragment = Integer.parseInt(entry.getName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

如果堆栈超出限制,它将自动弹回到初始片段。希望有人能帮助解决这个问题,因为异常和堆栈跟踪日志是相同的。因此,每当出现此问题时,请检查返回堆栈计数,如果正在使用片段和返回堆栈。


这个解决方案似乎可以处理我遇到的TransactionTooLargeException和FAILED BINDER TRANSACTION异常。想知道mCUrrentlyLoadedFragment是什么,并且为什么我们需要将entry.getName()解析给它。谢谢。 - tfad334

6
在我的情况下,我在一个片段中得到了这个异常,因为它的一个参数是一个非常大的字符串,我忘记删除它了(我只在onViewCreated()方法中使用了那个大字符串)。因此,要解决这个问题,我只需删除该参数。在您的情况下,在调用onPause()之前,您必须清除或将任何可疑字段置为空。
活动代码
Fragment fragment = new Fragment();
Bundle args = new Bundle();
args.putString("extremely large string", data.getValue());
fragment.setArguments(args);

片段代码

@Override 
public void onViewCreated(View view, Bundle savedInstanceState) {

    String largeString = arguments.get("extremely large string");       
    //Do Something with the large string   
    arguments.clear() //I forgot to execute this  
}

非常感谢,对我很有帮助,我一直在寻找这个问题的解决方法,尝试了所有可能的方法,最终这个方法奏效了。 - akashzincle
在移除片段后,参数不会被清除吗?为什么这部分逻辑不在片段中呢? - Mor Elmaliach

3

我的应用程序出现问题是因为我试图将过多的内容保存在savedInstanceState中,解决方法是在正确的时间识别需要保存的确切数据。基本上,仔细查看你的onSaveInstanceState,确保不要过度使用它:

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
    // Save the user's current state
    // Check carefully what you're adding into the savedInstanceState before saving it
    super.onSaveInstanceState(savedInstanceState);
}

1
我曾经遇到过同样的问题,最终通过删除一些保存在savedInstanceState中的数据来解决它。特别要注意的是outState.putParcelableArrayList(...)或包含可序列化列表的可序列化对象。 - anthorlop

2
在我的情况下,我使用TooLargeTool来追踪问题的来源,我发现来自onSaveInstanceStateBundle中的android:support:fragments键在应用程序崩溃时接近1mb。因此解决方案是:
@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.remove("android:support:fragments");
}

通过这样做,我避免了保存所有片段状态并保留其他需要保存的内容。

你将上述代码片段添加在哪里?是在父 Activity 中还是在 Fragment 本身中?此外,上述解决方案是否有效,以消除“事务过大异常”? - Jatin Jha
@JatinJha 我在活动中添加了那段代码,它可以防止出现“TransactionTooLargeException”的情况。如果这对你没有起作用,最好使用https://github.com/guardian/toolargetool来检查正在执行的操作。 - Lennon Spirlandelli

1

以上的答案都对我没有用,问题的原因很简单,正如某些人所述,我正在使用FragmentStatePagerAdapter和它的saveState方法保存片段的状态。由于我的一个片段相当大,因此保存这个片段会导致TransactionTooLargeExecption。

我尝试按@IK828所述覆盖pager的saveState方法,但这无法解决崩溃问题。

我的片段有一个EditText,用来保存非常大的文本,这是我遇到问题的罪魁祸首,所以在片段的onPause()中,我将edittext文本设置为空字符串。

@Override
    public void onPause() {
       edittext.setText("");
}

现在,当FragmentStatePagerAdapter尝试保存状态时,这个大块文本就不会存在于其中以消耗更多的空间,从而解决了崩溃问题。
在您的情况下,您需要找到罪魁祸首,可能是带有一些位图的ImageView、带有大量文本的TextView或任何其他高内存消耗的视图,您需要释放它的内存,在您的片段的onPause()中,您可以设置imageview.setImageResource(null)或类似的内容。
更新:在调用super之前,onSaveInstanceState是更好的位置用于此目的。
@Override
    public void onSaveInstanceState(Bundle outState) {
        edittext.setText("");
        super.onSaveInstanceState(outState);
    }

或者像@Vladimir指出的那样,您可以在视图或自定义视图上使用android:saveEnabled="false"或view.setSaveEnabled(false);并确保在onResume中将文本设置回来,否则当Activity恢复时它将为空。


3
更好的解决方案是在特定的视图上使用 android:saveEnabled="false"。您可以在这里了解更多信息。 - Vladimir Jovanović

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接