Tất cả ứng dụng Android đều dùng một luồng chính để xử lý các thao tác trên giao diện người dùng. Gọi trong thời gian dài Các hoạt động từ luồng chính này có thể khiến bạn bị treo và không phản hồi. Cho ví dụ: nếu ứng dụng của bạn đưa ra yêu cầu mạng từ luồng chính, thì giao diện người dùng của ứng dụng bị treo cho đến khi nhận được phản hồi mạng. Nếu sử dụng Java, bạn có thể tạo các luồng trong nền bổ sung để xử lý các thao tác diễn ra trong thời gian dài trong khi luồng chính sẽ tiếp tục xử lý các bản cập nhật giao diện người dùng.
Hướng dẫn này cho biết cách nhà phát triển sử dụng Ngôn ngữ lập trình Java có thể sử dụng nhóm luồng để thiết lập và sử dụng nhiều luồng trong một ứng dụng Android. Hướng dẫn này hướng dẫn bạn cách xác định mã chạy trên một luồng cũng như cách giao tiếp giữa một trong các luồng này và luồng chính.
Thư viện đồng thời
Bạn cần nắm được kiến thức cơ bản về việc tạo luồng và cơ sở cơ chế cụ thể. Tuy nhiên, có nhiều thư viện phổ biến cung cấp cấp độ cao hơn trừu tượng hơn các khái niệm này và các tiện ích sẵn sàng sử dụng để truyền dữ liệu giữa các chuỗi. Các thư viện này bao gồm Guava và RxJava cho người dùng Ngôn ngữ lập trình Java và Coroutine, Đây là tính năng mà chúng tôi đề xuất cho người dùng Kotlin.
Trên thực tế, bạn nên chọn phương thức phù hợp nhất với ứng dụng và mặc dù các quy tắc về luồng vẫn giữ nguyên.
Tổng quan về ví dụ
Dựa trên Hướng dẫn về cấu trúc ứng dụng, các ví dụ trong chủ đề này sẽ tạo ra yêu cầu mạng và trả về kết quả cho luồng chính, nơi sau đó ứng dụng có thể hiển thị kết quả đó trên màn hình.
Cụ thể, ViewModel
gọi lớp dữ liệu trên luồng chính để
kích hoạt yêu cầu mạng. Lớp dữ liệu chịu trách nhiệm di chuyển
thực thi yêu cầu mạng bên ngoài luồng chính rồi đăng lại kết quả
đến luồng chính bằng cách sử dụng lệnh gọi lại.
Để chuyển việc thực thi yêu cầu mạng ra khỏi luồng chính, chúng ta cần phải tạo các luồng khác trong ứng dụng của chúng tôi.
Tạo nhiều chuỗi
Nhóm luồng là một tập hợp các luồng được quản lý, chạy các tác vụ trong
song song từ một hàng đợi. Các tác vụ mới được thực thi trên các luồng hiện có dưới dạng những luồng đó
các luồng ở trạng thái rảnh. Để gửi một việc cần làm đến một nhóm luồng, hãy sử dụng
Giao diện ExecutorService
. Lưu ý rằng ExecutorService
không có tác dụng gì
với Dịch vụ, thành phần ứng dụng Android.
Việc tạo luồng rất tốn kém, vì vậy bạn chỉ nên tạo một nhóm luồng một lần vì
ứng dụng của bạn khởi chạy. Hãy nhớ lưu bản sao của ExecutorService
trong lớp Application
hoặc trong vùng chứa chèn phần phụ thuộc.
Ví dụ sau đây sẽ tạo một nhóm luồng gồm 4 luồng mà chúng ta có thể sử dụng để
chạy tác vụ trong nền.
public class MyApplication extends Application {
ExecutorService executorService = Executors.newFixedThreadPool(4);
}
Có nhiều cách khác để bạn định cấu hình nhóm luồng tuỳ thuộc vào dự kiến khối lượng công việc. Xem phần Định cấu hình nhóm luồng để biết thêm thông tin.
Thực thi trong luồng ở chế độ nền
Việc tạo yêu cầu mạng trên luồng chính sẽ khiến luồng này phải đợi, hoặc
block cho đến khi nhận được phản hồi. Vì luồng này bị chặn nên hệ điều hành không thể
gọi onDraw()
và ứng dụng của bạn bị treo, có khả năng dẫn đến trường hợp Ứng dụng không
Hộp thoại phản hồi (ANR). Thay vào đó, hãy chạy thao tác này ở chế độ nền
chuỗi.
Đưa ra yêu cầu
Trước tiên, hãy xem lớp LoginRepository
của chúng ta để biết cách tạo
yêu cầu mạng:
// 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()
đồng bộ và chặn luồng lệnh gọi. Để lập mô hình
phản hồi của yêu cầu mạng, chúng ta có lớp Result
của riêng mình.
Kích hoạt yêu cầu
ViewModel
kích hoạt yêu cầu mạng khi người dùng nhấn vào, chẳng hạn như bật
nút:
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);
}
}
Với mã trước, LoginViewModel
đang chặn luồng chính khi thực hiện
yêu cầu mạng. Chúng ta có thể dùng nhóm luồng mà chúng ta đã tạo thực thể để di chuyển
quá trình thực thi đến một luồng trong nền.
Xử lý thao tác chèn phần phụ thuộc
Trước tiên, tuân theo nguyên tắc chèn phần phụ thuộc, LoginRepository
lấy một thực thể của Trình thực thi thay vì ExecutorService
vì đây là
thực thi mã và không quản lý luồng:
public class LoginRepository {
...
private final Executor executor;
public LoginRepository(LoginResponseParser responseParser, Executor executor) {
this.responseParser = responseParser;
this.executor = executor;
}
...
}
Phương thức execute() của Trình thực thi sẽ lấy một Runnable (Có thể chạy). Runnable
là một
Giao diện Phương thức trừu tượng đơn (SAM) có phương thức run()
được thực thi trong
một luồng khi được gọi.
Thực thi ở chế độ nền
Hãy tạo một hàm khác có tên là makeLoginRequest()
. Hàm này di chuyển
thực thi luồng trong nền và tạm thời bỏ qua phản hồi:
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
}
...
}
Bên trong phương thức execute()
, chúng ta tạo một Runnable
mới bằng khối mã
mà chúng ta muốn thực thi trong luồng nền—trong trường hợp của chúng ta là mạng đồng bộ
. Trong nội bộ, ExecutorService
quản lý Runnable
và
thực thi phương thức đó trong một chuỗi có sẵn.
Những yếu tố nên cân nhắc
Bất kỳ luồng nào trong ứng dụng của bạn đều có thể chạy song song với các luồng khác, bao gồm cả luồng chính luồng, do đó bạn cần đảm bảo mã của mình an toàn cho luồng. Lưu ý rằng trong Ví dụ: chúng ta tránh ghi vào các biến được chia sẻ giữa các luồng, truyền thay vào đó là dữ liệu bất biến. Đây là một phương pháp hay vì mỗi luồng hoạt động với thực thể dữ liệu riêng và chúng tôi tránh được việc đồng bộ hoá phức tạp.
Nếu cần chia sẻ trạng thái giữa các luồng, bạn phải cẩn thận quản lý quyền truy cập từ các luồng bằng cơ chế đồng bộ hoá chẳng hạn như khoá. Địa điểm này nằm ngoài phạm vi của hướng dẫn này. Nhìn chung, bạn nên tránh chia sẻ trạng thái có thể thay đổi giữa các chuỗi bất cứ khi nào có thể.
Giao tiếp với luồng chính
Trong bước trước, chúng ta đã bỏ qua phản hồi yêu cầu mạng. Để hiển thị
kết quả trên màn hình, LoginViewModel
cần biết về kết quả đó. Chúng tôi có thể thực hiện điều đó bằng cách
bằng cách sử dụng lệnh gọi lại.
Hàm makeLoginRequest()
phải lấy lệnh gọi lại làm tham số để
mã này có thể trả về giá trị không đồng bộ. Lệnh gọi lại với kết quả được gọi
bất cứ khi nào yêu cầu mạng hoàn tất hoặc có lỗi. Trong Kotlin, chúng ta có thể
dùng hàm bậc cao hơn. Tuy nhiên, trong Java, chúng ta phải tạo một lệnh gọi lại mới
giao diện có cùng chức năng:
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
cần triển khai lệnh gọi lại ngay bây giờ. Có thể thực hiện nhiều hoạt động
logic tùy thuộc vào kết quả:
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
}
}
});
}
}
Trong ví dụ này, lệnh gọi lại được thực thi trong luồng gọi, đây là một luồng trong nền. Điều này có nghĩa là bạn không thể sửa đổi hoặc trao đổi trực tiếp với lớp giao diện người dùng cho đến khi bạn quay lại luồng chính.
Sử dụng trình xử lý
Bạn có thể sử dụng Trình xử lý để thêm một hành động cần thực hiện vào hàng đợi
chuỗi. Để chỉ định luồng cần thực hiện thao tác, hãy tạo hàm
Handler
sử dụng Looper cho luồng. Looper
là một đối tượng có thể chạy
vòng lặp tin nhắn cho một chuỗi thư liên quan. Sau khi tạo một Handler
, bạn
sau đó có thể sử dụng phương thức post(Runnable) để chạy một khối mã trong
chuỗi tương ứng.
Looper
bao gồm một hàm trợ giúp getMainLooper(), giúp truy xuất
Looper
của luồng chính. Bạn có thể chạy mã trong luồng chính bằng cách dùng
Looper
để tạo một Handler
. Vì đây là việc bạn có thể làm khá thường xuyên,
bạn cũng có thể lưu bản sao của Handler
ở cùng vị trí bạn đã lưu
ExecutorService
:
public class MyApplication extends Application {
ExecutorService executorService = Executors.newFixedThreadPool(4);
Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
}
Bạn nên chèn trình xử lý vào kho lưu trữ vì nó sẽ
cho bạn linh hoạt hơn. Ví dụ: trong tương lai, bạn có thể muốn chuyển một
Handler
khác nhau để lên lịch các tác vụ trên một luồng riêng. Nếu bạn thường xuyên
giao tiếp lại với cùng một luồng, bạn có thể truyền Handler
vào
hàm khởi tạo kho lưu trữ như trong ví dụ sau.
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);
}
});
}
...
}
Ngoài ra, nếu muốn linh hoạt hơn, bạn có thể truyền Handler
cho mỗi
hàm:
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);
}
});
}
}
Trong ví dụ này, lệnh gọi lại đã truyền vào makeLoginRequest
của Kho lưu trữ
lệnh gọi được thực thi trên luồng chính. Tức là bạn có thể trực tiếp sửa đổi giao diện người dùng
từ lệnh gọi lại hoặc sử dụng LiveData.setValue()
để giao tiếp với giao diện người dùng.
Định cấu hình nhóm luồng
Bạn có thể tạo một nhóm luồng bằng một trong các hàm trợ giúp của Executor (Trình thực thi) với các chế độ cài đặt định sẵn, như được thể hiện trong mã ví dụ trước. Ngoài ra, nếu muốn tuỳ chỉnh thông tin chi tiết của nhóm luồng, bạn có thể tạo một thực thể bằng cách sử dụng trực tiếp ThreadPoolExecutor. Bạn có thể định cấu hình các mục sau chi tiết:
- Quy mô nhóm ban đầu và tối đa.
- Duy trì thời gian tồn tại và đơn vị thời gian. Thời gian tồn tại là khoảng thời gian tối đa mà một luồng có thể duy trì trạng thái rảnh trước khi tắt.
- Hàng đợi đầu vào chứa
Runnable
tác vụ. Hàng đợi này phải triển khai Giao diện BlockingQueue. Để đáp ứng các yêu cầu của ứng dụng, bạn có thể chọn từ các triển khai hàng đợi có sẵn. Để tìm hiểu thêm, hãy xem lớp học tổng quan về ThreadPoolExecutor.
Dưới đây là ví dụ chỉ định quy mô nhóm luồng dựa trên tổng số lõi xử lý, thời gian hoạt động là một giây và hàng đợi đầu vào.
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
);
...
}