Folyamatok és szálak

Gyorsnézet

  • Minden alkalmazás saját folyamatban fut, és az alkalmazás minden összetevője alapértelmezés szerint ebben a folyamatban fut
  • Minden lassú, blokkoló műveletet egy tevékenységben egy új szálban kell elvégezni, a felhasználói felület lelassulásának elkerülése érdekében

Ez a dokumentum

  1. Folyamatok
    1. Folyamatok életciklusa
  2. Szálak
    1. Munkaszálak
    2. Szálak-biztonságos módszerek
  3. Folyamatközi kommunikáció

Amikor egy alkalmazáskomponens elindul, és az alkalmazásban nem fut más komponens, az Android rendszer egy új Linux-folyamatot indít az alkalmazás számára egyetlen végrehajtószállal. Alapértelmezés szerint ugyanazon alkalmazás minden komponense ugyanabban a folyamatban és szálban (az úgynevezett “fő” szálban) fut. Ha egy alkalmazáskomponens elindul, és már létezik egy folyamat az adott alkalmazáshoz (mert létezik egy másik komponens az alkalmazásból), akkor a komponens azon a folyamaton belül indul el, és ugyanazt a végrehajtási szálat használja. Elintézheti azonban, hogy az alkalmazás különböző összetevői különálló folyamatokban fussanak, és bármelyik folyamathoz létrehozhat további szálakat.

Ez a dokumentum azt tárgyalja, hogyan működnek a folyamatok és a szálak egy Android-alkalmazásban.

Folyamatok

Egy alkalmazás minden összetevője alapértelmezés szerint ugyanabban a folyamatban fut, és a legtöbb alkalmazásnak ezt nem kell megváltoztatnia. Ha azonban úgy találja, hogy szabályoznia kell, hogy egy adott komponens melyik folyamathoz tartozik, akkor ezt megteheti a manifeszt fájlban.

A manifeszt bejegyzés minden egyes komponens elemtípushoz –<activity>, <service>, <receiver> és <provider> – tartalmaz egy android:process attribútumot, amely megadhat egyfolyamatot, amelyben az adott komponensnek futnia kell. Ezt az attribútumot beállíthatja úgy, hogy minden komponens a saját folyamatában fusson, vagy úgy, hogy egyes komponensek osztoznak egy folyamaton, míg mások nem. A android:process beállítható úgy is, hogy a különböző alkalmazások összetevői ugyanabban a folyamatban fussanak – feltéve, hogy az alkalmazások ugyanazzal a Linux felhasználói azonosítóval rendelkeznek és ugyanazokkal a tanúsítványokkal vannak aláírva.

A <application> elem is támogat egy android:process attribútumot, amellyel beállítható egy alapértelmezett érték, amely minden összetevőre vonatkozik.

Az Android dönthet úgy, hogy egy bizonyos ponton leállít egy folyamatot, amikor a memória kevés és más, a felhasználót közvetlenebbül kiszolgáló folyamatok igénylik. A leállított folyamatban futó alkalmazáskomponensek ennek következtében megsemmisülnek. Ezeknek az összetevőknek újra elindul egy folyamat, amikor újra van számukra munka.

Az Android rendszer a felhasználó számára való relatív fontosságukat mérlegeli annak eldöntésekor, hogy mely folyamatokat kell megölni. Például könnyebben leállít egy olyan folyamatot, amely a képernyőn már nem látható tevékenységeket tárol, mint egy olyan folyamatot, amely látható tevékenységeket tárol. A döntés, hogy egy folyamatot meg kell-e szüntetni, tehát a folyamatban futó komponensek állapotától függ. A folyamatok leállításának eldöntésére használt szabályokat az alábbiakban tárgyaljuk.

Process lifecycle

Az Android rendszer igyekszik minél tovább fenntartani egy alkalmazási folyamatot, de végül el kell távolítania a régi folyamatokat, hogy új vagy fontosabb folyamatok számára visszaszerezze a memóriát. Annak meghatározásához, hogy mely folyamatokat tartsa meg és melyeket kell megölnie, a rendszer minden folyamatot egy “fontossági hierarchiába” sorol a folyamatban futó komponensek és azok állapota alapján. Először a legalacsonyabb fontosságú folyamatok kerülnek kiiktatásra, majd a következő legalacsonyabb fontosságúak, és így tovább, a rendszer erőforrásainak visszanyerése érdekében.

A fontossági hierarchia öt szintje van. A következő lista a különböző típusú folyamatokat mutatja be fontossági sorrendben (az első folyamat a legfontosabb és utoljára törlődik):

  1. Előtérben lévő folyamat

    A folyamat, amely szükséges ahhoz, amit a felhasználó éppen csinál. Egy folyamat akkor tekinthető előtérben lévőnek, ha az alábbi feltételek bármelyike igaz:

    • Ez egy Activity-nek ad otthont, amellyel a felhasználó interakcióban van (a Activity onResume() metódusa meghívásra került).
    • Ez egy Service-nek ad otthont, amely ahhoz a tevékenységhez van kötve, amellyel a felhasználó interakcióba lép.
    • Ez egy Service-nek ad otthont, amely “előtérben” fut – a szolgáltatás meghívta a startForeground() metódust.
    • Ez egy Service-nek ad otthont, amely az életciklus-visszahívások (onCreate(), onStart() vagy onDestroy()) egyikét hajtja végre.
    • Ez egy BroadcastReceiver-nek ad otthont, amely a onReceive() módszerét hajtja végre.

    Általában mindig csak néhány előtérben lévő folyamat létezik. Ezeket csak végső esetben ölik meg – ha a memória olyan kevés, hogy nem tudják mindet tovább futtatni. Általában ekkor az eszköz elérte a memória lapozó állapotát, így néhány előtérben lévő folyamat megölése szükséges ahhoz, hogy a felhasználói felület érzékeny maradjon.

  2. Látható folyamat

    Egy olyan folyamat, amelynek nincsenek előtérben lévő összetevői, de mégis hatással lehet arra, hogy a felhasználó mit lát a képernyőn. Egy folyamat akkor tekinthető láthatónak, ha az alábbi feltételek valamelyike igaz:

    • Egy Activity-nek ad otthont, amely nincs előtérben, de a felhasználó számára mégis látható (onPause() metódusa meghívásra került). Ez például akkor fordulhat elő, ha az előtérben lévő tevékenység elindított egy párbeszédet, ami lehetővé teszi, hogy a mögötte lévő korábbi tevékenység látható legyen.
    • Ez egy Service-nek ad otthont, amely egy látható (vagy előtérben lévő)tevékenységhez van kötve.

    A látható folyamatot rendkívül fontosnak tartják, és nem fogják megölni, kivéve, ha ez szükséges az összes előtérben lévő folyamat futásának fenntartásához.

  3. Szolgáltatási folyamat

    Egy olyan folyamat, amely a startService() módszerrel indított szolgáltatást futtat, és nem tartozik a két magasabb kategória egyikébe sem. Bár a szolgáltatási folyamatok nem kapcsolódnak közvetlenül semmihez, amit a felhasználó lát, általában olyan dolgokat végeznek, amelyek a felhasználót érdeklik (például zenelejátszás a háttérben vagy adatok letöltése a hálózaton), ezért a rendszer futtatja őket, kivéve, ha nincs elég memória, hogy az összes előtérben lévő és látható folyamattal együtt megtartsa őket.

  4. Háttérfolyamat

    A felhasználó számára jelenleg nem látható tevékenységet tartó folyamat (a tevékenységonStop() metódusa meghívásra került). Ezeknek a folyamatoknak nincs közvetlen hatásuk a felhasználói élményre, és a rendszer bármikor leállíthatja őket, hogy visszanyerje a memóriát egy előtérben lévő,látható vagy szolgáltató folyamat számára. Általában sok háttérfolyamat fut, ezért ezeket egy LRU (least recently used) listában tartják, hogy a felhasználó által legfrissebben látott tevékenységet végző folyamat legyen az utolsó, amelyet meg kell ölni. Ha egy tevékenység helyesen hajtja végre életciklus-módszereit, és elmenti az aktuális állapotát, a folyamat megölése nem lesz látható hatással a felhasználói élményre, mert amikor a felhasználó visszanavigál a tevékenységhez, a tevékenység visszaállítja az összes látható állapotát. Az állapot mentésével és visszaállításával kapcsolatos információkért lásd a Tevékenységek-dokumentumot.

  5. Üres folyamat

    Egy olyan folyamat, amely nem tartalmaz aktív alkalmazáskomponenseket. Az egyetlen ok az ilyen típusú folyamatok életben tartására a gyorsítótárazási célokból van, hogy javítsa az indítási időt, amikor legközelebb egy komponensnek futtatnia kell benne. A rendszer gyakran megöli ezeket a folyamatokat annak érdekében, hogy a teljes rendszer erőforrásai egyensúlyban legyenek a folyamatok gyorsítótárak és a mögöttes kernel gyorsítótárak között.

Az Android a folyamatot a lehető legmagasabb szintre sorolja, a folyamatban jelenleg aktív komponensek fontossága alapján. Például, ha egy folyamat egy szolgáltatást és egy láthatóaktivitást tartalmaz, a folyamatot látható folyamatként rangsorolja, nem pedig szolgáltatási folyamatként.

Ezeken túlmenően egy folyamat rangsorát növelheti, hogy más folyamatok függnek tőle – egy olyan folyamat, amely egy másik folyamatot szolgál ki, soha nem rangsorolható alacsonyabbra, mint az a folyamat, amelyet kiszolgál. Például, ha egy tartalomszolgáltató az A folyamatban kiszolgál egy ügyfelet a B folyamatban, vagy ha az A folyamatban lévő szolgáltatás a B folyamatban lévő komponenshez van kötve, az A folyamat mindig legalább olyan fontosnak tekinthető, mint a B folyamat.

Mivel egy szolgáltatást futtató folyamat magasabb rangsorolást kap, mint egy háttértevékenységet végző folyamat,egy olyan tevékenység, amely hosszú ideig tartó műveletet indít, jól teszi, ha inkább szolgáltatást indít a művelethez, minthogy egyszerűen létrehoz egy munkaszálatot – különösen, ha a művelet valószínűleg tovább tart, mint a tevékenység.Például egy olyan tevékenységnek, amely egy képet tölt fel egy weboldalra, érdemes egy szolgáltatást indítania a feltöltéshez, hogy a feltöltés akkor is folytatódhasson a háttérben, ha a felhasználó elhagyja a tevékenységet.A szolgáltatás használata garantálja, hogy a műveletnek legalább “szolgáltatási folyamat” prioritása lesz, függetlenül attól, hogy mi történik a tevékenységgel. Ugyanez az oka annak, hogy az adásfogadóknak inkább szolgáltatásokat kell alkalmaznia, mint egyszerűen egy szálba helyezni az időigényes műveleteket.

Szálak

Az alkalmazás indításakor a rendszer létrehoz egy végrehajtási szálat az alkalmazás számára,amelyet “main”-nak neveznek. Ez a szál nagyon fontos, mert ez felel az események megfelelő felhasználói felület widgetjeihez való eljuttatásáért, beleértve a rajzolási eseményeket is. Ez az a szál, amelyben az alkalmazás interakcióba lép az Android UI eszköztár komponenseivel (a android.widget és android.view csomagok komponensei). Ezért a főszálat néha UI szálnak is nevezik.

A rendszer nem hoz létre külön szálat egy komponens minden egyes példányához. Az összes olyan komponens, amely ugyanabban a folyamatban fut, a UI szálban instanciálódik, és az egyes komponensek rendszerhívásait ez a szál indítja el. Következésképpen a rendszer-visszahívásokra reagáló metódusok (például onKeyDown() a felhasználói műveletek jelentésére vagy egy életciklus-visszahívási metódus) mindig a folyamat UI szálában futnak.

Amikor például a felhasználó megérint egy gombot a képernyőn, az alkalmazás UI szála elküldi az érintési eseményt a widgetnek, amely viszont beállítja a megnyomott állapotát, és érvénytelenítési kérelmet küld az eseménysorba. Az UI szál visszaveszi a kérést a sorból, és értesíti a widgetet, hogy újra kell rajzolnia magát.

Ha az alkalmazás intenzív munkát végez a felhasználói interakciókra reagálva, ez az egyszálas modell gyenge teljesítményt eredményezhet, hacsak nem valósítja meg megfelelően az alkalmazást. Konkrétan, ha minden az UI szálban történik, a hosszú műveletek, például a hálózati hozzáférés vagy az adatbázis-lekérdezések végrehajtása blokkolni fogja az egész UI-t. Amikor a szál blokkolva van, semmilyen esemény nem küldhető el, beleértve a rajzolási eseményeket is. A felhasználó szemszögéből nézve az alkalmazás akadozni látszik. Még rosszabb, ha a felhasználói felület szála néhány másodpercnél hosszabb ideig (jelenleg kb. 5 másodpercig) blokkolva van, a felhasználónak megjelenik a hírhedt “az alkalmazás nem válaszol” (ANR) párbeszédpanel. A felhasználó ekkor úgy dönthet, hogy kilép az alkalmazásból és eltávolítja azt, ha elégedetlen.

Az Andoid UI toolkit ráadásul nem szálbiztos. Tehát nem szabad manipulálnod a felhasználói felületedet egy munkaszálból – minden manipulációt a felhasználói felületeden a UIszálból kell elvégezned. Így az Android egyszálas modelljének egyszerűen két szabálya van:

  1. Ne blokkolja az UI-szálat
  2. Ne férjen hozzá az Android UI eszköztárához az UI-szálon kívülről

Munkaszálak

A fent leírt egyszálas modell miatt az alkalmazás UI-jának reakciókészsége szempontjából létfontosságú, hogy ne blokkolja az UI-szálat. Ha olyan műveleteket kell végrehajtania, amelyek nem azonnaliak, akkor gondoskodnia kell arról, hogy azokat külön szálakban (“háttér” vagy “munkás” szálakban) végezze el.

Az alábbiakban például egy kattintáshallgató kódja látható, amely egy képet tölt le egy külön szálból, és megjeleníti azt egy ImageView:

public void onClick(View v) { new Thread(new Runnable() { public void run() { Bitmap b = loadImageFromNetwork("http://example.com/image.png"); mImageView.setImageBitmap(b); } }).start();}

Előre úgy tűnik, hogy ez jól működik, mert egy új szálat hoz létre a hálózati művelet kezelésére. Ez azonban megszegi az egyszálas modell második szabályát: ne érintsük meg azAndroid UI eszköztárat az UI szálon kívülről – ez a minta a ImageView-t a munkaszálból módosítja az UI szál helyett. Ez meghatározatlan és váratlan viselkedést eredményezhet, aminek a nyomon követése nehéz és időigényes lehet.

A probléma megoldására az Android több lehetőséget is kínál arra, hogy az UI szálhoz más szálakból is hozzáférjünk. Az alábbiakban felsoroljuk azokat a módszereket, amelyek segíthetnek:

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • View.postDelayed(Runnable,long)

A fenti kódot például a View.post(Runnable) módszerrel javíthatja:

public void onClick(View v) { new Thread(new Runnable() { public void run() { final Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png"); mImageView.post(new Runnable() { public void run() { mImageView.setImageBitmap(bitmap); } }); } }).start();}

Most ez a megvalósítás szálbiztos: a hálózati művelet egy külön szálból történik, míg a ImageView manipulálása a UI szálból történik.

A művelet összetettségének növekedésével azonban az ilyen kód bonyolulttá és nehezen karbantarthatóvá válhat. A munkaszállal való összetettebb interakciók kezeléséhez megfontolhatod egy Handler használatát a munkaszálban, a UIszálból érkező üzenetek feldolgozására. Talán a legjobb megoldás azonban a AsyncTask osztály kiterjesztése,amely leegyszerűsíti a felhasználói felülettel interakcióba lépő munkaszál feladatok végrehajtását.

Az AsyncTask

AsyncTask használata lehetővé teszi, hogy aszinkron munkát végezzen a felhasználói felületen. A blokkoló műveleteket egy munkaszálban hajtja végre, majd az eredményeket közzéteszi az UI szálon, anélkül, hogy magának kellene szálakat és/vagy kezelőket kezelnie.

A használatához a AsyncTask alosztályba kell sorolnia és implementálnia kell a doInBackground() visszahívási metódust, amely a háttérszálak egy pooljában fut. A felhasználói felület frissítéséhez implementálnod kell a onPostExecute()-et, amely a doInBackground() eredményét szállítja és a felhasználói felület szálában fut, így biztonságosan frissítheted a felhasználói felületedet. Ezután a feladatot a execute()meghívásával futtathatja az UI szálból.

Az előző példát például a AsyncTask segítségével így valósíthatja meg:

public void onClick(View v) { new DownloadImageTask().execute("http://example.com/image.png");}private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> { /** The system calls this to perform work in a worker thread and * delivers it the parameters given to AsyncTask.execute() */ protected Bitmap doInBackground(String... urls) { return loadImageFromNetwork(urls); } /** The system calls this to perform work in the UI thread and delivers * the result from doInBackground() */ protected void onPostExecute(Bitmap result) { mImageView.setImageBitmap(result); }}

Most az UI biztonságos és a kód egyszerűbb, mert szétválasztja a munkát arra a részre, amit egy munkás szálon kell elvégezni, és arra, amit az UI szálon kell elvégezni.

Az osztály használatának teljes megértéséhez olvassa el a AsyncTask referenciát, de itt egy gyors áttekintés a működéséről:

  • Megadhatja a paraméterek típusát, az előrehaladási értékeket és a feladat végértékét, generikumok használatával
  • A doInBackground() módszer automatikusan végrehajtódik egy munkaszálon
  • onPreExecute(), onPostExecute(), és a onProgressUpdate() mind meghívásra kerül a felhasználói szálon
  • A doInBackground() által visszaadott értéket elküldi aonPostExecute()
  • A doInBackground()-ben bármikor meghívhatja a publishProgress()-t, hogy a onProgressUpdate() végrehajtódjon a felhasználói szálon
  • A feladatot bármikor, bármelyik szálból törölheti

Vigyázat! Egy másik probléma, amivel a workerthread használatakor találkozhatsz, a tevékenységed váratlan újraindítása a futásidejű konfigurációváltás miatt(például amikor a felhasználó megváltoztatja a képernyő tájolását), ami tönkreteheti a workerthreadet. Azt, hogy miként tarthatja fenn a feladatát egy ilyen újraindítás során, és hogyan törölheti megfelelően a feladatot, amikor a tevékenység megsemmisül, lásd a Shelves mintaalkalmazás forráskódjában.

Szálbiztos módszerek

Bizonyos helyzetekben az implementált módszereket egynél több szálból hívhatják meg, ezért azokat úgy kell megírni, hogy szálbiztosak legyenek.

Ez elsősorban a távolról hívható metódusokra igaz – például egy kötött szolgáltatás metódusaira. Ha egy IBinder-ben implementált metódus hívása ugyanabból a folyamatból származik, amelyben a IBinder fut, a metódus a hívó szálában kerül végrehajtásra.Ha azonban a hívás egy másik folyamatból származik, a metódus egy olyan szálban kerül végrehajtásra, amelyet a rendszer a IBinder-vel azonos folyamatban fenntartott szálakból választ ki (nem a folyamat UI szálában kerül végrehajtásra). Például, míg egy szolgáltatásonBind() metódusát a szolgáltatás folyamatának UI szálából hívják meg, a onBind() által visszaadott objektumban (például egy RPC metódusokat megvalósító alosztályban) implementált metódusokat a poolban lévő szálakból hívják meg. Mivel egy szolgáltatásnak egynél több kliense is lehet, egynél több pool-szál egyszerre több IBinder metódust is meghívhat. A IBinder metódusokat ezért szálbiztosra kell implementálni.

Hasonlóképpen, egy tartalomszolgáltató fogadhat olyan adatkéréseket, amelyek más folyamatokból származnak.Bár a ContentResolver és ContentProvider osztályok elrejtik a folyamatok közötti kommunikáció kezelésének részleteit, az ilyen kérésekre válaszoló ContentProvider metódusok – a query(), insert(), delete(), update() és getType() metódusok – a tartalomszolgáltató folyamatának egy szálkészletéből hívódnak, nem pedig a folyamat UIthreadjéből. Mivel ezeket a metódusokat tetszőleges számú szálból hívhatják meg egyidejűleg, ezeket is úgy kell implementálni, hogy szálbiztosak legyenek.

Folyamatközi kommunikáció

Az Android egy mechanizmust kínál a folyamatok közötti kommunikációra (IPC) a távoli eljáráshívások (RPC) segítségével, amelyben egy metódust egy tevékenység vagy más alkalmazáskomponens hív meg, de távol (egy másik folyamatban) hajtják végre, és minden eredményt visszaküldenek a hívónak. Ez azt jelenti, hogy a metódushívást és annak adatait az operációs rendszer által érthető szintre kell bontani, a helyi folyamatból és címtartományból a távoli folyamatba és címtartományba kell továbbítani, majd a hívást újra össze kell állítani és ott újra kell végrehajtani. A visszatérési értékeket ellenkező irányban továbbítja. Az Android biztosítja az összes kódot ezen IPCtranzakciók végrehajtásához, így Ön az RPC programozási felület meghatározására és megvalósítására összpontosíthat.

Az IPC végrehajtásához az alkalmazásnak egy szolgáltatáshoz kell kötődnie a bindService() használatával. További információért lásd a Szolgáltatások fejlesztői útmutatóját.

Szólj hozzá!