Procesamiento lento

La renderización de la IU consiste en generar un fotograma desde tu app y mostrarlo en la pantalla. Para garantizar que la interacción del usuario con tu app sea fluida, esta debe renderizar los fotogramas en menos de 16 ms para lograr 60 fotogramas por segundo (FPS). Para comprender por qué se prefieren 60 FPS, consulta Android Performance Patterns: Why 60fps? Si intentas alcanzar 90 FPS, esta ventana disminuye a 11 ms y, para 120 FPS, 8 ms.

Si superas esta ventana por un 1 ms, no significa que el fotograma se vaya a mostrar 1 ms tarde, sino que Choreographer lo descartará por completo. Si tu app tiene una renderización lenta de la IU, el sistema se verá obligado a omitir fotogramas, y el usuario notará un salto en tu app. Esto se denomina bloqueo. En esta página, se muestra cómo diagnosticar y solucionar bloqueos.

Si desarrollas juegos que no usan el sistema View, omite Choreographer. En este caso, la biblioteca de Frame Pacing ayuda a los juegos de OpenGL y Vulkan a lograr una renderización y una renderización fluida, y a corregir el ritmo de los fotogramas en Android.

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. Si quieres obtener información sobre cómo se recopilan los datos, consulta Controla la calidad técnica de tu app con Android vitals.

Identifica bloqueos

Encontrar 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 todos los casos de uso en tu app en solo unos minutos, pero no proporciona tantos detalles como Systrace. Systrace ofrece más detalles, pero si ejecutas Systrace en todos los casos de uso de tu app, es posible que tengas tantos datos que resultaría difícil analizarlos. Tanto la inspección visual como Systrace detectan bloqueos en tu dispositivo local. Si no puedes 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 de uso que producen bloqueos. Para realizar una inspección visual, abre tu app, revisa sus distintas partes de forma manual y busca bloqueos en tu IU.

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.
  • Ejecuta componentes que son fuentes comunes de bloqueos, como RecyclerView.
  • Inicia la app desde un inicio en frío.
  • Ejecuta tu app en un dispositivo más lento para agravar el problema.

Cuando encuentras casos de uso que producen bloqueos, es posible que tengas una buena noción de lo que los genera. Si necesitas más información, puedes usar Systrace para analizar la causa.

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 puedes 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. Para obtener instrucciones sobre cómo usar Systrace, consulta Captura un registro del sistema en la línea de comandos. Systrace se divide en procesos y subprocesos. Busca el proceso de tu app en Systrace, que se parece al de la figura 1.

Ejemplo de Systrace
Figura 1: Ejemplo de Systrace

En la figura 1, Systrace contiene la siguiente información para identificar el bloqueo:

  1. Systrace muestra cuándo se obtiene cada fotograma y los codifica por colores para destacar los tiempos de renderización lentos. 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.
  2. Systrace detecta problemas en tu app y muestra alertas tanto en los fotogramas individuales como en el panel de alertas. Recomendamos seguir las instrucciones de la alerta.
  3. 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 eventos de seguimientos personalizados al código en cuestión y, luego, volver a ejecutar Systrace para obtener más información. En el nuevo registro de Systrace, se muestra el momento en que se llama a los métodos de tu app y el tiempo que tardan en ejecutarse.

Si Systrace no te muestra detalles sobre por qué el trabajo del subproceso de IU tarda mucho tiempo, usa el Generador de perfiles de CPU de Android para 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 gran sobrecarga 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. Después de identificar estos métodos, agrega marcadores de seguimiento y vuelve a ejecutar Systrace para comprobar si estos 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, compila 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 Primeros pasos con Performance Monitoring para Android.

Fotogramas congelados

Los fotogramas congelados son fotogramas de la IU que tardan más de 700 ms en renderizarse. Generan un problema, ya que tu app parece estar bloqueada y no responde a las entradas que realiza el usuario durante casi un segundo completo mientras se renderiza el fotograma. Recomendamos que se optimicen las apps para que rendericen un fotograma en 16 ms y así garantizar un funcionamiento correcto de la IU. Sin embargo, durante el inicio de la app o durante la transición a una pantalla diferente, es normal que el fotograma inicial tarde más de 16 ms, ya que la app debe aumentar las vistas, establecer el diseño de la pantalla y realizar el dibujo inicial desde cero. Por ello, Android realiza un seguimiento de los fotogramas congelados por separado de la renderización lenta. Ninguno de ellos debe tardar más de 700 ms en renderizarse.

Para mejorar la calidad de tu app, Android la supervisa automáticamente en busca de fotogramas congelados y muestra la información en el panel de Android vitals. Si quieres obtener información sobre cómo se recopilan los datos, consulta Controla la calidad técnica de tu app con Android vitals.

Los fotogramas congelados son una forma extrema de renderización lenta. Por lo tanto, el procedimiento para diagnosticar el problema y corregirlo es el mismo.

Seguimiento de bloqueos

Cronograma de fotogramas en Perfetto puede ayudar a hacer un seguimiento lento o fotogramas congelados.

Relación entre los fotogramas lentos, los fotogramas congelados y los errores de ANR

Los fotogramas lentos, los congelados y los errores de ANR son formas diferentes de bloqueos que puede generar la app. Consulta la siguiente tabla para comprender la diferencia.

Fotogramas lentos Fotogramas congelados Errores de ANR
Tiempo de renderización Entre 16 ms y 700 ms Entre 700 ms y 5 s Mayor a 5 s
Área de impacto del usuario visible
  • El desplazamiento de RecyclerView se comporta de manera abrupta.
  • En pantallas con animaciones complejas que no se animan de forma correcta.
  • Durante el inicio de la app.
  • Cuando se pasa de una pantalla a otra (por ejemplo, en cambios de pantalla).
  • No se produce una respuesta a un evento de entrada (como tocar la pantalla o presionar las teclas) o a BroadcastReceiver después de cinco segundos mientras tu actividad está en primer plano.
  • No terminó de ejecutarse tu componente BroadcastReceiver después de un tiempo considerable mientras no tienes una actividad en primer plano.

Cómo hacer un seguimiento de los fotogramas lentos y los congelados de manera independiente

Durante el inicio de la app o durante la transición a una pantalla diferente, es normal que el fotograma inicial tarde más de 16 ms, ya que la app debe aumentar las vistas, establecer el diseño de la pantalla y realizar el dibujo inicial desde cero.

Prácticas recomendadas para priorizar y resolver los bloqueos

Ten en cuenta las siguientes prácticas recomendadas para resolver los bloqueos en tu app:

  • Identifica y resuelve las instancias de bloqueo más fáciles de reproducir.
  • Prioriza los errores de ANR. Si bien los fotogramas lentos o congelados pueden hacer que una app resulte lenta, los errores de ANR provocan que la app deje de responder.
  • La renderización lenta es difícil de reproducir, pero puedes comenzar por finalizar los fotogramas congelados de 700 ms. Esto es más frecuente cuando la app se inicia o cambia de pantalla.

Cómo corregir los bloqueos

Para solucionar un bloqueo, inspecciona qué fotogramas no se completan en 16 ms y busca el problema. Verifica si Record View#draw o Layout tardan demasiado en algunos fotogramas. Consulta Fuentes comunes de bloqueos para ver estos problemas y otros.

Para evitar los bloqueos, ejecuta tareas de larga duración 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 para evitar regresiones.

Fuentes comunes de bloqueos

En las siguientes secciones, se explican las fuentes comunes de bloqueos en las apps que usan el sistema View y las prácticas recomendadas para solucionarlos. Si quieres obtener información para solucionar problemas de rendimiento de Jetpack Compose, consulta Rendimiento de Jetpack Compose.

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. Ambos contienen marcadores de Systrace que permiten determinar si contribuyen al bloqueo en tu app. Pasa el argumento de línea de comandos -a <your-package-name> para mostrar las secciones de seguimiento en RecyclerView, además de los marcadores de seguimiento que agregaste. Si hubiere, sigue las instrucciones de las alertas que genera Systrace. Dentro de Systrace, puedes hacer clic en las secciones con seguimiento de RecyclerView para ver una explicación del trabajo que realiza RecyclerView.

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 hay cambios en todo el contenido de la lista y aparecen 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 esta información en el adaptador, es posible llamar a notifyDataSetChanged(), como se muestra en el siguiente ejemplo:

Kotlin

fun onNewDataArrived(news: List<News>) {
    myAdapter.news = news
    myAdapter.notifyDataSetChanged()
}

Java

void onNewDataArrived(List<News> news) {
    myAdapter.setNews(news);
    myAdapter.notifyDataSetChanged();
}

La desventaja de esto es que, si hay un cambio trivial, como un solo elemento agregado en la parte superior, RecyclerView no sabrá sobre él. Por lo tanto, se le solicita que descarte todo el estado del elemento almacenado en caché y, entonces, se debe volver a vincular todo.

Te recomendamos que uses DiffUtil, que calcula y envía actualizaciones mínimas por ti:

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

Para informar a DiffUtil cómo inspeccionar tus listas, define tu MyCallback como una implementación de Callback.

RecyclerView: RecyclerViews anidadas

Es común anidar varias instancias de RecyclerView, especialmente con una lista vertical de listas de desplazamiento horizontal. Un ejemplo de esto son las 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 instancias internas (horizontales) de RecyclerView. De forma predeterminada, cada RecyclerView tiene su propio grupo de elementos. Sin embargo, si hay una docena de itemViews en la pantalla al mismo tiempo, resulta problemático cuando itemViews no se puede compartir con 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 optimizar mucho más, también puedes llamar a setInitialPrefetchItemCount(int) en el objeto LinearLayoutManager del RecyclerView interno. Por ejemplo, si siempre tienes 3.5 elementos visibles en una fila, llama a innerLLM.setInitialItemPrefetchCount(4). Esto le indica 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 está tardando demasiado

En la mayoría de los casos, la función de carga previa en RecyclerView puede ayudar a solucionar el costo de aumento realizando el trabajo con anticipación mientras el subproceso de IU está inactivo. Si observas un aumento durante un fotograma y no en una sección etiquetada como RV Prefetch, asegúrate de estar realizando la prueba en un dispositivo compatible y de usar una versión reciente de la biblioteca de compatibilidad. La carga previa solo es compatible con el nivel de API 21 y versiones posteriores de Android 5.0.

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, menor será el aumento que se deberá realizar cuando aparezcan nuevos tipos de elementos en la pantalla. Si es posible, combina los tipos de vistas cuando sea razonable. Si solo cambia un ícono, un color o una porción de texto entre los tipos, puedes realizar ese cambio durante la vinculación y evitar el aumento, lo que reduce el espacio en memoria de tu app al mismo tiempo.

Si tus tipos de vistas no presentan inconvenientes, reduce el costo del aumento. Reducir las vistas estructurales y de contenedores innecesarias puede ayudar. Procura crear itemViews con ConstraintLayout, que puede ayudar a reducir las vistas estructurales.

Si deseas optimizar mucho más el rendimiento y las jerarquías de elementos son simples y no necesitas funciones de temas y estilo complejas, considera llamar a los constructores por tu cuenta. Sin embargo, a menudo no 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 directa y tardar mucho menos de un milisegundo para todo, excepto los elementos más complejos. Debe tomar elementos de objetos Java antiguos sin formato (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 vinculación de datos.

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 Rendimiento del diseño y 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 devuelva. Si tu implementación de getView() siempre aumenta, tu app no obtiene los beneficios del reciclaje en ListView. La estructura de tu getView() casi siempre debe ser similar a la siguiente implementació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 funciona demasiado o 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 y 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. Puedes hacerlo de las siguientes maneras:

  • Reorganiza tus vistas estructurales.
  • Define 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.

En general, las animaciones deben ejecutarse en propiedades de dibujo de View, como las siguientes:

Puedes cambiar todos estos elementos mucho más económicos que las propiedades de diseño, como el relleno o los márgenes. En general, también es mucho más económico cambiar las propiedades de dibujo de una vista llamando a un método set que active un invalidate() seguido de 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, que ejecuta draw(Canvas) en cada vista invalidada y puede invocar llamadas en vistas personalizadas o en tu código.
  • DrawFrame en el RenderThread, que se ejecuta en el RenderThread nativo, pero funciona según el trabajo que genera la fase Record View#draw.

Rendimiento de la renderización: Subproceso de IU

Si Record View#draw tarda mucho tiempo, es común que se esté pintando un mapa de bits en el subproceso de IU. Cuando se pinta un mapa de bits, se utiliza la renderización de CPU. Por lo general, 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, como agregar esquinas redondeadas):

Kotlin

val paint = Paint().apply {
    isAntiAlias = true
}
Canvas(roundedOutputBitmap).apply {
    // Draw a round rect to define the 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 the 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 en el ejemplo anterior, incluso puedes hacer el trabajo al momento de dibujar. Por lo tanto, 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);
}

También puedes hacerlo para la protección en segundo plano, como cuando se dibuja un gradiente en la parte superior del mapa de bits y el filtrado de imágenes con ColorMatrixColorFilter, otras dos operaciones comunes para modificar los mapas de bits.

Si deseas dibujar un mapa de bits por otro motivo (posiblemente para usarlo como caché), intenta dibujar en el Canvas acelerado por hardware que se pasa directamente a View o Drawable. Si es necesario, también considera llamar a setLayerType() con LAYER_TYPE_HARDWARE para almacenar en caché el resultado de renderización compleja y aprovechar de todos modos la renderización de GPU.

Rendimiento de la renderización: RenderThread

El registro de algunas operaciones de Canvas es económico, pero activa cálculos costosos en RenderThread. Por lo general, Systrace las llama con alertas.

Cómo animar rutas grandes

Cuando se llama a Canvas.drawPath() en el Canvas acelerado por hardware pasado a View, Android dibuja esas rutas primero en la CPU y, luego, las sube a 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 y mejores, incluso si usas 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 se puede expresar de manera diferente:

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();

En su lugar, expresa el ejemplo anterior 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) width x height. Puede tardar varios milisegundos, como se muestra en 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 suelen realizar 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. Puedes hacerlo después de la decodificación o cuando vinculas 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.

Una app tarda mucho tiempo en un fotograma que sube un mapa de bits grande
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 lo decodifiques 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 se ejecutan 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 usa diferentes colores, como se muestra en la figura 3, para indicar cuándo un subproceso está suspendido (gris), ejecutable (azul: puede ejecutarse, pero el programador no lo seleccionó para que se ejecute aún), activo en ejecución (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.

Destaca un período en el que el subproceso de IU está suspendido
Figura 3: Destaca un período en el que el subproceso de IU está suspendido

A menudo, las llamadas a Binder, el mecanismo de comunicación entre procesos (IPC) de Android, provocan pausas prolongadas en la ejecución de tu app. 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 llamar a funciones que hagan llamadas a Binder. Si no se puede evitar, almacena en caché el valor o mueve el trabajo a subprocesos en segundo plano. A medida que aumenta el tamaño de las bases de código, puedes agregar por error una llamada a Binder invocando algún método de bajo nivel si no tienes cuidado. Sin embargo, puedes encontrarlos y corregirlos con el registro.

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 veces, las llamadas que parecen 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.

Muestra el subproceso de IU suspendido debido a transacciones de Binder en un lanzamiento de RV. Usa una lógica de vinculación simple y trace-ipc para buscar llamadas de Binder y quitarlas.
Figura 4: El subproceso de IU está 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 tiene que esperar los resultados de otros subprocesos. Otras conversaciones deben publicar información al respecto.

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 recolección de elementos no utilizados.

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, en especial en bucles cerrados, es menos probable que tengas problemas.

Muestra una recolección de elementos no usados de 94 ms en HeapTaskDaemon
Figura 5: 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. Si se realizan 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.