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

Khi một thành phần của ứng dụng khởi động và ứng dụng không có thành phần nào khác hệ thống Android sẽ khởi động một quy trình Linux mới cho ứng dụng bằng một luồng duy nhất thực thi. Theo mặc định, tất cả cá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ó tên 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 đã khởi động, thì thành phần đó 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ể thu xếp để các thành phần khác nhau trong ứng dụng của bạn để chạy trong các tiến trình riêng biệt. Đồng thời, bạn có thể tạo thêm 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 đều chạy trong cùng một quy trình và hầu hết các ứng dụng đừng thay đổi thông tin này. Tuy nhiên, nếu thấy cần kiểm soát quá trình thuộc về, bạn có thể thực hiện việc này trong tệp kê khai.

Mục nhập tệp kê khai cho mỗi 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 một xử lý thành phần chạy vào. Bạn có thể đặt thuộc tính này để mỗi thành phần chạy trong quy trình riêng của nó hoặc do đó một số thành phần dùng chung một quy trình trong khi các thành phần khác thì không.

Bạn cũng có thể đặt android:process để các thành phần của nhiều ứng dụng chạy trên cùng một ứng dụng 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ỉ.

<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ả cá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 tài nguyên theo yêu cầu của các quy trình phân phối ngay lập tức cho người dùng. Mục đích sử dụng do đó các thành phần đang chạy trong quá trình tắt sẽ bị huỷ bỏ. Đã bắt đầu một quy trình cho các thành phần đó khi chúng cần phải làm việc.

Khi quyết định tắt quy trình nào, hệ thống Android cân nhắc tầm quan trọng tương đối của chúng với người dùng. Ví dụ: tính năng này dễ dàng tắt một quy trình lưu trữ các hoạt động không còn 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ị. Quyết định có chấm dứt một tiến trình, do đó 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ệ của nó với các trạng thái của ứng dụng được thảo luận trong 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, có tên là luồng chính. Luồng này rất quan trọng vì chịu trách nhiệm gửi các sự kiện đến các tiện ích giao diện người dùng thích hợp, bao gồm cả các sự kiện vẽ. Điều nà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 qua android.widget của bộ công cụ giao diện người dùng Android và android.view gói. Vì lý do này, đôi khi luồng chính có tên là luồng giao diện người dùng. Tuy nhiên, trong các trường hợp đặc biệt, vai trò chính của 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 Thread chú thích.

Hệ thống không tạo luồng riêng cho mỗi thực thể của một thành phần. Tất cả các thành phần chạy trong cùng một quy trình được tạo thực thể trong luồng giao diện người dùng và các lệnh gọi hệ thống đến mỗi thành phần sẽ được gửi đi từ luồng đó. Do đó, các phương thức phản hồi lại hệ thống lệnh gọi lại, chẳng hạn như onKeyDown() để báo cáo hành động của người dùng hoặc phương thức 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 vào tiện ích, sự kiện này sẽ đặt trạng thái nhấn và đăng yêu cầu vô hiệu hoá thành hàng đợi sự kiện. Luồng giao diện người dùng sẽ xếp yêu cầu vào hàng đợi và thông báo cho tiện ích vẽ lại .

Mô hình đơn luồng này trừ phi bạn triển khai ứng dụng đúng cách 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 công việc để phản hồi tương tác của người dùng. 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ư truy cập mạng hoặc các truy vấn cơ sở dữ liệu, chặn toàn bộ giao diện người dùng. Khi luồng bị chặn, không thể gửi sự kiện nào, bao gồm cả sự kiện vẽ.

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

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úng giao diện người dùng từ một luồng worker. Thực hiện mọi thao tác đối với giao diện người dùng từ giao diện người dùng chuỗi. Có 2 quy tắc cho 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 Worker

Do mô hình đơn luồng này, điều quan trọng là 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ần thực hiện các thao tác không diễn ra ngay lập tức, hãy nhớ thực hiện chúng trong nền riêng biệt hoặc worker. 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 luồng 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 vào luồng giao diện người dùng từ luồng. Dưới đâ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ì hoạt động ở chế độ nền được thực hiện qua một luồng riêng biệt trong khi ImageView luôn được thao tác từ luồng giao diện người dùng.

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

Các phương thức an toàn cho luồng

Trong một số trường hợp, các phương thức mà 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 đối với các phương thức có thể gọi từ xa, chẳng hạn như các phương thức trong dịch vụ ràng buộc. Khi một cuộc gọi trên 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 đượ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 trong một quy trình khác, phương thức này sẽ thực thi trong một luồng đã chọn từ một nhóm luồng mà hệ thống duy trì trong cùng một quy trình như IBinder. Không được thực thi trong luồng giao diện người dùng của quy trình này.

Ví dụ: trong khi dịch vụ Phương thức onBind() được gọi từ luồng giao diện người dùng của quy trình của dịch vụ, 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 các phương thức gọi quy trình từ xa (RPC) được gọi từ các luồng trong bể bơi. Vì một dịch vụ có thể có nhiều khách hàng, nên nhiều chuỗi nhóm có thể tương tác với nhau cùng một phương thức IBinder tại cùng một thời điểm, vì vậy các phương thức IBinder phải được triển khai 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. ContentResolverContentProvider các lớp ẩn chi tiết về cách quản lý giao tiếp liên quy trình (IPC) nhưng phương thức ContentProvider phản hồi các yêu cầu đó – phương thức query(), insert(), delete(), update(), và getType()—là được gọi từ mộ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 từ giao diện người dùng luồng cho quy trình. Vì các phương thức này có thể được gọi từ số lượng luồng bất kỳ tại đồng thời, các giá trị này cũng phải được triển khai để an toàn cho luồng.

Giao tiếp liên quy trình

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

Giá trị trả về sẽ là sau đó được truyền theo hướng ngược lại. Android cung cấp tất cả các mã để thực hiện các IPC này giao dịch, do đó 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 bài viết Tổng quan về dịch vụ.