Praktisk samtidighet på Android med HaMeR

Vid förståelse av samtidighet på Android Med hjälp av HaMeR talade vi om grunderna i HaMeR (Hanterare, Meddelande, och Runnable) ramverk. Vi täckte dess alternativ, samt när och hur man använder den. 

Idag skapar vi en enkel applikation för att utforska de begrepp som lärt sig. Med ett praktiskt tillvägagångssätt ser vi hur man tillämpar de olika möjligheterna hos HaMeR i hantering av samtidighet på Android.

1. Sample Application

Låt oss komma till jobbet och skicka några Runnable och skicka Meddelande objekt på en provapplikation. För att hålla det så enkelt som möjligt, utforskar vi bara de mest intressanta delarna. Alla resursfiler och standardaktivitetssamtal kommer att ignoreras här. Så jag rekommenderar starkt dig att kolla källkoden för provprogrammet med sina omfattande kommentarer. 

Appen kommer att bestå av:

  • Två aktiviteter, en för Runnable en annan för Meddelande samtal
  • Två HandlerThread objekt:
    • WorkerThread att ta emot och behandla samtal från användargränssnittet
    • motgänga att motta Meddelande samtal från WorkerThread
  • Vissa verktygsklasser (för att bevara objekt under konfigurationsändringar och för layout)

2. Utstationering och mottagning av färdigheter

Låt oss börja experimentera med Handler.post (Runnable) metod och dess variationer, som lägger till en runnable till a Meddelandekö associerad med en tråd. Vi skapar en aktivitet som heter RunnableActivity, som kommunicerar med en bakgrundsgänga som heter WorkerThread.

De RunnableActivity instantiates en bakgrundsgänga som heter WorkerThread, passerar a Hanterare och a WorkerThread.Callback som parametrar. Verksamheten kan ringa upp WorkerThread att asynkront ladda ner en bitmapp och visa en toast vid en viss tidpunkt. Resultaten av de uppgifter som gjorts av arbetstråden skickas till RunnableActivity av runnables publicerade på Hanterare mottaget av WorkerThread.

2.1 Förbereda en hanterare för RunnableActivity

RunnableActivity vi ska skapa en Hanterare att vidarebefordras till WorkerThread. De uiHandler kommer att vara associerad med Looper från användargränssnittet, eftersom det kallas från den tråden.

allmän klass RunnableActivity utökar aktivitet // Handler som tillåter kommunikation mellan // WorkerThread och Aktivitetsskyddad Handler uiHandler; @Override protected void onCreate (Bundle savedInstanceState) super.onCreate (savedInstanceState); setContentView (R.layout.activity_main); // förbereda UI Handler för att skicka till WorkerThread uiHandler = new Handler (); 

2.2 Deklarera WorkerThread och dess återkopplingsgränssnitt

De WorkerThread är en bakgrundstråd där vi ska starta olika typer av uppgifter. Den kommunicerar med användargränssnittet med hjälp av responseHandler och ett återkopplingsgränssnitt mottaget under dess instansiering. Referenser från verksamheten är WeakReference <> typ, eftersom en aktivitet kan förstöras och referensen förloras.

Klassen erbjuder ett gränssnitt som kan implementeras av användargränssnittet. Det sträcker sig också HandlerThread, en hjälparklass byggd ovanpå Tråd som redan innehåller en Looper, och a Meddelandekö. Därför har den rätt inrättaatt använda HaMeR-ramverket.

offentlig klass WorkerThread utökar HandlerThread / ** * Interface för att underlätta samtal på användargränssnittet. * / public interface Återuppringning void loadImage (Bitmap image); void showToast (String msg);  // Den här hanteraren är endast ansvarig // för att posta Runnables på den här Thread Private Handler postHandler; // Handler är mottagen från MessageActivity och RunnableActivity // ansvarig för att ta emot Runnable-samtal som ska behandlas // på användargränssnittet. Återuppringningen hjälper till med denna process. privat weakreference responseHandler; // Återuppringning från UI // det är en WeakReference eftersom det kan ogiltigförklaras // under "konfigurationsändringar" och andra händelser privata WeakReference ring tillbaka; privat sista sträng imageAUrl = "https://pixabay.com/static/uploads/photo/2016/08/05/18/28/mobile-phone-1572901_960_720.jpg"; / ** * Konstruktören tar emot en hanterare och en återuppringning från användargränssnittet * @param responseHandler ansvarig för att skicka Runnable till användargränssnittet * @param callback fungerar tillsammans med responseHandler * tillåter samtal direkt på UI * / Public WorkerThread (HandlerThread responseHandler, återuppringning) super (TAG); this.responseHandler = new WeakReference <> (responseHandler); this.callback = new WeakReference <> (callback); 

2.3 Initialisering WorkerThread

Vi måste lägga till en metod till WorkerThread att kallas av de aktiviteter som förbereder tråden postHandler för användning. Metoden behöver endast kallas efter att tråden är igång.

offentlig klass WorkerThread utökar HandlerThread / ** * Förbered posthanteraren. * Det måste kallas efter att tråden har startat * / public void preparationHandler () postHandler = new Handler (getLooper ()); 

RunnableActivity vi måste genomföra WorkerThread.Callback och initiera tråden så att den kan användas.

offentlig klass RunnableActivity utökar Aktivitetsverktyg WorkerThread.Callback // BackgroundThread ansvarig för nedladdning av den bildskyddade WorkerThread workerThread; / ** * Initiera @link WorkerThread -exemplet * endast om det inte har initialiserats än. * / public void initWorkerThread () if (workerThread == null) workerThread = new WorkerThread (uiHandler, detta); workerThread.start (); workerThread.prepareHandler ();  / ** * Ställ in bilden som laddats ner på bg-tråden till bildvynn / / Överför allmän tomtlastImage (Bitmap-bild) myImage.setImageBitmap (bild);  @Override public void showToast (finalstring msg) // för att implementeras

2.4 Användning Handler.post () på WorkerThread

De WorkerThread.downloadWithRunnable () Metoden hämtar en bitmapp och skickar den till RunnableActivity att visas i en bildvisning. Det illustrerar två grundläggande användningsområden för Handler.post (Runnable Run) kommando:

  • För att tillåta en tråd att lägga ett Runnable-objekt till ett MessageQueue som är associerat med sig själv när .posta() kallas på en Handler i samband med trådens Looper.
  • För att tillåta kommunikation med andra trådar, när .posta() kallas på en Handler som är associerad med andra trådens Looper.
  1. De WorkerThread.downloadWithRunnable () metodposter a Runnable till WorkerThread's Meddelandekö använda postHandler, en Hanterare associerad med WorkThread's Looper.
  2. När runnable bearbetas hämtar den a BitmapWorkerThread.
  3. När bitmapen har laddats ner, responseHandler, en hanterare som är associerad med användargränssnittet, används för att lägga in en runnable på RunnableActivity innehållande bitmappen.
  4. Runnable är bearbetad, och WorkerThread.Callback.loadImage används för att visa den nedladdade bilden på en Imageview.
public class WorkerThread utökar HandlerThread / ** * posta en Runnable till WorkerThread * Ladda ner en bitmapp och skicka bilden * till användargränssnittet @link RunnableActivity * med hjälp av @link #responseHandler med * hjälp från @link #callback * / public void downloadWithRunnable () // post Runnable till WorkerThread postHandler.post (nytt Runnable () @Override public void run () försök // sover i 2 sekunder för att emulera lång löpande drift TimeUnit.SECONDS .sleep (2); // Ladda ner bild och skickar till UI downloadImage (imageAUrl); catch (InterruptedException e) e.printStackTrace (););  / ** * Ladda ner en bitmapp med sin webbadress och * skicka till den grafiska bilden den nedladdade bilden * / privat tomt downloadImage (String urlStr) // Skapa en anslutning HttpURLConnection connection = null; prova URL url = ny URL (urlStr); anslutning = (HttpURLConnection) url.openConnection (); // få strömmen från url InputStream in = nya BufferedInputStream (connection.getInputStream ()); Final Bitmap bitmap = BitmapFactory.decodeStream (in); om (bitmapp! = null) // skicka bitmappen nerladdad och en återkoppling till UI loadImageOnUI (bitmapp);  annars  fånga (IOException e) e.printStackTrace ();  äntligen om (anslutning! = null) connection.disconnect ();  / ** * skickar en bitmapp till ui * postar en Runnable till @link #responseHandler * och använder @link Callback * / privat void loadImageOnUI (slutlig bitmappsbild) Log.d (TAG, "loadImageOnUI (" + bild + ")"); om (checkResponse ()) responseHandler.get (). post (nytt Runnable () @Override public void run () callback.get (). loadImage (bild););  // verifiera om responseHandler är tillgänglig // om inte aktiviteten passerar någon förstörelse händelse privat booleansk checkResponse () return responseHandler.get ()! = null; 

2.5 Användning Handler.postAtTime () och Activity.runOnUiThread ()

De WorkerThread.toastAtTime ()schemalägger en uppgift som ska utföras vid en viss tid och uppvisar en Rostat bröd till användaren. Metoden illustrerar användningen av Handler.postAtTime () och den Activity.runOnUiThread ().

  • Handler.postAtTime (Runnable Run, lång upptidMillis) postar en runnable vid en given tidpunkt.
  • Activity.runOnUiThread (Runnable run) använder standardgränssnittshanteraren för att skicka en runnbar till huvudtråden.
offentlig klass WorkerThread utökar HandlerThread / ** * visar en Toast på UI. * schemalägger uppgiften med tanke på den aktuella tiden. * Det kan vara planerat när som helst, vi använder * 5 sekunder för att underlätta debugging * / public void toastAtTime () Log.d (TAG, "toastAtTime (): current -" + Calendar.getInstance (). ToString ()); // sekunder att lägga till vid aktuell tid int delaySeconds = 5; // testning med ett riktigt datum Kalender scheduledDate = Calendar.getInstance (); // Ange ett framtida datum med tanke på fördröjningen i sekunder definiera // Vi använder den här metoden för att underlätta testningen. // det kan göras med ett användardefinierat datum även scheduledDate.set (scheduledDate.get (Calendar.YEAR), scheduledDate.get (Calendar.MONTH), scheduledDate.get (Kalender.DAY_OF_MONTH), scheduledDate.get (Kalender.HOUR_OF_DAY ), scheduledDate.get (Calendar.MINUTE), scheduledDate.get (Calendar.SECOND) + delaySeconds); Log.d (TAG, "toastAtTime (): scheduling at -" + scheduledDate.toString ()); long scheduled = calculateUptimeMillis (scheduledDate); // posting Runnable vid bestämd tid postHandler.postAtTime (new Runnable () @Override public void run () if (callback! = null) callback.get (). showToast ("Toast kallas med" postAtTime () ". ");, schemalagt);  / ** * Beräknar @link SystemClock # uptimeMillis () till * ett givet kalenderdatum. * / Private Long CalculateUptimeMillis (Kalender kalender) long time = calendar.getTimeInMillis (); long currentTime = Calendar.getInstance (). getTimeInMillis (); long diff = time - currentTime; returnera SystemClock.uptimeMillis () + diff; 
public class RunnableActivity utökar Aktivitetsverktyg WorkerThread.Callback / ** * Återuppringning från @link WorkerThread * Använder @link #runOnUiThread (Runnable) för att illustrera * en sådan metod * / @Override public void showToast (final string msg)  Log.d (TAG, "showToast (" + msg + ")"); runOnUiThread (new Runnable () @Override public void run () Toast.makeText (getApplicationContext (), msg, Toast.LENGTH_LONG) .show ();); 

3. Skicka meddelanden med MessageActivity & WorkerThread

Låt oss sedan undersöka olika sätt att använda MessageActivity  att skicka och bearbeta Meddelande objekt. De MessageActivity instansierar WorkerThread, passerar a Hanterare som en parameter. De WorkerThread har några offentliga metoder med uppgifter som ska ringas av aktiviteten för att ladda ner en bitmapp, ladda ner en slumpmässig bitmapp eller visa en Rostat bröd efter en viss försenad tid. Resultaten av alla dessa verksamheter skickas tillbaka till MessageActivity använder sig av Meddelande objekt som skickas av responseHandler.

3.1 Förbereda svarhanteraren från MessageActivity

Som i RunnableActivity, i MessageActivity vi måste inställa och initiera en WorkerThread skicka en Hanterare att ta emot data från bakgrundsgängan. Men den här gången kommer vi inte att genomföra WorkerThread.Callback; istället får vi svar från WorkerThread uteslutande av Meddelande objekt.

Sedan de flesta av MessageActivity och RunnableActivity koden är i princip densamma, vi koncentrerar oss bara på uiHandler förberedelse, som kommer att skickas till WorkerThread att ta emot meddelanden från den.

Först låt oss ge några int nycklar som ska användas som identifierare till meddelandeobjekten.

public class MessageActivity utökar aktivitet // Meddelandekod som används i Message.what () -fältet offentliga statiska slutliga int KEY_MSG_IMAGE = 2; offentlig statisk slutlig int KEY_MSG_PROGRESS = 3; offentlig statisk slutlig int KEY_MSG_TOAST = 4; 

message genomförandet måste vi förlänga Hanterare och implementera handleMessage (Message) metod där alla meddelanden kommer att behandlas. Observera att vi hämtar Message.what för att identifiera meddelandet, och vi får också olika typer av data från Message.obj. Låt oss snabbt granska det viktigaste Meddelande egenskaper innan du dyker in i koden.

  • Message.whatint identifiera Meddelande
  • Message.arg1int godtyckligt argument
  • Message.arg2int godtyckligt argument
  • Message.objObjekt att lagra olika typer av data
public class MessageActivity utökar aktivitet / ** * Handler ansvarig för att hantera kommunikation * från @link WorkerThread. Den skickar meddelanden * tillbaka till @link MessageActivity och hanterar * de meddelandena * / public class MessageHandler utökar hanteraren @Override public void handleMessage (Meddelande msg) switch (msg.what) // hantera bildfallet KEY_MSG_IMAGE:  Bitmapp bmp = (Bitmapp) msg.obj; myImage.setImageBitmap (BMP); ha sönder;  // hantera framstegBar samtal fall KEY_MSG_PROGRESS: if ((boolean) msg.obj) progressBar.setVisibility (View.VISIBLE); annars progressBar.setVisibility (View.GONE); ha sönder;  // hantera toast skickat med ett meddelande för fördröjning av meddelande KEY_MSG_TOAST: String msgText = (String) msg.obj; Toast.makeText (getApplicationContext (), msgText, Toast.LENGTH_LONG) .show (); ha sönder;  // Handler som tillåter kommunikation mellan // WorkerThread och den aktivitetsskyddade MessageHandler uiHandler;  

3.2 Skicka meddelanden med WorkerThread

Låt oss nu komma tillbaka till WorkerThread klass. Vi lägger till en kod för att ladda ner en specifik bitmapp och även kod för att ladda ner en slumpmässig. För att utföra dessa uppgifter skickar vi Meddelande föremål från WorkerThread till sig själv och skicka resultaten tillbaka till MessageActivity använder exakt samma logik som tidigare tillämpats för RunnableActivity.

Först måste vi förlänga Hanterare att bearbeta de nedladdade meddelandena.

offentlig klass WorkerThread utökar HandlerThread // skicka och behandla nedladdning Meddelanden på WorkerThread privat HandlerMsgImgDownloader handlerMsgImgDownloader; / ** * Nycklar för att identifiera nycklarna till @link Message # what * från meddelanden som skickas av @link #handlerMsgImgDownloader * / privat slutlig int MSG_DOWNLOAD_IMG = 0; // msg som hämtar en enda IMG privat slutlig int MSG_DOWNLOAD_RANDOM_IMG = 1; // msg som hämtar slumpmässigt img / ** * Handler ansvarig för hanteringen av bildnedladdning * Den skickar och hanterar meddelanden som identifierar sedan med * @link Message # what * @link #MSG_DOWNLOAD_IMG: enstaka bild * @link #MSG_DOWNLOAD_RANDOM_IMG: slumpmässig bild * / privat klass HandlerMsgImgDownloader utökar Handler privat HandlerMsgImgDownloader (Looper Looper) super (looper);  @Override public void handleMessage (Meddelande msg) showProgressMSG (true); switch (msg.what) case MSG_DOWNLOAD_IMG: // tar emot en enda webbadress och laddar ner den String url = (String) msg.obj; downloadImageMSG (url); ha sönder;  fallet MSG_DOWNLOAD_RANDOM_IMG: // tar emot en sträng [] med flera webbadresser // hämtar en bild slumpmässigt String [] urls = (String []) msg.obj; Slumpmässig slumpmässig = ny slumpmässig (); String url = url [random.nextInt (urls.length)]; downloadImageMSG (url);  showProgressMSG (false); 

De downloadImageMSG (String url) Metoden är i grunden densamma som downloadImage (String url) metod. Den enda skillnaden är att den första skickar den nedladdade bitmappen tillbaka till användargränssnittet genom att skicka ett meddelande med hjälp av responseHandler.

offentlig klass WorkerThread utökar HandlerThread / ** * Ladda ner en bitmapp med dess webbadress och * visa den till användargränssnittet. * Den enda skillnaden med @link #downloadImage (String) * är att den skickar bilden tillbaka till användargränssnittet * med ett meddelande * / privat tomt downloadImageMSG (String urlStr) // Skapa en anslutning HttpURLConnection connection = null; prova URL url = ny URL (urlStr); anslutning = (HttpURLConnection) url.openConnection (); // få strömmen från url InputStream in = nya BufferedInputStream (connection.getInputStream ()); Final Bitmap bitmap = BitmapFactory.decodeStream (in); om (bitmapp! = null) // skicka bitmappen nerladdad och en återkoppling till UI loadImageOnUIMSG (bitmapp);  fånga (IOException e) e.printStackTrace ();  äntligen om (anslutning! = null) connection.disconnect (); 

De loadImageOnUIMSG (Bitmap-bild) ansvarar för att skicka ett meddelande med den nedladdade bitmappen till MessageActivity

 / ** * skickar en bitmapp till ui * skickar ett meddelande till @link #responseHandler * / privat void loadImageOnUIMSG (slutlig bitmappsbild) if (checkResponse ()) sendMsgToUI (responseHandler.get (). MessageActivity.KEY_MSG_IMAGE, bild));  / ** * Visa / Dölj progressBar på användargränssnittet. * Den använder @link #responseHandler till * skicka ett meddelande på UI * / privat void showProgressMSG (boolean show) Log.d (TAG, "showProgressMSG ()"); om (checkResponse ()) sendMsgToUI (responseHandler.get (). obtainMessage (MessageActivity.KEY_MSG_PROGRESS, visa)); 

Observera att istället för att skapa en Meddelande objekt från början, vi använder Handler.obtainMessage (int vad, Objekt obj) metod för att hämta a Meddelande från den globala poolen, spara några resurser. Det är också viktigt att notera att vi ringer till obtainMessage ()responseHandler, få en Meddelande associerad med MessageActivity's Looper. Det finns två sätt att hämta en Meddelande från den globala poolen: Message.obtain () och Handler.obtainMessage ().

Det enda som kvarstår vid uppladdning av uppgiften är att ge metoderna att skicka en Meddelande till WorkerThread för att starta nedladdningsprocessen. Observera att den här gången vi ringer Message.obtain (Handler handler, int vad, Objekt obj) på handlerMsgImgDownloader, associera meddelandet med WorkerThreads looper.

 / ** * skickar ett meddelande till den aktuella tråden * med hjälp av @link #handlerMsgImgDownloader * för att ladda ner en enda bild. * / public void downloadWithMessage () Log.d (TAG, "downloadWithMessage ()"); showOperationOnUIMSG ("Skickar meddelande ..."); om (handlerMsgImgDownloader == null) handlerMsgImgDownloader = ny HandlerMsgImgDownloader (getLooper ()); Message message = Message.obtain (handlerMsgImgDownloader, MSG_DOWNLOAD_IMG, imageBUrl); handlerMsgImgDownloader.sendMessage (message);  / ** * skickar ett meddelande till den aktuella tråden * med hjälp av @link #handlerMsgImgDownloader * för att ladda ner en slumpmässig bild. * / public void downloadRandomWithMessage () Log.d (TAG, "downloadRandomWithMessage ()"); showOperationOnUIMSG ("Skickar meddelande ..."); om (handlerMsgImgDownloader == null) handlerMsgImgDownloader = ny HandlerMsgImgDownloader (getLooper ()); Message message = Message.obtain (handlerMsgImgDownloader, MSG_DOWNLOAD_RANDOM_IMG, imagesUrls); handlerMsgImgDownloader.sendMessage (message); 

En annan intressant möjlighet är att skicka Meddelande objekt som ska behandlas vid ett senare tillfälle med kommandot Message.sendMessageDelayed (Message msg, long timeMillis).

/ ** * Visa en skål efter en försenad tid. * * Skicka ett meddelande med försenad tid på WorkerThread * och skicka ett nytt meddelande till @link MessageActivity * med en text efter att meddelandet har behandlats * / public void startMessageDelay () // meddelandefördröjning lång fördröjning = 5000; String msgText = "Hej från WorkerThread!"; // Handler ansvarig för att skicka meddelande till WorkerThread // med hjälp av Handler.Callback () för att undvika behovet av att förlänga Handler-klassen Handler handler = ny Handler (new Handler.Callback () @Override public boolean handleMessage (Message msg) responseHandler .get (). sendMessage (responseHandler.get (). obtainMessage (MessageActivity.KEY_MSG_TOAST, msg.obj)); returnera true;); // skicka meddelandehandler.sendMessageDelayed (handler.obtainMessage (0, msgText), delay); 

Vi skapade en Hanterare uttryckligen för att skicka det försenade meddelandet. Istället för att förlänga Hanterare klass tog vi vägen att instansera a Hanterare genom att använda Handler.Callback gränssnitt, för vilket vi genomförde handleMessage (Message msg) Metod för att bearbeta fördröjda Meddelande.

4. Slutsats

Du har nu sett tillräckligt med kod för att förstå hur man tillämpar de grundläggande HaMeR-ramkoncepten för att hantera samtidighet på Android. Det finns några andra intressanta funktioner i det slutliga projektet som lagras på GitHub, och jag rekommenderar starkt att du kolla in det. 

Slutligen har jag några sista överväganden som du bör tänka på:

  • Glöm inte att överväga Androids aktivitetslivscykel när man arbetar med HaMeR och trådar i allmänhet. Annars kan din app misslyckas när tråden försöker få åtkomst till aktiviteter som har förstörts på grund av konfigurationsändringar eller av andra skäl. En vanlig lösning är att använda a RetainedFragment att lagra tråden och fylla i bakgrundsgängan med aktivitetens referens varje gång aktiviteten förstörs. Ta en titt på lösningen i det slutliga projektet på GitHub.
  • Uppgifter som körs på grund av Runnable och Meddelande föremål bearbetade på Handlar kör inte asynkront. De körs synkront på tråden som är associerad med hanteraren. För att göra den asynkron måste du skapa en annan tråd, skicka / skicka den Meddelande/Runnable objekt på det och ta emot resultaten vid rätt tidpunkt.

Som du kan se har HaMeR-ramen många olika möjligheter, och det är en ganska öppen lösning med många alternativ för hantering av samtidighet på Android. Dessa egenskaper kan vara fördelar över AsyncTask, beroende på dina behov. Utforska mer av ramverket och läs dokumentationen, och du kommer att skapa bra saker med det.

Ses snart!