Consejos de Rendimiento de Android

Este documento cubre principalmente micro-optimizaciones que pueden mejorar el rendimiento general de la aplicación cuando se combinan, pero es poco probable que estos cambios den lugar a efectos de rendimiento dramáticos. Elegir los algoritmos correctos y la estructura de datos siempre debe ser la prioridad, pero está fuera del alcance de este artículo. Debe utilizar los consejos de este documento como prácticas generales de codificación que pueden incorporar a sus hábitos para la eficacia general del código.

Hay dos reglas básicas para escribir código eficientemente:

  • No trabajes en aquello que no necesita
  • No asigne memoria si puede evitarla

Uno de los problemas más complicados que enfrentará al micro-optimizar una aplicación de Android es que la aplicación seguramente se ejecutará en múltiples tipos de hardware. Diferentes versiones de la máquina virtual que se ejecutan en diferentes procesadores que se ejecutan a diferentes velocidades. En general, no es el caso que simplemente pueda decir “el dispositivo X es un factor F más rápido/más lento que el dispositivo Y”, y escalar los resultados de un dispositivo a otro. En particular, la medición en el emulador le dice muy poco sobre el rendimiento en cualquier dispositivo. También existen grandes diferencias entre los dispositivos con y sin JIT: el mejor código para un dispositivo con un JIT no siempre es el mejor código para un dispositivo sin él.

Para garantizar que su aplicación tenga un buen rendimiento en una amplia variedad de dispositivos, asegúrese de que su código sea eficiente en todos los niveles y optimice de forma agresiva el rendimiento.

Evite crear objetos Innecesarios

La creación de objetos nunca es gratis. Un recolector de basura generacional con grupos de asignación de sub-procesos para objetos temporales puede abaratar la asignación, pero asignar memoria siempre es más costoso que no asignar memoria.

A medida que distribuye más objetos en su aplicación, forzará una recolección periódica de basura, creando pequeños “contratiempos” en la experiencia del usuario. Un recolector de basura concurrente introducido en Android 2.3 ayuda, pero siempre se debe evitar el trabajo innecesario.

Por lo tanto, debe evitar crear instancias de objetos que no necesita. Algunos ejemplos que pueden ayudar:

  • Si tiene un método que devuelve una cadena y sabe que el resultados siempre se agregará a StringBuffer de todos modos, cambie la definición y la implementación para que la función la agregue directamente, en lugar de crear un objeto temporal efímero.
  • Al extraer cadenas de un conjunto de datos de entrada, intente devolver una sub cadena de los datos originales, en lugar de crear una copia. Creará un nuevo objeto String, pero compartirá el char[] con los datos.

Una idea algo más radical es dividir las matrices multi-dimensionales en matrices paralelas de una sola dimensión:

  • Un arreglo de int es mejor que un arreglo de Integer, pero esto también se generaliza al hecho de que dos matrices paralelas de int son también mucho más eficiente que una matriz de objetos(int, int). Lo mismo aplica para cualquier combinación de tipos primitivos.
  • Si necesita implementar un contenedor que almacene tuplas de objetos (Foo, Bar), intente recordar que dos matrices Foo[] y Bar[] paralelas son generalmente mucho mejores que una única matriz de objetos personalizados (Foo, Bar). (La excepción a esto, por supuesto, es cuando está diseñando una API para que otros códigos accedan. En estos casos, generalmente es mejor hacer un pequeño compromiso con la velocidad para lograr un buen diseño del API. Pero en su propio código interno, debes tratar de ser lo más eficiente posible).

En términos generales, evite crear objetos temporales a corto plazo si puede. Menos objetos creados significa recolección de basura menos frecuente, lo que tiene un impacto directo en la experiencia del usuario.

Mejor Static sobre Virtual

Si no necesita acceder a los campos de un objeto, realiza el método static. Las invocaciones son 15%-20% más rápidos. Es también una buena práctica, ya que puede observar a partir de la definición del método que llamar al método no puede alterar el estado del objeto.

Utilizar static final para Constantes

Considere la siguiente declaración en la parte superior de la clase:

El compilador genera un método de inicialización de clase, llamado <cinit>, que se ejecuta cuando la clase se ejecuta por primera vez. El método almacena el valor 42 en intVal, y extrae una referencia de la tabla de constantes de cadena classfile para strVal. Cuando se hace referencia a estos valores más adelante, se accede a ellos con búsqueda en el campo.

Se pueden mejorar las cosas con la palabra reservada “final”:

La clase ya no requiere de un método <cinit>, por que las constantes van a los inicializadores de campo estático en el archivo dex. El código que hace referencia a intVal usará el valor entero 42 directamente, y los accesos a strVal usarán una instrucción de “constantes de cadena” relativamente económica en lugar de una búsqueda de campo.

Nota: Esta optimización aplica solo a tipos primitivos y String del tipo constantes, no a valores de referencia en forma arbitraria. Igual, es una buena práctica declarar constantes static final cuando sea posible.

Utilice la sintaxis mejoras para el bucle For

El bucle for mejorado (también conocido como bucle for-each) se puede usar para colecciones que implementan la interface Iterable y para matrices. Con las colecciones, se asigna un iterador para hacer llamadas de interface a hasNext() y next(). Con ArrayList, un bucle con contador escrito a mano es aproximadamente 3 veces más rápido (con o sin JIT), para para otras colecciones, la sintaxis mejorada del bucle forzado será exactamente equivalente al uso explícito del iterador.

Hay varias alternativas para iterar a través de una matriz:

zero() es la más lenta, porque el JIT aún no puede optimizar el costo de obtener la longitud de la matriz una vez por cada iteración a través del ciclo.

one() es más rápido. Extrae todo en variables locales, evitando las búsquedas. Solo la longitud de la matriz ofrece un beneficio de rendimiento.

two() es más rápido para dispositivos sin JIT e indistingible de one() para dispositivos con JIT.

Por lo tanto, utilice el bucle for mejorado de forma predeterminada, pero considere un bucle for con contador escrito a mano para la iteración de ArrayList para rendimiento crítico.

Considere el paquete en lugar de acceso privado con clases internas privadas

Considere la siguiente definición de la clase:

Lo importante aquí es que definimos una clase interna privada (Foo$Inner) que accede directamente a un método privado y a un campo de instancia privada en la clase externa. Esto es legal y el código muestra “el valor de 27” como se esperaba.

El problema es que la VM considera que el acceso directo a los miembros privados de Foo desde Foo$Inner es ilegal porque Foo y Foo$Inner son clases diferentes, aunque el lenguaje Java permite que una clase interna acceda a los miembros privados de la clase externa. Para cerrar la brecha, el compilador genera un par de métodos sintéticos:

El código de clase interno llama a estos métodos estáticos cada vez que necesita acceder al campo mValue o invocar el método doStuff() en la clase externa. Lo que esto significa es que el código anterior realmente se reduce a un caso en el que está accediendo a los campos de miembros a través de métodos de acceso. Se debe considerar que los acesors son más lentos que los accesos directos a los campos, por lo que este es un ejemplo del idioma que resulta en un golpe de desempeño “invisible”.

Si esta utilizando un código como este es un punto de acceso de alto rendimiento, puede evitar la sobre carga declarando campos y métodos a los que acceden las clases internas para tener acceso a paquetes, en lugar de acceso privado. Desafortunadamente, esto significa que los campos pueden ser accedidos directamente por otras clases en el mismo paquete, por lo que no debe usar esto en la API pública.

Evitar utilizar el punto flotante

Como regla general, el punto flotante es dos veces más lento que el entero en dispositivos Android.

En términos de velocidad, no hay diferencia entre flotante y doble en hardware más moderno. En cuanto al espacio, el doble es 2 veces más grande. Al igual que con las máquinas de escritorio, suponiendo que el espacio no es un problema, debe preferir double a float.

Además, incluso para enteros, algunos procesadores tienen multiplicación de hardware pero carecen de división de hardware. En tales casos, las operaciones de división y módulos enteros se realizan en software, algo en lo que hay que pensar si esta diseñando una tabla hash o haciendo muchas operaciones matemáticas.

Use los métodos nativos con cuidado

Desarrollar la aplicación con código nativo utilizando NDK no es necesariamente más eficiente que la programación con el lenguaje Java. Por un lado, hay un costo asociado con la transición nativa de Java, y el JIT no puede optimizar a través de estos límites. Si está asignando recursos nativos (memoria en el heap nativo, descriptores de archivos, o lo que sea), puede ser mucho más difícil organizar la recolección oportuna de estos recursos. También necesita compilar su código para cada arquitectura en la que desea ejecutar (en lugar de confiar de que tiene un JIT). Incluso puede que tenga que compilar varias versiones para lo que considera la misma arquitectura: el código nativo compilado para el procesador ARM en el Nexus One, y el código compilado para el ARM en el Nexus One no se ejecutará en el ARM en el G1.

El código nativo es principalmente útil cuando tienes una base de código nativa existente a la que quieras transferir a Android, no para “acelerar” partes de tu aplicación de Android escritas con el lenguaje Java.

Siempre Medir

Antes de comenzar a optimizar, asegúrese de tener un problema que deba resolver. Asegúrese de poder medir con precisión el rendimiento actual, o no podrá medir el beneficio de las alternativas que intenta.

También puede considerar que Traceview es útil para crear perfiles, pero es importante darse cuenta de que actualmente deshabilita el JIT, lo que puede hacer que atribuya erróneamente el tiempo al código que el JIT puede recuperar. Es especialmente importante después de realizar los cambios sugeridos por los datos Traceview para garantizar que el código resultante realmente se ejecuta más rápido cuando se ejecuta sin Traceview.

Para obtener más ayuda sobre la creación de perfiles puede leer:


Este artículo esta en base a Performante Tips

Si te gusto, comparte ...Email this to someone
email
Share on Facebook
Facebook
Tweet about this on Twitter
Twitter
Share on LinkedIn
Linkedin
Share on Google+
Google+
Facebook