Last Updated: 2021-05-31
Al momento de desarrollar una aplicación móvil, un desarrollador puede incurrir en prácticas que afectan el rendimiento de la aplicación en términos de memoria, tiempo de ejecución, y consumo general de recursos. Sin embargo, algunos de estos problemas no son tan evidentes, y parecen ser de bajo impacto para el rendimiento, por lo cual suelen ser ignorados.
Es importante tener en cuenta que, como se ha mencionado en tutoriales anteriores, el entorno móvil tiene restricciones en cuanto a sus capacidades. De esta forma, realizar micro optimizaciones es es una parte fundamental del desarrollo de las aplicaciones móviles, pues el impacto resulta bastante significativo para el resultado final de la aplicación.
En este tutorial partirá de un proyecto que presenta problemas de rendimiento de varios tipos, y los corregirá para mejorar el desempeño de la aplicación.
Al final de este tutorial usted tendrá:
Al desarrollar este tutorial aprenderá:
A nivel general, las guías de desarrollo, incluyendo el sitio de Android Developers, reconocen dos reglas principales que deberían guiar un desarrollo con buenas prácticas. Estas reglas son:
Se puede ver que ambas reglas se asocian a los recursos computacionales más limitados en un entorno móvil: la CPU y la RAM. A comparación de otros entornos computacionales, estas limitaciones hacen necesario prestar atención a la eficiencia de los algoritmos y la creación de recursos utilizados por el programa.
Las recomendaciones asociadas a estas reglas, no obstante, pueden tener efectos distintos según el entorno de ejecución, lo que incluye factores como la máquina virtual, el método de compilación, la arquitectura del procesador, entre otros. A pesar de esto, es posible optimizar el código para ser eficiente en varios niveles con las siguientes sugerencias puntuales:
Esta recomendación cobra particular relevancia en el caso de Java, pues Kotlin, por su parte, no cuenta con tipos primitivos de datos y, por ende, no cuenta con una alternativa para definir datos que no sea por medio de objetos. A pesar de esto, la sugerencia de reducir su uso sigue teniendo vigencia.
En el caso del lenguaje Kotlin, no existen los iteradores, por lo cual hay que recurrir a otras formas de ejercer ciclos. En el caso de Java, es importante recordar la posibilidad de optimizar el uso de objetos al reemplazarlos por tipos primitivos de datos. Por este motivo, es posible reconocer una mejora en cuanto al uso de los ciclos indexados.
Para el caso de este tutorial, se creó una versión del proyecto de los vinilos, el cual se ha venido trabajando en el curso. En esta versión, se modificó el código intencionalmente para incluir varias oportunidades de mejora en términos del rendimiento. A lo largo del tutorial, usted implementará dichas mejoras con el fin de que aprenda a reconocer y evitar ciertas malas prácticas al momento de desarrollar, las cuales pueden impactar el rendimiento de sus aplicaciones.
Es necesario, entonces, obtener la nueva versión del código fuente para poder trabajar sobre este proyecto. Para esto, acceda al repositorio, alojado en el siguiente enlace: https://github.com/TheSoftwareDesignLab/MISW4104-Ejemplos/tree/main/starters/CL17-microoptimizations, descargue el código fuente en su máquina y abra el proyecto en Android Studio.
Como con todos los proyectos de GitHub, usted puede descargar los archivos del repositorio a su máquina local por varios medios.
Si lo desea, puede optar por descargar el código fuente directamente en forma de archivo .zip desde la página web del repositorio en GitHub, haciendo clic en el botón "Download Zip" del desplegable Code, el cual se ve en la siguiente imagen:
Imagen 1. Vista desplegable del botón Code
No obstante, si cuenta con GitHub Desktop, puede hacer clic en el botón Open with GitHub Desktop. Si cuenta con Git instalado localmente en su máquina, puede clonar el repositorio a un directorio local vacío por medio del comando git clone
https://github.com/TheSoftwareDesignLab/MISW4104-Ejemplos.git
, o por medio de la interfaz gráfica de git. Finalmente, también puede optar por descargar el proyecto directamente con Android Studio haciendo uso de la opción Get from Version Control del menú inicial de Android Studio, la cual se muestra en la siguiente imagen:
Imagen 2. Menú de inicio de Android Studio
Una vez haya importado el proyecto en Android Studio, inspeccione el contenido del mismo en el panel lateral izquierdo, llamado "Project Structure
". Podrá notar que, en un principio, la estructura de la aplicación es exactamente la misma que en los tutoriales anteriores. Lea ahora los siguientes pasos para continuar con la exploración del proyecto en busca de las oportunidades de mejora.
En Android Studio existen herramientas que permiten realizar análisis estático del código teniendo en cuenta posibles mejoras en desempeño o buenas prácticas. Una de estas herramientas es conocida como Lint. Esta permite realizar validaciones con respecto al código fuente del proyecto, y es ampliamente utilizada por las librerías para generar advertencias al desarrollador sobre oportunidades de mejora en diferentes aspectos tales como usos inadecuados de las funciones o de los recursos importados.
Lint tiene múltiples formas de ser utilizado: por consola, por el IDE, por medio de un standalone, y generando alertas automáticas. En el caso más general, usted querrá realizar una inspección amplia del código de su proyecto. Para este fin, en su ventana Android Studio, con el proyecto abierto, seleccione la opción Analyze > Inspect Code
del menú superior. Esta opción abrirá un panel como el de la siguiente imagen:
Imagen 3. Panel de configuración de la inspección
Con las opciones tal como vienen por defecto, presione el botón OK para iniciar la inspección del código. Podrá ver un panel de nombre "Inspection results" en la parte inferior del IDE. En este panel se muestran los problemas diagnosticados organizados por su categoría. Preste atención en particular a las advertencias categorizadas como Android/Lint/Performance
para identificar problemas como los que se han mencionado en este tutorial. Podrá ver varias advertencias relacionadas a los recursos no utilizados, como se muestra a continuación:
Imagen 4. Categorización de los problemas detectados
Si bien estas advertencias son valiosas e indican lugares del código donde existen recursos no utilizados, se puede ver que el análisis estático no advierte de todas las oportunidades de mejora del código, sobre todo si estas se relacionan a la ejecución dinámica del programa.
Como pudo notar, la inspección de código apunta puntualmente a los recursos que no están siendo utilizados. Estos recursos se ubican en el directorio res
del proyecto, y, en particular, las cadenas de texto del archivo strings.xml
identificadas con los ids hello_blank_fragment
, txt_no_collectors
y txt_no_albums
pueden ser fácilmente eliminadas. Abra dicho archivo y elimine las etiquetas que corresponden a estos identificadores.
Además de estos recursos, podrá ver que en el archivo network/NetworkServiceAdapter.kt
existen imports
que no son utilizados. Estos no son detectados como parte de la inspección en la misma categoría, pero sí generan advertencias del linter en el IDE, como se muestra a continuación:
Imagen 5. Alerta acerca de una directiva import
no utilizada
Podrá identificar todos los imports
no utilizados por su color gris, el ícono del bombillo amarillo junto a la línea correspondiente, y por la advertencia que se muestra al pasar el cursor encima de las líneas de código. Elimine todas las líneas de código que cumplan con estas características para evitar el uso de recursos innecesarios de las librerías.
Preste atención, primeramente, al archivo network/NetworkServiceAdapter.kt
. Preste atención a los métodos getAlbums
, getCollectors
y getComments
. Notará que cada uno de ellos está implementado de la misma forma. Cada uno de estos métodos busca el mismo objetivo, que es llenar una lista con los elementos que se leen desde una respuesta en forma de String
desde el servidor. No obstante, el problema con la implementación consiste en la cantidad de objetos que se crean para cada traducción de las lecturas. El código dentro del ResponseListener
, para el método getComments
actualmente se ve de la siguiente forma:
val resp = JSONArray(response)
val list = mutableListOf<Comment>()
for (i in 0 until resp.length()) {
val item = resp.getJSONObject(i)
val rating = item.getInt("rating").toString()
val description = item.getString("description")
Log.d("Response", item.toString())
list.add(i, Comment(albumId = albumId, rating = rating, description = description))
}
onComplete(list)
Como puede notarlo, dentro del ciclo for que se hace sobre el JSONArray
de respuesta, se crea una variable llamada item
para cada iteración. Esto implica que en cada ciclo se reserva un nuevo espacio de memoria con la declaración de esta variable y su valor cambia al elemento actual de la iteración. No obstante, la referencia creada en la iteración anterior se pierde y no es eliminada hasta que ocurre un evento de gc. Esto mismo ocurre con las variables rating
y description
, las cuales extraen una propiedad del JSONObject. En el caso de estas últimas variables, es posible eliminarlas y utilizar el retorno del método getInt
y getString
de forma directa.
Así, la primera optimización que puede incluir en la aplicación consiste en extraer la variable item
del ciclo para utilizar un único espacio de memoria que cambia de valor instantáneamente, y acceder a sus propiedades por medio de los métodos, en lugar de utilizar variables con un corto tiempo de vida. De esta forma, el código resultante se ve de la siguiente forma:
val resp = JSONArray(response)
val list = mutableListOf<Comment>()
var item:JSONObject? = null
for (i in 0 until resp.length()) {
item = resp.getJSONObject(i)
Log.d("Response", item.toString())
list.add(i, Comment(albumId = albumId, rating = item.getInt("rating").toString(), description = item.getString("description")))
}
onComplete(list)
En segundo lugar, observe el archivo viewmodels/CommentsViewModel.kt.
Note que existe un método llamado printByRating
, el cual imprime en el registro del sistema los comentarios que corresponden a un rango de calificaciones. Este método itera sobre la lista de comentarios que existe actualmente en el LiveData
, y filtra aquellos que cumplen con el criterio de la calificación para sumar los primeros caracteres de su descripción a un StringBuffer
. Finalmente el resultado construido con el StringBuffer
se imprime en los registros del sistema. El código actual se ve de la siguiente forma:
fun printByRating(lower:Int, upper:Int){
var stringBuffer = StringBuffer()
if(!_comments.value.isNullOrEmpty()){
for (i in 0 until _comments.value!!.size) {
if (_comments.value!!.get(i).rating.toInt() < upper && _comments.value!!.get(i).rating.toInt() > lower) {
val string = _comments.value!!.get(i).description
stringBuffer.append(string + "\n")
}
}
}
Log.d("result", "Comentarios en rating: [ $lower , $upper ]: ${stringBuffer.toString()}")
}
En este punto existen múltiples optimizaciones por hacer. En primer lugar, es importante tener en cuenta que la forma en que se concatena el texto por medio de un StringBuffer
implica una cantidad innecesaria de objetos con corto tiempo de vida. Por este motivo, se debe modificar el procedimiento para utilizar una única variable de tipo String e ir concatenando el contenido directamente en ella.
Además de esto, el ciclo for se realiza con un rango creado a partir de la cantidad de elementos en la lista. Esto es innecesario, dado que la clase de tipo List
expone un iterador que ya permite recorrer todos los elementos de la lista sin necesidad de llamar a la operación de acceso ni de conteo de elementos. Luego de modificar los ciclos para hacer uso del iterador, el código debe verse de la siguiente forma:
fun printByRating(lower:Int, upper:Int){
var stringBuffer = StringBuffer()
_comments.value?.forEach{
if(it.rating.toInt() in lower until upper){
stringBuffer.append("${it.description}\n")
}
}
Log.d("result", "Comentarios en rating [ $lower , $upper ]: ${stringBuffer.toString()}")
}
Note que, ahora, el recorrido sobre la lista de comentarios se realiza con la sintaxis forEach
. Esto evita que el ciclo dependa de una consulta a la longitud del arreglo, y evita así mismo la necesidad de utilizar un rango extra a partir de ella. Además, al concatenar el contenido de la lista directamente, se evita la necesidad de crear objetos nuevos para este propósito.
Ahora, preste atención al método printListOfCommentsStartingUpper
, presente en el archivo de este mismo ViewModel. Este método filtra e imprime los comentarios cuya descripción comienza con un carácter en mayúscula. Actualmente, esta implementación se realiza de forma manual con un recorrido sobre la lista, almacenando en cada iteración el valor del comentario en un objeto, y concatenando en la lista de resultado los comentarios que cumplan con el criterio de la primera letra en mayúscula. El código que encontrará es el siguiente:
fun printListOfCommentsStartingUpper(){
var list = mutableListOf<String>()
if(!_comments.value.isNullOrEmpty()){
for (i in 0 until _comments.value!!.size) {
val comment = _comments.value!!.get(i).description
if (comment.toCharArray()[0].isUpperCase()) {
list.add(comment)
}
}
}
Log.d("result", "Comentarios con mayúscula:"+list.toString())
}
Allí, es posible reducir todo el trabajo a la función filter
de las librerías de colecciones de Kotlin. Como se mencionó en pasos anteriores, una de las sugerencias más comunes para el desempeño consiste en aprovechar los métodos de las librerías que puedan tener implementaciones más eficientes, y este es un claro caso. Así mismo, hacer uso de la función get
es una implementación mucho más directa comparada con acceder al arreglo de caracteres de forma manual y luego acceder al índice. Luego de las optimizaciones, el código de este método debe verse de la siguiente forma:
fun printListOfCommentsStartingUpper(){ if(!_comments.value.isNullOrEmpty()){ Log.d("result", "Comentarios con mayúscula: ${_comments.value!!.filter { it.description[0].isUpperCase() }}") } }
Note que, además de los beneficios mencionados anteriormente, el código también resulta simplificado y es más fácil de comprender.
¡Felicidades!
Al finalizar este tutorial, pudo familiarizarse con algunos de los problemas más comunes relacionados al rendimiento de una aplicación en Android, y pudo conocer consejos de cómo optimizarlos. Así mismo, pudo implementar mejoras al código fuente, las cuales hacen que su aplicación tenga un mejor rendimiento en los dispositivos móviles donde se ejecutará.
Ahora podrá incluir estas sugerencias al código fuente de sus proyectos en Android, de forma que sus desarrollos incluyan siempre las mejores prácticas.
Juan Sebastián Espitia Acero | Autor |
Norma Rocio Héndez Puerto | Revisora |
Mario Linares Vásquez | Revisor |