在牛轧糖上的android.os.TransactionTooLargeException
我更新了Nexus 5X到Android N,现在当我安装应用程序(debugging或发布)时,我得到了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)
最后,我的问题是保存在SaveInstance上的东西,而不是发送到下一个活动的东西。 我删除了所有无法控制对象大小的保存(networking响应),现在它正在工作。
更新:
为了保留大量的数据,Googlebuild议使用保留实例的Fragment来实现。 想法是创造空的片段,没有所有必要的领域的视图,否则将被保存在捆绑。 添加setRetainInstance(true);
到片段的onCreate方法。 然后将数据保存在Activity的onDestroy上的Fragment中,并将它们加载到Create上。 这里是一个活动的例子:
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; } }
更多关于它,你可以在这里阅读。
TransactionTooLargeException现在一直困扰我们大约4个月,我们终于解决了这个问题!
发生了什么事情是我们在ViewPager中使用FragmentStatePagerAdapter。 用户将通过并创build100多个片段(其阅读应用程序)。
尽pipe我们在destroyItem()中正确地pipe理了这些片段,但是在FragmentStatePagerAdapter的Androids实现中存在一个错误,它保留了对以下列表的引用:
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; }
如您所见,即使您正确pipe理FragmentStatePagerAdapter子类中的片段,基类仍将为创build的每个片段存储Fragment.SavedState。 TransactionTooLargeException发生时,该数组被转储到parcelableArray和操作系统不会喜欢它100 +项目。
因此,我们的修补程序是覆盖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; }
无论何时,当Activity
处于停止过程中时,如果发生TransactionTooLargeException
,就意味着Activity
正试图将其保存的状态Bundles
发送到系统操作系统,以便以后(在configuration更改或进程死亡后)安全地恢复,但是那个或更多的它发送的Bundles
太大了。 所有此类交易一次发生的最高限额约为1MB,即使没有单一Bundle
超过该限额,也可达到限额。
这里的罪魁祸首通常是将太多的数据保存在Activity
或者Activity
任何Fragments
onSaveInstanceState
中。 通常情况下,这种情况发生时保存像Bitmap
一样特别大的东西,但也可以发送大量的小数据,如Parcelable
对象的列表。 Android团队已经多次明确表示,只有less量的视图相关的数据应该保存在onSavedInstanceState
。 但是,开发人员经常保存networking数据页面,以便不必再次重新获取相同的数据,从而使configuration更改尽可能平滑。 从Google I / O 2017开始,Android团队明确表示,Android应用的首选架构可以节省networking数据
- 在内存中,因此可以轻松地在configuration更改中重复使用
- 到磁盘,以便它可以很容易地恢复过程中死亡和应用程序会话
他们新的ViewModel
框架和Room
持久性库旨在帮助开发人员适应这种模式。 如果您的问题是在onSaveInstanceState
保存了太多的数据,那么使用这些工具更新到像这样的架构应该可以解决您的问题。
就个人而言,在更新到这种新的模式之前,我想拿我现有的应用程序,并在此期间解决TransactionTooLargeException
。 我写了一个快速的库来做到这一点: https : //github.com/livefront/bridge 。 它使用了相同的一般思路,即通过configuration更改和进程死亡后的磁盘恢复内存中的状态,而不是通过onSaveInstanceState
将所有状态发送到操作系统,但只需要对现有代码进行非常小的更改即可使用。 任何适合这两个目标的策略都应该帮助你避免这个例外,但是不会牺牲你保存状态的能力。
最后要注意的是,在Nougat +上看到这个的唯一原因是,如果超过了活页夹事务限制,则将保存的状态发送到操作系统的过程将失败,只有Logcat中显示此错误:
! 失败的粘合剂交易!
在牛轧糖中,这种沉默的失败被升级为一场严重的崩溃。 值得肯定的是,这是开发团队在牛轧糖发行说明中logging的内容 :
现在许多平台API已经开始检查通过Binder事务发送的大量有效负载,现在系统将TransactionTooLargeExceptions作为RuntimeExceptions重新引发,而不是以静默方式logging或禁止它们。 一个常见的例子是在Activity.onSaveInstanceState()中存储太多数据,导致ActivityThread.StopInfo在您的应用程序的目标为Android 7.0时抛出RuntimeException。
我也在我的牛轧糖设备上面对这个问题。 我的应用程序使用包含4个片段的视图分页器的片段。 我把一些大的build设论点传给了导致问题的四个片段。
我跟踪了Bundle
的大小,在TooLargeTool的帮助下引起了这个问题 。
最后,我使用putSerializable
在一个实现了Serializable
的POJO对象上parsing了它,而不是在片段初始化过程中使用putSerializable
传递一个大的原始String
。 这减less了一半的Bundle
的大小,并不会抛出TransactionTooLargeException
。 因此,请确保您不要将大量的parameter passing给Fragment
。
Google问题跟踪器中的PS相关问题: https : //issuetracker.google.com/issues/37103380
我面临类似的问题。 问题和情况有点不同,我以下面的方式解决它。 请检查场景和解决scheme。
情景:我在Google Nexus 6P设备(7 OS)中遇到了一个奇怪的问题,因为我的应用程序在工作4个小时后会崩溃。 后来我发现它是抛出类似的(android.os.TransactionTooLargeException :)exception。
解决scheme:日志没有指向应用程序中的任何特定的类,后来我发现这是因为保留了碎片堆栈。 就我而言,在自动屏幕移动animation的帮助下,将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(); } }
如果堆栈超出限制,它会自动popup到初始片段。 我希望有人会帮助这个答案,因为exception和堆栈跟踪日志是相同的。 所以每当这个问题发生的时候,请检查后面的堆栈数量,如果你正在使用碎片和回栈。
只需在您的活动上覆盖此方法:
@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了解更多信息。;
我面临同样的问题。 我的解决方法将savedInstanceState卸载到caching目录中的文件。
我做了下面的工具类。
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
我担心,包裹#马歇尔不应该用于执行。 但是,我没有任何其他的想法。
当Android N改变行为并抛出TransactionTooLargeException而不是logging错误。
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(); }
我的解决scheme是钩住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; } } }
在我的应用程序的问题是,我试图保存到savedInstanceState太多,解决scheme是确定哪些数据应该保存在正确的时间。 基本上仔细看你的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); }
做了一次打击和审判,终于解决了我的问题。 将此添加到您的Activity
@Override protected void onSaveInstanceState(Bundle oldInstanceState) { super.onSaveInstanceState(oldInstanceState); oldInstanceState.clear(); }