La renderización de la IU consiste en generar un fotograma desde tu app y mostrarlo en la pantalla. A fin de garantizar una interacción continua del usuario, tu app debe renderizar los fotogramas en menos de 16 ms para lograr una velocidad de 60 fotogramas por segundo (¿Por qué 60 FPS?). Si la renderización de IU de tu app es lenta, el sistema se verá obligado a omitir fotogramas, y el usuario notará un salto en tu app. Esto se denomina bloqueo.
Para ayudarte a mejorar la calidad de tu app, Android la supervisa automáticamente en busca de bloqueos y muestra la información en el panel de Android vitals. Para obtener información sobre cómo se recopilan estos datos, consulta la documentación de Play Console.
Si se bloquea tu app, podrás encontrar instrucciones para diagnosticar el problema y corregirlo en esta página.
Cómo identificar un bloqueo
Identificar el código que ocasiona el bloqueo en tu app puede resultar difícil. En esta sección, se describen tres métodos para identificar un bloqueo:
La inspección visual te permite ejecutar con rapidez todos los casos prácticos en tu app en solo unos minutos, pero no proporciona tantos detalles como Systrace. Systrace ofrece más nivel de detalle, pero si lo ejecutaras en todos los casos prácticos de tu app, arrojaría tantos datos que resultaría difícil analizarlos. Tanto la inspección visual como Systrace detectan bloqueos en tu dispositivo local. Sin embargo, si no se puede reproducir el bloqueo en dispositivos locales, puedes compilar una supervisión de rendimiento personalizada para medir partes específicas de tu app en los dispositivos que se ejecutan en el campo.
Inspección visual
La inspección visual te ayuda a identificar los casos prácticos que producen bloqueos. Para llevarla a cabo, abre tu app y revisa sus distintas partes de forma manual. Luego, busca la IU que genera el bloqueo. A continuación, se incluyen algunas sugerencias para realizar inspecciones visuales:
- Ejecuta una versión de lanzamiento de tu app (o, al menos, una no depurable). El tiempo de ejecución de ART inhabilita varias optimizaciones importantes para admitir funciones de depuración. Por lo tanto, asegúrate de obtener una vista similar a la que tendrá un usuario.
- Habilita Profile GPU Rendering. Profile GPU Rendering muestra barras en la pantalla que ofrecen una representación visual rápida de cuánto tiempo tardan en renderizarse los fotogramas de una ventana de IU en relación con la comparativa de 16 ms por fotograma. Cada barra contiene componentes de colores que corresponden a una etapa en la canalización de renderización para que puedas ver qué sección tarda más tiempo. Por ejemplo, si un fotograma requiere mucho tiempo para procesar una entrada, debes revisar el código de tu app que procesa las entradas del usuario.
- Hay ciertos componentes, como
RecyclerView
, que son una fuente común de bloqueos. Si tu app usa dichos componentes, te recomendamos que los ejecutes para verificar si producen bloqueos. - A menudo, se puede reproducir el bloqueo solo durante el inicio en frío de la app.
- Intenta ejecutar tu app en un dispositivo más lento para exacerbar el problema.
Cuando hayas encontrado los casos prácticos que producen el bloqueo, podrás tener una buena noción de lo que lo ocasiona. Sin embargo, si necesitas más información, puedes usar Systrace para obtener aún más detalles.
Systrace
Si bien Systrace es una herramienta que muestra el funcionamiento general del dispositivo, también sirve para identificar bloqueos en tu app. Systrace genera una sobrecarga mínima del sistema, por lo que podrás observar un nivel de bloqueo realista durante la instrumentación.
Elabora un seguimiento con Systrace mientras ejecutas el caso de uso que genera el bloqueo en tu dispositivo. Consulta la Explicación de Systrace para obtener instrucciones sobre cómo usarlo. Systrace está compuesto por procesos y subprocesos. En Systrace, busca el proceso de tu app, que debería ser similar al que se muestra en la Figura 1.
Figura 1: Systrace
En la Figura 1, Systrace contiene la siguiente información para identificar el bloqueo:
- Systrace muestra cuándo se obtiene cada fotograma y los codifica por colores para destacar los tiempos de renderización lenta. De esta forma, podrás encontrar los fotogramas con bloqueos con mayor exactitud que con la inspección visual. Para obtener más información, consulta Cómo inspeccionar alertas y fotogramas de IU.
- Systrace detecta problemas en tu app y muestra alertas tanto en los fotogramas individuales como en el panel de alertas. Te recomendamos que sigas las instrucciones que se incluyen en la alerta.
- Algunas partes del framework y las bibliotecas de Android, como
RecyclerView
, contienen marcadores de seguimiento. Por lo tanto, en el cronograma de Systrace se muestra cuándo se ejecutan esos métodos en el subproceso de IU y cuánto tardan.
Cuando analices el resultado de Systrace, es posible que encuentres métodos en tu app que creas que pueden generar el bloqueo. Por ejemplo, si en el cronograma se observa que RecyclerView
genera un fotograma lento, puedes agregar marcadores de seguimiento al código en cuestión y, luego, volver a ejecutar Systrace para obtener más información. En el nuevo registro de Systrace, el cronograma mostrará cuándo se llaman a los métodos de tu app y cuánto tardan en ejecutarse.
Si Systrace no muestra detalles que expliquen el retraso en el trabajo del subproceso de IU, usa el Generador de perfiles de CPU de Android a fin de registrar un seguimiento de método muestreado o instrumentado. En general, los seguimientos de método no son útiles para identificar bloqueos porque producen falsos positivos debido a la sobrecarga pesada y, además, no permiten ver cuándo los subprocesos están en ejecución y cuándo están bloqueados. Sin embargo, ayudan a identificar los métodos de tu app que tardan más tiempo. Una vez que los hayas identificado, agrega marcadores de seguimiento y vuelve a ejecutar Systrace para determinar si esos métodos son los responsables del bloqueo.
Para obtener más información, consulta Información sobre Systrace.
Supervisión de rendimiento personalizada
Si no puedes reproducir el bloqueo en un dispositivo local, puedes compilar una supervisión de rendimiento personalizada en tu app para identificar el origen del bloqueo en los dispositivos en el campo.
Para ello, recopila los tiempos de renderización de los fotogramas de partes específicas de tu app con FrameMetricsAggregator
. Luego, registra y analiza los datos con Firebase Performance Monitoring.
Para obtener más información, consulta Cómo usar Firebase Performance Monitoring con Android vitals.
Cómo corregir los bloqueos
Para corregir un bloqueo, inspecciona qué fotogramas superan el tiempo de 16.7 ms y, luego, busca el problema. ¿Record View#draw tarda demasiado en algunos fotogramas? ¿O, quizás, Layout? Consulta la sección Fuentes comunes de bloqueos, que aparece a continuación, para obtener información sobre estos problemas y otros más.
Para evitar los bloqueos, las tareas prolongadas deben ejecutarse de forma asíncrona fuera del subproceso de IU. Siempre debes saber en qué subproceso se ejecuta tu código y debes tomar precauciones cuando publiques tareas importantes en el subproceso principal.
Si tienes una IU principal importante y compleja para tu app (como una lista de desplazamiento central), te recomendamos que escribas pruebas de instrumentación que puedan detectar automáticamente tiempos de renderización lenta y ejecutarlas con frecuencia a fin de evitar regresiones. Para obtener más información, consulta el Codelab de pruebas automatizadas de rendimiento.
Fuentes comunes de bloqueos
En las siguientes secciones, se explican las fuentes comunes de bloqueos en las apps y las prácticas recomendadas para corregirlas.
Listas de desplazamiento
Por lo general, se usa ListView
y, en especial, RecyclerView
para las listas de desplazamiento complejas que son más proclives a los bloqueos. Contienen marcadores de Systrace que permiten determinar si contribuyen al bloqueo en tu app. Asegúrate de pasar el argumento de línea de comandos -a
<your-package-name>
para mostrar las secciones de seguimiento en RecyclerView (así como cualquier marcador de seguimiento que hayas agregado). Si hubiere, sigue las instrucciones de las alertas que genera Systrace. En Systrace, puedes seleccionar las secciones analizadas de RecyclerView para ver una explicación del trabajo que realiza.
RecyclerView: notifyDataSetChanged
Si observas que en un fotograma rebotan todos los elementos de RecyclerView
(y, por lo tanto, se vuelven a transmitir y a recolectar), asegúrate de no estar llamando a notifyDataSetChanged()
, setAdapter(Adapter)
ni swapAdapter(Adapter, boolean)
para pequeñas actualizaciones. Estos métodos indican que cambió todo el contenido de la lista y aparecerán en Systrace como RV FullInvalidate. En su lugar, usa SortedList
o DiffUtil
para generar actualizaciones mínimas cuando se cambia o se agrega contenido.
Por ejemplo, piensa en una app que recibe una versión nueva de una lista de contenido de noticias del servidor. Cuando publicas esa información en el adaptador, es posible llamar a notifyDataSetChanged()
, como se muestra a continuación:
Kotlin
fun onNewDataArrived(news: List<News>) { myAdapter.news = news myAdapter.notifyDataSetChanged() }
Java
void onNewDataArrived(List<News> news) { myAdapter.setNews(news); myAdapter.notifyDataSetChanged(); }
Sin embargo, esto genera un gran inconveniente: si se trata de un cambio sencillo (como agregar un solo elemento a la parte superior), RecyclerView
no está al tanto; y se le solicita que libere su estado de elemento en caché y, por lo tanto, debe volver a vincular todo.
Es preferible usar DiffUtil
, que calculará y realizará actualizaciones mínimas automáticamente.
Kotlin
fun onNewDataArrived(news: List<News>) { val oldNews = myAdapter.items val result = DiffUtil.calculateDiff(MyCallback(oldNews, news)) myAdapter.news = news result.dispatchUpdatesTo(myAdapter) }
Java
void onNewDataArrived(List<News> news) { List<News> oldNews = myAdapter.getItems(); DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldNews, news)); myAdapter.setNews(news); result.dispatchUpdatesTo(myAdapter); }
Solo define tu MyCallback como una implementación de DiffUtil.Callback
para informar a DiffUtil
cómo inspeccionar tus listas.
RecyclerView: RecyclerViews anidados
Es común anidar RecyclerView
, en especial con una lista vertical de listas de desplazamiento horizontal (como cuadrículas de apps en la página principal de Play Store). Esto puede resultar muy útil, pero también implica el movimiento de muchas vistas. Si observas que aumentan muchos elementos internos cuando te desplazas hacia abajo en la página por primera vez, verifica si estás compartiendo RecyclerView.RecycledViewPool
entre RecyclerViews internas (horizontales). De forma predeterminada, cada RecyclerView tendrá su propio grupo de elementos. Sin embargo, si hay una docena de itemViews
en la pantalla a la vez, se genera un problema cuando no se puede compartir itemViews
por las distintas listas horizontales si todas las filas muestran tipos de vistas similares.
Kotlin
class OuterAdapter : RecyclerView.Adapter<OuterAdapter.ViewHolder>() { ... override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { // inflate inner item, find innerRecyclerView by ID… val innerLLM = LinearLayoutManager(parent.context, LinearLayoutManager.HORIZONTAL, false) innerRv.apply { layoutManager = innerLLM recycledViewPool = sharedPool } return OuterAdapter.ViewHolder(innerRv) } ...
Java
class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> { RecyclerView.RecycledViewPool sharedPool = new RecyclerView.RecycledViewPool(); ... @Override public void onCreateViewHolder(ViewGroup parent, int viewType) { // inflate inner item, find innerRecyclerView by ID… LinearLayoutManager innerLLM = new LinearLayoutManager(parent.getContext(), LinearLayoutManager.HORIZONTAL); innerRv.setLayoutManager(innerLLM); innerRv.setRecycledViewPool(sharedPool); return new OuterAdapter.ViewHolder(innerRv); } ...
Si deseas lograr una mayor optimización, también puedes llamar a setInitialPrefetchItemCount(int)
en el objeto LinearLayoutManager
del RecyclerView interno. Por ejemplo, si siempre tendrás 3.5 elementos visibles en una fila, llama a innerLLM.setInitialItemPrefetchCount(4);
. Esto le indicará a RecyclerView que cuando una fila horizontal está a punto de aparecer en la pantalla, debe intentar cargar previamente los elementos en el interior si hay tiempo libre en el subproceso de IU.
RecyclerView: Se produjo un aumento excesivo o el proceso de creación tarda demasiado
En la mayoría de los casos, la función de carga previa de RecyclerView
debería ayudar a solucionar el costo de aumento realizando el trabajo con anticipación, mientras el subproceso de IU está inactivo. Si adviertes un aumento durante un fotograma (y no en una sección con la etiqueta RV Prefetch), asegúrate de estar realizando la prueba en un dispositivo reciente (por el momento, la carga previa solo es compatible con Android 5.0, nivel de API 21 y versiones posteriores) y de estar usando una versión reciente de la biblioteca de compatibilidad.
Si observas con frecuencia que el aumento ocasiona bloqueos a medida que aparecen elementos nuevos en la pantalla, verifica que no tengas más tipos de vistas de los necesarios. Cuantos menos tipos de vistas haya en el contenido de un RecyclerView, se deberán realizar menos aumentos cuando aparezcan nuevos tipos de elementos en la pantalla. Combina los tipos de vistas cuando sea razonable. Si solo cambia un ícono, un color o un fragmento del texto entre los tipos, puedes realizar ese cambio durante la vinculación y evitar el aumento (lo que, al mismo tiempo, reduce el uso de memoria de tu app).
Si tus tipos de vistas no presentan inconvenientes, reduce el costo del aumento.
Puedes reducir las vistas estructuradas y de contenedores que no sean necesarias. Además, puedes compilar itemViews
con ConstraintLayout a fin de reducir con facilidad las vistas estructuradas. Si realmente deseas optimizar el rendimiento, las jerarquías de elementos deben ser simples, no debes incluir funciones complejas de tema y estilo, y debes intentar llamar a los constructores. Sin embargo, ten en cuenta que no siempre vale la pena perder la simplicidad y las funciones de XML.
RecyclerView: La vinculación tarda demasiado
La vinculación (es decir, onBindViewHolder(VH, int)
) debe ser un proceso muy simple y tardar mucho menos de un milisegundo para todos los elementos, excepto los más complejos. Solo debe tomar los elementos POJO de los datos de elementos internos de tu adaptador y llamar a los métodos set en las vistas de ViewHolder. Si RV OnBindView tarda mucho, verifica si estás haciendo trabajo mínimo en tu código de vinculación.
Si utilizas objetos POJO simples para retener datos en tu adaptador, puedes evitar escribir el código de vinculación en onBindViewHolder con la biblioteca de Data Binding.
RecyclerView o ListView: El diseño o el dibujo tarda demasiado
Para obtener información sobre problemas relacionados con el dibujo y el diseño, consulta las secciones sobre el Rendimiento del diseño y el Rendimiento de la renderización.
ListView: Aumento
Si no tienes cuidado, podrías inhabilitar el reciclaje por error en ListView
. Si observas un aumento cada vez que aparece un elemento en la pantalla, verifica que tu implementación de Adapter.getView()
esté usando el parámetro convertView
, lo vuelva a vincular y lo muestre. Si tu implementación de getView()
aumenta siempre, tu app no obtendrá los beneficios de reciclar en ListView. La estructura de tu getView()
casi siempre debe ser similar a la implementación que se muestra a continuación:
Kotlin
fun getView(position: Int, convertView: View?, parent: ViewGroup): View { return (convertView ?: layoutInflater.inflate(R.layout.my_layout, parent, false)).apply { // … bind content from position to convertView … } }
Java
View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { // only inflate if no convertView passed convertView = layoutInflater.inflate(R.layout.my_layout, parent, false) } // … bind content from position to convertView … return convertView; }
Rendimiento del diseño
Si Systrace muestra que el segmento Layout de Choreographer#doFrame realiza demasiados trabajos o los hace con demasiada frecuencia, significa que hay problemas de rendimiento del diseño. El rendimiento del diseño de tu app depende de qué parte de la jerarquía de vista tiene entradas o parámetros de diseño que cambian.
Rendimiento del diseño: Costo
Si los segmentos tardan más de unos milisegundos, es posible que se esté alcanzando el peor rendimiento de anidamiento para RelativeLayouts o weighted-LinearLayouts. Cada uno de estos diseños puede activar varios pases de medición/diseño de sus elementos secundarios, por lo que anidarlos puede generar un comportamiento O(n^2) en la profundidad de anidación. Intenta evitar RelativeLayout o la función de peso de LinearLayout en todos los nodos de la jerarquía, excepto en los más bajos. Estas son algunas maneras de hacerlo:
- Puedes reorganizar tus vistas estructuradas.
- Puedes definir una lógica de diseño personalizada. Consulta Cómo optimizar jerarquías de diseño para ver un ejemplo específico. Puedes realizar una conversión a ConstraintLayout, que proporciona funciones similares, pero sin los inconvenientes de rendimiento.
Rendimiento del diseño: Frecuencia
El diseño se produce cuando aparece contenido nuevo en la pantalla, por ejemplo, cuando se visualiza un elemento nuevo en RecyclerView
. Si produces un diseño significativo en cada fotograma, es posible que estés realizando animaciones, lo que puede provocar una disminución de los fotogramas. Por lo general, se deben ejecutar las animaciones en las propiedades de dibujo de View
(p. ej., setTranslationX/Y/Z()
, setRotation()
, setAlpha()
, etc.). Cambiarlas resulta mucho más económico que las propiedades de diseño (como el relleno o los márgenes). Además, es mucho más económico modificar las propiedades de dibujo de una vista llamando a un método set que active invalidate()
y, luego, draw(Canvas)
en el siguiente fotograma. De esta forma, se volverán a registrar las operaciones de dibujo para la vista no válida y, en general, esto es mucho más económico que el cambio de diseño.
Rendimiento de la renderización
La IU de Android funciona en dos fases: Record View#draw en el subproceso de IU y DrawFrame en RenderThread. La primera ejecuta draw(Canvas)
en cada View
no válida y puede invocar llamadas en vistas personalizadas o en tu código.
La segunda se ejecuta en el RenderThread nativo, pero funcionará según el trabajo que genere la fase Record View#draw.
Rendimiento de la renderización: subproceso de IU
Si Record View#draw tarda mucho tiempo, a menudo, se debe a que se está pintando un mapa de bits en el subproceso de IU. Si se pinta un mapa de bits, se utiliza la renderización de CPU. Por lo tanto, debes evitarlo siempre que sea posible. Puedes utilizar el seguimiento de método con el Generador de perfiles de CPU de Android para determinar si el problema se debe a esto.
Por lo general, se pinta el mapa de bits cuando una app quiere decorarlo antes de mostrarlo. A veces, la decoración consiste en agregar esquinas redondeadas:
Kotlin
val paint = Paint().apply { isAntiAlias = true } Canvas(roundedOutputBitmap).apply { // draw a round rect to define shape: drawRoundRect( 0f, 0f, roundedOutputBitmap.width.toFloat(), roundedOutputBitmap.height.toFloat(), 20f, 20f, paint ) paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY) // multiply content on top, to make it rounded drawBitmap(sourceBitmap, 0f, 0f, paint) setBitmap(null) // now roundedOutputBitmap has sourceBitmap inside, but as a circle }
Java
Canvas bitmapCanvas = new Canvas(roundedOutputBitmap); Paint paint = new Paint(); paint.setAntiAlias(true); // draw a round rect to define shape: bitmapCanvas.drawRoundRect(0, 0, roundedOutputBitmap.getWidth(), roundedOutputBitmap.getHeight(), 20, 20, paint); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)); // multiply content on top, to make it rounded bitmapCanvas.drawBitmap(sourceBitmap, 0, 0, paint); bitmapCanvas.setBitmap(null); // now roundedOutputBitmap has sourceBitmap inside, but as a circle
Si este es el tipo de trabajo que deseas realizar en el subproceso de IU, puedes hacerlo en el subproceso de decodificación en segundo plano. En algunos casos como este, incluso puedes hacer el trabajo al momento de dibujar. Por ese motivo, si tu código Drawable
o View
se ve de la siguiente manera:
Kotlin
fun setBitmap(bitmap: Bitmap) { mBitmap = bitmap invalidate() } override fun onDraw(canvas: Canvas) { canvas.drawBitmap(mBitmap, null, paint) }
Java
void setBitmap(Bitmap bitmap) { mBitmap = bitmap; invalidate(); } void onDraw(Canvas canvas) { canvas.drawBitmap(mBitmap, null, paint); }
Puedes reemplazarlo por lo siguiente:
Kotlin
fun setBitmap(bitmap: Bitmap) { shaderPaint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) invalidate() } override fun onDraw(canvas: Canvas) { canvas.drawRoundRect(0f, 0f, width, height, 20f, 20f, shaderPaint) }
Java
void setBitmap(Bitmap bitmap) { shaderPaint.setShader( new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP)); invalidate(); } void onDraw(Canvas canvas) { canvas.drawRoundRect(0, 0, width, height, 20, 20, shaderPaint); }
Ten en cuenta que también se puede realizar para la protección en segundo plano (dibujar un gradiente en la parte superior del mapa de bits) y el filtrado de imágenes (con ColorMatrixColorFilter
), que son dos operaciones comunes que se efectúan modificando el mapa de bits.
Si deseas dibujar un mapa de bits por otro motivo, posiblemente para usarlo como caché, dibuja en el lienzo acelerado por hardware que se pasa directamente a tu View o Drawable. Además, si es necesario, llama a setLayerType()
con LAYER_TYPE_HARDWARE
a fin de almacenar en caché el resultado de renderización compleja y aprovechar la renderización de GPU.
Rendimiento de la renderización: RenderThread
El registro de algunas operaciones de lienzo es económico. Sin embargo, activan procesos de cómputo costosos en RenderThread. Por lo general, Systrace las llamará con alertas.
Canvas.saveLayer()
Evita Canvas.saveLayer()
, ya que puede activar renderizaciones costosas, que no se almacenan en caché y están fuera de la pantalla en cada fotograma. Si bien se mejoró el rendimiento en Android 6.0 (cuando se realizaron optimizaciones para evitar el cambio de destino de renderización en la GPU), es conveniente evitar esta API costosa si es posible o, al menos, asegurarte de pasar Canvas.CLIP_TO_LAYER_SAVE_FLAG
(o llamar a una variante que no admita marcas).
Cómo animar rutas grandes
Cuando se llama a Canvas.drawPath()
en el lienzo acelerado por hardware pasado a Views, Android dibuja esas rutas primero en la CPU y, luego, las carga en la GPU. Si tienes rutas grandes, no las edites de fotograma a fotograma, para que puedan almacenarse en caché y, luego, dibujarse correctamente. drawPoints()
, drawLines()
y drawRect/Circle/Oval/RoundRect()
son más eficientes; por ello, es más conveniente usarlos incluso aunque termines utilizando más llamadas de dibujo.
Canvas.clipPath
clipPath(Path)
activa un comportamiento de recorte costoso y, por lo general, debe evitarse. Cuando sea posible, dibuja formas en lugar de recortarlas como no rectángulos. Funciona mejor y admite el suavizado de contorno. Por ejemplo, la siguiente llamada a clipPath
:
Kotlin
canvas.apply { save() clipPath(circlePath) drawBitmap(bitmap, 0f, 0f, paint) restore() }
Java
canvas.save(); canvas.clipPath(circlePath); canvas.drawBitmap(bitmap, 0f, 0f, paint); canvas.restore();
Se puede expresar de la siguiente manera:
Kotlin
paint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) // at draw time: canvas.drawPath(circlePath, mPaint)
Java
// one time init: paint.setShader(new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP)); // at draw time: canvas.drawPath(circlePath, mPaint);
Cargas de mapas de bits
Android muestra los mapas de bits como texturas OpenGL, y la primera vez que se muestra un mapa de bits en un fotograma, se sube a la GPU. En Systrace, puede observarse como Texture upload(id) ancho x alto. Puede tardar varios milisegundos (consulta la Figura 2), pero es necesario para mostrar la imagen con la GPU.
Si tarda mucho tiempo, primero verifica los números de ancho y alto en el seguimiento. Asegúrate de que el mapa de bits no sea mucho más grande que el área de la pantalla donde se muestra. Si es así, esto genera un uso deficiente del tiempo de carga y de la memoria. En general, las bibliotecas de carga de mapas de bits son una forma sencilla de solicitar un mapa de bits de tamaño correcto.
En Android 7.0, el código de carga de mapas de bits (que realizan las bibliotecas) puede llamar a prepareToDraw()
para activar una carga antes de que sea necesaria. De esta manera, se realiza la carga con anticipación, mientras RenderThread está inactivo. Esto se puede realizar tras la decodificación o cuando se vincula un mapa de bits a una vista, siempre que conozcas el mapa de bits. Lo ideal sería que tu biblioteca de carga de mapas de bits se encargara de hacerlo. Sin embargo, si administras una propia o si deseas asegurarte de no seleccionar cargas en dispositivos más nuevos, puedes llamar a prepareToDraw()
en tu propio código.
Figura 2: Una app tarda mucho tiempo en un fotograma que sube un mapa de bits grande. Reduce su tamaño o actívalo con anticipación cuando se decodifique con prepareToDraw()
.
Retrasos en la programación de subprocesos
El programador de subprocesos es la parte del sistema operativo Android que decide qué subprocesos del sistema se deben ejecutar, cuándo deben hacerlo y durante cuánto tiempo. A menudo, los bloqueos se producen porque el subproceso de IU de tu app se interrumpe o no se ejecuta. Systrace utiliza diferentes colores (consulta la Figura 3) para indicar cuando un subproceso está suspendido (gris), ejecutable (azul: podría ejecutarse, pero el programador no se lo indicó aún), en ejecución activa (verde) o en suspensión ininterrumpida (rojo o naranja). Esta información es muy útil para depurar problemas de bloqueos causados por retrasos en la programación de subprocesos.
Figura 3: Se destaca el período de suspensión del subproceso de IU.
A menudo, las pausas prolongadas en la ejecución de tu app se deben a las llamadas a Binder, el mecanismo de comunicación entre procesos (IPC) de Android. En las versiones recientes de Android, es una de las razones más comunes por la que el subproceso de IU deja de ejecutarse. En general, para corregir este problema, no debes invocar funciones que hagan llamadas a Binder. Si no puedes evitarlo, debes almacenar en caché el valor o pasar el trabajo a subprocesos en segundo plano. A medida que aumentan las bases de código, puedes agregar por error una llamada a Binder invocando un método de bajo nivel si no tienes cuidado. No obstante, es sencillo encontrar el error y corregirlo con el seguimiento.
Si tienes transacciones de Binder, puedes capturar sus pilas de llamadas con los siguientes comandos de adb:
$ adb shell am trace-ipc start
… use the app - scroll/animate ...
$ adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt
$ adb pull /data/local/tmp/ipc-trace.txt
A menudo, las llamadas aparentemente inocuas, como getRefreshRate()
, pueden activar transacciones de Binder y causar problemas importantes cuando se llaman con frecuencia. Si realizas un seguimiento periódico, puedes detectar estos problemas y corregirlos a medida que surgen.
Figura 4: Se muestra un subproceso de IU suspendido debido a transacciones de Binder en un lanzamiento de RV. Mantén una lógica de vinculación simple y usa
trace-ipc
para buscar y quitar las llamadas de carpetas.
Si no observas actividad de Binder, pero todavía no se ejecuta tu subproceso de IU, asegúrate de no estar esperando algún bloqueo u otra operación de otro subproceso. Por lo general, el subproceso de IU no debería tener que esperar los resultados de otros subprocesos, sino que estos deberían publicar información en él.
Asignación de objetos y recolección de elementos no utilizados
La asignación de objetos y la recolección de elementos no utilizados (GC) dejaron de ser un problema importante cuando se introdujo ART como tiempo de ejecución predeterminado en Android 5.0. Sin embargo, aún pueden generar una carga en tus subprocesos con este trabajo adicional. Se puede realizar una asignación en respuesta a un evento excepcional que no ocurre muchas veces por segundo (como cuando un usuario hace clic en un botón), pero recuerda que cada asignación tiene un costo. Si está en un bucle cerrado al que se llama con frecuencia, debes evitar la asignación para reducir la carga en la GC.
Systrace te mostrará si se está ejecutando GC con frecuencia, y Android Memory Profiler te permite ver de dónde provienen las asignaciones. Si evitas las asignaciones cuando sea posible, especialmente en los bucles cerrados, no deberías tener problemas.
Figura 5: Se muestra una recolección de elementos no usados de 94 ms en el subproceso HeapTaskDaemon.
En las versiones recientes de Android, la recolección de elementos no usados generalmente se ejecuta en un subproceso en segundo plano llamado HeapTaskDaemon. Ten en cuenta que si realizas muchas asignaciones, se consumirán más recursos de CPU en la recolección de elementos no utilizados, como se observa en la Figura 5.