Încărcarea asincronă a imaginilor în liste - abordări și cele mai bune practici (partea 2) SIC! software

În ultima mea postare, am prezentat deja dificultăți generale în legătură cu procesele de încărcare asincronă din liste. Folosind un scenariu simplu, am descoperit complexitatea acestui caz de utilizare fără a intra în prea multe detalii.

încărcarea

Pe de altă parte, aș dori să conduc acest articol într-o direcție ceva mai tehnică - și astfel îmi îndeplinesc promisiunea de a dezvălui câteva bune practici și trucuri care s-au dovedit în proiectele noastre. Aș dori să arăt cât de ușor pot fi implementate relativ ușor listele de derulare și să introduc câteva instrumente utile care vă pot ajuta în acest sens. Între rânduri, voi aborda câteva abordări divergente, care sunt în general recomandate, dar care sunt pline de probleme deloc nesemnificative.

O listă slab performantă se manifestă printr-un comportament de derulare sacadat sau chiar printr-o blocare prelungită a întregii interacțiuni cu utilizatorul. Motivele pentru aceasta sunt blocarea apelurilor către firul principal, care este uneori responsabil pentru desenarea elementelor de interfață sau a trimiterii evenimentelor. Aceste apeluri nu trebuie întotdeauna efectuate în mod explicit, dar pot fi, de asemenea, rezultatul unei gestionări neglijente a memoriei, așa cum se poate vedea în Figura 1. Cu siguranță, acest lucru a devenit mult mai bun odată cu introducerea Concurrent Garbage Collector în Android 2.3 - dar merită totuși să creați obiecte numai atunci când este cu adevărat necesar, deoarece se știe că este o operațiune costisitoare.

Pentru a depista obiecte alocate inutil, s-a dovedit utilizarea Android Allocation Tracker, care face parte din Android SDK - sau mai exact instrumentul Dalvik Debug Monitor Server (DDMS). Aceasta permite înregistrarea tuturor generațiilor de obiecte într-o fereastră de timp liber selectabilă.

Figura 2 prezintă o înregistrare făcută în timp ce derulați o listă. Intrarea evidențiată spune, de exemplu, că un obiect BitmapFactory.Options de 92 de octeți (dimensiunea alocării) (clasa alocată) a fost creat dintr-un fir de lucru (ID fir) în timp ce o imagine a fost încărcată din cache (alocată în). Prin sortarea coloanelor individuale în consecință, instanțierile multiple inutile pot fi detectate relativ ușor.

Optimizarea are întotdeauna sens dacă un bloc de cod este apelat frecvent sau obiecte mai mari, cum ar fi Bufferul poate fi creat de mai multe ori. În cazul nostru, o atenție specială este acordată metodei getView () a adaptorului de listă. Deoarece acest lucru este apelat doar din firul principal oricum, obiectele pot fi ținute ca variabile membre - și astfel reutilizate. Șablonul ViewHolder urmează deja un principiu similar. În acest context, ar trebui verificat, de asemenea, dacă sunt utilizate structuri de date mai eficiente can: Tipurile de date primitive sunt, în general, preferabile claselor de împachetare corespunzătoare. Trebuie acordată o atenție deosebită autobuzelor implicite și costisitoare. Cu toate acestea, dezvoltatorul ar trebui să fie, de asemenea, familiarizat cu structurile de date recent adăugate, cum ar fi matricea sparse sau memoria cache LRU (disponibilă și ca clasă de compatibilitate).

Câteva recomandări suplimentare cu privire la gestionarea memoriei:

Pentru ca o listă să deruleze fără probleme, trebuie redate cel puțin 25 de cadre pe secundă. Aceasta corespunde unei ferestre de timp de maximum 40 de milisecunde pe cadru. Mai simplu spus, apelarea metodei getView () a adaptorului de listă, inclusiv toate aspectele și operațiile de redare ale ierarhiei de vizualizare, poate dura maximum 40 de milisecunde pentru a fi percepută ca fluidă. Orice depășire s-ar manifesta printr-o ușoară sau chiar începând bâlbâială.

La prima vedere, aceasta pare a fi o sarcină ușor de gestionat pentru procesoarele mobile moderne. La o inspecție mai atentă, însă, ne dăm seama rapid că această fereastră de timp este mai mult decât îngustă. Accesul la funcții native, cum ar fi o simplă operație de fișier exist (), poate consuma 25% (= 10 ms) din acest timp. Prin urmare, același lucru se aplică și accesului la baza de date. Accesul la citire și în special la scriere la stocarea persistentă este lent. În plus, timpul de răspuns al apelurilor uneori fluctuează enorm. Același acces la scriere la sistemul de fișiere poate dura 20 ms și 2 secunde. Din acest motiv, este cea mai bună practică să externalizați astfel de operațiuni către fire de lucru. Modul strict, oferit de Android SDK, acordă o atenție deosebită respectării acestei reguli (dacă se dorește).

Pentru a depista apelurile costisitoare în cadrul firului principal, utilizarea Traceview s-a dovedit. De asemenea, face parte din Instrumentele SDK Android.

Figura 3 prezintă un exemplu de extras de urmărire care a fost înregistrat în timp ce derulați printr-o listă. Aici puteți vedea că firul principal face deja ceva lucru pentru firul principal. De exemplu, Thread-15 citește în prezent date dintr-un flux, în timp ce firul principal poate transmite evenimente și astfel rămâne receptiv. Apelurile pot fi subdivizate suplimentar după cum este necesar, astfel încât să obțineți o imagine foarte precisă a firului care efectuează operația la ce oră sau cât de scumpe sunt acestea în detaliu.

În general, ar trebui să aruncați o privire mai atentă asupra momentului firului principal. Toate operațiunile care blochează acest lucru în mod inutil ar trebui externalizate către fire de lucru. Pentru aceasta, pe de o parte, sunt disponibile AsyncTasks, care vă scutesc deja de gestionarea unui pool de fire, precum și de sincronizarea cu firul principal. Dar și numeroasele implementări ale ExecutorService, care pot fi instanțiate prin intermediul clasei Executors Factory, sunt o alternativă foarte bună și adaptabilă. Pe de altă parte, ar trebui să vă abțineți de la crearea și pornirea manuală a firelor, deoarece acest lucru creează o suprasolicitare prea mare și în cel mai rău caz Căderea ar putea duce chiar la un accident (parcurgeți rapid 10.000 de intrări în listă).

În timp ce derulați, o mare parte din timpul de calcul este cheltuit pe operațiile de aspect și redare ale vizualizării listei și ale vizualizărilor sale secundare. Deoarece acest lucru trebuie făcut din thread-ul principal datorită modelului single-thread, există, de asemenea, un potențial considerabil de optimizare aici. Pentru a descoperi acest lucru, ne place să folosim Hirarchy Viewer, care face parte, de asemenea, din Instrumentele SDK Android.

Figura 4 prezintă un exemplu de structură în formă de copac a unui ListView, ale cărui elemente conțin fiecare o imagine și un text. În această perspectivă, containerele de amenajare inutile, precum și operațiunile costisitoare de aspect și desen pot fi identificate folosind indicatorii colorați. Trebuie menționat, totuși, că culorile trebuie înțelese ca valori relative și nu absolute: aspectul cadrului de aici din imagine are rolul de a măsura dimensiunea (măsura), aranjarea și alinierea (aspectul) și desenarea în sine și în sine Vizualizările pentru copii (extragere) au fiecare un indicator roșu, deoarece 100% din timpul de calcul din vizualizarea părinte a fost necesar pentru aceasta - și nu din cauza timpului absolut.

Pentru a optimiza performanța propriului aspect, trebuie respectate următoarele reguli generale:

  • Structura arborescentă a ierarhiei de vizualizare ar trebui să fie cât mai plană posibil. Cu fiecare cuibărire, timpul de calcul pentru măsurare + aspect crește semnificativ.
  • RelativeLayout este mai puternic și mai performant decât, de exemplu, aspectele liniare imbricate și ar trebui să fie întotdeauna preferat acestora.
  • Dacă dimensiunea vizualizărilor este deja cunoscută la momentul compilării, specificațiile dimensiunii fixe în pixeli independenți de densitate (dip) ar trebui să fie preferate față de wrap_content. Acest lucru accelerează oarecum procesul de măsurare.
  • Cu cât sunt utilizate mai puține vizualizări, cu atât mai bine. Adesea este suficient să setați unul sau mai multe desenabile compuse pentru un TextView în loc să utilizați un aspect profund imbricat!

Pentru a finaliza aspectul general al unei liste cu derulare ușoară, vă recomandăm să vă estompați în și să eliminați imaginile ascuțite animate declanșate ImageViews sau ProgressViews. Întreaga interacțiune a utilizatorului pare mai ușoară datorită tranzițiilor mai ușoare. Fragmentul de cod de mai jos arată cum se poate face acest lucru foarte ușor:

Cadrul prevede notificarea DataDetSetChanged a adaptorului care trebuie apelat atunci când se fac modificări la date (inclusiv, de exemplu, când a fost încărcată o imagine). Acesta este un semnal pentru ei să redeseneze întreaga listă într-un moment nedefinit în viitor. Deși această abordare nu este inacceptabilă, în principiu, mergem de obicei într-un mod diferit: un apel invers este transmis procesului de încărcare asincronă în cadrul metodei getView. Aceasta este o implementare a unei clase interioare și, prin urmare, deține o referință la ConvertedView sau ViewHolder-ul respectiv. În acest fel, ne asigurăm că sistemul trebuie doar să redeseneze modificările reale. În plus, această abordare este mai compatibilă cu afișarea unui ProgressView, așa cum veți vedea rapid.

În această postare am arătat cele mai frecvente cauze ale „listelor sacadate”. Deoarece experiența a arătat că acestea nu pot fi întotdeauna localizate direct în cod, am prezentat două instrumente care vă pot sprijini în căutarea dvs.: Allocation Tracker, pentru a depista generarea obiectelor inutile și Traceview, pentru a găsi apeluri costisitoare, care blochează. Pentru a preveni apariția acestor probleme în viitor, am formulat câteva sfaturi generale care s-au dovedit în dezvoltarea proiectelor noastre.

Practic, multe pot fi realizate prin gestionarea atentă a memoriei, utilizarea consecventă a firelor de lucru, aspectele de înaltă performanță și o cunoaștere corespunzătoare a relațiilor. Chiar dacă am reușit să ating foarte multe lucruri în acest articol, sper totuși că am fost la înălțimea așteptărilor dvs. față de promisiunea pe care am făcut-o în detaliu și că am putut să vă dau câteva impresii noi!