Red de conocimiento informático - Material del sitio web - Cómo escribir código Java eficiente en Android

Cómo escribir código Java eficiente en Android

La plataforma Java generalmente tiene tres versiones: Java ME (versión micro, utilizada para algunos teléfonos móviles), Java SE (versión estándar, utilizada para ordenadores de escritorio), Java EE (versión empresarial, utilizada para servidores). aplicaciones laterales) ). Cuando hablamos de Java solemos hacer referencia a Java SE, ya que sólo esta versión incluye máquina virtual y compilador.

Primero, el código Java se compila en un formato intermedio llamado bytecode. Cuando el código de bytes se ejecuta en la computadora de destino, la máquina virtual lo analiza rápidamente al formato nativo requerido por el hardware y el sistema operativo de la computadora de destino.

Además de brindar a los desarrolladores la ventaja de "escribir una vez, ejecutar en cualquier lugar", Java también puede implementar la administración automática de memoria a través del recolector de basura (GC), para que los desarrolladores puedan evitar la necesidad de liberar manualmente objetos inútiles. en el código. Si bien esta característica es muy útil y reduce en gran medida el riesgo de introducir problemas de memoria en su código, aumenta la sobrecarga del tiempo de ejecución debido a la necesidad de ejecutar constantemente el proceso de recolección de basura.

Este artículo comenzará comparando las diferencias entre Java SE y Java para el desarrollo de Android. Primero, presentaré las estructuras del lenguaje Java

SE a las que los desarrolladores están acostumbrados y cómo se ejecutan en Android. En segundo lugar, presentaré cómo optimizar el código Java en Android, cómo optimizar la asignación de memoria y cómo manejar adecuadamente los subprocesos múltiples.

Comparación de Dalvik Java y Java SE en Android

Aunque los desarrolladores podían usar el lenguaje de programación Java para escribir aplicaciones para dispositivos móviles mucho antes que Android, solo estaba en la versión Java A con características extremadamente altas. La funcionalidad limitada se llama Java

ME (Micro Edition). Los diferentes dispositivos móviles también requieren que se escriba código diferente, por lo que es casi imposible escribir una aplicación que pueda ejecutarse en cualquier teléfono que admita Java

ME. Además, el proceso de publicación de la aplicación era extremadamente complicado ya que en ese momento no existían buenas tiendas online.

La llegada de Android brinda a los desarrolladores la oportunidad de crear potentes aplicaciones para teléfonos inteligentes escribiendo código en el lenguaje de programación Java y la

API estándar de Java con la que están familiarizados. Sin embargo, aunque los desarrolladores de Android todavía usan el compilador Java SE para compilar aplicaciones, encontrará que existen muchas diferencias entre el Java desarrollado por James Gosling y el Java en dispositivos Android.

La VM (máquina virtual) que se ejecuta en dispositivos Android se llama Dalvik. Fue desarrollado originalmente por Dan Bornstein de Google y es adecuado para dispositivos móviles con CPU y memoria limitadas. Existen algunas diferencias entre Java SE y Dalvik Java, que se reflejan principalmente en la máquina virtual. Java

SE utiliza un diseño de máquina de pila, mientras que Dalvik está diseñado como una máquina basada en registros. Hay una herramienta dx en el SDK de Android, que convierte el código de bytes de la máquina de pila Java

SE en el código de bytes de la máquina Dalvik basado en registros. El IDE completa automáticamente este paso de conversión.

Las definiciones y diferencias entre máquinas virtuales basadas en pila y máquinas virtuales basadas en registros no se incluirán en el alcance de nuestra discusión. Por razones históricas, Android utiliza una máquina virtual basada en registros. Si bien las máquinas virtuales basadas en registros pueden ser hasta un 32 % más rápidas que las basadas en pilas, esto solo es cierto para las máquinas virtuales que interpretan el código de bytes en el momento de la ejecución (es decir, las máquinas virtuales interpretadas). Antes de la versión 2.2 de Android (también conocida como Froyo), la máquina virtual Dalvik era puramente interpretada. La versión Froyo introduce un compilador JIT (compilación justo a tiempo), que es una ventaja que Java

SE ha tenido durante mucho tiempo.

Compilación JIT, también conocida como traducción dinámica.

Traduce el código de bytes a código nativo antes de la ejecución (como se muestra en la Figura 1), lo que tiene dos beneficios principales. En primer lugar, elimina la sobrecarga de aquellas máquinas virtuales puramente interpretadas; en segundo lugar, puede realizar optimizaciones en código nativo que normalmente no son posibles con código compilado estáticamente. Por ejemplo, el compilador JIT puede seleccionar las optimizaciones más apropiadas en la CPU en la que se está ejecutando y también puede analizar cómo se ejecuta el código en función de la entrada de la aplicación para realizar más optimizaciones.

Figura 1 Pasos de traducción de Android Java y Java SE

Aunque el compilador Dalvik JIT de Android tiene grandes perspectivas de desarrollo, debe ser tan estable y estable como el compilador JIT de Java SE. muy lejos. Sin embargo, la aparición de Dalvik JIT proporciona a Android enormes ventajas de rendimiento y mejora constantemente.

JAVA

Otra diferencia entre la máquina virtual SE y la máquina virtual Dalvik es que esta última está optimizada para ejecutarse en múltiples instancias en la misma máquina. Cuando se inicia, inicia un proceso llamado cigoto, que crea la primera instancia de Dalvik a partir de la cual se crean todas las demás instancias. Cuando se inicia la aplicación, el proceso cigoto recibe una solicitud para crear una nueva instancia de máquina virtual y crea un nuevo proceso para la aplicación (como se muestra en la Figura 2). Si los desarrolladores están acostumbrados al desarrollo Java

SE, este diseño puede parecer poco práctico, pero tiene una gran ventaja al evitar el fallo de la máquina virtual Dalvik causado por un fallo de la aplicación. Esto a su vez provoca el fallo de varias aplicaciones. .

Figura 2: Inicio de una nueva instancia de máquina virtual Dalvik en Android

Android y Java

Además de las máquinas virtuales en ejecución, SE implementa API de diferentes maneras Tampoco es lo mismo. Las API de los paquetes java y javax en Android provienen de Apache

Harmony (este es un proyecto de código abierto destinado a reimplementar la pila de software Java SE. El proyecto ya no se mantiene desde noviembre de 2011). En términos de desarrollo, estas API son similares a las del paquete Java

SE, pero existen algunas diferencias. Por ejemplo, Google ha realizado una actualización importante de la clase HttpUrlConnection que no está disponible en la versión Java SE.

Además, la plataforma Android ha eliminado las API irrelevantes en Java

SE. Por ejemplo, el paquete Swing/AWT se eliminó por completo porque Android usa un marco de interfaz de usuario diferente. Otras API que se han eliminado incluyen RMI, CORBA, ImageIO y JMX. Se reemplazan con una versión específica de Android (dentro del espacio del paquete de Android) o simplemente no existen por razones prácticas.

Optimización del código Java en Android

Después de años de mejoras, Java

SE tiene algunas características nuevas que simplifican la escritura de estructuras de código complejas. Algunas de estas funciones facilitarán todo el proceso, pero los desarrolladores necesitan saber cuándo y cómo utilizarlas correctamente. Además, dado que Java

SE se utiliza principalmente para el desarrollo del lado del servidor (utilizando la API Java Enterprise Edition), los desarrolladores han optimizado específicamente el código Java del lado del servidor. Las anotaciones y el soporte de la máquina virtual Java para lenguajes de secuencias de comandos son ejemplos de optimizaciones para el desarrollo del lado del servidor. Si bien estas herramientas son poderosas a la hora de crear desarrollo back-end, estas características son de poca utilidad o incluso contraproducentes al desarrollar código de cliente de Android. Los desarrolladores de Java se han acostumbrado a cantidades ilimitadas de RAM y CPU, mientras que el desarrollo de Android requiere mucha atención al rendimiento y la asignación de memoria. En pocas palabras, los desarrolladores deben abordar el desarrollo de Android y backend de manera ligeramente diferente.

Sin embargo, con el lanzamiento inicial de Android, la cosa cambió.

Se recomiendan nuevamente algunas especificaciones de Java que alguna vez se evitaron tanto como sea posible en Android, principalmente porque el compilador JIT actual de Android resuelve los problemas de rendimiento causados ​​por estas especificaciones.

Este artículo analizará lo que necesita saber sobre el código Java para escribir aplicaciones de Android. No profundizaremos en los detalles del lenguaje de programación Java, sino que nos centraremos en las cosas que son importantes para el desarrollo de Android. Sin embargo, los desarrolladores aún deben comprender que la mayoría de las reglas y recomendaciones que se aplican a Java SE también se aplican a las máquinas virtuales de Android y Dalvik.

Enumeraciones de tipo seguro en Android

Java SE 5.0 ha agregado muchas características nuevas que son convenientes para los desarrolladores. La más esperada es la introducción de enumeraciones con seguridad de tipos. Las enumeraciones se utilizan en código para representar varias opciones que pertenecen a un determinado grupo. En versiones anteriores de Java, este problema se podía resolver con múltiples constantes enteras. Si bien esto es técnicamente posible, es fácil cometer errores. Mire el siguiente código:

public class Machine {

public static final int STOPPED = 10

public static final intIALIZING = 20; p >

public static final int INICIO = 30;

public static final int EN EJECUCIÓN = 40;

public static final int DETENIENDO = 50;

public static final int CRASHED = 60;

private int mState;

public Machine() {

mState = STOPPED;

}

public int getState() {

return mState;

}

public void setState(int estado) {

mState = state;

}

}

El problema es que aunque se esperan estas constantes, no existe ningún mecanismo para garantizar que setState( ) el método recibe un valor diferente. Si se agrega una verificación al método de configuración, el desarrollador deberá manejar los errores si se obtiene un valor inesperado. Lo que los desarrolladores necesitan es comprobar si hay asignaciones ilegales en el momento de la compilación. Las enumeraciones de tipo seguro resuelven este problema de la siguiente manera:

clase pública Máquina {

enumación pública Estado {

DETENIDO, INICIALIZANDO, INICIANDO, EJECUTANDO, DETENIENDO, CRASHED

}

Estado privado mState;

Máquina pública() {

mState = Estado.STOPPED;

p>

}

Estado público getState() {

return mState;

}

estado público void setState( State state ) {

mState = state;

}

}

Tenga en cuenta la enumeración interna recién agregada donde se muestran diferentes tipos de valores seguros ​​se declaran.Dar una clase.

Esto resolverá el problema de las asignaciones ilegales en tiempo de compilación, por lo que el código es menos propenso a errores.

Si la máquina virtual Dalvik no tiene un compilador JIT para optimizar el código, no se recomienda utilizar tipos de enumeración en la plataforma Android, porque este diseño provocará mayores pérdidas de memoria y rendimiento que el uso de constantes enteras. . grande. Esta es la razón por la que hay tantas constantes enteras en algunas versiones anteriores de la API de Android. Ahora, con un compilador JIT más potente y una máquina virtual Dalvik en constante mejora, los desarrolladores ya no tienen que preocuparse por este problema y pueden utilizar enumeraciones con seguridad de tipos con confianza.

Sin embargo, todavía hay algunas situaciones en las que usar constantes enteras es una mejor opción. Los tipos básicos de Java como int no aumentan la sobrecarga de GC. Además, muchas de las API existentes en el SDK de Android todavía dependen de tipos básicos, como la clase Handler; en este caso, no tienes muchas opciones.

Versión mejorada de bucle for en Android

Java SE 5.0 también introduce una versión mejorada de bucle for, que proporciona una expresión abreviada general para recorrer colecciones y matrices. Primero, compare los siguientes cinco métodos:

void loopOne(String[] nombres) {

int size = nombres.length;

for (int i = 0; i lt; tamaño; i ) {

printName(nombres[i]);

}

}

bucle vacíoDos (String[] nombres) {

for (String nombre: nombres) {

printName(nombre);

}

}

void loopThree(Collectionlt; Stringgt; nombres) {

for (String nombre: nombres) {

printName(nombre);

}

}

void loopFour(Collectionlt; Stringgt; nombres) {

Iteratorlt; Stringgt = nombres.iterator();

mientras (iterator.hasNext()) {

printName(iterator.next());

}

}

//No utilice el bucle for mejorado en ArrayList

void loopFive(ArrayListlt; Stringgt; nombres) {

int size = nombres.size();

for (int i = 0; i lt; tamaño; i) {

printName(names.get(i));

}

}

Lo anterior muestra cuatro formas diferentes de recorrer colecciones y matrices. Los dos primeros tienen el mismo rendimiento, por lo que si solo estás leyendo elementos, puedes usar de forma segura el bucle for mejorado en la matriz. Para los objetos de la Colección, el bucle for mejorado tiene el mismo rendimiento que usar un iterador para atravesar elementos. Los objetos ArrayList deben evitar el uso de bucles for mejorados.

Si necesita no solo iterar sobre los elementos, sino también la posición de los elementos, debe usar una matriz o ArrayList, porque todas las demás clases de Colección serán más lentas en estos casos.

En general, si tienes requisitos de alto rendimiento al leer un conjunto de datos cuyos elementos son casi constantes, se recomienda utilizar una matriz normal. Sin embargo, las matrices tienen un tamaño fijo y agregar datos puede afectar el rendimiento, así que tenga en cuenta todos los factores al escribir su código.

Cola, sincronización y bloqueos

Normalmente, las aplicaciones producen datos en un hilo y los consumen en otro hilo. Un ejemplo común es obtener datos de la red en un hilo y mostrar los datos al usuario en otro hilo (el hilo principal que opera la interfaz de usuario). Este patrón se llama patrón productor/consumidor y, en los cursos de programación orientada a objetos, los desarrolladores pueden pasar horas implementándolo utilizando algoritmos. A continuación se muestran algunas clases listas para usar que simplifican la implementación del patrón productor/consumidor.

1. Colas más inteligentes

Aunque existen clases que pueden implementar esta función con menos código, muchos desarrolladores de Java todavía optan por utilizar LinkedList y la función de cola sincronizada. Los desarrolladores pueden encontrar clases relacionadas con la sincronización en el paquete java.util.concurrent. Además, este paquete contiene semáforos, bloqueos y clases para realizar operaciones atómicas en variables individuales. Considere el siguiente código que utiliza una LinkedList estándar para implementar una cola segura para subprocesos.

Clase pública ThreadSafeQueue {

LinkedListlt privado; Stringgt = new LinkedListlt ();

Objeto final privado mLock = new Object();

oferta pública nula (valor de cadena) {

sincronizada (mLock) {

mList.offer(valor);

mLock. notifyAll();

}

}

encuesta de cadena pública sincronizada() {

sincronizada (mLock) {

mientras (mList.isEmpty()) {

prueba {

mLock.wait();

} captura (InterruptedException e) {

//Ignorar el manejo de excepciones por razones de brevedad

}

}

return mList.poll();

}

}

}

Aunque este código es correcto y tiene el potencial de obtener la máxima puntuación en el examen, implementar y probar tal pieza de El código es solo una pérdida de tiempo. De hecho, todo el código anterior se puede sustituir por la siguiente línea.

LinkedBlockingQueuelt; Stringgt; blockingQueue =

new LinkedBlockingQueuelt ();

La línea de código anterior proporciona el mismo tipo de bloqueo que el ejemplo anterior. Las colas pueden incluso proporcionar operaciones adicionales seguras para subprocesos. java.util.concurrent contiene una serie de colas opcionales y clases de mapas concurrentes, por lo que, en general, se recomienda utilizarlas en lugar de utilizar más código como en el ejemplo anterior.

2. Bloqueos más inteligentes

La palabra clave sincronizada proporcionada por Java permite a los desarrolladores crear métodos y bloques de código seguros para subprocesos. La palabra clave sincronizada es fácil de usar, pero también fácil de abusar, lo que afecta negativamente al rendimiento. La palabra clave sincronizada no es la más efectiva cuando es necesario distinguir entre leer datos y escribir datos. Afortunadamente, las clases de utilidad del paquete java.util.concurrent.locks brindan un buen soporte para esta situación.

clase pública ReadWriteLockDemo {

privada final ReentrantReadWriteLock mLock;

cadena privada mName;

privada int mAge;

cadena privada mAddress;

public ReadWriteLockDemo() {

mLock = new ReentrantReadWriteLock();

}

public void setPersonData (Nombre de cadena, edad int, dirección de cadena) {

ReentrantReadWriteLock.WriteLock writeLock = mLock.writeLock();

prueba {

writeLock.lock() ;

mName = nombre;

mAge = edad;

mAddress = dirección;

} finalmente {

writeLock.unlock();

}

}

public String getName() {

ReentrantReadWriteLock.ReadLock readLock = mLock. readLock();

prueba {

readLock.lock();

devuelve mName;

} finalmente {

readLock.unlock();

}

}

//El código repetido no se repetirá

}

El código anterior muestra dónde usar ReentrantReadWriteLock, que permite que varios subprocesos simultáneos tengan acceso de solo lectura a los datos y garantiza que solo un subproceso escriba los mismos datos al mismo tiempo.

Usar la palabra clave sincronizada en el código sigue siendo una forma efectiva de lidiar con los problemas de bloqueo, pero en cualquier caso, considere si ReentrantReadWriteLock lo es