Tổng quan về quy trình và luồng

Khi một thành phần ứng dụng bắt đầu và ứng dụng không có bất kỳ thành phần nào khác đang chạy, hệ thống Android sẽ bắt đầu một quy trình Linux mới cho ứng dụng bằng một luồng thực thi duy nhất. Theo mặc định, tất cả thành phần của cùng một ứng dụng sẽ chạy trong cùng một quy trình và luồng, được gọi là luồng chính.

Nếu một thành phần của ứng dụng bắt đầu và đã có một quy trình cho ứng dụng đó vì một thành phần khác từ ứng dụng đã bắt đầu, thì thành phần đó sẽ bắt đầu trong quy trình đó và sử dụng cùng một luồng thực thi. Tuy nhiên, bạn có thể sắp xếp để các thành phần khác nhau trong ứng dụng chạy trong các quy trình riêng biệt cũng như có thể tạo thêm luồng cho bất kỳ quy trình nào.

Tài liệu này thảo luận cách hoạt động của các quy trình và luồng trong ứng dụng Android.

Quá trình

Theo mặc định, tất cả thành phần của ứng dụng sẽ chạy trong cùng một quy trình và hầu hết các ứng dụng không thay đổi điều này. Tuy nhiên, nếu thấy cần kiểm soát quy trình thuộc về một thành phần nhất định, bạn có thể thực hiện trong tệp kê khai.

Mục kê khai cho từng loại phần tử thành phần (<activity>, <service>, <receiver><provider>) hỗ trợ thuộc tính android:process có thể chỉ định quy trình mà thành phần chạy. Bạn có thể đặt thuộc tính này để mỗi thành phần chạy theo quy trình riêng hoặc để một số thành phần dùng chung một quy trình trong khi những thành phần khác thì không.

Bạn cũng có thể thiết lập android:process để các thành phần của nhiều ứng dụng chạy trong cùng một quy trình, miễn là các ứng dụng đó có cùng mã nhận dạng người dùng Linux và được ký bằng cùng một chứng chỉ.

Phần tử <application> cũng hỗ trợ thuộc tính android:process mà bạn có thể sử dụng để đặt giá trị mặc định áp dụng cho tất cả thành phần.

Android có thể quyết định tắt một quy trình vào một thời điểm nào đó, khi các quy trình khác cần tài nguyên để phục vụ người dùng ngay lập tức. Do đó, các thành phần ứng dụng đang chạy trong quy trình bị tắt sẽ bị huỷ bỏ. Một quy trình sẽ bắt đầu lại cho các thành phần đó khi các thành phần đó có công việc.

Khi quyết định tắt quy trình nào, hệ thống Android sẽ cân nhắc tầm quan trọng tương đối của các quy trình đó đối với người dùng. Ví dụ: nó dễ dàng tắt một quy trình lưu trữ hoạt động không còn hiển thị trên màn hình so với một quy trình lưu trữ các hoạt động hiển thị. Do đó, quyết định chấm dứt một quy trình phụ thuộc vào trạng thái của các thành phần đang chạy trong quy trình đó.

Thông tin chi tiết về vòng đời của quy trình và mối quan hệ với các trạng thái của ứng dụng được thảo luận trong bài viết Quy trình và vòng đời của ứng dụng.

Luồng

Khi chạy một ứng dụng, hệ thống sẽ tạo một luồng thực thi cho ứng dụng, gọi là luồng chính. Luồng này rất quan trọng vì chịu trách nhiệm gửi sự kiện đến các tiện ích giao diện người dùng thích hợp, bao gồm cả sự kiện vẽ. Đây cũng hầu như luôn là luồng mà ứng dụng của bạn tương tác với các thành phần từ các gói android.widgetandroid.view của bộ công cụ giao diện người dùng Android. Vì lý do này, đôi khi luồng chính được gọi là luồng giao diện người dùng. Tuy nhiên, trong các trường hợp đặc biệt, luồng chính của ứng dụng có thể không phải là luồng giao diện người dùng. Để biết thêm thông tin, hãy xem phần Chú thích luồng.

Hệ thống không tạo một luồng riêng cho mỗi bản sao của thành phần. Tất cả các thành phần chạy trong cùng một quy trình đều được tạo thực thể trong luồng giao diện người dùng và lệnh gọi hệ thống đến từng thành phần sẽ được gửi từ luồng đó. Do đó, các phương thức phản hồi lệnh gọi lại hệ thống – chẳng hạn như onKeyDown() để báo cáo hành động của người dùng hoặc phương pháp gọi lại trong vòng đời – luôn chạy trong luồng giao diện người dùng của quy trình.

Ví dụ: khi người dùng nhấn vào một nút trên màn hình, luồng giao diện người dùng của ứng dụng sẽ gửi sự kiện chạm đến tiện ích. Tiện ích này sẽ đặt trạng thái được nhấn và đăng yêu cầu vô hiệu hoá lên hàng đợi sự kiện. Luồng giao diện người dùng sẽ loại bỏ yêu cầu khỏi hàng đợi và thông báo để tiện ích tự vẽ lại.

Trừ phi bạn triển khai ứng dụng đúng cách, mô hình đơn luồng này có thể mang lại hiệu suất kém khi ứng dụng của bạn thực hiện nhiều thao tác để phản hồi tương tác của người dùng. Việc thực hiện các thao tác dài trong luồng giao diện người dùng, chẳng hạn như quyền truy cập mạng hoặc truy vấn cơ sở dữ liệu, sẽ chặn toàn bộ giao diện người dùng. Khi luồng bị chặn, không sự kiện nào có thể được gửi, bao gồm cả sự kiện vẽ.

Từ góc độ của người dùng, ứng dụng có vẻ như bị treo. Thậm chí tệ hơn, nếu luồng giao diện người dùng bị chặn trong hơn vài giây, thì người dùng sẽ thấy hộp thoại "ứng dụng không phản hồi" (ANR). Sau đó, người dùng có thể quyết định thoát khỏi ứng dụng hoặc thậm chí gỡ cài đặt ứng dụng.

Xin lưu ý rằng bộ công cụ giao diện người dùng Android không an toàn cho luồng. Vì vậy, đừng thao tác giao diện người dùng từ một luồng thực thi. Thực hiện mọi thao tác đối với giao diện người dùng từ luồng giao diện người dùng. Có hai quy tắc đối với mô hình đơn luồng của Android:

  1. Đừng chặn luồng giao diện người dùng.
  2. Không truy cập vào bộ công cụ giao diện người dùng Android từ bên ngoài luồng giao diện người dùng.

Luồng trình chạy

Do mô hình đơn luồng này, điều quan trọng là phải đảm bảo khả năng phản hồi của giao diện người dùng của ứng dụng mà bạn không chặn luồng giao diện người dùng. Nếu bạn có các thao tác cần thực hiện không tức thì, hãy nhớ thực hiện các thao tác đó trong các luồng nền hoặc worker riêng biệt. Hãy nhớ rằng bạn không thể cập nhật giao diện người dùng từ bất kỳ luồng nào khác ngoài giao diện người dùng hoặc luồng chính.

Để giúp bạn tuân thủ các quy tắc này, Android cung cấp một số cách để truy cập luồng giao diện người dùng từ các luồng khác. Sau đây là danh sách các phương thức có thể giúp ích cho bạn:

Ví dụ sau đây sử dụng View.post(Runnable):

Kotlin

fun onClick(v: View) {
    Thread(Runnable {
        // A potentially time consuming task.
        val bitmap = processBitMap("image.png")
        imageView.post {
            imageView.setImageBitmap(bitmap)
        }
    }).start()
}

Java

public void onClick(View v) {
    new Thread(new Runnable() {
        public void run() {
            // A potentially time consuming task.
            final Bitmap bitmap =
                    processBitMap("image.png");
            imageView.post(new Runnable() {
                public void run() {
                    imageView.setImageBitmap(bitmap);
                }
            });
        }
    }).start();
}

Cách triển khai này an toàn cho luồng vì thao tác trong nền được thực hiện từ một luồng riêng biệt, trong khi ImageView luôn được điều khiển từ luồng giao diện người dùng.

Tuy nhiên, khi hoạt động ngày càng phức tạp, loại mã này có thể trở nên phức tạp và khó duy trì. Để xử lý các tương tác phức tạp hơn với luồng worker, bạn có thể cân nhắc sử dụng Handler trong luồng worker để xử lý các thông báo được gửi từ luồng giao diện người dùng. Để biết nội dung giải thích đầy đủ về cách lên lịch công việc trên các luồng trong nền và giao tiếp trở lại với luồng giao diện người dùng, hãy xem phần Tổng quan về tác vụ trong nền.

Phương thức an toàn cho luồng

Trong một số trường hợp, các phương thức bạn triển khai được gọi từ nhiều luồng và do đó, phải được ghi để an toàn cho luồng.

Điều này chủ yếu đúng với các phương thức có thể được gọi từ xa, chẳng hạn như các phương thức trong một dịch vụ ràng buộc. Khi lệnh gọi trên một phương thức được triển khai trong IBinder bắt nguồn trong cùng một quy trình mà IBinder đang chạy, phương thức này sẽ được thực thi trong luồng của phương thức gọi. Tuy nhiên, khi lệnh gọi bắt nguồn từ một quy trình khác, phương thức này sẽ thực thi trong một luồng được chọn từ nhóm các luồng mà hệ thống duy trì trong chính quy trình đó với IBinder. Yêu cầu này không được thực thi trong luồng giao diện người dùng của quy trình.

Ví dụ: trong khi phương thức onBind() của một dịch vụ được gọi từ luồng giao diện người dùng của quy trình dịch vụ, thì các phương thức được triển khai trong đối tượng mà onBind() trả về (chẳng hạn như một lớp con triển khai phương thức lệnh gọi quy trình từ xa (RPC) sẽ được gọi từ các luồng trong nhóm. Vì một dịch vụ có thể có nhiều ứng dụng, nên nhiều luồng nhóm có thể sử dụng cùng một phương thức IBinder cùng lúc. Vì vậy, bạn phải triển khai các phương thức IBinder để đảm bảo an toàn cho luồng.

Tương tự, trình cung cấp nội dung có thể nhận yêu cầu dữ liệu bắt nguồn từ các quy trình khác. Các lớp ContentResolverContentProvider ẩn thông tin chi tiết về cách quản lý giao tiếp liên quy trình (IPC), nhưng các phương thức ContentProvider phản hồi các yêu cầu đó – phương thức query(), insert(), delete(), update()getType() – được gọi từ nhóm các luồng trong quy trình của trình cung cấp nội dung, chứ không phải luồng giao diện người dùng cho quy trình này. Vì các phương thức này có thể được gọi từ số lượng luồng bất kỳ cùng một lúc, nên bạn cũng phải triển khai các phương thức này để đảm bảo an toàn cho luồng.

Giao tiếp liên quy trình

Android cung cấp cơ chế cho IPC bằng cách sử dụng RPC, trong đó một phương thức được một hoạt động hoặc thành phần khác của ứng dụng gọi nhưng được thực thi từ xa trong một quy trình khác, trong đó mọi kết quả được trả về cho phương thức gọi. Điều này đòi hỏi phải phân tích lệnh gọi phương thức và dữ liệu của lệnh gọi đó ở mức mà hệ điều hành có thể hiểu được, truyền lệnh gọi đó từ quy trình và không gian địa chỉ cục bộ đến không gian xử lý và địa chỉ từ xa, sau đó tập hợp và tái hiện lệnh gọi ở đó.

Sau đó, các giá trị trả về sẽ được truyền theo hướng ngược lại. Android cung cấp tất cả mã để thực hiện những giao dịch IPC này, vì vậy, bạn có thể tập trung vào việc xác định và triển khai giao diện lập trình RPC.

Để thực hiện IPC, ứng dụng của bạn phải liên kết với một dịch vụ bằng bindService(). Để biết thêm thông tin, hãy xem phần Tổng quan về dịch vụ.