Multithreading Di Android


Pada program yang saya buat di artikel Belajar Memakai Implicit Intent Di Android, sistem operasi Android akan terlihat seperti hang bila saya membaca file dengan ukuran besar. Tombol yang ada di perangkat keras juga tidak bisa dipakai, misalnya saya tidak membatalkan proses dan tidak bisa kembali ke menu utama. Mengapa demikian? Hal ini berkaitan dengan threading (concurrency). Setiap aplikasi Android dimulai dengan sebuah thread tunggal yang diberi nama ‘main’. Semua operasi yang berkaitan dengan UI seperti event ditangani oleh thread ini. Pada Swing, thread ini memiliki fungsi yang sama seperti event dispatch thread (EDT). Bedanya, di Swing, aplikasi tidak langsung mulai dari EDT melainkan thread non-UI (pengguna memakai SwingUtilities.invokeLater() atau SwingUtilities.invokeAndWait() untuk mengerjakan kode program di EDT). Bila saya mengerjakan sebuah proses yang sangat lama di thread UI, maka ia tidak bisa bekerja menangani event UI. Ia akan menunggu hingga proses lama ini selesai baru bisa lanjut bekerja. Ini yang membuat aplikasi menjadi tidak responsif.

Bila pada aplikasi Java, saya menggunakan JVisualVM (bawaan JDK) untuk memantau thread yang ada di sebuah aplikasi, maka untuk Android, saya dapat menggunakan Android Device Monitor (bawaan Android SDK). Sebagai contoh, bila saya memilih aplikasi dan men-klik icon Update Threads, saya dapat melihat tampilan seperti pada gambar berikut ini di tab Threads:

Melihat thread di aplikasi Android

Melihat thread di aplikasi Android

Walaupun ada banyak thread yang dibuat, satu-satunya thread yang menjalankan kode program aplikasi saya saat ini adalah thread 1 dengan nama ‘main’. Thread lainnya adalah bawaan Android, misalnya thread ‘GC’ untuk membebaskan memori yang tidak dipakai lagi, thread ‘JDWP’ untuk keperluan debugging, dan sebagainya.

Sekarang, saya akan melakukan perubahan pada kode program di method tampilkan() agar ia mengerjakan tugasnya di thread terpisah:

private void tampilkan(Intent data) {
    Thread.start('ProsesFile') {
        TextView output = (TextView) findViewById(OUTPUT_VIEW_ID)
        Uri uri = data.getData()
        byte[] bytes = getContentResolver().openInputStream(uri).bytes
        StringBuilder hexdump = new StringBuilder()
        StringBuilder ascii = new StringBuilder()
        int i
        for (i = 0; i < bytes.length; i++) {
            hexdump.append(String.format("%02x", bytes[i]))
            hexdump.append(' ')
            ascii.append(Character.isLetterOrDigit(bytes[i]) ? (char) bytes[i] : '.')
            if ((i > 0) && (((i + 1) % 8) == 0)) {
                hexdump.append(ascii.toString())
                hexdump.append('n')
                ascii = new StringBuilder()
            }

            output.post({
                output.setText("Memproses byte $i dari ${bytes.length}")
            } as Runnable)

            Thread.sleep(1)
        }
        if (ascii) {
            (1..(8 - ((i - 1) % 8))).each { hexdump.append('   ') }
            hexdump.append(ascii.toString())
        }

        output.post({
            output.setText(hexdump.toString())
            Intent sendIntent = new Intent(Intent.ACTION_SEND)
            sendIntent.putExtra(Intent.EXTRA_TEXT, hexdump.toString())
            sendIntent.setType('text/plain')
            shareActionProvider.setShareIntent(sendIntent)
        } as Runnable)

    }
}   

Pada kode program di atas, saya memakai Thread.start() dari Groovy untuk membuat sebuah thread baru dengan nama ‘ProsesFile’. Selama berada di dalam thread baru ini, saya tidak boleh mengerjakan method yang berhubungan dengan UI secara langsung. Hal ini karena method pada UI tidak thread-safe. Oleh sebab itu, untuk menjadwalkan kode program agar dikerjakan oleh thread ‘Main’ (yang menangani UI), saya menggunakan post().

Bila saya menjalankan aplikasi, ia akan lebih responsif. Saya juga akan menemukan sebuah thread baru dengan nama ‘ProsesFile’ bila memantau aplikasi melalui Android Device Monitor, seperti yang terlihat pada gambar berikut ini:

Membuat thread baru

Membuat thread baru

Masalah lain yang saya hadapi adalah penggunaan memori yang besar karena memproses isi file sekaligus. Oleh sebab itu, saya akan menggunakan seek bar sehingga saya bisa menampilkan isi file per kilobyte sesuai dengan posisi seek bar. Agar lebih mudah dalam merancang UI, saya akan menggunakan designer bawaan Android Studio. Untuk itu, saya membuat folder baru bernama layout di lokasi src/main/res. Setelah itu, saya men-klik kanan pada folder layout dan memilih New, Layout Reource Folder. Saya mengisi dialog yang muncul dengan mainscreen pada File name dan RelativeLayout pada Root element. Saya kemudian merancang UI sehingga terlihat seperti berikut ini:

Merancang UI secara visual

Merancang UI secara visual

Saya perlu mengubah method onCreate() untuk membaca layout dari XML sehingga isinya menjadi seperti:

public class MainActivity extends Activity implements SeekBar.OnSeekBarChangeListener {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.mainscreen)
    ((SeekBar) findViewById(R.id.posisi)).onSeekBarChangeListener = this
    if (intent.data) {
        tampilkan(intent)
    }
  }

  ...

  @Override
  void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}

  @Override
  void onStartTrackingTouch(SeekBar seekBar) {}

  @Override
  void onStopTrackingTouch(SeekBar seekBar) {}

}

Kali ini saya perlu men-share variabel bytes yang berisi data lengkap yang sudah dibaca agar dapat diakses dari thread berbeda. Karena ia bytes hanya ditulis sekali, setelah itu dibaca oleh beberapa thread berbeda, saya boleh saja men-share variabel ini dengan cara yang tidak thread-safe. Akan tetapi, agar lebih aman, saya akan menggunakan ReadWriteLock sehingga pada saat variabel ini ditulis oleh sebuah thread, maka thread lain yang ingin membaca harus menunggu sampai penulisan selesai. Groovy menyediakan AST transformation @WithWriteLock dan @WithReadLock untuk keperluan tersebut. Sebagai contoh, saya bisa menambahkannya seperti:

public class MainActivity extends Activity implements SeekBar.OnSeekBarChangeListener {

  private byte[] bytes

  ...

  @Override @WithReadLock
  void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}

  @WithWriteLock
  private void tampilkan(Intent data) { ... }

}

Sekarang, fungsi dari kode program tampilkan() hanya untuk membaca isi file saja:

@WithWriteLock
private void tampilkan(Intent data) {
    Thread.start('BacaFile') {
        Uri uri = data.getData()
        byte[] bytes = getContentResolver().openInputStream(uri).bytes
        SeekBar seekBar = (SeekBar) findViewById(R.id.posisi)
        seekBar.post({
            seekBar.max = bytes.length / 1024
        })
    }
}

Kode program di onProgressChanged akan dikerjakan setiap kali pengguna menggeser seek bar. Karena proses untuk menghasilkan hexdump cukup lama, maka saya akan mengerjakannya pada sebuah thread terpisah, misalnya:

@Override @WithReadLock
void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    Thread.start('ProsesHexdump') {
        TextView output = (TextView) findViewById(R.id.output)
        StringBuilder hexdump = new StringBuilder()
        StringBuilder ascii = new StringBuilder()
        int i = progress * 1024
        int max = Math.min(i + 1024, bytes.length)
        for (; i < max; i++) {
            hexdump.append(String.format("%02x ", bytes[i]))
            ascii.append(Character.isLetterOrDigit(bytes[i]) ? (char) bytes[i] : '.')
            if ((i > 0) && (((i + 1) % 8) == 0)) {
                hexdump.append(ascii.toString())
                hexdump.append('n')
                ascii = new StringBuilder()
            }
        }
        if (ascii) {
            (1..(8 - (i % 8))).each { hexdump.append('   ') }
            hexdump.append(ascii.toString())
        }

        output.post({
            String hasil = hexdump.toString()
            output.setText(hasil)
            Intent sendIntent = new Intent(Intent.ACTION_SEND)
            sendIntent.putExtra(Intent.EXTRA_TEXT, hasil)
            sendIntent.setType('text/plain')
            shareActionProvider.setShareIntent(sendIntent)
        } as Runnable)
    }
}

Sekarang, setiap kali membaca file, thread ‘BacaFile’ akan dibuat. Setiap kali seek bar digeser, thread ‘ProsesHexDump’ akan tercipta. Thread baru ini juga akan menunggu hingga ‘BacaFile’ selesai dikerjakan bila thread tersebut masih bekerja.

Perihal Solid Snake
I'm nothing...

One Response to Multithreading Di Android

  1. Heri Cahyono mengatakan:

    Makasih artikelnya sangat jelas..

Apa komentar Anda?

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s

%d blogger menyukai ini: