当出现TransactionTooLargeException时应该怎么做?

325
我遇到了一个 TransactionTooLargeException 。无法复现。文档中提到:

Binder 事务失败,因为它太大。

在远程过程调用期间,调用的参数和返回值将作为存储在 Binder 事务缓冲区中的 Parcel 对象进行传输。如果参数或返回值过大而无法适合事务缓冲区,则调用将失败,并引发 TransactionTooLargeException。

...

当远程过程调用引发 TransactionTooLargeException 时,有两种可能的结果。要么客户端无法将其请求发送到服务(如果参数过大而无法适合事务缓冲区,则最有可能),要么服务无法将其响应发送回客户端(如果返回值过大而无法适合事务缓冲区,则最有可能)。

...

所以我在某个地方通过或接收超过某个未知限制的参数。在哪里?

堆栈跟踪没有显示任何有用信息:

java.lang.RuntimeException: Adding window failed
at android.view.ViewRootImpl.setView(ViewRootImpl.java:548)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:406)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:320)
at android.view.WindowManagerImpl$CompatModeWrapper.addView(WindowManagerImpl.java:152)
at android.view.Window$LocalWindowManager.addView(Window.java:557)
at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:2897)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2245)
at android.app.ActivityThread.access$600(ActivityThread.java:139)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1262)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:4977)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551)
at dalvik.system.NativeStart.main(Native Method)
Caused by: android.os.TransactionTooLargeException
at android.os.BinderProxy.transact(Native Method)
at android.view.IWindowSession$Stub$Proxy.add(IWindowSession.java:569)
at android.view.ViewRootImpl.setView(ViewRootImpl.java:538)
... 16 more
android.os.TransactionTooLargeException
at android.os.BinderProxy.transact(Native Method)
at android.view.IWindowSession$Stub$Proxy.add(IWindowSession.java:569)
at android.view.ViewRootImpl.setView(ViewRootImpl.java:538)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:406)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:320)
at android.view.WindowManagerImpl$CompatModeWrapper.addView(WindowManagerImpl.java:152)
at android.view.Window$LocalWindowManager.addView(Window.java:557)
at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:2897)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2245)
at android.app.ActivityThread.access$600(ActivityThread.java:139)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1262)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:4977)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551)
at dalvik.system.NativeStart.main(Native Method)

这似乎与视图有关?这与远程过程调用有什么关系吗?

可能重要的信息:Android版本:4.0.3,设备:HTC One X。


我今天在我的一个应用程序中刚刚遇到了这个问题。这种情况也只发生了一次,而且是在 Galaxy S3 上发生的。有趣的是,这似乎只在更强大的设备上出现。 - danwms
那个异常在API 15中被添加, http://developer.android.com/reference/android/os/TransactionTooLargeException.html我在地图上滚动时重现了它。直到垃圾回收器写入我没有内存了。(这花了我几分钟) - meh
交易缓冲区在所有设备上都限制为1MB,该缓冲区保存每个交易。因此,设备越强大,可以同时执行的交易就越多,所有交易共用同一个1MB缓冲区。话虽如此,你的回答不是答案而是评论。 - 3c71
我在我的应用程序中从未遇到过这个异常,但自从KitKat以来,每天都会有许多报告出现这个错误,而且出现在不同的上下文中。有人知道KitKat是否对此进行了更改,我个人对此进行的搜索没有任何结果。所以也许缓冲区大小已经改变了? - ToYonos
这是由于位图大小过大引起的,为什么不为该位图创建URI并将其传递给意图。 - Mohd Qasim
显示剩余3条评论
45个回答

194

我遇到了这个问题,发现当一个服务和应用程序之间交换大量数据时(包括传输大量缩略图),数据大小实际上只有约500KB,而IPC事务缓冲区大小被设置为1024KB。不确定为什么会超出事务缓冲区。

当你通过意图传递大量数据时,也会出现这种情况。

如果在应用程序中遇到这个异常,请分析你的代码。

  1. 你是否在服务和应用程序之间交换大量数据?
  2. 使用意图传递大量数据(例如用户从相册选择大量文件进行共享,所选文件的URI将使用意图进行传递)。
  3. 从服务接收位图文件。
  4. 等待Android返回大量数据(例如当用户安装了许多应用程序时,使用getInstalledApplications())。
  5. 使用applyBatch()处理大量挂起的操作。

如何处理这个异常

如果可能的话,将大操作分成小块,例如,不要使用1000个操作来调用applyBatch(),而是使用每次100个操作。

不要在服务和应用程序之间交换大量数据(>1MB)。

我不知道如何做到这一点,但请勿查询可能返回大量数据的Android :-)


16
我在调用getInstalledApplications时遇到了这个异常,该怎么解决? - Stan
7
我可以确认你对限制大约在500KB左右的结论是正确的,但这取决于设备,在某些设备上,你可以传输接近1MB的数据。我也遇到过这个异常情况,所以我进行了一些调查,并写了一篇文章,可能会对遇到这个问题的人有所帮助。http://nemanjakovacevic.net/blog/english/2015/03/24/yet-another-post-on-serializable-vs-parcelable/ - Nemanja Kovacevic
20
如果您发现很难找出导致崩溃的确切状态,那么您可能会发现TooLargeTool很有用。 - Max Spencer
1
@Stan @Gem @peacepassion,你们有没有找到解决问题的方法?因为我也遇到了同样的问题。 - mrid
1
另外,来自Crashlytics Insight的信息显示,“当同时传输太多数据时,就会出现此异常。基础的Binder事务缓冲区具有有限的固定大小,当前为1Mb,由进程中所有正在进行的事务共享。因此,即使大部分单独的事务都是适度大小,当有许多事务正在进行时,也可能抛出此异常。” 这意味着,如果多个小型事务同时发生,也可能会触发此异常。 - Simon Ninon
显示剩余17条评论

90

如果你需要查找导致崩溃的Parcel,你可以考虑尝试使用TooLargeTool

(我在接受的答案下面的@Max Spencer的评论中发现了这个工具,在我的情况下很有帮助。)


15
最被低估的解决方案。这个工具可以帮助你缩小问题活动的范围。 - Kedar Paranjape
这个工具是为Kotlin设计的:/ 有没有Java的替代方案? - maxwellnewage
2
@maxwellnewage:看起来最新版本(0.2.1,0.2.0也是)目前在仅使用Java的应用程序中无法工作。我不得不使用版本0.1.6,然后它就可以正常工作了。 - Danny
3
使用这个工具,我发现我的片段中使用了巨大的包大小。我所做的是从包中提取参数并使用bundle.clear()清除包。 - E J Chathuranga
我的项目没有使用AndroidX,所以只有进行以下更改后,toolargetool才能为我工作: 1)implementation 'com.gu.android:toolargetool:0.2.1@aar' -> implementation 'com.gu.android:toolargetool:0.1.6@aar' 2)targetSdkVersion 28 -> targetSdkVersion 23 - Maxim
显示剩余2条评论

49

这并不是一个明确的答案,但它可能会揭示导致 TransactionTooLargeException 的原因并帮助找到问题所在。

尽管大多数答案都涉及传输大量数据,但我看到这个异常在重复打开 ActionBar 下拉菜单、反复滚动和缩放后偶然抛出。崩溃发生在点击操作栏时。(这是一个自定义映射应用程序)

唯一传递的数据似乎只是来自“输入调度程序”到应用程序的触摸事件。我认为这不可能在“事务缓冲区”中合理地达到1 mb以上。

我的应用程序在四核1.6 GHz设备上运行,并使用3个线程进行繁重的工作,使一个核心空闲以供UI线程使用。此外,该应用程序使用android:largeHeap,在堆中剩余10 mb未使用,并且剩余100 mb可用于增加堆大小。因此,我不认为这是一个资源问题。

崩溃总是紧随着这些行之前:

W/InputDispatcher( 2271): channel ~ Consumer closed input channel or an error occurred.  events=0x9
E/InputDispatcher( 2271): channel ~ Channel is unrecoverably broken and will be disposed!
E/JavaBinder(28182): !!! FAILED BINDER TRANSACTION !!!

这些事件的顺序不一定会按照打印顺序,但(据我所检查)发生在同一毫秒内。

为了更清晰明了,堆栈跟踪本身与问题中的相同:

E/AndroidRuntime(28182): java.lang.RuntimeException: Adding window failed
..
E/AndroidRuntime(28182): Caused by: android.os.TransactionTooLargeException

深入研究Android的源代码,人们会发现这些代码行:

frameworks/base/core/jni/android_util_Binder.cpp:

case FAILED_TRANSACTION:
    ALOGE("!!! FAILED BINDER TRANSACTION !!!");
    // TransactionTooLargeException is a checked exception, only throw from certain methods.
    // FIXME: Transaction too large is the most common reason for FAILED_TRANSACTION
    //        but it is not the only one.  The Binder driver can return BR_FAILED_REPLY
    //        for other reasons also, such as if the transaction is malformed or
    //        refers to an FD that has been closed.  We should change the driver
    //        to enable us to distinguish these cases in the future.
    jniThrowException(env, canThrowRemoteException
            ? "android/os/TransactionTooLargeException"
                    : "java/lang/RuntimeException", NULL);

对我来说,听起来可能是我正在触发这个未记录的功能,导致事务失败的原因并不仅仅是因为事务过大。他们应该将其命名为TransactionTooLargeOrAnotherReasonException

目前我尚未解决此问题,但如果我找到有用的东西,我会更新这个答案。

更新:事实证明我的代码泄漏了一些文件描述符,在Linux中最多可以达到1024个,这似乎触发了异常。所以它实际上是一个资源问题。我通过打开/dev/zero 1024次进行验证,结果在与UI相关的操作中产生了各种奇怪的异常,包括上面提到的异常,甚至还有一些SIGSEGV。显然,在Android中,无法打开文件/套接字不是被处理/报告得非常干净的事情。


43

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

问题出在我们在一个 ViewPager 中使用了 FragmentStatePagerAdapter。用户会通过翻页创建100多个片段(这是一个阅读应用程序)。

尽管我们在 destroyItem() 方法中正确地管理了片段,但在Android的 FragmentStatePagerAdapter 实现中存在一个 bug,导致其保留了以下列表的引用:

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()方法,并"states"存储任何内容。

@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;
}

在所有可能性中,对我来说这似乎是唯一的选择。存储在状态中的内容是什么,使其能够达到这个大小?如果状态如此重要以至于需要编写保存代码,那么这不会引起其他问题吗?谢谢。 - Kenny
2
`@Override public Parcelable saveState() { Bundle bundle = (Bundle) super.saveState(); if (bundle != null) { Parcelable[] states = bundle.getParcelableArray("states"); // Subset only last 3 states if (states != null) states = Arrays.copyOfRange(states, states.length > 3 ? states.length - 3 : 0, states.length - 1); bundle.putParcelableArray("states", states); } else bundle = new Bundle(); return bundle; }` - Ramy Sabry
我只保留了最后3个状态,请查看上面的代码。 - Ramy Sabry
我不明白。为什么你要覆盖saveState,而你又没有在里面做任何事情。那就不要覆盖它怎么样?这样有什么区别吗? - daparic
6
我曾面临与ViewPager2和FragmentStateAdapter相关的同样问题,其中问题在于FragmentStateAdapter将'saveState()'标记为final,而ViewPager2类也是final的,因此我们无法覆盖这些方法。然而,我通过在View pager的XML条目中添加'\android:saveEnabled="false"'成功解决了Transaction过大的问题。<androidx.viewpager2.widget.ViewPager2 android:saveEnabled="false" android:id="@+id/viewPager" android:layout_width="fill_parent" android:layout_height="fill_parent" /> - Dev
显示剩余3条评论

26

对于那些在寻找TransactionTooLargeException出现原因的答案时感到非常失望的人,请尝试检查您在实例状态中保存了多少信息。

在编译/ targetSdkVersion <= 23 时,我们仅会收到有关已保存状态大小的内部警告,但不会发生任何崩溃:

E/ActivityThread: App sent too much data in instance state, so it was ignored
    android.os.TransactionTooLargeException: data parcel size 713856 bytes
    at android.os.BinderProxy.transactNative(Native Method)
    at android.os.BinderProxy.transact(Binder.java:615)
    at android.app.ActivityManagerProxy.activityStopped(ActivityManagerNative.java:3604)
    at android.app.ActivityThread$StopInfo.run(ActivityThread.java:3729)
    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:6044)
    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)

但在编译/ targetSdkVersion >= 24 的情况下,我们会遇到真正的RuntimeException崩溃:

java.lang.RuntimeException: android.os.TransactionTooLargeException: data parcel size 713860 bytes
    at android.app.ActivityThread$StopInfo.run(ActivityThread.java:3737)
    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:6044)
    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 713860 bytes
   at android.os.BinderProxy.transactNative(Native Method)
   at android.os.BinderProxy.transact(Binder.java:615)
   at android.app.ActivityManagerProxy.activityStopped(ActivityManagerNative.java:3604)
   at android.app.ActivityThread$StopInfo.run(ActivityThread.java:3729)
   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:6044) 
   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) 

该怎么做?

将数据保存在本地数据库中,并仅在实例状态中保留可用于检索此数据的ID。


能否将其保留为全局参数并稍后使用? - Jitendra Bhadja
@Jitendraramoliya 是的,你可以。那正是我所说的。 - Yazon2006

18

当应用程序被发送到后台时,通常会抛出此异常。

因此,我决定使用数据Fragment模式来完全避开onSavedInstanceState生命周期。我的解决方案还处理复杂的实例状态并尽快释放内存。

首先,我创建了一个简单的Fragment来存储数据:

package info.peakapps.peaksdk.logic;
import android.app.Fragment;
import android.app.FragmentManager;
import android.os.Bundle;

/**
 * A neat trick to avoid TransactionTooLargeException while saving our instance state
 */

public class SavedInstanceFragment extends Fragment {

    private static final String TAG = "SavedInstanceFragment";
    private Bundle mInstanceBundle = null;

    public SavedInstanceFragment() { // This will only be called once be cause of setRetainInstance()
        super();
        setRetainInstance( true );
    }

    public SavedInstanceFragment pushData( Bundle instanceState )
    {
        if ( this.mInstanceBundle == null ) {
            this.mInstanceBundle = instanceState;
        }
        else
        {
            this.mInstanceBundle.putAll( instanceState );
        }
        return this;
    }

    public Bundle popData()
    {
        Bundle out = this.mInstanceBundle;
        this.mInstanceBundle = null;
        return out;
    }

    public static final SavedInstanceFragment getInstance(FragmentManager fragmentManager )
    {
        SavedInstanceFragment out = (SavedInstanceFragment) fragmentManager.findFragmentByTag( TAG );

        if ( out == null )
        {
            out = new SavedInstanceFragment();
            fragmentManager.beginTransaction().add( out, TAG ).commit();
        }
        return out;
    }
}

然后在我的主活动中,我完全规避了保存实例周期,并把责任推迟给了我的数据片段。不需要在片段本身上使用此功能,因为它们的状态会自动添加到活动的状态中:

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);

    SavedInstanceFragment.getInstance( getFragmentManager() ).pushData( (Bundle) outState.clone() );
    outState.clear(); // We don't want a TransactionTooLargeException, so we handle things via the SavedInstanceFragment
}

现在只需弹出保存的实例即可:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(SavedInstanceFragment.getInstance(getFragmentManager()).popData());
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState( SavedInstanceFragment.getInstance( getFragmentManager() ).popData() );
}

完整详情: http://www.devsbedevin.net/avoiding-transactiontoolargeexception-on-android-nougat-and-up/


1
当应用程序在后台运行并且您尝试将活动置于前台时,如果活动被销毁会发生什么? - Master Disaster
在这种情况下,没有状态被保存,因为持有实例碎片的进程已经死亡。 - Vaiden
1
它可以在操作系统触发 onSavedState() 时工作,这种情况很常见。其中之一是配置更改。切换应用程序并转到后台是另一种情况。还有更多情况。 - Vaiden
@Vaiden,在这种情况下,磁盘缓存更好,因为我认为它可以在应用程序从后台重新创建时恢复数据。 - CoolMind
1
我认为这个解决方案应该扩展以保存来自不同来源的数据。可能使用哈希映射,以标签作为键,以束作为值。 - CoolMind
显示剩余5条评论

14

这个问题没有一个具体的原因。对于我来说,在我的Fragment类中我正在做这个:

public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    super.onCreateView(inflater, container, savedInstanceState);
    View rootView = inflater.inflate(R.layout.snacks_layout, container); //<-- notice the absence of the false argument
    return rootView;
}

而不是这个:

View rootView = inflater.inflate(R.layout.softs_layout, container, false);

1
哇!非常感谢!我百万年也想不到这个解决方案!我仍然困惑为什么不正确的视图填充方式会造成如此大的混乱!有人有一个有效的解释吗? - Priya Sindkar

11

重要的是要了解事务缓冲区被限制为1 MB,无论设备能力或应用程序如何。该缓冲区与您进行的每个API调用一起使用,并在应用当前运行的所有事务之间共享。

我认为它还保存了一些特定对象,比如包裹等(Parcel.obtain()),因此始终很重要的是将每个obtain()与一个recycle()匹配。

即使返回数据小于1 MB(如果其他事务仍在运行),在返回大量数据的API调用上也很容易发生此错误。

例如,PackageManager.getInstalledApplication()调用返回所有安装的应用程序的列表。添加特定标志允许检索大量额外数据。这样做很可能会失败,因此建议不检索任何额外数据并按应用程序检索。

但是,调用仍可能失败,因此重要的是将其包围在catch中并在需要时能够重试。

据我所知,除了重试并确保尽可能少地检索信息外,没有解决此问题的方法。


谢谢您提供的信息,这个缓冲区在应用程序中所有事务之间是共享的! - Artem Mostyaev
1
如果是这样的话,为什么我只在Android 4.4手机上看到了这种情况,而其他地方却没有呢?我认为这更像是4.4版本中的一个错误,我无法弄清楚原因。 - JPM

9

我也在三星S3上遇到了这个异常。

我怀疑有两个根本原因:

  1. 您的位图加载并占用了过多的内存,请使用缩小功能。
  2. 您在drawable-_dpi文件夹中缺少某些可绘制对象,Android会在drawable中查找它们并调整它们的大小,使得您的setContentView突然跳动并且占用大量内存。

使用DDMS并在玩应用程序时查看堆,这将为您提供有关哪个setcontentview正在创建问题的指示。

我将所有可绘制对象复制到所有文件夹中,以消除问题2。

问题已经解决。


内存/位图异常通常看起来不同。我已经在测试Android 2.x - 4.x时看到了很多这样的异常,而且异常总是看起来不同。但是,谁知道呢,也许这也与4.x版本有关。 - User
14
这只是一个糟糕的例外,就信息而言,它并没有提供任何有关问题来源的线索。 - User

9

将此代码添加到您的Activity中

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

这对我有用,希望它也能帮到你


14
我认为这是最有害的提示。为什么我们要在 onCreate() 中清除我们想要恢复的数据? - CoolMind
3
这段代码的含义是你最终没有保存实例状态... - Justin
不好的解决方案。不要做那样的事情。 - HeyAlex
我使用它后,在onDestroy和onCreate方面遇到了更大的问题。 - Nicu Besliu

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