Friday, June 27, 2014

Process và Thread

Khi một application component khởi chạy và application không có bất kỳ component nào khác đang chạy, Android system khởi động một Linux process mới cho application với một thread thực thi. Mặc định tất cả các component trong cùng một ứng dụng chạy trên cùng một process và thread(main thread). 


Process

Mặc định tất cả các component của cùng một Activity chạy trong cùng một process. Nếu bạn muốn kiểm soát process nào dành cho component nào thì bạn có thể làm việc đó tại file manifest.

Mỗi loại component <activity>, <service>, <receiver>, <provider> hỗ trợ một thuộc tính android:process mà chỉ ra một process mà component chạy trên. Có thể có các component của các ứng dụng khác nhau chạy trên cùng một process.

<application> hỗ trợ thuộc tính android:process để thiết lập process mặc định cho tất cả components

Android có thể kill một process tại một thời điểm bất kỳ khi bộ nhớ không đủ hoặc được yêu cầu bởi các process khác đang trực tiếp phục vụ người dùng. Process được khởi động lại cho các component đó khi chúng làm việc trở lại

Khi quyết định process nào sẽ bị kill, Android sẽ so sánh mức độ quan trọng của process đó với người dùng. Ví dụ android sẽ kill process đang host một activity trong trạng thái paused thay vì process đang host activity trong trạng thái resumed.

Process Lifecycle
Android system cố gắng duy trì một process của ứng dụng càng lâu càng tốt, nhưng khi có một process mới quan trọng nó sẽ xóa bỏ process cũ để lấy lại bộ nhớ. Để chỉ ra process nào sẽ bị kill, hệ thống sẽ gán cho mỗi process một "importance hierarchy" dựa trên các component  chạy trên process và trạng thái của các component  đó. Các process có mức độ importance thấp sẽ bị loại bỏ trước...

Có 5 cấp độ importance hierarchy như sau:

1. Foreground process:
Là một process được yêu cầu cho các hành động hiện hành của user. Một process được coi là foreground nếu bất kỳ điều kiện nào sau đây là đúng:
  • Nếu nó host một Activity mà user đang tương tác với (trạng thái activity là resumed)
  • Nếu host một Service mà rằng buộc với activity mà user tương tác
  • Nếu host một Service đang chạy ở foreground - service đã gọi phương thức startForeground()
  • Nếu nó host một Service mà đang thực thi một trong các lifecycle callback của nó (onCreate(), onStart() hoặc onDestroy()).
  • Nếu nó host một BroadcastReceiver mà đang thực thi onReceiver()
Thông thường chỉ có một vài foreground process tồn tại ở một thời điểm, và chúng chỉ bị kill khi bộ nhớ không đủ để chạy toàn bộ foreground process cùng lúc.

2. Visible process:
Là một process không có bất kỳ foregound component nào, nhưng vẫn có thể tác động tới nhưng gì user nhìn thấy trên màn hình. Một process visible nếu bất kì điều kiện nào sau đây đúng:

  • Nếu nó host cho một Activity mà không ở trạng thái foreground, nhưng vẫn visible cho user (khi hàm onPause() được gọi). Ví dụ: khi một foreground Activity khỏi động một Dialog, Activity lúc này sẽ hiện ỏ phía sau dialog
  • Nó host cho một Service mà ràng buộc với một visible Activity (foreground Activity)
Visible process rất quan trọng và sẽ không bị kill trừ khi việc kill chúng là để giữ cho foreground process chạy.

3. Serivce process
Một process chạy một service khi hàm startService() được gọi và không rơi vào một trong hai category cao hơn. Mặc dù service process không trực tiếp gắn với bất cứ thứ gì mà user có thể nhìn thấy nhưng chúng làm những việc mà user quan tâm (như là play music in background, hoặc là download dữ liệu từ trên mạng), hệ thống sẽ duy trì chúng đến khi không còn đủ bộ nhớ cho foreground và visible process chạy

4. Background process
Một process dữ một Activity mà không còn visible cho user (hàm onStop() của Activity được gọi). Process không còn tác động tới user experience, và hệ thống có thể kill chúng bất kỳ lúc nào để duy trì bộ nhớ cho 3 loại process bên trên. Thông thường có rất nhiều background process chạy, chúng được lưu trong một danh sách LRU(least recently used) để đảm bảo rằng process và activity nào vừa mới được user sử dụng sẽ bị kill sau cùng.  Nếu một activity implement các method của lifecycle một cách đúng đắn thì việc kill process đó sẽ không ảnh hưởng tới user experience, bởi vì khi user quay trở lại activty đó nó sẽ khôi phục lại toàn bộ trạng thái.

5. Empty process
Một process không dữ bất kỳ một active application component nào, lý do duy nhất để duy trì process này là để caching, cải thiện thời gian startup. Hệ thống thường xuyên kill các process này theo thứ tự để duy trì sự cân bằng tài nguyên của toàn bộ hệ thống giữa các process cache và các kernal cache cơ bản.


Lưu ý: Android xếp hạng process ở cấp độ cao nhất mà nó có thể đạt được, dựa trên mức độ quan trọng của các component đang được active trong process. Ví dụ, nếu một proess host cho một service và một visible activiy thì process được xếp hạng là một visible process chứ không phải là service process.

Cấp độ của process có thể tăng lên bởi vì có các process khác phụ thuộc vào nó - process mà đang phục vụ cho các process khác thì cấp độ sẽ không bao giờ thấp hơn process mà nó đang phục vụ. Ví du: nếu một content provider trong process A đang phục vụ một client trong process B hoặc một service trong process A rằng buộc với một component trong process B thì cấp độ process A luôn bằng hoặc lớn hơn B.


Threads

Khi ứng dụng được khởi động, hệ thống sẽ tạo ra một thread cho ứng dụng gọi là "main". Thread rất quan trọng bởi vì nó chịu trách nhiệm điều phối các Event tới các user interface widget thích hợp, bao gồm cả các draw events. Nó cũng là thread mà chương trình của bạn sử dụng để tương tác với các component từ Android UI toolkit (các component từ android.widget và android.view package). Main thread còn được gọi là UI thread.

Hệ thông không tạo ra thread riêng cho từng component. Tất cả các thành phần chạy trên cùng process đều được khởi tạo trên UI thread, và hệ thống sẽ gọi cho từng component được đưa ra từ thread đó. Vì thế các method mà phản hồi cho system callback(như onKeyDown() hoặc một lifecycle callback) lúc nào cũng chạy trên UI thread của process.

Ví dụ, khi user touch vào một button trên màn hình, UI thread của bạn sẽ gửi touch event cho widget để chuyển trạng thái của nó sang pressed và post một invalidate request vào event queue. UI thread sẽ lấy các request từ trong queue và notify cho widget mà cần phải redraw.

Khi ứng dụng của bạn thực hiện một công việc chuyên sâu để đáp ứng sự tương tác của người dùng. Mô hình sử dụng một thread có thể mạng lại hiệu suất kém. Đặc biệt nếu bạn làm mọi thứ trên UI thread, thực hiện các tình toán cần nhiều thời gian như là truy cập network hoặc truy vấn dữ liệu sẽ block UI. Khi UI bị block, không event nào được gửi đi bao gồm cả drawing event. Và theo góc nhìn của người dùng thì chương trinh đang bị treo. Tệ hơn nữa nếu chương trình treo lâu hơn 5s cửa sổ "Application not responding" sẽ hiện ra, người dùng có thể sẽ đóng chương trình lại và uninstall nó nếu họ cảm thấy bực bội.

Android UI toolket is not thread-safe. Vì vậy bạn không thể thao tác với UI từ một worker thread, bạn phải làm mọi thao tác cho UI thread trên UI thread. Có 2 nguyên tắc cơ bản bạn cần phải lưu ý:

  1. Không được block UI thread
  2. Không được truy cập Android UI toolkit từ bên ngoài UI thread.

Worker threads

Nếu bạn phải thực hiện các thao tác không tức thời, bạn phải đảm bảo rằng các thao tác đó được thực hiện trên các thread tách biệt với UI thread ("backgound" hoặc "worker" thread)

Ví dụ bên dưới, một đoạn code đơn giản thự hiện việc lắng nghe sự kiện click của user download một tấm hình sau đó hiển thị lên trong một thread riêng:


public void onClick(View v) {
    new Thread(new Runnable() {
        public void run() {
            Bitmap b = loadImageFromNetwork("http://example.com/image.png");
            mImageView.setImageBitmap(b);
        }
    }).start();
}

Trước hết, ta thấy đoạng code này có vẻ ổn, bởi vì nó tạo ra một thread mới để xử lý nework operation. Tuy nhiên nó đã vi phạm nguyên tắc thứ 2 nêu trên. Ví dụ này đã modify ImageView từ một worker thread thay vì làm nó trong UI thread.

Để giải quết vấn đề này, Android đã cung cấp một vài cách để truy cập UI thread từ các thread khác:

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • View.postDelayed(Runnable, long)
Ví dụ, chúng ta có thể fix đoạn code bên trên như sau:

public void onClick(View v) {
    new Thread(new Runnable() {
        public void run() {
            final Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png");
            mImageView.post(new Runnable() {
                public void run() {
                    mImageView.setImageBitmap(bitmap);
                }
            });
        }
    }).start();
}
 Đoạn code trên là thread-safe: network operation được thực hiện trong thread riêng trong khi ImageView được xử lý trong UI thread.

Tuy nhiên nếu bạn code theo cách này thi chương trình của bạn sẽ rất phức tạp và khó phát triển lên. Để giải quyết những thao tác phức tạp hơn với một worker thread, bạn nên sử dụng Handler trong worker thread để gửi message cho UI thread. Nhưng giải pháp tốt nhất là extend AsyncTask class, điều này đơn giản hóa việc hoạt động của worker thread.

Sử dụng AsyncTask

AsyncTask cho phép bạn thực hiện các việc bất đồng bộ trên user interface. Nó thực hiện các công việc tốn nhiều thời gian trên worker thread sau đó thông báo kết quả về cho UI thread.

Để sử dụng bạn vào subclass AsynTask và implement phương thức callback doInBackground(), cái mà chạy trên backgroud thread. Để cập nhật UI, bạn phải implement phương thức onPostExecute(), phương thức này nhận kết quả từ doInBackground và chạy trên UI thread. Bạn có thể chạy task bằng cách gọi excute() trên UI thread.

Ví dụ:
public void onClick(View v) {
    new DownloadImageTask().execute("http://example.com/image.png");
}
private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
    /** The system calls this to perform work in a worker thread and
      * delivers it the parameters given to AsyncTask.execute() */
    protected Bitmap doInBackground(String... urls) {
        return loadImageFromNetwork(urls[0]);
    }
    
    /** The system c
om doInBackground() */
    protected void onPostExecute(Bitmap result) {
        mImageView.setImageBitmap(result);
    }
}

Chú ý: Một vấn đề khác có thể xuất hiện khi sử dụng một worker thread đó là Activity bị khởi động lại do runtime configuration thay đổi (như khi user xoay màn hình) có thế kill worker thread của bạn.


2 comments: