Procesamiento lento

El procesamiento 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 procesar los fotogramas en menos de 16 ms para lograr una velocidad de 60 fotogramas por segundo (¿Por qué 60 FPS?). Si el procesamiento de IU de tu app es lento, el sistema se verá obligado a omitir fotogramas, y el usuario notará una interrupción. Esto se conoce como 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 tu app se bloquea, 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 el bloqueo no se puede reproducir 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 se tarda en procesar 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 procesamiento para que puedas ver qué sección tarda más tiempo. Por ejemplo, si un marco requiere mucho tiempo para procesar una entrada, debes revisar el código de tu app que maneja las entradas del usuario.
  • Ciertos componentes, como RecyclerView, son una fuente común de bloqueos. Si tu app usa dichos componentes, te recomendamos ejecutarlos 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 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, resulta útil 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 práctico 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:

  1. Systrace muestra cuándo se obtiene cada fotograma y los codifica por colores para destacar los tiempos de procesamiento lento. 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 fotogramas.
  2. Systrace detecta problemas en tu app y muestra alertas tanto en los fotogramas individuales como en el panel de alertas. Te recomendamos seguir las instrucciones que se incluyen en la alerta.
  3. Algunas partes del marco de trabajo 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 dichos 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, vuelve 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 Android CPU Profiler 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 en 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 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, 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 procesamiento 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 escribir pruebas de instrumentación que puedan detectar automáticamente tiempos de procesamiento lentos 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 marco 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 realizar 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 dicha 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 MyCallback como una implementación DiffUtil.Callback para informar a DiffUtil cómo debe inspeccionar tus listas.

RecyclerView: RecyclerViews anidadas

Es común anidar RecyclerView, en especial, con una lista vertical de listas de desplazamiento horizontal (como cuadrículas de app 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 muchos elementos internos aumentan 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, cuando hay una docena de itemViews en la pantalla a la vez, resulta un problema cuando itemViews no se puede compartir 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 LinearLayoutManager de innerRecyclerView. 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 buscar 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

La función de precarga en RecyclerView debería ayudar a solucionar el costo de aumento en la mayoría de los casos al realizar 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 que estés realizando la prueba en un dispositivo reciente (por el momento, la precarga solo es compatible con Android 5.0, API nivel 21 y versiones posteriores) y de que estés 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 dicho cambio durante la vinculación y evitar el aumento (lo que reduce el uso de memoria de tu app al mismo tiempo).

Si tus tipos de vistas no presentan inconvenientes, intenta reducir 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 [onBindViewHolder(VH, int)] debe ser un proceso muy simple y debe 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 establecedores en las vistas en 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 no 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 sobre el Rendimiento del diseño y el Rendimiento del procesamiento.

ListView: Aumento

Puedes inhabilitar el reciclaje por error en ListView si no tienes cuidado. 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 reciclaje 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 la guía Cómo optimizar tu diseño para obtener ejemplos específicos.
  • Puedes intentar 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 un elemento nuevo se visualiza 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 se deben ejecutar en las propiedades de dibujo de View (p. ej., setTranslationX/Y/Z(), setRotation(), setAlpha(), entre otras). 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 barato cambiar las propiedades de dibujo de una vista al llamar a un establecedor que activa 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 del procesamiento

La IU de Android funciona en dos fases: Record View#draw en el subproceso de IU y DrawFrame en RenderThread. La primera fase 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 genera la fase Record View#draw.

Rendimiento del procesamiento: 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. Al pintar un mapa de bits, se utiliza el procesamiento de CPU. Por lo tanto, debes evitarlo siempre que sea posible. Puedes utilizar el seguimiento de método con Android CPU Profiler 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, puedes hacer el trabajo, incluso, al momento de dibujar. Por ello, 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 mediante la modificación del mapa de bits.

Si deseas dibujar un mapa de bits por otro motivo y usarlo como caché, intenta dibujar en el lienzo acelerado por hardware que se pasa directamente a tu View o Drawable. Además, si es necesario, intenta llamar a setLayerType() con LAYER_TYPE_HARDWARE para almacenar en caché el resultado de procesamiento complejo y también aprovechar el procesamiento de GPU.

Rendimiento del procesamiento: 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 procesamientos costosos, 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 procesamiento en la GPU), es conveniente evitar esta API costosa si es posible o, al menos, asegúrate de pasar Canvas.CLIP_TO_LAYER_SAVE_FLAG (o llamar a una variante que no acepte marcas).

Cómo animar rutas grandes

Cuando se llama a Canvas.drawPath() en el lienzo acelerado por hardware que se pasó a Views, Android dibuja estas 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, debes evitarlo. Cuando sea posible, dibuja formas, en lugar de recortarlas sin 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 Subir textura ancho x alto. Puede tardar varios milisegundos (consulta la Figura 2), pero se necesita 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, la carga se realiza con anticipación, mientras RenderThread está inactivo. Esto se puede realizar tras la decodificación o al vincular 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 encargará 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 más de 10 ms en un fotograma que sube un mapa de bits de 1.8 MP. 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, se produce un bloqueo 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 hacen 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 al invocar 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 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. Usa una lógica de vinculación simple y trace-ipc para buscar llamadas de Binder y quitarlas.

Si no observas actividad de Binder, pero todavía no se ejecuta tu subproceso de IU, asegúrate de que no estés 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 circuito cerrado que se llama con frecuencia, debes evitar la asignación para reducir la carga en la GC.

Systrace te mostrará si GC se está ejecutando 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 al realizar muchas asignaciones se consumen más recursos de CPU en la recolección de elementos no utilizados, como se observa en la Figura 5.