Semua aplikasi Android menggunakan thread utama untuk menangani operasi UI. Memanggil berdurasi panjang operasi dari thread utama ini dapat menyebabkan berhenti berfungsi dan tidak responsif. Sebagai misalnya, jika aplikasi Anda membuat permintaan jaringan dari thread utama, maka UI aplikasi Anda dibekukan sampai menerima respons jaringan. Jika Anda menggunakan Java, Anda dapat membuat thread latar belakang tambahan untuk menangani operasi yang berjalan lama sekaligus thread utama terus menangani update UI.
Panduan ini menunjukkan bagaimana pengembang yang menggunakan Bahasa Pemrograman Java dapat menggunakan kumpulan thread untuk menyiapkan dan menggunakan beberapa thread di aplikasi Android. Panduan ini juga menunjukkan cara menentukan kode untuk dijalankan di thread dan cara berkomunikasi antara salah satu thread ini dan thread utama.
Library serentak
Penting untuk memahami dasar-dasar threading dan mekanisme yang mendasarinya. Namun, ada banyak perpustakaan populer yang menawarkan abstraksi terhadap konsep ini dan utilitas yang siap digunakan untuk meneruskan data antar-thread. Library ini mencakup Guava dan RxJava untuk pengguna dan Coroutine Bahasa Pemrograman Java, yang kami rekomendasikan untuk pengguna Kotlin.
Dalam praktiknya, Anda harus memilih salah satu yang paling sesuai untuk aplikasi Anda dan pengembangan web, meskipun aturan threading tetap sama.
Ringkasan contoh
Berdasarkan Panduan arsitektur aplikasi, contoh dalam topik ini menghasilkan permintaan jaringan dan menampilkan hasilnya ke thread utama, tempat aplikasi kemudian yang mungkin menampilkannya di layar.
Secara khusus, ViewModel
memanggil lapisan data pada thread utama untuk
memicu permintaan jaringan. Lapisan data bertugas memindahkan
eksekusi permintaan jaringan dari thread utama dan memposting hasilnya kembali
ke thread utama menggunakan callback.
Untuk memindahkan eksekusi permintaan jaringan dari thread utama, kita harus membuat thread lain di aplikasi kita.
Membuat beberapa thread
Kumpulan thread adalah kumpulan thread terkelola yang menjalankan tugas di
paralel dari antrean. Tugas baru dijalankan pada thread yang ada sebagaimana
dan thread menjadi tidak ada aktivitas. Untuk mengirim tugas ke kumpulan thread, gunakan metode
Antarmuka ExecutorService
. Perlu diketahui bahwa ExecutorService
tidak ada hubungannya
dengan Layanan, yaitu komponen aplikasi Android.
Pembuatan thread itu mahal, jadi sebaiknya Anda membuat kumpulan thread sekali saja
aplikasi Anda melakukan inisialisasi. Pastikan untuk menyimpan instance ExecutorService
baik di class Application
atau di penampung injeksi dependensi.
Contoh berikut membuat kumpulan thread yang terdiri dari empat thread yang dapat kita gunakan untuk
menjalankan tugas latar belakang.
public class MyApplication extends Application {
ExecutorService executorService = Executors.newFixedThreadPool(4);
}
Ada cara lain untuk mengonfigurasi kumpulan thread, bergantung pada sebagian besar workload standar dan berbasis cloud. Lihat Mengonfigurasi kumpulan thread untuk mengetahui informasi selengkapnya.
Menjalankan eksekusi di thread latar belakang
Membuat permintaan jaringan pada thread utama akan menyebabkan thread menunggu, atau
blokir, hingga aplikasi menerima respons. Karena thread diblokir, OS tidak dapat
panggil onDraw()
, dan aplikasi Anda berhenti berfungsi, yang berpotensi menyebabkan Aplikasi Tidak
Merespons dialog (ANR). Sebagai gantinya, mari kita jalankan operasi ini di latar belakang
.
Membuat Permintaan
Pertama, mari kita lihat class LoginRepository
dan lihat cara pembuatannya
permintaan jaringan:
// Result.java
public abstract class Result<T> {
private Result() {}
public static final class Success<T> extends Result<T> {
public T data;
public Success(T data) {
this.data = data;
}
}
public static final class Error<T> extends Result<T> {
public Exception exception;
public Error(Exception exception) {
this.exception = exception;
}
}
}
// LoginRepository.java
public class LoginRepository {
private final String loginUrl = "https://example.com/login";
private final LoginResponseParser responseParser;
public LoginRepository(LoginResponseParser responseParser) {
this.responseParser = responseParser;
}
public Result<LoginResponse> makeLoginRequest(String jsonBody) {
try {
URL url = new URL(loginUrl);
HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
httpConnection.setRequestMethod("POST");
httpConnection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
httpConnection.setRequestProperty("Accept", "application/json");
httpConnection.setDoOutput(true);
httpConnection.getOutputStream().write(jsonBody.getBytes("utf-8"));
LoginResponse loginResponse = responseParser.parse(httpConnection.getInputStream());
return new Result.Success<LoginResponse>(loginResponse);
} catch (Exception e) {
return new Result.Error<LoginResponse>(e);
}
}
}
makeLoginRequest()
bersifat sinkron dan memblokir thread pemanggil. Untuk membuat model
respons permintaan jaringan, kita memiliki class Result
sendiri.
Memicu permintaan
ViewModel
memicu permintaan jaringan saat pengguna mengetuk, misalnya,
tombol:
public class LoginViewModel {
private final LoginRepository loginRepository;
public LoginViewModel(LoginRepository loginRepository) {
this.loginRepository = loginRepository;
}
public void makeLoginRequest(String username, String token) {
String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
loginRepository.makeLoginRequest(jsonBody);
}
}
Dengan kode sebelumnya, LoginViewModel
memblokir thread utama saat membuat
terhadap permintaan jaringan. Kita dapat menggunakan kumpulan thread yang telah dibuat instance-nya untuk memindahkan
eksekusinya ke thread latar belakang.
Menangani injeksi dependensi
Pertama, mengikuti prinsip injeksi dependensi, LoginRepository
mengambil instance Executor, bukan ExecutorService
karena
mengeksekusi kode dan tidak mengelola thread:
public class LoginRepository {
...
private final Executor executor;
public LoginRepository(LoginResponseParser responseParser, Executor executor) {
this.responseParser = responseParser;
this.executor = executor;
}
...
}
Metode execute() Executor mengambil Runnable. Runnable
adalah
Antarmuka Single Abstrak Method (SAM) dengan metode run()
yang dijalankan di
sebuah thread saat dipanggil.
Jalankan di latar belakang
Mari kita buat fungsi lain bernama makeLoginRequest()
, yang memindahkan
eksekusi ke thread latar belakang dan mengabaikan respons untuk saat ini:
public class LoginRepository {
...
public void makeLoginRequest(final String jsonBody) {
executor.execute(new Runnable() {
@Override
public void run() {
Result<LoginResponse> ignoredResponse = makeSynchronousLoginRequest(jsonBody);
}
});
}
public Result<LoginResponse> makeSynchronousLoginRequest(String jsonBody) {
... // HttpURLConnection logic
}
...
}
Di dalam metode execute()
, kita membuat Runnable
baru dengan blok kode
yang ingin kita jalankan di thread latar belakang—dalam hal ini, jaringan sinkron
. Secara internal, ExecutorService
mengelola Runnable
dan
mengeksekusinya di thread yang tersedia.
Pertimbangan
Setiap thread di aplikasi Anda dapat berjalan secara paralel dengan thread lainnya, termasuk thread utama , jadi Anda harus memastikan bahwa kode Anda aman untuk thread. Perhatikan bahwa di kita menghindari penulisan ke variabel yang dibagikan antar-thread, data yang tidak dapat diubah. Ini adalah praktik yang baik, karena setiap thread bekerja dengan instance data itu sendiri, dan kita menghindari kerumitan sinkronisasi.
Jika Anda perlu berbagi status antar-thread, Anda harus berhati-hati dalam mengelola akses dari thread yang menggunakan mekanisme sinkronisasi seperti kunci. Ini di luar cakupan panduan ini. Secara umum, Anda harus menghindari berbagi status yang dapat berubah antar utas jika memungkinkan.
Berkomunikasi dengan thread utama
Dalam langkah sebelumnya, kita mengabaikan respons permintaan jaringan. Untuk menampilkan
di layar, LoginViewModel
perlu mengetahuinya. Kita bisa melakukannya dengan
menggunakan callback.
Fungsi makeLoginRequest()
harus mengambil callback sebagai parameter agar
fungsi tersebut dapat menampilkan
nilai secara asinkron. Callback dengan hasil dipanggil
setiap kali permintaan jaringan selesai atau terjadi kegagalan. Di Kotlin, kita dapat
menggunakan fungsi tingkat tinggi. Namun, di Java, kita harus membuat callback baru
antarmuka agar memiliki fungsi yang sama:
interface RepositoryCallback<T> {
void onComplete(Result<T> result);
}
public class LoginRepository {
...
public void makeLoginRequest(
final String jsonBody,
final RepositoryCallback<LoginResponse> callback
) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
callback.onComplete(result);
} catch (Exception e) {
Result<LoginResponse> errorResult = new Result.Error<>(e);
callback.onComplete(errorResult);
}
}
});
}
...
}
ViewModel
perlu mengimplementasikan callback sekarang. Sistem ini dapat menjalankan logika
yang berbeda, bergantung pada hasilnya:
public class LoginViewModel {
...
public void makeLoginRequest(String username, String token) {
String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
loginRepository.makeLoginRequest(jsonBody, new RepositoryCallback<LoginResponse>() {
@Override
public void onComplete(Result<LoginResponse> result) {
if (result instanceof Result.Success) {
// Happy path
} else {
// Show error in UI
}
}
});
}
}
Dalam contoh ini, callback dieksekusi dalam thread panggilan, yang merupakan di thread latar belakang. Ini berarti Anda tidak dapat memodifikasi atau berkomunikasi secara langsung dengan lapisan UI hingga Anda beralih kembali ke thread utama.
Menggunakan pengendali
Anda dapat menggunakan Handler untuk mengantrekan tindakan yang akan dilakukan pada
. Untuk menentukan thread tempat menjalankan tindakan, buat metode
Handler
menggunakan Looper untuk thread. Looper
adalah objek yang menjalankan
loop pesan untuk thread terkait. Setelah membuat Handler
, Anda
Anda dapat menggunakan metode post(Runnable) untuk menjalankan blok kode di
thread terkait.
Looper
menyertakan fungsi bantuan, getMainLooper(), yang mengambil
Looper
dari thread utama. Anda dapat menjalankan kode di thread utama menggunakan
Looper
untuk membuat Handler
. Karena ini adalah sesuatu yang
mungkin sering Anda lakukan,
Anda juga dapat menyimpan instance Handler
di tempat yang sama dengan Anda menyimpan
ExecutorService
:
public class MyApplication extends Application {
ExecutorService executorService = Executors.newFixedThreadPool(4);
Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
}
Ini adalah praktik yang baik untuk menginjeksikan
pengendali ke dalam repositori, karena memberikan
Anda lebih fleksibel. Misalnya, di masa mendatang Anda mungkin ingin meneruskan
Handler
yang berbeda untuk menjadwalkan tugas di thread terpisah. Jika Anda selalu
berkomunikasi kembali ke thread yang sama, Anda dapat meneruskan Handler
ke
repositori repositori, seperti yang ditampilkan dalam contoh berikut.
public class LoginRepository {
...
private final Handler resultHandler;
public LoginRepository(LoginResponseParser responseParser, Executor executor,
Handler resultHandler) {
this.responseParser = responseParser;
this.executor = executor;
this.resultHandler = resultHandler;
}
public void makeLoginRequest(
final String jsonBody,
final RepositoryCallback<LoginResponse> callback
) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
notifyResult(result, callback);
} catch (Exception e) {
Result<LoginResponse> errorResult = new Result.Error<>(e);
notifyResult(errorResult, callback);
}
}
});
}
private void notifyResult(
final Result<LoginResponse> result,
final RepositoryCallback<LoginResponse> callback,
) {
resultHandler.post(new Runnable() {
@Override
public void run() {
callback.onComplete(result);
}
});
}
...
}
Atau, jika ingin lebih fleksibel, Anda dapat meneruskan Handler
ke setiap
{i>function<i}:
public class LoginRepository {
...
public void makeLoginRequest(
final String jsonBody,
final RepositoryCallback<LoginResponse> callback,
final Handler resultHandler,
) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
notifyResult(result, callback, resultHandler);
} catch (Exception e) {
Result<LoginResponse> errorResult = new Result.Error<>(e);
notifyResult(errorResult, callback, resultHandler);
}
}
});
}
private void notifyResult(
final Result<LoginResponse> result,
final RepositoryCallback<LoginResponse> callback,
final Handler resultHandler
) {
resultHandler.post(new Runnable() {
@Override
public void run() {
callback.onComplete(result);
}
});
}
}
Dalam contoh ini, callback yang diteruskan ke panggilan makeLoginRequest
Repositori akan dieksekusi di thread utama. Itu berarti Anda dapat langsung memodifikasi UI
dari callback atau gunakan LiveData.setValue()
untuk berkomunikasi dengan UI.
Mengonfigurasi kumpulan thread
Anda dapat membuat kumpulan thread menggunakan salah satu fungsi bantuan Executor dengan setelan yang telah ditentukan, seperti yang ditunjukkan dalam kode contoh sebelumnya. Sebagai alternatif, jika ingin menyesuaikan detail kumpulan thread, Anda dapat membuat instance yang menggunakan ThreadPoolExecutor secara langsung. Anda dapat mengonfigurasi detail:
- Ukuran kumpulan awal dan maksimum.
- Waktu keep alive dan satuan waktu. Waktu tetap aktif adalah durasi maksimum yang thread dapat tetap tidak ada aktivitas sebelum dimatikan.
- Antrean input yang berisi tugas
Runnable
. Antrean ini harus mengimplementasikan BlockingQueue. Agar sesuai dengan persyaratan aplikasi, Anda dapat pilih dari implementasi antrean yang tersedia. Untuk mempelajari lebih lanjut, lihat kelas ringkasan untuk ThreadPoolExecutor.
Berikut contoh yang menentukan ukuran kumpulan thread berdasarkan jumlah total inti prosesor, waktu aktif selama satu detik, dan antrean input.
public class MyApplication extends Application {
/*
* Gets the number of available cores
* (not always the same as the maximum number of cores)
*/
private static int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();
// Instantiates the queue of Runnables as a LinkedBlockingQueue
private final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>();
// Sets the amount of time an idle thread waits before terminating
private static final int KEEP_ALIVE_TIME = 1;
// Sets the Time Unit to seconds
private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;
// Creates a thread pool manager
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
NUMBER_OF_CORES, // Initial pool size
NUMBER_OF_CORES, // Max pool size
KEEP_ALIVE_TIME,
KEEP_ALIVE_TIME_UNIT,
workQueue
);
...
}