-
Android AsyncTask안드로이드 2020. 5. 10. 12:20728x90
지난 글에서 안드로이드의 메인 스레드와 작업 스레드의 역할과 통신에 대해서 알아봤다면 이번에는 이를 편하게 해주는 Helper Class 중 하나인 AsyncTask 에 대해서 알아볼거에요
메소드
메인스레드에서 동작하는 메소드
onPreExecute()
onProgressUpdate()
onCancelled()
onPostExecute()
작업스레드에서 동작하는 메소드
doInBackground()
onPreExecute()
작업 시작 전에 실행되는 메소드로, 주로 변수 초기화 작업이 이루어집니다.
doInBackground()
작업 스레드가 실행시키는 메소드로 Looping 기능이 없어서 자체적으로 무한 루프를 돌게 만들어야 합니다. 리턴되면 작업 스레드도 같이 사라집니다.
onProgressUpdate()
작업 스레드가 하고 있는 일의 진척도를 레이아웃에 반영해야할 때 주로 사용합니다. 게임에서 리소스 파일 다운로드 할 때 몇% 완료 이런거 띄우는 기능이 여기서 이루어집니다.
onCancelled()
doInBackground() 가 종료되기 전에 단 한 번이라도 cancel() 이 호출된 적이 있다면 doInBackground() 리턴 후 실행됩니다.
onPostExecute()
doInBackground() 가 종료되기 전에 단 한 번도 cancel() 이 호출된 적이 없다면 doInBackground() 리턴 후 실행됩니다.
AsyncTask를 만들 때 3개의 Generic 이 필요한데 각 역할은 다음과 같습니다.
첫 번 째 Generic : doInBackground 의 작업 내용을 전달하는 용도
두 번 째 Generic : 작업 스레드의 작업 내용을 표현하는 방식
세 번 째 Generic : AsyncTask의 최종 결과물을 표현하는 방식
예제
<layout/activity_main.xml>
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <Button android:id="@+id/btn" android:text="버튼" android:layout_width="match_parent" android:layout_height="match_parent"/> </LinearLayout>
<MainActivity.java>
package kr.co.sample; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.Log; import android.view.View; import android.widget.Button; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; public class MainActivity extends AppCompatActivity { Button btn; private class MyTask extends AsyncTask<Integer, Integer, String> { private int num; @Override protected void onPreExecute() { super.onPreExecute(); Log.d("MyTask", "onPreExecute"); num = 0; } @Override protected String doInBackground(Integer... integers) { Log.d("MyTask", "doInBackground"); for (int i = 0; i < integers[0]; i++) { num++; // onProgressUpdate() 를 호출해주는 메소드 publishProgress(num, integers[0]); try { // 0.1초 대기 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } // onPostExecute("성공했습니다"); 호출 return "성공했습니다."; } @Override protected void onProgressUpdate(Integer... values) { super.onProgressUpdate(values); Log.d("MyTask", "onProgressUpdate"); // 몇 % 까지 진행되었는지 화면에 보여주는 용도 int percentage = (int) ((double) values[0] / values[1] * 100); Log.i("MyTask", percentage + "% 완료"); btn.setText(percentage + "% 완료"); } @Override protected void onCancelled() { super.onCancelled(); Log.d("MyTask", "onCancelled"); } @Override protected void onCancelled(String s) { super.onCancelled(s); Log.d("MyTask", "onCancelled " + s); } @Override protected void onPostExecute(String s) { super.onPostExecute(s); // s == "성공했습니다." Log.d("MyTask", "onPostExecute " + s); btn.setText(s); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn = findViewById(R.id.btn); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { MyTask myTask = new MyTask(); // AsyncTask 실행 // doInBackground([3]); myTask.execute(3); } }); } }
결과
특이한 점은 doInBackground() 메소드에서 onProgressUpdate() 를 직접 호출하면 에러가 납니다.
이는 onProgressUpdate() 가 작업 스레드에서 호출되었기 때문이구요. publishProgress() 메소드를 호출해주면 안드로이드가 알아서 메인스레드를 통해 onProgressUpdate() 를 호출해준답니다.
여러 개의 AsyncTask 실행해보기
@Override public void onClick(View v) { MyTask myTask1 = new MyTask(); MyTask myTask2 = new MyTask(); myTask1.execute(3); myTask2.execute(3); }
onClick 메소드를 위와 같이 수정하고 돌려보시면
작업 스레드라서 굉장히 두 AsyncTask 가 동시에 실행될 것 같지만 그렇지 않고, myTask1 이 모두 완료된 뒤에 myTask2 가 실행됨을 알 수 있습니다.
여러 개의 AsyncTask 가 동시에 실행되려면 executeOnExecutor() 메소드와 AsyncTask.THREAD_POOL_EXECUTOR 옵션을 사용하시면 됩니다.
MyTask myTask1 = new MyTask(); MyTask myTask2 = new MyTask(); myTask1.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, 3); myTask2.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, 3);
명시적으로 순차 실행을 원하면 AsyncTask.SERIAL_EXECUTOR 옵션을 사용할 수도 있습니다.
AsyncTask 중단하기
package kr.co.sample; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.Log; import android.view.View; import android.widget.Button; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; public class MainActivity extends AppCompatActivity { Button btn; MyTask myTask = new MyTask(); boolean isRunning = false; private class MyTask extends AsyncTask<Integer, Integer, String> { private int num; @Override protected void onPreExecute() { super.onPreExecute(); Log.d("MyTask", "onPreExecute"); num = 0; isRunning = true; } @Override protected String doInBackground(Integer... integers) { Log.d("MyTask", "doInBackground"); for (int i = 0; i < integers[0]; i++) { num++; publishProgress(num, integers[0]); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); return "실패했습니다."; } } return "성공했습니다."; } @Override protected void onProgressUpdate(Integer... values) { super.onProgressUpdate(values); Log.d("MyTask", "onProgressUpdate"); int percentage = (int) ((double) values[0] / values[1] * 100); Log.i("MyTask", percentage + "% 완료"); btn.setText(percentage + "% 완료"); } @Override protected void onCancelled() { super.onCancelled(); Log.d("MyTask", "onCancelled"); isRunning = false; } @Override protected void onCancelled(String s) { super.onCancelled(s); Log.d("MyTask", "onCancelled " + s); btn.setText(s); } @Override protected void onPostExecute(String s) { super.onPostExecute(s); Log.d("MyTask", "onPostExecute " + s); btn.setText(s); isRunning = false; } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn = findViewById(R.id.btn); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (!isRunning) { myTask.execute(100); } else { myTask.cancel(true); } } }); } }
myTask 를 Field 로 옮기고, isRunning 이라는 Field 도 추가했습니다. 태스크가 일을 하고 있는지 안하고 있는지 체크하기 위한 변수에요
doInBackground() 메소드에서도 catch() 구문에 들어가게 되면
return "실패했습니다.";
되도록 바뀌구, onClick() 메소드에서도 두 번 클릭 하면 태스크가 취소되게 바뀌었습니다.
결과
doInBackground() 내부에
try {
...
} catch (InterruptException e) {
}이런 구문이 있으면 myTask.cancel(true); 를 통해 doInBackground() 메소드에서 catch 문으로 빠지게 돼요
return "실패했습니다.";
근데 중요한 점은 위 return 구문을 추가해주지 않으면 다음과 같은 결과가 나와요
6%에서 멈췄는데 Cancelled 의 메시지는 성공했습니다. 라고 뜨는걸 볼 수 있어요.
이게 catch 문으로 들어간다고 doInBackground() 가 종료되는건 아니에요.
프로그래머가 catch 문에서 적절히 종료를 시켜줘야한다는거죠
그럼 6%에서 왜 멈췄을까요? publishProgress() 메소드는 cancel() 이 한 번이라도 호출되었다면 onProgressUpdate() 메소드를 실행시켜주지 않아요. 그래서 for 문은 열심히 돌고 있는데 onProgressUpdate()가 실행되지 않았고, 화면은 아무런 변화도 없게 되는거에요
그 다음 중요한 점은 myTask.cancel() 메소드에요.
myTask.cancel(true); // InterruptExecption 발생
myTask.cancel(false); // InterruptException 미발생코드를 원래 상태로 바꾸고, myTask.cancel(true); 를 false 로 바꿔보세요
이번에도 onCancelled() 에 성공했습니다. 가 떴죠?
원래대로라면 catch 문에 들어가서 return "실패했습니다."; 가 실행되어야 하는데 cancel(false); 는 InterruptException 을 발생시키지 않기 때문에 catch 문에 들어가질 않아요.
이를 해결하기 위해서는 doInBackground() 메소드를 다음과 같이 수정하면 돼요
@Override protected String doInBackground(Integer... integers) { Log.d("MyTask", "doInBackground"); for (int i = 0; i < integers[0]; i++) { num++; publishProgress(num, integers[0]); if (isCancelled()) return "실패했습니다."; try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); return "실패했습니다."; } } return "성공했습니다."; }
isCancelled() 메소드를 통해서 태스크가 종료되었는지 확인하는거죠
이제 잘 뜨네요 ㅎㅎ
InterruptException 에 대해서..
interruptException 은 쉬고 있는 스레드에 대해서만 전달되게 되어있어요.
이게 무슨 말이냐면 wait(), sleep(), join() 메소드 같이 스레드가 WAITING 혹은 TIMED_WAITING 상태일 때만 전달이 된다는거에요
package kr.co.sample; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.Log; import android.view.View; import android.widget.Button; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; public class MainActivity extends AppCompatActivity { Button btn; MyTask myTask = new MyTask(); boolean isRunning = false; private class MyTask extends AsyncTask<Integer, Integer, String> { private int num; @Override protected void onPreExecute() { super.onPreExecute(); Log.d("MyTask", "onPreExecute"); num = 0; isRunning = true; } @Override protected String doInBackground(Integer... integers) { Log.d("MyTask", "doInBackground"); for (int i = 0; i < integers[0]; i++) { num++; publishProgress(num, integers[0]); try { } catch (Exception e) { e.printStackTrace(); return "실패했습니다."; } } return "성공했습니다."; } @Override protected void onProgressUpdate(Integer... values) { super.onProgressUpdate(values); Log.d("MyTask", "onProgressUpdate"); int percentage = (int) ((double) values[0] / values[1] * 100); Log.i("MyTask", percentage + "% 완료"); btn.setText(percentage + "% 완료"); } @Override protected void onCancelled() { super.onCancelled(); Log.d("MyTask", "onCancelled"); isRunning = false; } @Override protected void onCancelled(String s) { super.onCancelled(s); Log.d("MyTask", "onCancelled " + s); btn.setText(s); isRunning = false; } @Override protected void onPostExecute(String s) { super.onPostExecute(s); Log.d("MyTask", "onPostExecute " + s); btn.setText(s); isRunning = false; } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn = findViewById(R.id.btn); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (!isRunning) { myTask.execute(10000000); } else { myTask.cancel(true); } } }); } }
예제를 살짝 바꿔서 doInBackground() 에서 sleep(); 을 없애고 InterruptException 을 그냥 Exception 으로 바꿨어요
그리고 isCancelled() 를 통한 탈출도 없애버렸죠. 무엇보다 sleep 없이 우리가 두 번 클릭 할 수 있는 시간동안 태스크가 끝나면 안되기에 myTask.execute(되게 큰 수); 를 넣어줬습니다. (컴퓨터의 for 문은 매우 빠르니까요...!)
마지막으로 myTask.cancel(false); 도 true 로 바꿨어요.
그리고 실행해보면
성공했습니다. 가 리턴되는게 보이시죠?
이게 sleep() 구문이 없이 정신 없이 돌고 있는 스레드에게는 InterruptException 이 전달되지 않기 때문이에요.
동료가 너무 열심히 일하고 있으면 약간 방해하기 미안하잖아요? 그래서 방해하지 않다가 잠깐 쉬는 상태(wait, sleep 등) 가 되면 그제서야 방해하는 그런 느낌이에요
'안드로이드' 카테고리의 다른 글
안드로이드 HandlerThread (0) 2020.05.13 안드로이드 CountDownTimer (0) 2020.05.13 안드로이드 Looper, Message Queue, Handler (0) 2020.05.09 안드로이드 핸들러 (0) 2020.05.09 안드로이드 멀티스레드 (0) 2020.05.09