Millorant el rendiment de les ListView Android

Gràcies a les trobades que anem fent els típics de CatDroid, la comunicació amb altres desenvolupadors Android és constant. I això ens aporta estar sempre a l’aguait de detalls que fan que les nostres apps siguin de més qualitat. I la eficiència està dintre de la ISO 9126, així que la eficiència ha de ser un objectiu del dia a dia.

Aquest article parlarà del ViewHolder, un patró extremadament senzill que és d’aplicació obligatòria a qualsevol ListView. I ListView, quasi que és vista obligatòria de qualsevol aplicació, així que… quan la dominis, pensaras “i què he fet jo tot aquest temps”? I començaràs a buscar aplicacions al teu mobil que no utilitzin ViewHolder… si, són aquelles que no van tant fluïdes com les que tu has fet.

Què és un ViewHolder?

ViewHolder no és una classe del sistema ni és una nova feature ni res similar: ViewHolder és un patró (pattern), una idea que un geni va tenir i que s’ha convertit en una best practice que Google encara no ha documentat enlloc (gràcies!).

Si no sabeu la màgia de la reutilització de les files que fa ListView internament, us recomano aquesta lectura: http://android.amberfog.com/?p=296. Si no us fa goig o no enteneu anglès, us ho resumeixo: Android reutilitza les files de la ListView que no estas veient. És a dir: quan es crea la ListView, no s’agafen les dades dels 1.000 elements que hi ha a la teva array, creant 1.000 rows que després es mostren quan fas scroll. Tan sols es processen els 15 primers elements, i es creen 15 rows. I què en fa dels altres 985 elements?

Ni els mira. Oi que quan tu vulguis veure el 16è element deixaras de veure el 1r, per culpa de l’scroll? Doncs bé, Android aconsegueix la instància del 1r element, que ja no es veu, li posa les dades del 16è element, i te la mostra a la 16à posició. I això t’ho fa oferint-te el paràmetre View convertView al mètode getView() del ArrayAdapter que ja tens. Poso una imatge genial que ha dissenyat l’autor de la web anterior: http://android.amberfog.com

ListView Recycler


Espero haver-me explicat bé. Si és així, i hem entès el concepte, passem al següent pas:

La manera típica de crear una row per a una ListView és anar carregant tots els elements gràfics que hi ha en una row, i posant-hi les dades una a una. És a dir, partint del disseny d’una row, que al cap i a la fi és tan sols un .xml del nostre directori de resources, anar carregant cada element d’aquesta row i editant el contingut: Faig findViewById() del TextView del nom, i hi poso el nom… faig findViewById() del TextView del cognom, i hi poso el cognom… faig findViewById() de tal ImageView, i hi poso el Drawable que vull… Doncs bé, findViewById() és molt costos. Android Developers tampoc ens ho comenta (gràcies!) però s’ha d’evitar sempre que es pugui.

Com fer-ho en una ListView? Molt senzill: Tot el que fem que té a veure amb findViewById(), fer-ho tan sols quan no hi ha més remei (és a dir, la primera vegada que s’utilitza una row, quan es crea). Quan Android reutilitzi aquesta row de la ListView, no hem de fer findViewById(), sino recuperar-ho d’una catxé. I qui es pot encarregar d’enmagatzemar aquesta catxé? setTag() i getTag(), uns mètodes molt pràctics que ens ofereix la classe de la row, View, i on hi podem guardar de tot. Així que al tag de la row, hi tindrem els Views que formen la row, i mai més caldrà recuperar-los amb findViewById().

Si no s’entèn… tocarà creure-s’ho. Al mirar el codi que us passo direu “Ah! Vale!”. Així que cap problema.

La part pràctica

Aquest petit how-to l’escriuré de la manera més fàcil possible:

  • Fent una aplicació amb una ListView, tal com ens recomanen a Android Developers: HelloListView
  • Modificant l’app, per a que funcioni amb un ArrayAdapter propi poc eficient.
  • Complicar les rows que es mostren (per a exagerar el baix rendiment).
  • Aplicar ViewHolder a l’ArrayAdapter.
  • Cachejar elements lents (Drawables).
  • Convertir tota la feina a 3 Activities accessibles mitjançant el launcher (per a que la comparació entre una i altra sigui senzilla).
  • Afegir logging per a comparar rendiment

Doncs allà anem… a escriure el projecte de les 3 Activities!

Thread.sleep(1000*60*60*1); //typing this foo project…

Comparació “without ViewHolder” i “with ViewHolder”

Ara que ja he escrit l’exemple, us penjo aquí una imatge on hi ha un diff entre el getView() de la versió sense ViewHolder, i el de la versió amb ViewHolder. Examinant amb calma aquesta diferència s’hauria d’entendre quina és la tasca del ViewHolder. Atenció: La única diferència és el mètode getView(). No hi ha més magia.

ViewHolder diff

Screenshots de les 3 Activities

Doncs aquí les tenim. La primera Activity, va lenta. La segona, va extremadament més rapida. I la tercera, és imperceptiblement més rapida encara. En screenshots és difícil d’expressar, però bé… un .gif quedaria una mica old-fashioned.

Without ViewHolder

With ViewHolder

With ViewHolder and cache

Resultat en números de fer scroll a les 3 Activities

Un detall: Aclarir que els test els he fet en un HTC Hero a.k.a. “Roc, canvia’t el mobil”, que corre Android 2.3.7. En un mobil d’última generació, els resultats són diferents.

Sense ViewHolder:

  • Crear una row costa uns 10ms
  • El Garbage Collector ha de passar 11 vegades
  • log

Amb ViewHolder:

  • Crear una row costa de 2 a 4 ms
  • El Garbage Collector ha de passar 1 vegada
  • log

Amb ViewHolder i cache de Drawables:

  • Crear una row a vegades costa 0 ms …
  • El Garbage Collector no ha de passar mai.
  • log

Per si ho voleu tocar vosaltres mateixos, aquí us deixo un .zip amb tot el codi que he escrit, un repositori públic a Google Code, i un link al Android Market amb el compilat. Veureu que hi ha 3 Activities: Cada una d’elles és la que he executat per a obtenir els resultats que acabo d’escriure. Si executeu el programa al vostre Android, al launcher us apareixeran les 3, així que la comparació és molt senzilla. I si mireu el logger, podeu filtrar per “RBLS”, i us acabareu de convèncer de l’importància de l’eficiència.

Ah! La icona que he posat a l’app és un plàtan genial Creative Commons (CC-BY-SA) que m’he baixat de Wikia.com:
http://ztreasureisle.wikia.com/wiki/Emerald_Island

Bé, i els plàtans que he posat al market, són de la wikipedia:
http://upload.wikimedia.org/wikipedia/commons/8/89/Green_yellow_bananas_dsc07775.jpg

Espero que aquest post us sigui d’utilitat, jo m’ho he passat bé escrivint-ho, així que… bé, al primer, ja li ha servit :·)

Llicència

Que me n’oblidava, tot això està alliberat sota GPLv3, així que feel free d’utilitzar-ho allà on volgueu: classes magistrals, xerrades amb els amics, powerpoints per al cole, projectes finals de carrera, doctorats… Per termes legals us haig de posar un link a la llicència, així que aquí teniu un link a la llicència: GPLv3

Enjoy coding!