Praktische concurrency op Android met HaMeR

In Understanding Concurrency op Android met HaMeR hebben we gesproken over de basisprincipes van de HaMeR (handler, Bericht, en uitvoerbare) kader. We hebben de opties behandeld, evenals wanneer en hoe het te gebruiken. 

Vandaag zullen we een eenvoudige applicatie maken om de geleerde concepten te verkennen. Met een praktische benadering zullen we zien hoe we de verschillende mogelijkheden van HaMeR kunnen toepassen bij het beheren van concurrency op Android.

1. De voorbeeldapplicatie

Laten we aan de slag gaan en wat posten uitvoerbare en verzend Bericht objecten op een voorbeeldtoepassing. Om het zo eenvoudig mogelijk te houden, zullen we alleen de meest interessante delen verkennen. Alle bronbestanden en standaardactiviteitsoproepen worden hier genegeerd. Daarom raad ik u ten zeerste aan de broncode van de voorbeeldtoepassing te bekijken met zijn uitgebreide opmerkingen. 

De app zal bestaan ​​uit:

  • Twee activiteiten, een voor uitvoerbare een andere voor Bericht calls
  • Twee HandlerThread voorwerpen:
    • WorkerThread om oproepen van de gebruikersinterface te ontvangen en te verwerken
    • CounterThread ontvangen Bericht oproepen van de WorkerThread
  • Sommige utiliteitsklassen (om objecten te behouden tijdens configuratiewijzigingen en voor lay-out)

2. Runnables plaatsen en ontvangen

Laten we beginnen met het experimenteren met de Handler.post (Runnable) methode en de bijbehorende variaties, die een a toevoegen aan een message queue geassocieerd met een thread. We maken een activiteit genaamd RunnableActivity, die communiceert met een geroepen achtergronddraad WorkerThread.

De RunnableActivity maakt een achtergronddraad die wordt aangeroepen WorkerThread, passeren van een handler en een WorkerThread.Callback als parameters. De activiteit kan oproepen aannemen WorkerThread om asynchroon een bitmap te downloaden en op een bepaald moment een toast te tonen. De resultaten van de taken die door de werkthread worden uitgevoerd, worden doorgegeven aan RunnableActivity door runnables geplaatst op de handler ontvangen door WorkerThread.

2.1 Een handler voorbereiden voor RunnableActivity

Op de RunnableActivity we zullen een maken handler worden doorgegeven aan WorkerThread. De uiHandler zal worden geassocieerd met de lussenmaker van de UI-thread, omdat het wordt aangeroepen vanuit die thread.

public class RunnableActivity breidt Activiteit uit // Handler die communicatie tussen // the WorkerThread en de Activity-handler uiHandler mogelijk maakt; @Override protected void onCreate (Bundle savedInstanceState) super.onCreate (savedInstanceState); setContentView (R.layout.activity_main); // bereid de UI-handler voor op verzenden naar WorkerThread uiHandler = new Handler (); 

2.2 Verklaringen WorkerThread en de terugbelinterface

De WorkerThread is een achtergrondthema waar we verschillende soorten taken zullen starten. Het communiceert met de gebruikersinterface met behulp van de responseHandler en een terugbelinterface ontvangen tijdens de instantiatie ervan. De referenties ontvangen van de activiteiten zijn WeakReference <> type, omdat een activiteit kan worden vernietigd en de referentie verloren gaat.

De klasse biedt een interface die kan worden geïmplementeerd door de gebruikersinterface. Het breidt zich ook uit HandlerThread, een helperklasse gebouwd bovenop Draad die al een bevat lussenmaker, en een message queue. Vandaar dat het correct is opstellingom het HaMeR-raamwerk te gebruiken.

openbare klasse WorkerThread breidt HandlerThread / ** * -interface uit om oproepen in de gebruikersinterface mogelijk te maken. * / openbare interface Callback void loadImage (Bitmap-afbeelding); void showToast (String msg);  // Deze handler is alleen verantwoordelijk // voor het plaatsen van Runnables op deze Thread private Handler postHandler; // Handler wordt ontvangen van de MessageActivity en RunnableActivity // die verantwoordelijk zijn voor het ontvangen van bruikbare oproepen die worden verwerkt // in de gebruikersinterface. De callback zal dit proces helpen. private WeakReference responseHandler; // Terugbellen vanuit de gebruikersinterface // het is een WeakReference omdat het ongeldig kan worden // tijdens "configuratiewijzigingen" en andere gebeurtenissen private WeakReference Bel terug; private final String imageAUrl = "https://pixabay.com/static/uploads/photo/2016/08/05/18/28/mobile-phone-1572901_960_720.jpg"; / ** * De constructor ontvangt een handler en een terugroepactie van de gebruikersinterface * @param responseHandler die verantwoordelijk is voor het plaatsen van het uitvoerbare bestand naar de UI * @param callback werkt samen met de responseHandler * waarmee rechtstreeks kan worden gebeld op de UI * / public WorkerThread (handler) responseHandler, callback callback) super (TAG); this.responseHandler = new WeakReference <> (responseHandler); this.callback = new WeakReference <> (callback); 

2.3 Initialiseren WorkerThread

We moeten een methode toevoegen aan WorkerThread worden aangeroepen door de activiteiten die de thread voorbereiden postHandler voor gebruik. De methode hoeft alleen te worden aangeroepen nadat de thread is gestart.

public class WorkerThread breidt HandlerThread uit / ** * Bereid de postHandler voor. * Het moet worden opgeroepen nadat de thread is gestart * / public void prepareHandler () postHandler = new Handler (getLooper ()); 

Op de RunnableActivity we moeten implementeren WorkerThread.Callback en initialiseer de thread zodat deze kan worden gebruikt.

public class RunnableActivity breidt activiteiten uit Werkt WorkThread.Callback // BackgroundThread verantwoordelijk voor het downloaden van de image protected WorkerThread workerThread; / ** * Initialiseer de @link WorkerThread -instantie * alleen als deze nog niet is geïnitialiseerd. * / public void initWorkerThread () if (workerThread == null) workerThread = new WorkerThread (uiHandler, this); workerThread.start (); workerThread.prepareHandler ();  / ** * zet de afbeelding die is gedownload op bg-thread naar de imageView * / @Override public void loadImage (Bitmap image) myImage.setImageBitmap (image);  @Override openbare ongeldige showToast (laatste string-msg) // te implementeren

2.4 Gebruik Handler.post () op de WorkerThread

De WorkerThread.downloadWithRunnable () methode downloadt een bitmap en stuurt deze naar RunnableActivity worden weergegeven in een afbeeldingsweergave. Het illustreert twee basisgebruiken van de Handler.post (loopbare run) commando:

  • Een thread toestaan ​​een werkbaar object te plaatsen bij een MessageQueue die aan zichzelf is gekoppeld wanneer .post() wordt aangeroepen op een handler die is gekoppeld aan de Thread's Looper.
  • Om communicatie met andere threads mogelijk te maken, wanneer .post() wordt aangeroepen op een handler die is gekoppeld aan andere Thread's Looper.
  1. De WorkerThread.downloadWithRunnable () methode posts a uitvoerbare naar de WorkerThread's message queue de ... gebruiken postHandler, een handler geassocieerd met WorkThread's lussenmaker.
  2. Wanneer het uitvoerbare bestand is verwerkt, wordt er een gedownload Bitmap op de WorkerThread.
  3. Nadat de bitmap is gedownload, wordt de responseHandler, een handler die is gekoppeld aan de UI-thread, wordt gebruikt om een ​​run te plaatsen op de RunnableActivity met de bitmap.
  4. Het uitvoerbare wordt verwerkt en de WorkerThread.Callback.loadImage wordt gebruikt om de gedownloade afbeelding weer te geven op een Figuurweergave.
public class WorkerThread breidt HandlerThread uit / ** * post een Runnable naar de WorkerThread * Download een bitmap en verzend de afbeelding * naar de UI @link RunnableActivity * met behulp van de @link #responseHandler met * help van de @link #callback * / public void downloadWithRunnable () // post Runnable to WorkerThread postHandler.post (nieuw Runnable () @Override public void run () try // slaapt gedurende 2 seconden om een ​​langlopende bewerking te emuleren TimeUnit.SECONDS .sleep (2); // Download afbeelding en verzendt naar UI downloadImage (imageAUrl); catch (InterruptedException e) e.printStackTrace (););  / ** * Download een bitmap met behulp van de URL en * stuur naar de UI de gedownloade afbeelding * / private void downloadImage (String urlStr) // Maak een verbinding HttpURLConnection connection = null; probeer URL url = nieuwe URL (urlStr); connection = (HttpURLConnection) url.openConnection (); // haal de stream uit de url InputStream in = new BufferedInputStream (connection.getInputStream ()); laatste Bitmap bitmap = BitmapFactory.decodeStream (in); if (bitmap! = null) // stuur de bitmap gedownload en een feedback naar de UI loadImageOnUI (bitmap);  else  catch (IOException e) e.printStackTrace ();  eindelijk if (connection! = null) connection.disconnect ();  / ** * stuurt een Bitmap naar de ui * een bericht naar de @link #responseHandler * posting en gebruikt de @link Callback * / private void loadImageOnUI (laatste bitmapafbeelding) Log.d (TAG, "loadImageOnUI (" + afbeelding + ")"); if (checkResponse ()) responseHandler.get (). post (nieuw Runnable () @Override public void run () callback.get (). loadImage (image););  // verifieer of responseHandler beschikbaar is // zo niet, dan gaat de activiteit voorbij aan een of andere vernietigingsgebeurtenis. private boolean checkResponse () return responseHandler.get ()! = null; 

2.5 Gebruiken Handler.postAtTime () en Activity.runOnUiThread ()

De WorkerThread.toastAtTime ()plant een taak die op een bepaald moment moet worden uitgevoerd, met een Geroosterd brood voor de gebruiker. De methode illustreert het gebruik van de Handler.postAtTime () en de Activity.runOnUiThread ().

  • Handler.postAtTime (Loopbare run, lange uptimeMillis) posts een loopbare op een gegeven moment.
  • Activity.runOnUiThread (Runnable run) gebruikt de standaard UI-handler om een ​​variabele naar de hoofdthread te posten.
public class WorkerThread breidt HandlerThread uit / ** * toont een Toast in de gebruikersinterface. * plant de taak op basis van de huidige tijd. * Het kan op elk moment worden gepland, we gebruiken * 5 seconden om het debuggen te vergemakkelijken * / public void toastAtTime () Log.d (TAG, "toastAtTime (): current -" + Calendar.getInstance (). ToString ()); // seconden om toe te voegen op de huidige tijd int delaySeconds = 5; // testing using a real date Calendar scheduledDate = Calendar.getInstance (); / / instellen van een datum in de toekomst gezien de vertraging in seconden definiëren // we gebruiken deze aanpak alleen maar om het testen te vergemakkelijken. // het kan worden gedaan met behulp van een door de gebruiker gedefinieerde datum ook scheduledDate.set (scheduledDate.get (Calendar.YEAR), scheduledDate.get (Calendar.MONTH), scheduledDate.get (Calendar.DAY_OF_MONTH), scheduledDate.get (Calendar.HOUR_OF_DAY ), scheduledDate.get (Calendar.MINUTE), scheduledDate.get (Calendar.SECOND) + delaySeconds); Log.d (TAG, "toastAtTime (): planning op -" + scheduledDate.toString ()); lang gepland = calculationUptimeMillis (scheduledDate); // posting Runnable op specifieke tijd postHandler.postAtTime (nieuw Runnable () @Override public void run () if (callback! = null) callback.get (). showToast ("Toast called using 'postAtTime ()'. ");, gepland);  / ** * Berekent de @link SystemClock # uptimeMillis () tot * een bepaalde kalenderdatum. * / private lange calculationUptimeMillis (Calendar calendar) long time = calendar.getTimeInMillis (); long currentTime = Calendar.getInstance (). getTimeInMillis (); long diff = time - currentTime; return SystemClock.uptimeMillis () + diff; 
public class RunnableActivity breidt activiteiten uit WorkerThread.Callback / ** * Callback van @link WorkerThread * Gebruik @link #runOnUiThread (Runnable) om * dergelijke methode * / @Override public-holt showToast (slot-string msg)  Log.d (TAG, "showToast (" + msg + ")"); runOnUiThread (nieuw Runnable () @Override public void run () Toast.makeText (getApplicationContext (), msg, Toast.LENGTH_LONG) .show ();); 

3. Berichten verzenden met de MessageActivity & WorkerThread

Laten we vervolgens een aantal verschillende manieren van gebruik verkennen MessageActivity  verzenden en verwerken Bericht voorwerpen. De MessageActivity concretiseert WorkerThread, passeren van een handler als een parameter. De WorkerThread heeft een aantal openbare methoden met taken die door de activiteit moeten worden aangeroepen om een ​​bitmap te downloaden, een willekeurige bitmap te downloaden of een a te vertonen Geroosterd brood na enige uitgestelde tijd. De resultaten van al die bewerkingen worden teruggestuurd naar MessageActivity gebruik makend van Bericht objecten verzonden door de responseHandler.

3.1 De Response Handler voorbereiden vanaf MessageActivity

Als in de RunnableActivity, in de MessageActivity we zullen een a moeten instantiëren en initialiseren WorkerThread verzenden van een handler om gegevens van de achtergronddraad te ontvangen. Deze keer zullen we echter niet implementeren WorkerThread.Callback; in plaats daarvan ontvangen we reacties van de WorkerThread exclusief door Bericht voorwerpen.

Aangezien de meeste van de MessageActivity en RunnableActivity code is in principe hetzelfde, we concentreren ons alleen op de uiHandler voorbereiding, die wordt verzonden naar WorkerThread om er berichten van te ontvangen.

Laten we eerst wat verstrekken int toetsen die moeten worden gebruikt als ID's voor de Message-objecten.

public class MessageActivity breidt Activiteit uit // Bericht-ID gebruikt op Message.what () veld public static final int KEY_MSG_IMAGE = 2; public static final int KEY_MSG_PROGRESS = 3; public static final int KEY_MSG_TOAST = 4; 

Op messageHandler implementatie, moeten we uitbreiden handler en implementeer de handleMessage (Message) methode, waar alle berichten worden verwerkt. Merk op dat we aan het halen zijn Message.what om het bericht te identificeren en we krijgen ook verschillende soorten gegevens van Message.obj. Laten we snel de belangrijkste bekijken Bericht eigenschappen voordat u in de code duikt.

  • Message.whatint het identificeren van de Bericht
  • Message.arg1int willekeurig argument
  • Message.arg2int willekeurig argument
  • Message.objVoorwerp om verschillende soorten gegevens op te slaan
public class MessageActivity breidt activiteit uit / ** * Handler verantwoordelijk voor het beheren van communicatie * vanuit de @link WorkerThread. Het stuurt Berichten * terug naar de @link MessageActivity en verwerkt * die Berichten * / public class MessageHandler verlengt Handler @Override public void handleMessage (Message msg) switch (msg.what) // behandel afbeelding case KEY_MSG_IMAGE:  Bitmap bmp = (Bitmap) msg.obj; myImage.setImageBitmap (BMP); breken;  // handle progressBar calls case KEY_MSG_PROGRESS: if ((boolean) msg.obj) progressBar.setVisibility (View.VISIBLE); else progressBar.setVisibility (View.GONE); breken;  // omgaan met toast verzonden met een berichtvertragingsgeval KEY_MSG_TOAST: String msgText = (String) msg.obj; Toast.makeText (getApplicationContext (), msgText, Toast.LENGTH_LONG) .show (); breken;  // Handler voor communicatie tussen // the WorkerThread en de activiteitsbeschermde MessageHandler uiHandler;  

3.2 Berichten verzenden met WorkThread

Laten we nu teruggaan naar de WorkerThread klasse. We zullen een code toevoegen om een ​​specifieke bitmap te downloaden en ook code om een ​​willekeurige bitmap te downloaden. Om deze taken te volbrengen, sturen we Bericht objecten uit de WorkerThread naar zichzelf en stuur de resultaten terug naar MessageActivity met behulp van exact dezelfde logica eerder toegepast voor de RunnableActivity.

Eerst moeten we het handler om de gedownloade berichten te verwerken.

openbare klasse WorkerThread breidt HandlerThread uit // verzendt en verwerkt downloadberichten Berichten op de WorkerThread privé HandlerMsgImgDownloader handlerMsgImgDownloader; / ** * Toetsen om de sleutels van @link Message # what * te identificeren van berichten die zijn verzonden door @link #handlerMsgImgDownloader * / private final int MSG_DOWNLOAD_IMG = 0; // msg dat download een enkele img private finale int MSG_DOWNLOAD_RANDOM_IMG = 1; // msg die willekeurige img downloaden / ** * Handler die verantwoordelijk is voor het beheren van de afbeeldingdownload * Het verzendt en verwerkt berichten die identificeren en gebruikt vervolgens * het @link Message # what * @link #MSG_DOWNLOAD_IMG: één afbeelding * @link #MSG_DOWNLOAD_RANDOM_IMG: willekeurige afbeelding * / private class HandlerMsgImgDownloader breidt Handler uit private HandlerMsgImgDownloader (Looper looper) super (looper);  @Override public void handleMessage (Message msg) showProgressMSG (true); switch (msg.what) case MSG_DOWNLOAD_IMG: // ontvangt één URL en download deze String url = (String) msg.obj; downloadImageMSG (url); breken;  case MSG_DOWNLOAD_RANDOM_IMG: // ontvangt een String [] met meerdere urls // downloadt willekeurig een afbeelding String [] urls = (String []) msg.obj; Willekeurig willekeurig = nieuw Willekeurig (); String url = urls [random.nextInt (urls.length)]; downloadImageMSG (url);  showProgressMSG (false); 

De downloadImageMSG (String-URL) methode is in principe hetzelfde als de downloadImage (reeks-URL) methode. Het enige verschil is dat de eerste de gedownloade bitmap terugzendt naar de gebruikersinterface door een bericht te verzenden met behulp van de responseHandler.

openbare klasse WorkerThread breidt HandlerThread uit / ** * Download een bitmap met behulp van de URL en * geef deze weer aan de gebruikersinterface. * Het enige verschil met @link #downloadImage (String) * is dat het de afbeelding terugstuurt naar de UI * met een Message * / private void downloadImageMSG (String urlStr) // Maak een verbinding HttpURLConnection connection = null; probeer URL url = nieuwe URL (urlStr); connection = (HttpURLConnection) url.openConnection (); // haal de stream uit de url InputStream in = new BufferedInputStream (connection.getInputStream ()); laatste Bitmap bitmap = BitmapFactory.decodeStream (in); if (bitmap! = null) // stuur de bitmap gedownload en een feedback naar de UI loadImageOnUIMSG (bitmap);  catch (IOException e) e.printStackTrace ();  eindelijk if (connection! = null) connection.disconnect (); 

De loadImageOnUIMSG (Bitmap-afbeelding) is verantwoordelijk voor het verzenden van een bericht met de gedownloade bitmap naar MessageActivity

 / ** * stuurt een Bitmap naar de ui * een bericht verzenden naar de @link #responseHandler * / private void loadImageOnUIMSG (laatste bitmapafbeelding) if (checkResponse ()) sendMsgToUI (responseHandler.get (). obtainMessage ( MessageActivity.KEY_MSG_IMAGE, afbeelding));  / ** * Show / Hide progressBar in de gebruikersinterface. * Het gebruikt de @link #responseHandler om * een bericht te verzenden op de UI * / private void showProgressMSG (booleaanse show) Log.d (TAG, "showProgressMSG ()"); if (checkResponse ()) sendMsgToUI (responseHandler.get (). obtainMessage (MessageActivity.KEY_MSG_PROGRESS, show)); 

Merk op dat in plaats van het creëren van een Bericht object from scratch, we gebruiken de Handler.obtainMessage (int what, Object obj) methode om een ​​te verkrijgen Bericht van de globale pool, wat middelen bespaart. Het is ook belangrijk op te merken dat we de obtainMessage () op de responseHandler, verkrijgen van een Bericht geassocieerd met MessageActivity's lussenmaker. Er zijn twee manieren om een ​​te verkrijgen Bericht uit de global pool: Message.obtain () en Handler.obtainMessage ().

Het enige dat u nog moet doen aan de downloadtaak voor afbeeldingen, is het bieden van de verzendmethoden a Bericht naar WorkerThread om het downloadproces te starten. Merk op dat we dit keer zullen bellen Message.obtain (Handlerhandler, int what, Object obj) op handlerMsgImgDownloader, associëren van het bericht met WorkerThreadlooper.

 / ** * stuurt een bericht naar de huidige thread * met behulp van de @link #handlerMsgImgDownloader * om één afbeelding te downloaden. * / public void downloadWithMessage () Log.d (TAG, "downloadWithMessage ()"); showOperationOnUIMSG ("Bericht verzenden ..."); if (handlerMsgImgDownloader == null) handlerMsgImgDownloader = new HandlerMsgImgDownloader (getLooper ()); Message message = Message.obtain (handlerMsgImgDownloader, MSG_DOWNLOAD_IMG, imageBUrl); handlerMsgImgDownloader.sendMessage (bericht);  / ** * stuurt een bericht naar de huidige thread * met behulp van de @link #handlerMsgImgDownloader * om een ​​willekeurige afbeelding te downloaden. * / public void downloadRandomWithMessage () Log.d (TAG, "downloadRandomWithMessage ()"); showOperationOnUIMSG ("Bericht verzenden ..."); if (handlerMsgImgDownloader == null) handlerMsgImgDownloader = new HandlerMsgImgDownloader (getLooper ()); Berichtbericht = Message.obtain (handlerMsgImgDownloader, MSG_DOWNLOAD_RANDOM_IMG, imagesUrls); handlerMsgImgDownloader.sendMessage (bericht); 

Een andere interessante mogelijkheid is verzenden Bericht objecten die op een later tijdstip moeten worden verwerkt met het commando Message.sendMessageDelayed (Message msg, long timeMillis).

/ ** * Laat een toast zien na een uitgestelde tijd. * * stuur een bericht met vertraagde tijd op de WorkerThread * en verzendt een nieuw bericht naar @link MessageActivity * met een tekst nadat het bericht is verwerkt * / public void startMessageDelay () // bericht vertraging lange vertraging = 5000; String msgText = "Hallo van WorkerThread!"; // Handler die verantwoordelijk is voor het verzenden van Message naar WorkerThread // met Handler.Callback () om te voorkomen dat de handlerklasse moet worden uitgebreid Handlerhandler = new Handler (nieuwe handler.Callback () @Override public Boolean handleMessage uitschakelen (berichtbericht) responseHandler .get (). sendMessage (responseHandler.get (). obtainMessage (MessageActivity.KEY_MSG_TOAST, msg.obj)); return true;); // send message handler.sendMessageDelayed (handler.obtainMessage (0, msgText), delay); 

We hebben een gemaakt handler uitdrukkelijk voor het verzenden van het vertraagde bericht. In plaats van de extensie uit te breiden handler klas, we hebben de route van instantiëren genomen handler door de Handler.Callback interface, waarvoor we het handleMessage (berichtbericht) methode om vertragingen te verwerken Bericht.

4. Conclusie

Je hebt nu genoeg code gezien om te begrijpen hoe je de basale HaMeR-raamwerkconcepten kunt toepassen om concurrency op Android te beheren. Er zijn enkele andere interessante kenmerken van het uiteindelijke project opgeslagen op GitHub, en ik adviseer je ten stelligste om het te bekijken. 

Tot slot, ik heb enkele laatste overwegingen die u in gedachten moet houden:

  • Vergeet niet de activiteitslevenscyclus van Android te overwegen bij het werken met HaMeR en Threads in het algemeen. Anders kan uw app mislukken wanneer de thread toegang probeert te krijgen tot activiteiten die zijn vernietigd als gevolg van wijzigingen in de configuratie of om andere redenen. Een veel voorkomende oplossing is om a te gebruiken RetainedFragment om de thread op te slaan en de achtergrondthread te vullen met de verwijzing naar de activiteit telkens wanneer de activiteit wordt vernietigd. Bekijk de oplossing in het laatste project op GitHub.
  • Taken die worden uitgevoerd als gevolg van uitvoerbare en Bericht objecten verwerkt op handlers niet asynchroon uitvoeren. Ze worden synchroon uitgevoerd op de thread die aan de handler is gekoppeld. Als u het asynchroon wilt maken, moet u nog een thread maken, het verzenden / plaatsen Bericht/uitvoerbare object erop en ontvang de resultaten op het juiste moment.

Zoals je ziet, heeft het HaMeR-framework veel verschillende mogelijkheden en het is een vrij open oplossing met veel opties voor het beheren van concurrency op Android. Deze kenmerken kunnen voordelen opleveren AsyncTask, afhankelijk van uw behoeften. Verken meer van het framework en lees de documentatie, en je zult er geweldige dingen mee creëren.

Tot ziens!