În Tips & Tricks

Procese și fire de execuție în Android

procese Android

În acest articol, vom arunca o privire asupra unui aspect mai avansat al platformei, dar esențial pentru dezvoltarea unor aplicații eficiente. De multe ori, o aplicație este asemănătoare unui iceberg, fiind mult mai complexă decât vede sau percepe utilizatorul care interacționează doar cu partea de UI. Acea parte ascunsă sub interfața grafică este alcătuită din procese și fire de execuție. Mai concret, o aplicație poate fi formată din mai multe procese, fiecare dintre ele putând să aibă mai multe fire de execuție. Dar să le luăm pe rând.

Procesele

Toate componentele unei aplicații vor rula într-un singur proces și, în general, pentru majoritatea aplicațiilor nu este nevoie de procese adiționale. Atunci când o aplicație pornește și nu are deja componente care rulează, Android va crea un proces UNIX nou pentru acea aplicație care va conține un singur fir de execuție (thread). Toate componentele care vor porni ulterior vor rula în acel thread, denumit și „main thread” sau „UI thread”. Vom discuta despre el mai jos, în detaliu.

Android are autoritate asupra proceselor care rulează și poate decide la un anumit moment să închidă un anumit proces care poate fi chiar cel al aplicației noastre. Motivele pot varia, dar principala cauză are legătură cu memoria care poate deveni insuficientă și este necesară eliberarea ei. În decizia de a opri un proces, Android ia în considerare mai multe aspecte, de exemplu, dacă are sau nu componente vizibile pe ecran.

Procesele sunt încadrate în 5 nivele de importanță. Atunci când unele procese trebuie „omorâte”, cel cu nivelul mai mare de importanță va fi „distrus” ultimul. Pe scurt, ele sunt:

1. Foreground Process

Un proces necesar pentru ceea ce utilizatorul face la un moment dat, de exemplu un activity care e folosit de utilizator sau un serviciu care e legat de activitatea cu care utilizatorul interacționează.

2. Visible Process

Un proces care nu are componente vizibile pe ecran, dar este necesar pentru componentele cu care interacționează utilizatorul, cum ar fi un proces cu un activity care nu e în față, dar are un dialog pornit în prim-plan.

3. Service Process

Un proces care conține un serviciu și nu e esențial pentru ceea ce este afișat utilizatorului, cum ar fi un serviciu care redă un MP3 în fundal în timp ce utilizatorul folosește alte aplicații.

4. Background Process

Un proces care conține o activitate care momentan nu e vizibilă utilizatorului. Pentru aceasta, există o listă LRU (least recently used) care este luată în considerare în importanța unui proces.

5. Empty Process

Un proces care nu are componente active și este pornit doar din motive de caching sau pentru a îmbunătăți timpul de start-up al unei aplicații.

Fire de execuție

Main Thread

Un proces poate avea mai multe fire de execuție. După cum am amintit mai sus, atunci când o aplicație e pornită, un proces UNIX este creat, iar în cadrul lui – un singur thread. Acesta poartă denumirea de „main thread”. El este extrem de important, pentru că este responsabil de interacțiunea cu utilizatorul, ca de exemplu lansarea de evenimente spre diferite componente de UI (inclusiv partea de desenare) și este și firul în care aplicația interacționează cu componentele de UI.

Sistemul nu va crea thread-uri diferite pentru fiecare componentă. Ce înseamnă asta? Atunci când atingem un buton, acest thread va lansa un eveniment de touch spre componenta respectivă care va lansa metoda responsabilă de schimbare a stării.

Fiind singurul thread responsabil de partea de UI, dacă aceasta este prea încărcată, poate avea o performanță slabă. Tot din același motiv, acest thread trebuie ținut cât mai liber. Fiecare acțiune pe acest thread, mai ales cele de durată, îl vor bloca și, implicit, aplicația nu va răspunde la acțiunile utilizatorului. Un exemplu în acest sens ar fi accesul la resurse de pe internet, cum ar fi încărcarea unei poze. Dacă am lăsa ca încărcarea ei să fie făcută în acest thread, el ar fi blocat până s-ar finaliza procesul de încărcare a pozei. Dacă poza este mare și conexiunea slabă, acest lucru poate dura câteva zeci de secunde. A ține interfața blocată pentru un timp atât de lung nu este acceptabil.

De aceea, există două reguli de bază care nu sunt negociabile atunci când lucrăm cu componentele de UI și main thread-ul:

  • Nu bloca niciodată thread-ul de UI

Pentru download-uri, query-uri în baze de date sau orice fel de operațiuni care nu afectează direct interfața cu utilizatorul și care ar putea fi de durată, trebuie folosite alte thread-uri.

  • Nu accesa direct partea de UI din alte thread-uri

Orice manipulare a interfeței de utilizator trebuie făcută din main thread, nu din thread-urile secundare.

Asynchronous Task

Există multe opțiuni pentru a folosi thread-uri secundare și a decongestiona main thread-ul: thread-uri clasice, servicii, mecanismul de broadcast & receive. Una dintre aceste opțiuni este și mecanismul de asynchronous task. Acesta este reprezentat de clasa AsyncTask și nu este nimic altceva decât un wrapper puternic parametrizat care oferă o serie de metode specifice pentru a ușura interacțiunea dintre thread-ul secundar și main thread. În alți termeni, clasa AsyncTask este construită ca și o clasă ajutătoare în jurul claselor Thread și Handler. Clasa e definită de un task care rulează pe un fir secundar în fundal și care publică automat rezultatul spre main thread.

Pentru a crea un AsyncTask, trebuie să extindem clasa de bază AsyncTask care este definită de 3 tipuri generice, denumite Params, Progress și Result și de 4 metode, dintre care una este obligatorie. Iată tipurile generice:

1. Params – Tipul parametrilor de intrare

Dacă task-ul nostru are nevoie de parametri, aici trebuie specificat tipul lor. De exemplu, dacă task-ul ar trebui să descarce o serie de fișiere, atunci tipul parametrilor ar putea fi URL sau String.

2. Progress – Tipul indicatorilor de progres

Dacă task-ul e de durată, este bine să informăm utilizatorul despre progresul făcut. În general, vom comunica o valoare integer sau double, care specifică cât din task a fost efectuat, dar putem folosi orice fel de obiect dacă dorim să trimitem informații complete. În cazul exemplului de mai sus, putem trimite ori o valoare care să reprezinte cât la sută din toate fișierele de download a fost deja descărcat sau putem returna un obiect care să conțină și numele fișierelor deja descărcate sau progresul individual pentru fiecare fișier în parte.

3. Result – Tipul rezultatului returnat

Acesta este tipul obiectului care rezultă în urma procesării.

Dacă task-ul nostru nu folosește parametri de intrare, nu dorește să publice progresul efectuat sau pur și simplu nu va returna nimic în urma procesării, putem folosi tipul Void.

Cele 4 metode pe care le oferă AsyncTask sunt fiecare dedicate unui pas din cadrul execuției unui task. Acestea sunt:

1. onPreExecute

Acest pas este executat înainte ca task-ul propriu-zis să înceapă execuția și este invocat în main thread. De obicei, îl putem folosi pentru a pregăti execuția (ex: să afișăm un indicator de progres pe ecran).

2. doInBackground

Această metodă e invocată pe thread-ul de fundal imediat după onPreExecute și este metoda care execută task-ul propriu-zis. Parametrii de intrare sunt trimiși acestei metode și ea va returna un obiect de tipul specificat pentru rezultat. În acestă metodă, putem folosi și metoda publishProgress(Progress…), pentru a face public progresul făcut de task.

3. onProgressUpdate

Acestă metodă este invocată atunci când facem public un progres prin publishProgress(Progress…) și o putem folosi pentru a actualiza un progress bar sau pentru a scrie în log-uri. Ea se va executa în paralel cu task-ul din fundal.

4. onPostExecute

Acestă metodă este invocată pe main thread imediat ce task-ul de fundal este terminat și primește ca parametru obiectul returnat de doInBackground.

Iată un exemplu de AsyncTask care este folosit pentru a descărca o serie de fișiere și care va returna numărul de bytes descărcat.

private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
     protected Long doInBackground(URL... urls) {
          int count = urls.length;
          long totalsize = 0;
          for (int i = 0; i < count; i++) {
              totalsize += Downloader.downloadFile(urls[i]);
             publishProgress((int) ((i / (float) count) * 100));
              // Escape early if cancel() is called
             if (isCancelled()) break;
         }
         return totalșize;
}
     protected void onProgressUpdate(Integer... progress) {
         setProgressPercent(progress[0]);
}
     protected void onPostExecute(Long result) {
         showDialog("Downloaded " + result + " bytes");
     }
}

Un AsyncTask poate fi pornit din main thread foarte simplu, apelând metoda execute(). De exemplu:

new DownloadFilesTask().execute(url1, url2, url3);

În același timp, un task poate fi oprit din execuție prin apelarea metodei cancel(boolean). În cazul acesta, se va executa o altă metodă, onCanceled(Object), imediat ce execuția lui doInBackground este oprită.

Reguli

Există câteva reguli atunci când folosim AsynTask:

  • clasa trebuie încărcată în main thread (ceea ce se face automat de la Jelly Bean încoace);
  • instanța task-ului trebuie creată;
  • execute() trebuie invocat pe main thread;
  • cele 4 metode care țin de stările prin care trece task-ul nu trebuie niciodată invocate direct.

Limitări

După cum putem observa, AsyncTask oferă un mecanism foarte lejer pentru a decongestiona main thread-ul și a procesa task-uri în fundal asigurând că aplicația rămâne atentă la acțiunile utilizatorului. Există totuși două limitări care fac ca AsyncTask-ul să nu poate fi folosit universal în aplicație.

Prima limitare, și poate cea mai importantă, este faptul că el poate rula doar o singură dată. Nu vom putea reporni un AsyncTask odată ce o instanță și-a terminat execuția. Pentru a-l putea reporni, Activity-ul legat de procesul ce-l conține trebuie să fie recreat. De aici rezultă cea de-a doua limitare: acest mecanism se pretează doar la task-uri de scurtă durată care pot ține doar câteva secunde în medie. Pentru task-uri long-running care pot dura minute sau a căror durată este incertă, trebuie folosit mecanismul de thread-uri, serviciile sau mecanismul de broadcast & receive. Totuși, AsyncTask oferă o soluție foarte comodă pentru a inițializa sau pentru a face operațiile de load, premergătoare pornirii unei activități.

Sper că acest articol a reușit să arunce un pic de lumină asupra câtorva dintre mecanismele care se ascund sub suprafața unei aplicații și, ca de obicei, vă aștept cu feedback și întrebări.