【问题标题】:android.os.TransactionTooLargeException on Nougat牛轧糖上的 android.os.TransactionTooLargeException
【发布时间】:2016-12-30 03:06:57
【问题描述】:

我将 Nexus 5X 更新为 Android N,现在当我在其上安装应用程序(调试或发布)时,我在每个具有 Bundle in 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) 

【问题讨论】:

标签: android android-7.0-nougat


【解决方案1】:

最后,我的问题在于保存在 SaveInstance 上的内容,而不是发送到下一个活动的内容。我删除了我无法控制对象大小(网络响应)的所有保存,现在它可以工作了。

更新 2:

Google 现在提供 AndroidX ViewModel,它基于与保留 Fragments 相同的技术,但更易于使用。现在 ViewModel 是首选方法。

更新 1:

为了保留大量数据,Google 建议使用保留实例的 Fragment。想法是创建一个没有包含所有必要字段的视图的空 Fragment,否则将保存在 Bundle 中。在 Fragment 的 onCreate 方法中添加setRetainInstance(true);。 然后在 Activity 的 onDestroy 上将 Fragment 中的数据保存并在 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());
    }
}

片段示例:

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

更多相关内容,可以阅读here

【讨论】:

  • 这是正确的答案!由于 targetSdkVersion 24+ 这个异常抛出而不是“应用程序在实例状态下发送了太多数据,所以它被忽略了。”一些想了解更多的细节:code.google.com/p/android/issues/detail?id=212316
  • 当应用程序只是后台运行并且 ViewState 导致这种情况时,这无济于事。有谁知道如何处理这个问题?
  • 只是好奇,如果您持有对相关数据的静态引用怎么办?我的案例是 POJO 对象的数组列表。
  • @X09 这种方法的问题是当你去另一个活动时你应该清除那个内存,这可能很棘手(你需要在 onDestory 上清理它,而不是在你旋转屏幕时清理它) .否则,您的所有活动都将包含一些您不需要的对象的静态变量。这种方法可行,但你的应用程序会非常混乱,所以我不推荐它。
【解决方案2】:

只需在您的活动上覆盖此方法即可:

@Override
protected void onSaveInstanceState(Bundle outState) {
    // below line to be commented to prevent crash on nougat.
    // http://blog.sqisland.com/2016/09/transactiontoolargeexception-crashes-nougat.html
    //
    //super.onSaveInstanceState(outState);
}

转至https://code.google.com/p/android/issues/detail?id=212316#makechanges 了解更多信息。

【讨论】:

  • 如果您可以添加一些用户在点击链接之前可以看到的更多信息,这可能会有所帮助
  • 这个答案本身没有帮助
  • transactiontoolargeexception 将大数据保存到 SaveInstanceState 时出现。通过此代码,它将阻止保存数据。
  • 这不是一个好的解决方案,因为您可能需要存储活动中的某些内容
【解决方案3】:

我面临类似的问题。问题和场景略有不同,我通过以下方式修复它。请检查场景和解决方案。

场景: 我在 Google Nexus 6P 设备(7 操作系统)中从客户那里得到了一个奇怪的错误,因为我的应用程序在工作 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();
        }
    }

如果堆栈超出限制,它将自动弹出到初始片段。我希望有人会帮助这个答案,因为异常和堆栈跟踪日志是相同的。因此,无论何时发生此问题,如果您使用的是 Fragments 和回栈,请检查回栈计数。

【讨论】:

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

我遇到了同样的问题。 我的解决方法将 savedInstanceState 卸载到缓存目录中的文件。

我制作了以下实用程序类。

package net.cattaka.android.snippets.issue;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

/**
 * To parry BUG of Android N. https://code.google.com/p/android/issues/detail?id=212316
 * <p>
 * Created by cattaka on 2017/01/12.
 */
public class Issue212316Parrier {
    public static final String DEFAULT_NAME = "Issue212316Parrier";
    private static final String KEY_STORED_BUNDLE_ID = "net.cattaka.android.snippets.issue.Issue212316Parrier.KEY_STORED_BUNDLE_ID";

    private String mName;
    private Context mContext;
    private String mAppVersionName;
    private int mAppVersionCode;
    private SharedPreferences mPreferences;
    private File mDirForStoredBundle;

    public Issue212316Parrier(Context context, String appVersionName, int appVersionCode) {
        this(context, appVersionName, appVersionCode, DEFAULT_NAME);
    }

    public Issue212316Parrier(Context context, String appVersionName, int appVersionCode, String name) {
        mName = name;
        mContext = context;
        mAppVersionName = appVersionName;
        mAppVersionCode = appVersionCode;
    }

    public void initialize() {
        mPreferences = mContext.getSharedPreferences(mName, Context.MODE_PRIVATE);

        File cacheDir = mContext.getCacheDir();
        mDirForStoredBundle = new File(cacheDir, mName);
        if (!mDirForStoredBundle.exists()) {
            mDirForStoredBundle.mkdirs();
        }

        long lastStoredBundleId = 1;
        boolean needReset = true;
        String fingerPrint = (Build.FINGERPRINT != null) ? Build.FINGERPRINT : "";
        needReset = !fingerPrint.equals(mPreferences.getString("deviceFingerprint", null))
                || !mAppVersionName.equals(mPreferences.getString("appVersionName", null))
                || (mAppVersionCode != mPreferences.getInt("appVersionCode", 0));
        lastStoredBundleId = mPreferences.getLong("lastStoredBundleId", 1);

        if (needReset) {
            clearDirForStoredBundle();

            mPreferences.edit()
                    .putString("deviceFingerprint", Build.FINGERPRINT)
                    .putString("appVersionName", mAppVersionName)
                    .putInt("appVersionCode", mAppVersionCode)
                    .putLong("lastStoredBundleId", lastStoredBundleId)
                    .apply();
        }
    }

    /**
     * Call this from {@link android.app.Activity#onCreate(Bundle)}, {@link android.app.Activity#onRestoreInstanceState(Bundle)} or {@link android.app.Activity#onPostCreate(Bundle)}
     */
    public void restoreSaveInstanceState(@Nullable Bundle savedInstanceState, boolean deleteStoredBundle) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            if (savedInstanceState != null && savedInstanceState.containsKey(KEY_STORED_BUNDLE_ID)) {
                long storedBundleId = savedInstanceState.getLong(KEY_STORED_BUNDLE_ID);
                File storedBundleFile = new File(mDirForStoredBundle, storedBundleId + ".bin");
                Bundle storedBundle = loadBundle(storedBundleFile);
                if (storedBundle != null) {
                    savedInstanceState.putAll(storedBundle);
                }
                if (deleteStoredBundle && storedBundleFile.exists()) {
                    storedBundleFile.delete();
                }
            }
        }
    }

    /**
     * Call this from {@link android.app.Activity#onSaveInstanceState(Bundle)}
     */
    public void saveInstanceState(Bundle outState) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            if (outState != null) {
                long nextStoredBundleId = mPreferences.getLong("lastStoredBundleId", 1) + 1;
                mPreferences.edit().putLong("lastStoredBundleId", nextStoredBundleId).apply();
                File storedBundleFile = new File(mDirForStoredBundle, nextStoredBundleId + ".bin");
                saveBundle(outState, storedBundleFile);
                outState.clear();
                outState.putLong(KEY_STORED_BUNDLE_ID, nextStoredBundleId);
            }
        }
    }

    private void saveBundle(@NonNull Bundle bundle, @NonNull File storedBundleFile) {
        byte[] blob = marshall(bundle);
        OutputStream out = null;
        try {
            out = new GZIPOutputStream(new FileOutputStream(storedBundleFile));
            out.write(blob);
            out.flush();
            out.close();
        } catch (IOException e) {
            // ignore
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    // ignore
                }
            }
        }
    }

    @Nullable
    private Bundle loadBundle(File storedBundleFile) {
        byte[] blob = null;
        InputStream in = null;
        try {
            in = new GZIPInputStream(new FileInputStream(storedBundleFile));
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            int n;
            byte[] buffer = new byte[1024];
            while ((n = in.read(buffer)) > -1) {
                bout.write(buffer, 0, n);   // Don't allow any extra bytes to creep in, final write
            }
            bout.close();
            blob = bout.toByteArray();
        } catch (IOException e) {
            // ignore
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    // ignore
                }
            }
        }

        try {
            return (blob != null) ? (Bundle) unmarshall(blob) : null;
        } catch (Exception e) {
            return null;
        }
    }

    private void clearDirForStoredBundle() {
        for (File file : mDirForStoredBundle.listFiles()) {
            if (file.isFile() && file.getName().endsWith(".bin")) {
                file.delete();
            }
        }
    }


    @NonNull
    private static <T extends Parcelable> byte[] marshall(@NonNull final T object) {
        Parcel p1 = Parcel.obtain();
        p1.writeValue(object);

        byte[] data = p1.marshall();
        p1.recycle();
        return data;
    }

    @SuppressWarnings("unchecked")
    @NonNull
    private static <T extends Parcelable> T unmarshall(@NonNull byte[] bytes) {
        Parcel p2 = Parcel.obtain();
        p2.unmarshall(bytes, 0, bytes.length);
        p2.setDataPosition(0);
        T result = (T) p2.readValue(Issue212316Parrier.class.getClassLoader());
        p2.recycle();
        return result;
    }
}

完整代码:https://github.com/cattaka/AndroidSnippets/pull/37

我担心 Parcel#marshall 不应该用于持久化。 但是,我没有任何其他想法。

【讨论】:

  • 你能看看我更新的答案吗?我不小心找到了那个解决方案。乍一看它看起来很奇怪,但它确实很好用。
  • 您的解决方案看起来不错。但我认为当进程被系统杀死时它无法恢复。但是它可能不是什么大问题,因为它是一个边缘案例。
【解决方案5】:

我的应用程序中的问题是我试图在 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);
}

【讨论】:

  • 我遇到了同样的问题,最后我修复了它,删除了一些我保存到 savedInstanceState 中的数据。特别要小心 outState.putParcelableArrayList(... 或包含 Parcelable 列表的 Parcelable 对象。
【解决方案6】:

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

【讨论】:

  • 在 saveState() 中,bundle 可以为 null,所以 if (bundle != null) bundle.putParcelableArray("states", null);
  • @IKK828 非常感谢您的回答,您拯救了我的一天!像魅力一样工作:)
【解决方案7】:

随着 Android N 改变行为并抛出 TransactionTooLargeException 而不是记录错误。

     try {
            if (DEBUG_MEMORY_TRIM) Slog.v(TAG, "Reporting activity stopped: " + activity);
            ActivityManagerNative.getDefault().activityStopped(
                activity.token, state, persistentState, description);
        } catch (RemoteException ex) {
            if (ex instanceof TransactionTooLargeException
                    && activity.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();
        }

我的解决方案是挂钩 ActivityMangerProxy 实例并尝试捕获 activityStopped 方法。

代码如下:

private boolean hookActivityManagerNative() {
    try {
        ClassLoader loader = ClassLoader.getSystemClassLoader();
        Field singletonField = ReflectUtils.findField(loader.loadClass("android.app.ActivityManagerNative"), "gDefault");
        ReflectUtils.ReflectObject singletonObjWrap = ReflectUtils.wrap(singletonField.get(null));
        Object realActivityManager = singletonObjWrap.getChildField("mInstance").get();
        Object fakeActivityManager = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
                new Class[]{loader.loadClass("android.app.IActivityManager")}, new ActivityManagerHook(realActivityManager));
        singletonObjWrap.setChildField("mInstance", fakeActivityManager);
        return true;
    } catch (Throwable e) {
        AppHolder.getThirdPartUtils().markException(e);
        return false;
    }
}

private static class ActivityManagerHook implements InvocationHandler {

    private Object origin;

    ActivityManagerHook(Object origin) {
       this.origin = origin;
    }

    public Object getOrigin() {
        return origin;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        switch (method.getName()) {
            //ActivityManagerNative.getDefault().activityStopped(activity.token, state, persistentState, description);
            case "activityStopped": {
                try {
                    return method.invoke(getOrigin(), args);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return null;
            }
        }
        return method.invoke(getOrigin(), args);
    }
}

反射辅助类是

public class ReflectUtils {

private static final HashMap<String, Field> fieldCache = new HashMap<>();
private static final HashMap<String, Method> methodCache = new HashMap<>();

public static Field findField(Class<?> clazz, String fieldName) throws Throwable {
    String fullFieldName = clazz.getName() + '#' + fieldName;

    if (fieldCache.containsKey(fullFieldName)) {
        Field field = fieldCache.get(fullFieldName);
        if (field == null)
            throw new NoSuchFieldError(fullFieldName);
        return field;
    }

    try {
        Field field = findFieldRecursiveImpl(clazz, fieldName);
        field.setAccessible(true);
        fieldCache.put(fullFieldName, field);
        return field;
    } catch (NoSuchFieldException e) {
        fieldCache.put(fullFieldName, null);
        throw new NoSuchFieldError(fullFieldName);
    }
}


private static Field findFieldRecursiveImpl(Class<?> clazz, String fieldName) throws NoSuchFieldException {
    try {
        return clazz.getDeclaredField(fieldName);
    } catch (NoSuchFieldException e) {
        while (true) {
            clazz = clazz.getSuperclass();
            if (clazz == null || clazz.equals(Object.class))
                break;

            try {
                return clazz.getDeclaredField(fieldName);
            } catch (NoSuchFieldException ignored) {
            }
        }
        throw e;
    }
}


public static Method findMethodExact(Class<?> clazz, String methodName, Class<?>... parameterTypes) throws Throwable {
    String fullMethodName = clazz.getName() + '#' + methodName + getParametersString(parameterTypes) + "#exact";

    if (methodCache.containsKey(fullMethodName)) {
        Method method = methodCache.get(fullMethodName);
        if (method == null)
            throw new NoSuchMethodError(fullMethodName);
        return method;
    }

    try {
        Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
        method.setAccessible(true);
        methodCache.put(fullMethodName, method);
        return method;
    } catch (NoSuchMethodException e) {
        methodCache.put(fullMethodName, null);
        throw new NoSuchMethodError(fullMethodName);
    }
}


/**
 * Returns an array of the given classes.
 */
public static Class<?>[] getClassesAsArray(Class<?>... clazzes) {
    return clazzes;
}

private static String getParametersString(Class<?>... clazzes) {
    StringBuilder sb = new StringBuilder("(");
    boolean first = true;
    for (Class<?> clazz : clazzes) {
        if (first)
            first = false;
        else
            sb.append(",");

        if (clazz != null)
            sb.append(clazz.getCanonicalName());
        else
            sb.append("null");
    }
    sb.append(")");
    return sb.toString();
}

/**
 * Retrieve classes from an array, where each element might either be a Class
 * already, or a String with the full class name.
 */
private static Class<?>[] getParameterClasses(ClassLoader classLoader, Object[] parameterTypes) throws ClassNotFoundException {
    Class<?>[] parameterClasses = null;
    for (int i = parameterTypes.length - 1; i >= 0; i--) {
        Object type = parameterTypes[i];
        if (type == null)
            throw new ClassNotFoundException("parameter type must not be null", null);

        if (parameterClasses == null)
            parameterClasses = new Class<?>[i + 1];

        if (type instanceof Class)
            parameterClasses[i] = (Class<?>) type;
        else if (type instanceof String)
            parameterClasses[i] = findClass((String) type, classLoader);
        else
            throw new ClassNotFoundException("parameter type must either be specified as Class or String", null);
    }

    // if there are no arguments for the method
    if (parameterClasses == null)
        parameterClasses = new Class<?>[0];

    return parameterClasses;
}

public static Class<?> findClass(String className, ClassLoader classLoader) throws ClassNotFoundException {
    if (classLoader == null)
        classLoader = ClassLoader.getSystemClassLoader();
    return classLoader.loadClass(className);
}


public static ReflectObject wrap(Object object) {
    return new ReflectObject(object);
}


public static class ReflectObject {

    private Object object;

    private ReflectObject(Object o) {
        this.object = o;
    }

    public ReflectObject getChildField(String fieldName) throws Throwable {
        Object child = ReflectUtils.findField(object.getClass(), fieldName).get(object);
        return ReflectUtils.wrap(child);
    }

    public void setChildField(String fieldName, Object o) throws Throwable {
        ReflectUtils.findField(object.getClass(), fieldName).set(object, o);
    }

    public ReflectObject callMethod(String methodName, Object... args) throws Throwable {
        Class<?>[] clazzs = new Class[args.length];
        for (int i = 0; i < args.length; i++) {
            clazzs[i] = args.getClass();
        }
        Method method = ReflectUtils.findMethodExact(object.getClass(), methodName, clazzs);
        return ReflectUtils.wrap(method.invoke(object, args));
    }

    public <T> T getAs(Class<T> clazz) {
        return (T) object;
    }

    public <T> T get() {
        return (T) object;
    }
}
}

【讨论】:

    【解决方案8】:

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

    我在TooLargeTool 的帮助下追踪了Bundle 的大小。

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

    附: Google 问题跟踪器中的相关问题:https://issuetracker.google.com/issues/37103380

    【讨论】:

    • 谢谢大卫。这个工具确实帮助我找到了问题的根本原因。你摇滚:)
    • 欢迎@NitinMesta,:)
    【解决方案9】:

    Activity 正在停止时,每当您看到TransactionTooLargeException 发生时,这意味着Activity 正试图将其保存的状态Bundles 发送到系统操作系统以安全保存以便以后恢复(在配置更改或进程死亡后)但它发送的一个或多个Bundles 太大。一次发生的所有此类事务的最大限制约为 1MB,即使没有单个 Bundle 超过该限制,也可以达到该限制。

    这里的罪魁祸首通常是在ActivityActivity 托管的任何Fragments 中的onSaveInstanceState 中保存了太多数据。通常这种情况发生在保存特别大的东西时,比如Bitmap,但它也可能发生在发送大量较小的数据时,比如Parcelable 对象的列表。 Android 团队多次明确表示,onSavedInstanceState 中只应保存少量与视图相关的数据。但是,开发人员通常会保存网络数据页面,以使配置更改看起来尽可能平滑,而不必再次重新获取相同的数据。从 2017 年 Google I/O 开始,Android 团队已经明确表示,Android 应用的首选架构可以节省网络数据

    • 在内存中,因此可以在配置更改时轻松重复使用
    • 到磁盘,以便在进程死亡和应用会话后轻松恢复

    他们新的ViewModel 框架和Room 持久性库旨在帮助开发人员适应这种模式。如果您的问题是在 onSaveInstanceState 中保存了太多数据,那么使用这些工具更新到这样的架构应该可以解决您的问题。

    就个人而言,在更新到该新模式之前,我想使用我现有的应用程序,同时绕过TransactionTooLargeException。我为此编写了一个快速库:https://github.com/livefront/bridge。它使用相同的一般想法,即在配置更改后从内存中恢复状态以及在进程死亡后从磁盘中恢复状态,而不是通过onSaveInstanceState 将所有状态发送到操作系统,但只需对现有代码进行非常小的更改即可使用。不过,任何符合这两个目标的策略都应该可以帮助您避免异常,同时又不会牺牲您保存状态的能力。

    最后一点:您在 Nougat+ 上看到此内容的唯一原因是,最初如果超出绑定器事务限制,将保存状态发送到操作系统的过程将静默失败,Logcat 中仅显示此错误:

    !!! Binder 交易失败!!!

    在 Nougat 中,无声故障升级为硬崩溃。值得称赞的是,这是开发团队在the release notes for Nougat 中记录的内容:

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

    【讨论】:

    • 我尝试实现您的库,但没有成功。仍然会发生同样的崩溃。
    • 我很想看看你的实现。随时将详细信息作为问题留在库中。如果使用得当,我没有任何关于此的报告。
    • 我按照库 github 页面上的说明进行操作。我需要在代码中添加任何其他内容吗?
    • 这取决于您之前保存状态的方式。如果您使用的是 Icepick 之类的东西,那么就不应该有任何额外的步骤。但是,如果您不是,那么您可能需要进行一些代码重组才能首先开始使用这样的库。如果您在 Github 项目上发布了一些带有代码示例的更多信息作为问题,我可以查看您的设置是否有任何问题。
    • 如果您还没有使用 Icepick(或类似的东西),那还不够。如文档中所述:“Bridge 旨在作为基于注释的状态保存库(如 Icepick、Android-State 和 Icekick)的简单包装器。”有多种方法可以在不使用其中一种的情况下让 Bridge 正常工作,但与仅更新您的应用以使用其中一种相比,它最终的工作量更大。
    【解决方案10】:

    试了试,终于解决了我的问题。 将此添加到您的Activity

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

    【讨论】:

    • 谢谢@Raj...你节省了我的时间
    • 你没有这样保存任何东西。清除您在 Activity 中创建的任何“进度”用户是一种不好的做法。
    • 如果您的实例很重要并且您想存储它们,那么您可以在清除之前保存它,//Create instance variable, Bundle oldInstance; @Override protected void onSaveInstanceState(Bundle oldInstanceState) { super.onSaveInstanceState(oldInstanceState); //save your instance here, here oldInstance = oldInstanceState oldInstanceState.clear(); }
    【解决方案11】:

    以上答案都不适合我,问题的原因很简单,正如我使用 FragmentStatePagerAdapter 的一些人所说,它的 saveState 方法保存了片段的状态,因为我的一个片段非常大,所以保存这个片段导致这个 TransactionTooLargeExecption。

    如@IK828 所述,我尝试在我的寻呼机实现中覆盖 saveState 方法,但这无法解决崩溃问题。

    我的片段有一个 EditText 用于保存非常大的文本,这在我的情况下是问题的罪魁祸首,所以简单地在片段的 onPause() 中,我将 edittext 文本设置为空字符串。 即:

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

    现在,当 FragmentStatePagerAdapter 将尝试保存状态时,这一大块文本将不会在那里消耗其中的大部分,从而解决了崩溃问题。

    在您的情况下,您需要找到罪魁祸首,它可能是带有一些位图的 ImageView、带有大量文本的 TextView 或任何其他高内存消耗视图,您需要释放它的内存,您可以设置 imageview .setImageResource(null) 或片段的 onPause() 中的类似内容。

    更新:在调用 super like 之前,onSaveInstanceState 是更好的地方:

    @Override
        public void onSaveInstanceState(Bundle outState) {
            edittext.setText("");
            super.onSaveInstanceState(outState);
        }
    

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

    【讨论】:

    • 更好的解决方案是在特定视图上使用android:saveEnabled="false"。你可以阅读更多关于它的信息here
    【解决方案12】:

    在我的例子中,我在片段中遇到了这个异常,因为它的一个参数是一个非常大的字符串,我忘记删除它(我只在 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  
    }
    

    【讨论】:

    • 非常感谢,为我工作,一直在寻找这个问题,尝试了一切,终于成功了。
    • 去掉片段后参数不就清楚了吗???为什么这部分逻辑不是片段中的...
    【解决方案13】:

    在我的例子中,我使用TooLargeTool 来跟踪问题的来源,并从我的onSaveInstanceState 中发现android:support:fragments 中的android:support:fragments 键在应用程序崩溃时达到了近1mb。所以解决方案是这样的:

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.remove("android:support:fragments");
    }
    

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

    【讨论】:

    • 上面的代码sn-p是在哪里添加的?在父活动或片段本身中。另外,上述解决方案是否对您有效以消除 Transaction Too Large 异常?
    • @JatinJha 我在活动中添加了该代码,它阻止了TransactionTooLargeException。如果这对您不起作用,最好使用 github.com/guardian/toolargetool 检查正在执行的操作
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-08-15
    • 1970-01-01
    • 2017-05-28
    相关资源
    最近更新 更多