Developing Frogtek

El blog del Departamento de Tecnología

Etiqueta: traceview

El findViewById(), y lo cargante que puede llegar a ser

Si no vienes del post introductorio, quizás te gustaría leerlo.

Segundo asalto. La aplicación sigue estando lenta, y no sabemos muy bien por qué. Ya no hay tantos onMeasure(); sólo los “necesarios”. Pero hay una nueva función que aparece en los primeros puestos de nuestro querido traceview: findViewTraversal(). ¿Y quién llama tantísimas veces a esta última? pues findViewById().
Y es que esta función, es la que enlaza un recurso de la interfaz de usuario de una aplicación, con una variable en nuestro código. O simplemente se encarga de acceder a ese recurso para cambiar una de sus propiedades.

Supongamos, pues, que tenemos un layout con una veintena de widgets entre layouts, Buttons, EditText, TextView, ImageView… Y que en un momento dado del ciclo de vida de la actividad, tenemos que acceder a uno ó varios de ellos para cambiar sus propiedades. Podemos acceder a ellos del modo:
((EditText) this.findViewById(R.id.edittext_cliente)).setOnClickListener(…);
Aunque también podemos guardar el valor que devuelve la función findViewById() en una variable, o variable miembro de la Activity para operar con ella varias veces a lo largo del citado ciclo de vida.

¿Cuál es la diferencia? y ya que estamos ¿cuál era nuestro error? Pues que cada vez que se llama a la función findViewById(), android recorre todo el árbol jerárquico del Layout que hemos definido previamente en XML, en busca del recurso al que queremos acceder. Como ya habréis averiguado, el costo de esa operación es muy alto si el control a encontrar es de los últimos en el árbol, o si hay muchos niveles de layouts anidados, o simplemente queremos cambiar el valor de una de las propiedades de ese recurso bastantes veces a lo largo del ciclo de vida de la aplicación.
Si a esto le sumamos que el cambio en una de las propiedades de un EditText (por ejemplo), afecta a cinco o seis widgets más; y a todos accedemos del modo findViewById(), estamos vendidos.

La solución para una Activity, cuyos widgets han de ser modificados con bastante asiduidad es el guardar el resultado de la función archinombrada en este post en variables miembro, a las cuales se accederá sin tener que recorrer (findViewTraversal()) todo el árbol jerárquico “n” veces, con la consiguiente pérdida de velocidad en la transición de un estado a otro de la Activity.
Eso sí: Podemos prescindir de estas variables miembro si accedemos una, y sólo una, vez al widget en cuestión.

Esto también hay que grabarlo a fuego en la mente.

Diseño de layouts XML, sucios pero eficientes, en android

Si no vienes del post introductorio, quizás te gustaría leerlo.

Bien, pues a medida que añadíamos nueva funcionalidad a la aplicación en la tablet, experimentábamos una lentitud tremenda en alguna de las actividades que abríamos. No era normal que si en el teléfono, el mismo layout funcionaba perfectamente, ¿por qué al unir dos layouts en uno sólo, pero más grande, vaya tan lento? Respuesta: no hay nada que el traceview no solucione. Nada.

Rápidamente, Julio comenzó a ver que había miles de llamadas a una función nativa de android que se llama onMeasure(). La misma, se encarga de que cada vez que hay que posicionar un widget dentro del layout, hacer las medidas necesarias de lo ya presente en el mismo para así determinar dónde debe de ir colocado. ¿Y qué problema hay con esto? Pues que si tu layout tiene muchos widgets, y estos widgets tienen algunas de sus propiedades (como el layout_width, o layout_height) con valores wrap_content o fill_parent, todo resultará en un festival de onMeasure(). Creo recordar, que el 90% del tiempo de carga de una pantalla ineficiente nuestra, se lo llevaba esta función. Obviamente, pasamos de medio segundo para cargar una pantalla determinada en el teléfono, a 2 ó 3 (incluso más) en la tablet.
Por lo tanto, uno de los nuevos mandamientos que hay que grabarse a fuego es que: siempre darás medidas a tus widgets (Button, TextView, Layouts, EditText, etc), en lugar de que las decida android. Siempre, a no ser que sea estrictamente necesario; que alguna vez lo es.
Este es el primer paso para que la aplicación fluya un poquito más rápido que antes. Pero sólo un poquito.

¿Qué, que aún no hemos ensuciado el layout? Bien, pues este punto tiene que ver con el nivel en que un widget se encuentra anidado. Podemos tener un layout muy sencillo, con un RelativeLayout y dos Button dentro, o podemos tener algo como esto (o peor). Es por esto lo que un día dije en la oficina. Somos unos barrocos.
Por lo tanto, desde frogtek recomendamos encarecidamente que, a poder ser, se tenga un RelativeLayout padre con todos sus hijos al mismo nivel. Si se tienen 4 o 5 widgets en el layout, serás afortunado; si tienes 30 o 40 como en alguno de nuestra tablet, puedes volverte loco buscando uno en concreto en el Outline de eclipse. Cierto es que queda todo mucho más ordenado si vamos anidando widgets dentro de Layouts, pero se pierde rapidez al cargar, ya que también hay muchos más onMeasures() que si estuviesen todos al mismo nivel. Y traceview no miente.

Por lo tanto, el primer paso para agilizar la carga (o recarga) de una pantalla, pasa por dar medidas a todo widget al que podamos dar medidas absolutas, e intentar que nuestros layouts sean lo menos barrocos posibles en cuanto a ordenación jerárquica se refiere.

Consejo extra que se me ocurre mientras escribo: si utilizas el LayoutInflater para cargar layouts dentro de otros, tener un layout con uno o dos niveles de anidación ayudará mucho. Nosotros, que tenemos layouts dentro de layouts, y de nuevo dentro de layouts (por necesidad), lo hemos notado.

Eficiencia en Android

Continuamos con nuestro afán por tener una aplicación más eficiente e iremos escribiendo con lo que nos vamos encontrado: tenemos una tabla con datos para sincronizar y mientras se sincronizan actualizamos una barra de progreso en la status bar. Actualmente, actualizábamos la barra de progreso cada vez que se procesaba una fila, con lo cual, actualizábamos la barra de progreso tantas veces como filas teniamos.

Julio (QA) al percatarse de que esta operación costaba mucho tiempo, lanzó su herramienta más preciada…, el traceview. Se pasó por mi mesa y vimos que la clase Notify de Android se nos estaba llevando un gran tiempo debido al problema explicado en el párrafo anterior.

Con lo que nos pusimos manos a la obra y escribimos el correspondiente método estático que nos calcula el porcentaje y nos dirá si tenemos que actualizar la progress bar, de este modo siempre la actualizaremos 100 veces. Ahí va:

/*** This method retuns true if an update is needed in the progess bar
 *
 * @param itemsCount The total number of items
 * @param iCount The actual item to be processed
 * @return True if is necesary to update the progress bar
 */
 public static boolean isNeededProgressBarUpdate(int itemsCount, int iCount) {
    if (itemsCount != 0 && iCount !=0) {
        int perActual = (iCount * 100) / itemsCount;
        int perPrevious = ((iCount - 1) * 100) / itemsCount;
        if (perActual == perPrevious) {
            return false;
        } else {
            return true;
        }
    } else {
       return true;
    }
 }

Por otro lado también hemos notado que el Log de Android consume bastantes recurso y así lo comentan en la documentación. Pero no tenemos muy claro qué pasa con el Log.d (DEBUG). Según dice en la documentación, no se escribe en tiempo de ejecución, pero… creemos que depende de si marcas el teléfono para debug o no.

Como conclusión final hemos conseguido reducir un proceso que le costaba una hora a unos 20 minutos.