Konkurensi Praktis di Android dengan HaMeR
Indonesian (Bahasa Indonesia) translation by ⚡ Rova Rindrata (you can also view the original English article)
Dalam Memahami Konkurensi di Android Menggunakan HaMeR, kita membahas dasar-dasar kerangka kerja HaMeR (Handler
, Message
, dan Runnable
). Kita membahas opsinya, begitu juga kapan dan bagaimana cara menggunakannya.
Hari ini, kita akan membuat aplikasi sederhana untuk mengeksplorasi konsep yang telah dipelajari. Dengan pendekatan langsung, kita akan melihat bagaimana menerapkan berbagai kemungkinan HaMeR dalam mengelola konkurensi di Android.
1. Aplikasi Sampel
Mari kita bekerja dan mengirim beberapa Runnable
dan mengirim objek Message
pada aplikasi sampel. Untuk membuatnya sesederhana mungkin, kita akan mengeksplorasi bagian-bagian yang paling menarik. Semua file sumber daya dan activity standar akan diabaikan di sini. Jadi saya sangat menyarankan Anda untuk memeriksa kode sumber dari aplikasi sampel dengan komentar ekstensifnya.



Aplikasi ini terdiri dari:
- Dua activities, satu untuk
Runnable
lainnya untuk panggilanMessage
- Dua objek
HandlerThread
:-
WorkerThread
untuk menerima dan memproses panggilan dari UI -
CounterThread
untuk menerimaMessage
dariWorkerThread
-
- Beberapa kelas utilitas (untuk menyimpan objek selama perubahan konfigurasi dan untuk layout)
2. Posting dan Menerima Runnables
Mari mulai bereksperimen dengan metode Handler.post(Runnable)
dan variasinya, yang menambahkan runnable ke MessageQueue
yang terkait dengan sebuah thread. Kita akan membuat sebuah activity bernama RunnableActivity
, yang berkomunikasi dengan thread latar belakang yang disebut WorkerThread
.
RunnableActivity
menginstansiasi sebuah thread latar belakang yang disebut WorkerThread
, mengirimkan Handler
dan sebuah WorkerThread.Callback
sebagai parameter. Activity tersebut dapat membuat panggilan ke WorkerThread
untuk mengunduh bitmap secara asinkron dan mempertunjukkannya pada waktu tertentu. Hasil tugas yang dilakukan oleh thread worker dikirim ke RunnableActivity
oleh runnable yang diposkan di Handler
yang diterima oleh WorkerThread
.
2.1 Mempersiapkan Handler untuk RunnableActivity
Pada RunnableActivity
kita akan membuat Handler
untuk dikirimkan ke WorkerThread
. uiHandler
akan dikaitkan dengan Looper
dari thread UI, karena dipanggil dari thread itu.
public class RunnableActivity extends Activity { // Handler that allows communication between // the WorkerThread and the Activity protected Handler uiHandler; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // prepare the UI Handler to send to WorkerThread uiHandler = new Handler(); } }
2.2 Mendeklarasikan WorkerThread
dan Antarmuka Callback-nya
WorkerThread
adalah thread latar dimana kita akan memulai berbagai jenis tugas. Ini berkomunikasi dengan antarmuka pengguna menggunakan responseHandler
dan antarmuka callback yang diterima selama instansiasinya. Referensi yang diterima dari activity tersebut adalah tipe WeakReference<>
, karena suatu activity dapat dihancurkan dan rujukannya hilang.
Kelasnya menawarkan antarmuka yang bisa diimplementasikan oleh UI. Ini juga memperluas HandlerThread
, kelas helper yang dibangun di atas Thread
yang sudah berisi Looper
, dan MessageQueue
. Karena itu memiliki setup yang benar untuk menggunakan kerangka kerja HaMeR.
public class WorkerThread extends HandlerThread { /** * Interface to facilitate calls on the UI. */ public interface Callback { void loadImage(Bitmap image); void showToast(String msg); } // This Handler will be responsible only // for posting Runnables on this Thread private Handler postHandler; // Handler is received from the MessageActivity and RunnableActivity // responsible for receiving Runnable calls that will be processed // on the UI. The callback will help this process. private WeakReference<Handler> responseHandler; // Callback from the UI // it is a WeakReference because it can be invalidated // during "configuration changes" and other events private WeakReference<Callback> callback; private final String imageAUrl = "https://pixabay.com/static/uploads/photo/2016/08/05/18/28/mobile-phone-1572901_960_720.jpg"; /** * The constructor receives a Handler and a Callback from the UI * @param responseHandler in charge of posting the Runnable to the UI * @param callback works together with the responseHandler * allowing calls directly on the UI */ public WorkerThread(Handler responseHandler, Callback callback) { super(TAG); this.responseHandler = new WeakReference<>(responseHandler); this.callback = new WeakReference<>(callback); } }
2.3 Inisialisasi WorkerThread
Kita perlu menambahkan metode ke WorkerThread
untuk dipanggil oleh activity yang menyiapkan postHandler
dari thread untuk digunakan. Metode ini perlu dipanggil hanya setelah thread dimulai.
public class WorkerThread extends HandlerThread { /** * Prepare the postHandler. * It must be called after the thread has started */ public void prepareHandler() { postHandler = new Handler(getLooper()); } }
Pada RunnableActivity
kita harus mengimplementasikan WorkerThread.Callback
dan menginisialisasi thread sehingga bisa digunakan.
public class RunnableActivity extends Activity implements WorkerThread.Callback { // BackgroundThread responsible for downloading the image protected WorkerThread workerThread; /** * Initialize the {@link WorkerThread} instance * only if it hasn't been initialized yet. */ public void initWorkerThread(){ if ( workerThread == null ) { workerThread = new WorkerThread(uiHandler, this); workerThread.start(); workerThread.prepareHandler(); } } /** * set the image downloaded on bg thread to the imageView */ @Override public void loadImage(Bitmap image) { myImage.setImageBitmap(image); } @Override public void showToast(final String msg) { // to be implemented } }
2.4 Menggunakan Handler.post()
di WorkerThread
Metode WorkerThread.downloadWithRunnable()
mengunduh bitmap dan mengirimkannya ke RunnableActivity
untuk ditampilkan dalam View gambar. Ini menggambarkan dua penggunaan dasar perintah Handler.post(Runnable run)
:
- Mengizinkan Thread agar memposkan objek Runnable ke MessageQueue yang terkait dengan dirinya sendiri saat
.post()
dipanggil pada Handler yang terkait dengan Looper Thread. - Memungkinkan komunikasi dengan Thread lain, ketika
.post()
dipanggil pada Handler yang terkait dengan Looper Thread lainnya.



- Metode
WorkerThread.downloadWithRunnable()
memposkan sebuahRunnable
keMessageQueue
dariWorkerThread
menggunakanpostHandler
, sebuahHandler
yang terkait denganLooper
WorkThread
. - Saat runnable diproses, ia mengunduh
Bitmap
diWorkerThread
. - Setelah bitmap diunduh,
responseHandler
, handler yang terkait dengan thread UI, digunakan untuk memposting runnable padaRunnableActivity
yang berisi bitmap. - Runnable diproses, dan
WorkerThread.Callback.loadImage
digunakan untuk mempertunjukkan gambar yang diunduh padaImageView
.
public class WorkerThread extends HandlerThread { /** * post a Runnable to the WorkerThread * Download a bitmap and sends the image * to the UI {@link RunnableActivity} * using the {@link #responseHandler} with * help from the {@link #callback} */ public void downloadWithRunnable() { // post Runnable to WorkerThread postHandler.post(new Runnable() { @Override public void run() { try { // sleeps for 2 seconds to emulate long running operation TimeUnit.SECONDS.sleep(2); // Download image and sends to UI downloadImage(imageAUrl); } catch (InterruptedException e) { e.printStackTrace(); } } }); } /** * Download a bitmap using its url and * send to the UI the image downloaded */ private void downloadImage(String urlStr){ // Create a connection HttpURLConnection connection = null; try { URL url = new URL(urlStr); connection = (HttpURLConnection) url.openConnection(); // get the stream from the url InputStream in = new BufferedInputStream(connection.getInputStream()); final Bitmap bitmap = BitmapFactory.decodeStream(in); if ( bitmap != null ) { // send the bitmap downloaded and a feedback to the UI loadImageOnUI( bitmap ); } else { } } catch (IOException e) { e.printStackTrace(); } finally { if ( connection != null ) connection.disconnect(); } } /** * sends a Bitmap to the ui * posting a Runnable to the {@link #responseHandler} * and using the {@link Callback} */ private void loadImageOnUI(final Bitmap image){ Log.d(TAG, "loadImageOnUI("+image+")"); if (checkResponse() ) { responseHandler.get().post( new Runnable() { @Override public void run() { callback.get().loadImage(image); } } ); } } // verify if responseHandler is available // if not the Activity is passing by some destruction event private boolean checkResponse(){ return responseHandler.get() != null; } }
2.5 Menggunakan Handler.postAtTime()
dan Activity.runOnUiThread()
WorkerThread.toastAtTime()
menjadwalkan tugas yang akan dijalankan pada waktu tertentu, menunjukkan Toast
kepada pengguna. Metode ini menggambarkan penggunaan Handler.postAtTime()
dan Activity.runOnUiThread()
.
-
Handler.postAtTime(Runnable run, long uptimeMillis)
memposting sebuah runnable pada waktu tertentu.
-
Activity.runOnUiThread(Runnable run)
menggunakan handler UI default untuk memposting sebuah runnable ke thread utama.
public class WorkerThread extends HandlerThread { /** * show a Toast on the UI. * schedules the task considering the current time. * It could be scheduled at any time, we're * using 5 seconds to facilitates the debugging */ public void toastAtTime(){ Log.d(TAG, "toastAtTime(): current - " + Calendar.getInstance().toString()); // seconds to add on current time int delaySeconds = 5; // testing using a real date Calendar scheduledDate = Calendar.getInstance(); // setting a future date considering the delay in seconds define // we're using this approach just to facilitate the testing. // it could be done using a user defined date also 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(): scheduling at - " + scheduledDate.toString()); long scheduled = calculateUptimeMillis(scheduledDate); // posting Runnable at specific time postHandler.postAtTime( new Runnable() { @Override public void run() { if ( callback != null ) { callback.get().showToast( "Toast called using 'postAtTime()'." ); } } }, scheduled); } /** * Calculates the {@link SystemClock#uptimeMillis()} to * a given Calendar date. */ private long calculateUptimeMillis(Calendar calendar){ long time = calendar.getTimeInMillis(); long currentTime = Calendar.getInstance().getTimeInMillis(); long diff = time - currentTime; return SystemClock.uptimeMillis() + diff; } }
public class RunnableActivity extends Activity implements WorkerThread.Callback { /** * Callback from {@link WorkerThread} * Uses {@link #runOnUiThread(Runnable)} to illustrate * such method */ @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. Mengirim Pesan dengan MessageActivity
& WorkerThread
Selanjutnya, mari menjelajahi beberapa cara yang berbeda untuk menggunakan MessageActivity
untuk mengirim dan memproses objek Message
. MessageActivity
menginstansiasi WorkerThread
, mengirimkan Handler
sebagai parameter. WorkerThread
memiliki beberapa metode public dengan tugas yang harus dipanggil oleh activity untuk mengunduh bitmap, mengunduh bitmap acak, atau menunjukkan Toast
setelah beberapa waktu yang tertunda. Hasil semua operasi tersebut dikirim kembali ke MessageActivity
dengan menggunakan objek Message
yang dikirim oleh responseHandler
.
3.1 Mempersiapkan Handler Respon dari MessageActivity
Seperti dalam RunnableActivity
, dalam MessageActivity
kita harus menginstansiasi dan menginisialisasi WorkerThread
yang mengirimkan Handler
untuk menerima data dari thread latar belakang. Namun, kali ini kita tidak akan mengimplementasikan WorkerThread.Callback
; sebagai gantinya, kita akan menerima tanggapan dari WorkerThread
secara eksklusif oleh objek Message
.
Karena sebagian besar kode MessageActivity
and RunnableActivity
pada dasarnya sama, kita hanya akan berkonsentrasi pada persiapan uiHandler
, yang akan dikirim ke WorkerThread
untuk menerima pesan darinya.
Pertama, mari kita berikan beberapa kunci int
untuk digunakan sebagai pengidentifikasi ke objek Message.
public class MessageActivity extends Activity { // Message identifier used on Message.what() field public static final int KEY_MSG_IMAGE = 2; public static final int KEY_MSG_PROGRESS = 3; public static final int KEY_MSG_TOAST = 4; }
Pada implementasi MessageHandler
, kita harus memperluas Handler
dan menerapkan metode handleMessage(Message)
, dimana semua pesan akan diproses. Perhatikan bahwa kami mengambil Message.what
untuk mengidentifikasi pesannya, dan kami juga mendapatkan berbagai jenis data dari Message.obj
. Mari meninjau dengan cepat properti Message
yang terpenting sebelum masuk ke kodenya.
-
Message.what
:int
mengidentifikasiMessage
-
Message.arg1
:int
argumen bebas -
Message.arg2
:int
argumen bebas -
Message.obj
:Object
untuk menyimpan berbagai jenis data
public class MessageActivity extends Activity { /** * Handler responsible to manage communication * from the {@link WorkerThread}. It sends Messages * back to the {@link MessageActivity} and handle * those Messages */ public class MessageHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { // handle image case KEY_MSG_IMAGE:{ Bitmap bmp = (Bitmap) msg.obj; myImage.setImageBitmap(bmp); break; } // handle progressBar calls case KEY_MSG_PROGRESS: { if ( (boolean) msg.obj ) progressBar.setVisibility(View.VISIBLE); else progressBar.setVisibility(View.GONE); break; } // handle toast sent with a Message delay case KEY_MSG_TOAST:{ String msgText = (String)msg.obj; Toast.makeText(getApplicationContext(), msgText, Toast.LENGTH_LONG ).show(); break; } } } } // Handler that allows communication between // the WorkerThread and the Activity protected MessageHandler uiHandler; }
3.2 Mengirim Pesan dengan WorkerThread
Sekarang mari kita kembali ke kelas WorkerThread
. Kita akan menambahkan beberapa kode untuk mengunduh bitmap tertentu dan juga kode untuk mengunduh yang acak. Untuk menyelesaikan tugas tersebut, kita akan mengirim objek Message
dari WorkerThread
ke dirinya sendiri dan mengirimkan hasilnya kembali ke MessageActivity
dengan menggunakan logika yang sama persis yang diterapkan sebelumnya untuk RunnableActivity
.
Pertama, kita perlu memperpanjang Handler
untuk memproses pesan yang diunduh.
public class WorkerThread extends HandlerThread { // send and processes download Messages on the WorkerThread private HandlerMsgImgDownloader handlerMsgImgDownloader; /** * Keys to identify the keys of {@link Message#what} * from Messages sent by the {@link #handlerMsgImgDownloader} */ private final int MSG_DOWNLOAD_IMG = 0; // msg that download a single img private final int MSG_DOWNLOAD_RANDOM_IMG = 1; // msg that download random img /** * Handler responsible for managing the image download * It send and handle Messages identifying then using * the {@link Message#what} * {@link #MSG_DOWNLOAD_IMG} : single image * {@link #MSG_DOWNLOAD_RANDOM_IMG} : random image */ private class HandlerMsgImgDownloader extends Handler { private HandlerMsgImgDownloader(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { showProgressMSG(true); switch ( msg.what ) { case MSG_DOWNLOAD_IMG: { // receives a single url and downloads it String url = (String) msg.obj; downloadImageMSG(url); break; } case MSG_DOWNLOAD_RANDOM_IMG: { // receives a String[] with multiple urls // downloads a image randomly String[] urls = (String[]) msg.obj; Random random = new Random(); String url = urls[random.nextInt(urls.length)]; downloadImageMSG(url); } } showProgressMSG(false); } } }
Metode downloadImageMSG(String url)
pada dasarnya sama dengan metode downloadImage(String url)
. Satu-satunya perbedaan adalah bahwa yang pertama mengirimkan bitmap yang diunduh kembali ke UI dengan mengirim pesan menggunakan responseHandler
.
public class WorkerThread extends HandlerThread { /** * Download a bitmap using its url and * display it to the UI. * The only difference with {@link #downloadImage(String)} * is that it sends the image back to the UI * using a Message */ private void downloadImageMSG(String urlStr){ // Create a connection HttpURLConnection connection = null; try { URL url = new URL(urlStr); connection = (HttpURLConnection) url.openConnection(); // get the stream from the url InputStream in = new BufferedInputStream(connection.getInputStream()); final Bitmap bitmap = BitmapFactory.decodeStream(in); if ( bitmap != null ) { // send the bitmap downloaded and a feedback to the UI loadImageOnUIMSG( bitmap ); } } catch (IOException e) { e.printStackTrace(); } finally { if ( connection != null ) connection.disconnect(); } } }
LoadImageOnUIMSG(Bitmap image)
bertanggung jawab untuk mengirim pesan dengan bitmap yang diunduh ke MessageActivity
.
/** * sends a Bitmap to the ui * sending a Message to the {@link #responseHandler} */ private void loadImageOnUIMSG(final Bitmap image){ if (checkResponse() ) { sendMsgToUI( responseHandler.get().obtainMessage(MessageActivity.KEY_MSG_IMAGE, image) ); } } /** * Show/Hide progressBar on the UI. * It uses the {@link #responseHandler} to * send a Message on the UI */ private void showProgressMSG(boolean show){ Log.d(TAG, "showProgressMSG()"); if ( checkResponse() ) { sendMsgToUI( responseHandler.get().obtainMessage(MessageActivity.KEY_MSG_PROGRESS, show) ); } }
Perhatikan bahwa alih-alih membuat objek Message
dari awal, kita menggunakan metode Handler.obtainMessage(int what, Object obj)
untuk mengambil Message
dari pool global, yang menghemat beberapa sumber daya. Penting juga untuk dicatat bahwa kita memanggil obtainMessage()
pada responseHandler
, mendapatkan Message
yang terkait dengan Looper
dari MessageActivity
. Ada dua cara untuk mengambil Message
dari pool global: Message.obtain()
dan Handler.obtainMessage()
.
Satu-satunya yang tersisa untuk dilakukan pada tugas mengunduh gambar adalah menyediakan metode untuk mengirim Message
ke WorkerThread
untuk memulai proses pengunduhan. Perhatikan bahwa kali ini kita akan memanggil Message.obtain(Handler handler, int what, Object obj)
pada handlerMsgImgDownloader
, mengaitkan pesan tersebut dengan looper WorkerThread
.
/** * sends a Message to the current Thread * using the {@link #handlerMsgImgDownloader} * to download a single image. */ public void downloadWithMessage(){ Log.d(TAG, "downloadWithMessage()"); showOperationOnUIMSG("Sending Message..."); if ( handlerMsgImgDownloader == null ) handlerMsgImgDownloader = new HandlerMsgImgDownloader(getLooper()); Message message = Message.obtain(handlerMsgImgDownloader, MSG_DOWNLOAD_IMG,imageBUrl); handlerMsgImgDownloader.sendMessage(message); } /** * sends a Message to the current Thread * using the {@link #handlerMsgImgDownloader} * to download a random image. */ public void downloadRandomWithMessage(){ Log.d(TAG, "downloadRandomWithMessage()"); showOperationOnUIMSG("Sending Message..."); if ( handlerMsgImgDownloader == null ) handlerMsgImgDownloader = new HandlerMsgImgDownloader(getLooper()); Message message = Message.obtain(handlerMsgImgDownloader, MSG_DOWNLOAD_RANDOM_IMG, imagesUrls); handlerMsgImgDownloader.sendMessage(message); }
Kemungkinan lain yang menarik adalah mengirim objek Message
untuk diproses di lain waktu dengan perintah Message.sendMessageDelayed(Message msg, long timeMillis)
.
/** * Show a Toast after a delayed time. * * send a Message with delayed time on the WorkerThread * and sends a new Message to {@link MessageActivity} * with a text after the message is processed */ public void startMessageDelay(){ // message delay long delay = 5000; String msgText = "Hello from WorkerThread!"; // Handler responsible for sending Message to WorkerThread // using Handler.Callback() to avoid the need to extend the Handler class Handler handler = new Handler(new Handler.Callback() { @Override public boolean handleMessage(Message msg) { responseHandler.get().sendMessage( responseHandler.get().obtainMessage(MessageActivity.KEY_MSG_TOAST, msg.obj) ); return true; } }); // sending message handler.sendMessageDelayed( handler.obtainMessage(0,msgText), delay ); }
Kami membuat Handler
secara tegas untuk mengirimkan pesan yang tertunda. Alih-alih memperluas kelas Handler
, kami mengambil rute menginstansiasi Handler
dengan menggunakan antarmuka Handler.Callback
, yang menerapkan metode handleMessage(Message msg)
untuk memproses Message
yang tertunda.
4. Kesimpulan
Sekarang Anda sudah melihat kode yang cukup untuk memahami bagaimana menerapkan konsep kerangka kerja HaMeR dasar untuk mengelola konkurensi di Android. Ada beberapa fitur menarik lainnya dari proyek akhir yang tersimpan di GitHub, dan saya sangat menyarankan Anda untuk memeriksanya.
Akhirnya, saya memiliki beberapa pertimbangan terakhir yang harus Anda ingat:
- Jangan lupa mempertimbangkan siklus hidup Activity Android saat bekerja dengan HaMeR dan Threads secara umum. Jika tidak, aplikasi Anda mungkin gagal saat thread mencoba mengakses activity yang telah dihancurkan karena perubahan konfigurasi atau karena alasan lain. Solusi yang umum adalah menggunakan
RetainedFragment
untuk menyimpan thread dan mengisi thread latar belakang dengan referensi activity setiap kali activity dihancurkan. Lihatlah solusinya di proyek akhir di GitHub. - Tugas yang dijalankan karena objek
Runnable
danMessage
yang diproses padaHandlers
tidak berjalan asinkron. Mereka akan berjalan sinkron di thread yang terkait dengan handler. Agar asinkron, Anda perlu membuat thread lain, mengirim/memposting objekMessage
/Runnable
di atasnya, dan menerima hasilnya pada waktu yang tepat.
Seperti yang Anda lihat, kerangka kerja HaMeR memiliki banyak kemungkinan yang berbeda, dan ini adalah solusi yang cukup terbuka dengan banyak pilihan untuk mengelola konkurensi di Android. Karakteristik ini bisa menjadi keunggulan dibanding AsyncTask
, tergantung kebutuhan Anda. Jelajahi lebih banyak kerangka kerja dan baca dokumentasinya, dan Anda akan menciptakan hal-hal hebat dengannya.
Sampai jumpa lagi!