Android.os.TransactionTooLargeException on Nougat

Я обновил Nexus 5X до Android N, и теперь, когда я устанавливаю приложение (отладка или выпуск), я получаю TransactionTooLargeException на каждом экране, который имеет Bundle в дополнительных настройках. Приложение работает на всех других устройствах. Старое приложение, которое находится в 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, а не в вещах, которые были отправлены в следующую активность. Я удалил все сэвы, где я не могу контролировать размер объектов (сетевые ответы), и теперь он работает.

Обновить:

Чтобы сохранить большие куски данных, Google предлагает сделать это с помощью фрагмента, который сохраняет экземпляр. Идея состоит в том, чтобы создать пустой фрагмент без представления со всеми необходимыми полями, которые в противном случае были бы сохранены в Bundle. Добавить setRetainInstance(true); К методу onCreate из фрагмента. А затем сохраните данные в Fragment на 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()); } } 

И пример фрагмента:

 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 месяца, и мы, наконец, решили проблему!

Что происходит, мы использовали FragmentStatePagerAdapter в ViewPager. Пользователь будет просматривать страницы и создавать более 100 фрагментов (это приложение для чтения).

Хотя мы правильно управляем фрагментами в destroyItem (), в реализации Androids для FragmentStatePagerAdapter появляется ошибка, в которой содержится ссылка на следующий список:

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

И когда FragmentStatePagerAdapter Android попытается сохранить состояние, он вызовет функцию

 @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 для каждого отдельного фрагмента, когда-либо созданного. TransactionTooLargeException произойдет, когда этот массив будет сброшен в parcelableArray, и OS не понравится более 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; } 

Всякий раз, когда вы видите TransactionTooLargeException происходящее, когда Activity находится в процессе остановки, это означает, что Activity пыталась отправить сохраненные состояния Bundles в системную ОС для безопасного сохранения для восстановления позже (после изменения конфигурации или смерти процесса), но тот Или больше из Bundles он отправил, были слишком большими. Максимальный предел около 1 МБ для всех таких транзакций происходит сразу, и этот предел может быть достигнут, даже если ни один Bundle превышает этот предел.

Главный виновник здесь обычно сохраняет слишком много данных внутри onSaveInstanceState либо Activity либо любых Fragments размещенных в Activity . Как правило, это происходит при сохранении чего-то особо большого, такого как Bitmap но оно также может возникать при отправке больших объемов меньших данных, таких как списки объектов Parcelable . Команда Android неоднократно onSavedInstanceState в onSavedInstanceState должны сохраняться только небольшие объемы связанных с просмотром данных. Тем не менее, разработчики часто сохраняли страницы сетевых данных, чтобы изменения конфигурации выглядели как можно более гладкими, так как не нужно было повторно получать одни и те же данные. Начиная с Google I / O 2017, команда Android четко указала, что предпочтительная архитектура приложения для Android сохраняет сетевые данные

  • В памяти, поэтому его можно легко повторно использовать при изменении конфигурации
  • На диск, чтобы его можно было легко восстановить после завершения процесса и сеансов приложений

Их новая инфраструктура ViewModel и библиотека сохранения пространства предназначены для того, чтобы помочь разработчикам соответствовать этому шаблону. Если ваша проблема onSaveInstanceState с сохранением слишком большого количества данных в onSaveInstanceState , обновление этой архитектуры, используемой этими инструментами, должно устранить вашу проблему.

Лично, прежде чем обновляться до этого нового шаблона, я бы хотел взять мои существующие приложения и просто обойти TransactionTooLargeException . Я написал быструю библиотеку, чтобы сделать именно это: https://github.com/livefront/bridge . Он использует те же общие идеи о восстановлении состояния из памяти при изменении конфигурации и с диска после смерти процесса, вместо того, чтобы отправлять все это состояние в ОС через onSaveInstanceState , но требует очень минимальных изменений в вашем существующем коде для использования. Любая стратегия, которая соответствует этим двум целям, должна помочь вам избежать исключения, не жертвуя способностью сохранять состояние.

В заключение отметим: единственная причина, по которой вы видите это в Nougat +, – это то, что изначально, если предел пересылки связующего был превышен, процесс отправки сохраненного состояния в ОС будет терпеть неудачу без появления этой ошибки в Logcat:

!!! НЕИСПРАВНОСТЬ СДЕЛКА СДЕЛКИ !!!

В Нуге, этот тихий провал был повышен до тяжелой аварии. К их чести, это то, что команда разработчиков документировала в примечаниях к выпуску для Nougat :

Многие API-интерфейсы платформ теперь начали проверять, что большие транзакции передаются по транзакциям Binder, и теперь система обновляет TransactionTooLargeExceptions как RuntimeExceptions, вместо того, чтобы тихо регистрировать или подавлять их. Одним из распространенных примеров является хранение слишком большого количества данных в Activity.onSaveInstanceState (), что заставляет ActivityThread.StopInfo вызывать исключение RuntimeException, когда ваше приложение нацелено на Android 7.0.

Я сталкиваюсь с подобной проблемой. Проблема и сценарий немного отличаются, и я исправляю это следующим образом. Проверьте сценарий и решение.

Сценарий: у меня возникла странная ошибка у клиента на устройстве 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(); } } 

Если стек превысит лимит, он автоматически пойдет в исходный фрагмент. Надеюсь, кто-то поможет в этом ответе, потому что журналы отслеживания исключений и стека одинаковы. Поэтому всякий раз, когда возникает эта проблема, проверьте счетчик обратного стека, если вы используете фрагменты и задний стек.

Я тоже сталкиваюсь с этой проблемой на своих устройствах Nougat. Мое приложение использует фрагмент с пейджером вида, который содержит 4 фрагмента. Я передал некоторые большие аргументы конструкции 4 фрагментам, которые вызвали проблему.

Я проследил размер Bundle вызывающий это с помощью TooLargeTool .

Наконец, я решил его использовать putSerializable на объекте POJO, который реализует Serializable вместо передачи большой необработанной String используя putString во время инициализации фрагмента. Это уменьшает размер пакета в два раза и не бросает TransactionTooLargeException . Поэтому, пожалуйста, убедитесь, что вы не передаете аргументы огромного размера в Fragment .

Проблема с PS связана с отслеживанием проблем Google: https://issuetracker.google.com/issues/37103380

Я столкнулся с той же проблемой. Мое обходное решение выгружает saveInstanceState в файлы в директории кеша.

Я сделал следующий класс утилиты.

 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 не следует использовать для постоянных. Но у меня нет другой идеи.

Просто переопределите этот метод в своей деятельности:

 @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 для получения дополнительной информации.

Поскольку 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; } } } 

Проблема в моем приложении заключалась в том, что я пытался слишком много сэкономить на 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); }