¿De dónde viene TransactionTooLargeException si no he hecho nada malo? | de Max Kachinkin | noviembre 2022

A veces, durante las entrevistas técnicas, a los desarrolladores de Android se les hacen preguntas como cómo lanzar fragmentos, cómo pasar datos a fragmentos, por qué no hay mucho razonamiento y cuánto es «mucho», qué puede salir mal, etc. Estas son las preguntas. nos preguntamos en Dodo Engineering, por cierto, a veces también. Para ser honesto, no me di cuenta de cuánto más hay sobre este tema hasta que me topé con la excepción TransactionTooLargeException que fallaba en nuestra aplicación Drinkit. Y ahora puedo responder esas preguntas con un buen ejemplo del mundo real. Mi ejemplo es la infame TransactionTooLargeException, una excepción a las llamadas IPC (comunicación entre procesos) y los ámbitos de enlace de Android. Pero podemos ponerlo en una situación inofensiva cuando parece que no hicimos nada malo y no usamos IPC. En este artículo, solucionaremos este bloqueo y hablaremos sobre llamadas y carpetas de IPC.

Contenidos

¿Qué tienen que ver IPC y Binder con esto?

Cuando escuché las palabras IPC y Binder en el pasado, pensé que eran comunicación entre procesos, algunos casos especiales en los que colocamos parte de la aplicación en un proceso separado y comenzamos a interactuar. Por ejemplo, vincular la actividad de un proceso al servicio de otro proceso e intercambiar datos. Y esas palabras (IPC y Binder) no me interesarían hasta que empezara a hacer algo así. Pero la realidad es diferente. Si sabes de esto, hazlo bien, sigue así. Y si su conocimiento es similar al mío, entonces es hora de que descubra por qué IPC y Binder están en todas partes con nosotros todos los días. Comenzaré haciendo referencia a la documentación oficial de Android sobre TransactionTooLargeException. El artículo Parcelables and Bundles explica brevemente que las transacciones vinculantes transfieren datos a través de lotes y lotes, cada proceso tiene un búfer de 1 MB para todas las transacciones actualmente ejecutables. Si en algún momento superamos 1 MB, obtendremos una TransactionTooLargeException. Sin embargo, aún no está claro por qué recibimos una TransactionTooLargeException cuando tenemos una aplicación y un proceso simples sin vincular nuestras actividades a servicios o intercambiar datos. Para responder a esta pregunta, debemos entender la teoría.

¿Con qué frecuencia usamos aglutinantes?

Como desarrolladores, no administramos los componentes del marco de Android (actividad, servicio, receptor de transmisión, proveedor de contenido), el sistema lo hace. Como hace eso Como ejemplo, veamos cómo funciona una de las acciones más básicas en Android: iniciar una actividad. El conocido método startActivity se implementa en la clase de instrumentación ContextImpl, con la que muchos están más familiarizados al trabajar con pruebas instrumentales. Se utiliza un fragmento del archivo Instrumentation.java Instrumentation para llamar a startActivity desde el objeto ActivityManagerNative.getDefault(). Y ese no es otro que el servicio ActivityManager. Lo conseguimos a través del singleton gDefault. Un fragmento del archivo ActivityManagerNative.java. Como resultado, llamamos a transact en el objeto mRemote, que a su vez es IBinder. Esquemáticamente se puede representar de la siguiente manera:Todo lo que sucede en nuestro proceso lo he resaltado en verde. Carpeta: en azul (es como un conector aquí). El servicio del administrador de actividades: en violeta porque es un proceso separado con el que se comunica nuestro proceso. De esta forma, abrir una nueva actividad es una transacción vinculante. ¿Y qué otras acciones son transacciones de Binder? Bueno, hay muchos ejemplos: comunicarse con Messenger, trabajar con Content Provider, todos los servicios del sistema: ActivityManagerService, WindowManagerService, AlarmManagerService, NotificationManagerService, ConnectivityManagerService y otros (acabo de enumerar los que cada uno de nosotros ha encontrado muchas veces). Estos son todos los servicios que podemos recuperar a través de Context::getSystemService. Estos servicios son procesos separados y nuestra aplicación puede interactuar con ellos a través de transacciones vinculantes.

Carpeta: ¿qué es eso en realidad?

Binder es una herramienta en la plataforma Android especialmente creada para una fácil y rápida comunicación entre procesos. Hay una imagen genial en la documentación oficial al respecto.Aquí es donde reside Binder en la arquitectura de Android. Fuente: https://developer.android.com/guide/platformBinder permanece en el nivel del kernel de Linux y toda la comunicación ocurre allí. En resumen, Binder procesa transacciones y empaqueta datos de manera eficiente y los transfiere entre procesos a través de ioctl. Hablamos de estas transacciones anteriormente cuando mencionamos el límite de búfer de 1 MB. Resulta que usamos mucho aglutinantes, pero no explícitamente. Por lo general, está oculto para nosotros por la API de Android Framework. Así es como obtenemos nuestra TransactionTooLargeException: del uso implícito de transacciones de enlace.

Resolvamos el misterio de la TransactionTooLargeException

Intentemos reproducir el error en la aplicación Drinkit. La pantalla principal muestra un menú de cafetería. El menú es un buscapersonas con fragmentos. Cada fragmento es una categoría en el menú con un conjunto de elementos propios. Volteamos el menú de un lado a otro, de un lado a otro, colapsamos y… colapsamos.Localizador de la pantalla principal de la aplicación Drinkit El bloqueo ocurre principalmente en segundo plano:El punto es que cuando se minimiza la aplicación, se guarda el estado de la actividad, y esto se hace a través de transacciones vinculantes. Miremos más de cerca. Todo comienza cuando se detiene la actividad: un fragmento del archivo ActivityThread.java. La llamada handleStopActivity se realiza a través de ActivityThread y hay dos lugares importantes para buscar: performStopActivityInner ypendienteActions.setStopInfo.

  • performStopActivityInner: esto llama a onSaveInstanceState, que conocemos bien y podemos anular. Ojo, por cierto, porque aquí es donde determinas cuándo se llama: antes o después de onStop(), dependiendo de la versión de Android.
  • pendienteAcciones.setStopInfo: aquí es donde almacenamos el objeto stopInfo para las acciones pendientes. stopInfo es el objeto donde ponemos todo lo que queremos almacenar. Entre otras cosas, stopInfo es ejecutable y alguien tiene que ejecutarlo.

PendingActions se guarda más tarde cuando ActivityThread llama a reportStop, y ya en ese momento enviamos StopInfo para que se ejecute como Runnable a través del controlador .getInstance(). activityStopped es una llamada de transacción de enlace a través de ActivityManagerService, similar a lo que ejecutamos antes de analizar las actividades. Lo extraño aquí es que para la versión N a continuación, se ignoraron las excepciones TransactionTooLargeException (tiempos sin preocupaciones). Volvamos a nuestra aplicación. Imaginemos que no se guarda nada en onSaveInstanceState. Entonces, ¿por qué hay desbordamientos de búfer? Necesitamos ver qué es exactamente lo que se está guardando. Un fragmento del archivo Activity.java Se guardan todos los estados de todos los fragmentos. El objeto mFragments es un FragmentController y, en este caso, reenvía la llamada a FragmentManager. Veamos qué es saveAllState: un fragmento del archivo FragmentManager.java Revisamos todos los fragmentos activos y guardamos sus estados en la matriz activa. Y cada estado contiene argumentos. Un fragmento del archivo FragmentState.java. Así que teníamos un localizador con fragmentos. Pasamos el buscapersonas, minimizamos la aplicación e intentamos guardar todos los argumentos de todos los fragmentos con la transacción del enlazador. Hay una herramienta útil para realizar un seguimiento de lo que sucede en las transacciones de Binder: toolargetool La agregamos para ver lo que estaba almacenado allí. Y vimos el siguiente registro: D/TooLargeTool: NavHostFragment.onSaveInstanceState escribió: Bundle211562248 contiene 7 claves y mide 510,5 KB cuando se serializa como un paquete* android:support:fragments = 508,7 KB* androidx.lifecycle .BundlableSavedStateRegistry.key = 0,1KB* android-support-nav:fragment:defaultHost = 0.1KB* android:view_registry_state = 0.2KB* android-support-nav:fragment:graphId = 0.1KB* android -support-nav:fragment:navControllerY aquí vemos que los fragmentos están ocupados más de 500 KB. Ahora vemos por qué, en lugar de pasar objetos grandes como argumentos, se recomienda enfáticamente usar el mínimo requerido, generalmente ID. En este caso, cada fragmento se hace cargo de los datos necesarios. Esa es la respuesta a la pregunta del título del artículo. Por lo general, los desarrolladores somos conscientes de este enfoque y respondemos con confianza a la pregunta en las entrevistas, mientras que generalmente estamos lejos de saber la verdadera razón detrás de esto. Después de una refactorización rápida, medimos nuevamente cuánta memoria consumen estas transacciones: D/TooLargeTool: NavHostFragment .onSaveInstanceState escribió: Bundle78254359 contiene 7 claves y mide 67,8 KB cuando se serializa como un paquete* android:support:fragments = 65,9 KB* androidx.lifecycle .BundlableSavedStateRegistry.key = 0,1 KB* android-support-nav:fragment:defaultHost = 0,1 KB * android:view_registry_state = 0,2 KB* android-support-nav:fragment: graphId = 0,1 KB* android-support-nav:fragment:navControllerState = 1,2 KB* android:view_state = 0,1 KBEl tamaño se ha reducido a 67,8 KB A veces, cuando tenemos prisa, hacemos una deuda técnica y luego nos olvidamos de pagarla a tiempo. Este fue el caso de un error bastante trivial en la aplicación Drinkit. Pasamos objetos de gran tamaño a los argumentos del fragmento para mostrar el menú. Quería compartir esta experiencia porque a simple vista este bicho es sencillo, pero si indagas un poco verás que sus raíces son mucho más profundas. Tropezar con esto puede llevarlo por la madriguera del conejo al mundo de la interacción de IPC y las transacciones de Binder. Comparta en los comentarios si alguna vez se ha enfrentado a bloqueos de TransactionTooLargeException y lo que está relacionado. Si te ha gustado este artículo, suscríbete y sígueme en twitter: https://twitter.com/makzimi

Deja una respuesta

Tu dirección de correo electrónico no será publicada.