Asynctask la gi

Khái niệm tiểu trình (thread) là một trong những khái niệm nền tảng giúp chúng ta hiểu được các dịch vụ Android (Android services) – là các tiến trình (processes) chạy ngầm và không có giao diện người dùng (xem lại //ngocminhtran.com/2018/07/29/cac-thanh-phan-co-ban-cua-ung-dung-android/ ). Sử dụng các dịch vụ Android để thực thi các nhiệm vụ cho các ứng dụng Android sẽ được tìm hiểu trong các bài viết kế tiếp.

Tổng quan về các tiểu trình (threads)

Các tiểu trình là thành phần cơ bản của bất kỳ một hệ điều hành đa nhiệm nào. Các tiểu trình được xem như các tiến trình mini (mini-processes) (xem lại khái niệm tiến trình trong Android tại //ngocminhtran.com/2018/07/29/chu-ky-song-cua-ung-dung-android-va-activity/ ) đang chạy với một tiến trình chính (main process), mục đích là cho phép ít nhất sự xuất hiện các đường dẫn thực thi song song trong các ứng dụng.

Tiểu trình chính (main thread)

Khi một ứng dụng Android thực thi lần đầu tiên, hệ thống sẽ tạo ra một tiểu trình chạy mặc định trong tất cả các thành phần của ứng dụng. Tiểu trình này được xem như là tiểu trình chính (main thread). Vai trò chính của nó là xử lý các sự kiện và tương tác với các views từ giao diện người dùng. Bất kỳ một thành phần bổ sung nào đến ứng dụng đều được thực thi trên tiểu trình chính này.

Khi một thành phần của ứng dụng đang thực thi một nhiệm vụ nào đó tiêu tốn nhiều thời gian dùng tiểu trình chính, hệ thống sẽ ngăn chặn các nhiệm vụ khác thực thi cho đến khi nhiệm vụ của thành phần hiện tại hoàn thành. Do đó, khi đang sử dụng, người dùng Android thỉnh thoảng nhận cảnh báo nội dung “Application is not responding”. Giải pháp cho vấn đề này là chuyển nhiệm vụ đang thực thi sang một tiểu trình khác tách bạch với tiểu trình chính và điều này cho phép tiểu trình chính tiếp tục làm việc với các nhiệm vụ khác.

Trình xử lý tiểu trình (Thread Handlers)

Do ứng xử của tiểu trình chính (main thread) khi đang thực thi một nhiệm vụ tiêu tốn thời gian là ngăn chặn các nhiệm vụ khác nên chúng ta cần chú ý hai luật quan trọng sau:

  • Thứ nhất, không bao giờ thực thi các nhiệm vụ tiêu tốn thời gian trên tiểu trình chính;
  • Thứ hai, và cũng quan trọng không kém, khi thực thi nhiệm vụ trên một tiểu trình khác thì nhiệm vụ này phải không liên quan đến giao diện người dùng bởi vì Android UI toolkit không hỗ trợ an toàn tiểu trình (thread-safe).

Vậy nếu chúng ta vẫn muốn thực thi một nhiệm vụ trong một tiểu trình (khác với tiểu trình chính) và nhiệm vụ này cũng liên quan đến giao diện người dùng thì sao? Giải pháp cho vấn đề này là sử dụng nguyên lý không đồng bộ với lớp AsyncTask.

Ứng dụng minh họa về sử dụng lớp AsyncTask

Để hiểu hơn về nguyên tắc không đồng bộ, trong phần này chúng ta sẽ tạo một ứng dụng minh họa tên AsyncDemo trong Android Studio (bài viết này đang dùng Android Studio 3.4):

Nhấn Finish. Trong giao diện Main Activity thêm một Button dưới TextView mặc định Hello World

Nhấn nút Infer Constraints để thiết lập các kết nối ràng buộc cho Button:

Thay đổi thuộc tính id cho TextView là myTextView; thuộc tính idtext cho Button là:

id button
text @string/press_me

Trong tập tin strings.xml bổ sung thêm dòng:

<string name="press_me">Press Me</string>

Mở tập tin MainActivity.java và thêm đối tượng TextView như sau:

package com.ngocminhtran.asyncdemo; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.widget.TextView; public class MainActivity extends AppCompatActivity { private TextView myTextView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); myTextView = (TextView)findViewById(R.id.myTextView); } }

Kế tiếp là thêm phương thức buttonClick đến lớp MainActivity:

import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.TextView; public class MainActivity extends AppCompatActivity { ... public void buttonClick(View view) { int i = 0; while (i <= 20) { try { Thread.sleep(1000); i++; } catch (Exception e) { } } myTextView.setText("Button Pressed"); } }

Vì mục tiêu của chúng ta là minh họa một nhiệm vụ tốn nhiều thời gian trên tiểu trình chính nên trong phương thức buttonClick chúng ta dùng phương thức sleep để tạm dừng việc hiển thị nội dung đến TextView sau 20 giây.

Mở lại tập tin layout activity_main.xml ở chế độ Text, tìm đến Button và thêm thuộc tính onClick với phương thức buttonClick:

<Button ... android:id="@+id/button" android:text="@string/press_me" android:onClick="buttonClick" app:layout_constraintTop_toBottomOf="@+id/myTextView" />

Thực thi ứng dụng và nhấn nút Press Me sẽ không có vấn đề gì xảy ra vì ứng dụng sẽ tạm dừng trong 20 giây. Nếu chúng ta tiếp tục nhấn nút, có thể một thông báo sẽ xuất hiện:

Như vậy, tiểu trình chính đã ngăn hay khóa các nhiệm vụ khác. Giải pháp là thực thi nhiệm vụ trên một tiểu trình khác và chúng ta sẽ dùng lớp AsyncTask để định nghĩa một tiểu trình mới.

Bước đầu tiên trong việc tạo một tiểu trình mới là định nghĩa một lớp tên MyTask thừa kế từ lớp AsyncTask trong lớp MainActivity. Lớp MyTask có thể được thêm bất kì vị trí nào trong lớp MainActivity, giả sử thêm tại vị trí đầu tiên:

import android.os.AsyncTask; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.TextView; public class MainActivity extends AppCompatActivity { private class MyTask extends AsyncTask<String, Void, String> { @Override protected void onPreExecute() { } @Override protected String doInBackground(String... params) { } @Override protected void onProgressUpdate(Void... values) { } @Override protected void onPostExecute(String result) { } } ... }

Lúc này có thể xuất hiện một lỗi từ tập tin MainActivity bởi vì phương thức doInBackground chưa trả về một giá trị kiểu String nào.

Chúng ta không cần bận tâm đến lỗi xảy ra, chỉ cần chú ý khai báo lớp AsyncTask (sau từ khóa extends) chứa 3 kiểu dữ liệu (String, Void, và String) cho 3 tham số. 3 kiểu dữ liệu này tương ứng với 3 kiểu dữ liệu của 3 tham số của phương thức theo thứ tự là doInBackground, onProgressUpdate và onPostExecute:

Do đó, khi chúng ta thay đổi kiểu dữ liệu cho tham số trong các phương thức thì cũng thay đổi kiểu dữ liệu một cách tương ứng cho khai báo lớp AsyncTask. Giả sử trong ứng dụng chúng ta muốn chuyển một tham số kiểu Integer đến phương thức onProgressUpdate thì kiểu Void trong khai báo lớp AsyncTask cũng thay đổi sang Integer:

Phương thức onPreExecute() có thể được dùng để thực thi các bước khởi tạo. Phương thức này chạy trong tiểu trình chính và cũng có thể được sử dụng để cập nhật giao diện người dùng.

Các đoạn mã thực thi trong tiểu trình mới phải được đặt trong phương thức doInBackground(). Phương thức này không thể truy cập đến tiểu trình chính nên nó cũng không thể can thiệp được giao diện người dùng.

Phương thức onProgressUpdate() được gọi mỗi lần phương thức publishProgress(), được đặt trong phương thức doInBackground(), được gọi và có thể được dùng để cập thông tin giao diện người dùng.

Phương thức onPostExecute() được gọi khi các nhiệm vụ thực thi với phương thức doInBackground() hoàn thành. Phương thức này nhận giá trị trả về từ phương thức doInBackground() và cập nhật thông tin đến giao diện người dùng.

Như đã đề cập ở trên, các đoạn mã trong tiểu trình mới phải được đặt trong phương thức doInBackground() nên chúng ta sẽ di chuyển toàn bộ nội dung trong phương thức buttonClick() đến phương thức doInBackground():

@Override protected String doInBackground(String... params) { int i = 0; while (i <= 20) { try { Thread.sleep(1000); publishProgress(i); i++; }catch (Exception e) { return(e.getLocalizedMessage()); } } return "Button Pressed"; }

Để ý rằng chúng ta gọi phương thức publishProgress() trong doInBackground(). Giá trị trả về từ phương thức doInBackground() sẽ được cập nhật đến giao diện người dùng, cụ thể là TextView, thông qua phương thức onPostExecute() nên myTextView sẽ được di chuyển đến phương thức này:

@Override protected void onPostExecute(String result) { myTextView.setText(result); }

Phương thức onProgressUpdate() cập nhật thông tin từ doInBackground() để phản ánh đến giao diện người dùng phải thông qua phương thức publishProgress() được gọi trong doInBackground() nên chúng ta sẽ thêm một lệnh gọi publishProcess trong doInBackground() như sau:

@Override protected String doInBackground(String... params) { int i = 0; while (i <= 20) { publishProgress(i); try { Thread.sleep(1000); publishProgress(i); i++; }catch (Exception e) { return(e.getLocalizedMessage()); } } return "Button Pressed"; }

Thêm đoạn mã cập nhật thông tin đến TextView trong phương thức onProgressUpdate():

@Override protected void onProgressUpdate(Integer... values) { myTextView.setText("Counter = " + values[0]); }

Cuối cùng chúng ta sẽ áp dụng nguyên tắc thực thi không đồng bộ bằng cách thực thi phương thức của đối tượng MyTask trong phương thức buttonClick() như sau:

public void buttonClick(View view) { AsyncTask task = new MyTask().execute(); }

Nếu thực thi ứng dụng và nhấn Button, sẽ xuất hiện bộ đếm giây Counter trên TextView phản ánh số giây và sau 20 giây sẽ xuất hiện thông điệp Button Pressed. Trong quá trình Counter cập nhật số giây nếu chúng ta nhấn Button thì sẽ không có cảnh báo nào xuất hiện (tuy nhiên bộ đếm Counter có thể nhảy lung tung)

Nhiệm vụ đang thực thi có thể bị hủy bằng lệnh cancel của đối tượng AsyncTask như sau:

task.cancel(true);

Chú ý rằng, nếu chúng ta thêm lệnh cancel như trên dưới lệnh execute() trong phương thức buttonClick thì khi thực thi ứng dụng và nhấn Button sẽ không có gì xảy ra.

Mặc định, khi một ứng dụng thực thi nhiều hơn một nhiệm vụ hay đồng thời thì chỉ có nhiệm vụ đầu tiên được thực thi, các nhiệm vụ khác sẽ được đặt trong một hàng đợi và thực thi một cách tuần tự. Để thực thi các nhiệm vụ một cách song song, chúng ta có thể dùng thuộc tính THREAD_POOL_EXECUTOR của lớp AsyncTask như sau:

AsyncTask task = new MyTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);

Số lượng các nhiệm vụ có thể thực thi song song phụ thuộc vào số lõi CPU trên thiết bị. Có thể dễ dàng kiểm tra số lõi CPU hợp lệ trên thiết bị của chúng ta với dòng lệnh:

int cpu_cores = Runtime.getRuntime().availableProcessors();

Trong nguyên tắc xử lý không đồng bộ của Android, số lượng tiểu trình tối thiểu cho phép sẽ bằng cpu_cores – 1 và số lượng tối đa là cpu_cores*2 + 1.

Bài viết này chỉ nêu tổng quan về tiểu trình trong ứng dụng Android. Vấn đề đa tiểu trình trên Android sẽ được bàn sâu hơn trong các bài viết khác.

Mã nguồn các tập tin trong ứng dụng AsyncDemo có thể xem tại GitHub

Lập trình Android với Android Studio 3.X >

Chủ đề