스레드는 하나의 프로세스안에서 동시수행작업을 위해 사용된다.
안드로이드 스레드도 표준 자바의 스레드를 그대로 사용한다. 하지만 직접만든 스레드는 UI객체에 접근하지 못하게 되어 있는데 UI를 관리하는 메인 스레드와 동시접근할때 발생하는 문제를 방지하기 위해서이다. 따라서 스레드 처리방법을 잘 알고 사용해야한다!
핸들러
새로운 프로젝트를 만들면 자동으로 생성되는 메인액티비티는 앱이 실행될 때 하나의 프로세스에서 처리가 된다. 따라서 메인 액티비티 내에서 이벤트처리를 하거나 메서드 정의해서 기능을 구현할 때도 같은 프로세스 안에서 실행된다. 같은 프로세스 안에서 일련의 기능이 순서대로 실행될때 대기시간이 길어지게되면 대기하는동안 멈추게 되는 문제점이 있을 수 있다. 이를 해결하기 위해 멀티스레딩 방식을 사용하는데 멀티스레드 방식은 같은 프로세스안에 들어있어 메모리 리소스를 공유하므로 효율적인 처리가 가능하다. 하지만 리소스에 동시접근할때 데드락이 발생할 수도 있어 조심해야한다. 데드락은 런타임시의 예외사항이라 디버깅하기 쉽지 않을 수 있다.
지연시간이 길어질 수 있는 앱이라면 오랜시간 작업을 수행해야하는 코드를 분리한다음 UI에 응답을 보내는 방식을 사용한다. 이를 위해 안드로이드가 제공하는 시나리오는 2가지가 있다.
1) 서비스 사용하기 : 백그라운드 작업을 서비스로 실행하고 사용자에게 알림 서비스로 알려준다. 만약 메인 액티비티로 결과값을 전달하고 이를 이용해 다른작업 수행하고 싶으면 브로드캐스팅으로 결과값을 전달할 수 있다.
2) 스레드 사용하기 : 스레드는 같은 프로세스 안에 있어서 작업수행의 결과를 바로 처리할 수 있다. 그러나 UI객체는 직접 접근이 불가해서 핸들러 객체를 사용한다.
결국 UI처리할 때 기본 스레드인 메인 스레드가 이미 UI접근중이어서 우린 핸들러라는 객체를 사용해서 메세지를 전달함으로써 메인 스레드에서 처리하도록 만들 수 있다.
스레드 사용하기
스레드는 표준 자바처럼 new 연산자로 객체를 생성한 후 start()를 호출하면 시작할 수 있다. Thread클래스에 정의된 생성자는 크게 파라미터가 없는 경우와 Runnable 객체를 파라미터로 갖는 두가지로 구분할 수 있다. 일반적으로는 Thread클래스를 상속한 새로운 클래스를 정의한 후 객체를 만들어 시작하는 방법을 사용한다.
public class thread_main extends AppCompatActivity {
int value =0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_thread_main);
Button button = findViewById(R.id.button13);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
BackgroundThread thread = new BackgroundThread();
thread.start();
}
});
}
class BackgroundThread extends Thread{
public void run(){
for(int i=0;i<100;i++){
try{
Thread.sleep(1000);
}catch (Exception e){}
value+=1;
Log.d("Thread", "value : "+value);
}
}
}
}
이제 이 value값을 xml상의 textView에 띄우고자 한다. 바로 setText로 접근하면 UI에 스레드 직접접근이 되어서 에러가 발생하게 된다. 이 때는 핸들러를 사용!
핸들러 사용
앱을 실행할때 프로세스가 만들어지면 그 안에 메인스레드가 함께 만들어진다. 그리고 최상위에서 관리되는 앱 구성요소인 액티비티, 브로드캐스트 수신자 등과 새로만들어지는 윈도우를 관리하기 위한 메시지 큐를 실행한다. 메시지큐를 사용하면 순차적으로 코드를 수행할 수 있는데 이렇게 메시지 큐로 메인스레드에서 처리할 메시지를 전달하는 역할을 핸들러 클래스가 담당한다. 결국 핸들러는 핸들러가 포함되어있는 스레드에서 순차적으로 실행시킬때 사용하게 된다. 핸들러를 이용하면 미래의 어떤 시점에 실행되도록 스케줄링할수도 있다.
새로만든 스레드가 수행하려는 정보를 메인스레드로 전달하기 위해선 우선 핸들러가 관리하는 메세지 큐에서 처리할 수 있는 메세지 객체하나를 참조해야한다. obtainMessage()를 이용할 수 있고 호출의 결과로 메세지 객체를 반환받을 수 있다. 이 메세지객체에 필요한 정보를 넣은 후 sendMessage()를 이용해 메세지 큐에 넣을 수 있다. 메세지 큐에 들어간 메세지는 순서대로 핸들러가 처리하고 handleMessage()에 정의된 기능이수행된다. 이 handleMessage()에 들어있는 코드가 수행되는 위치는 메인스레드 안이 된다.
public class thread_main extends AppCompatActivity {
int value =0;
TextView textView;
MainHandler handler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_thread_main);
Button button = findViewById(R.id.button13);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
BackgroundThread thread = new BackgroundThread();
thread.start();
}
});
textView = findViewById(R.id.textView6);
handler = new MainHandler();
}
class BackgroundThread extends Thread{
public void run(){
for(int i=0;i<10;i++){
try{
Thread.sleep(1000);
}catch (Exception e){}
value+=1;
Log.d("Thread", "value : "+value);
Message message = handler.obtainMessage();
Bundle bundle = new Bundle();
bundle.putInt("value",value);
message.setData(bundle);
handler.sendMessage(message);
}
}
}
class MainHandler extends Handler{
@Override
public void handleMessage(Message msg){
super.handleMessage(msg);
Bundle bundle = msg.getData();
int value = bundle.getInt("value");
textView.setText("value: "+value);
}
}
}
Runnable 객체
핸들러 클래스는 메세지 전송방법 외에 Runnable 객체를 실행시킬 수 있는 방법을 제공한다. 새로만든 Runnable 객체를 핸들러의 post()로 전달해주면 이 객체에 정의된 run()안의 코드들은 메인 스레드 안에서 실행되게 된다.
public class thread_main extends AppCompatActivity {
int value =0;
TextView textView;
Handler handler = new Handler(); //API 기본 핸들러
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_thread_main);
Button button = findViewById(R.id.button13);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
BackgroundThread thread = new BackgroundThread();
thread.start();
}
});
textView = findViewById(R.id.textView6);
}
class BackgroundThread extends Thread{
public void run(){
for(int i=0;i<10;i++){
try{
Thread.sleep(1000);
}catch (Exception e){}
value+=1;
Log.d("Thread", "value : "+value);
handler.post(new Runnable(){ //핸들러의 post 호출하기
@Override
public void run(){
textView.setText("value: "+value);
}
});
}
}
}
}
post()로 전달되는 Runnable 객체는 스레드의 작업결과물로 만들어지는 데이터를 처리해야한다. 따라서 결과물이 화면에 보여지는 경우 new연산자로 Runnable 인터페이스를 구현하는 새로운 객체를 만들어 사용하는것이 일반적이다.
일정시간 후에 실행하기
웹서버와 같은 원격서버에 접속한 후 응답이 늦어지거나 없으면 앱이 대기하고 있는 상황이 지속되는 문제가 생긴다. 이 경우 별도의 스레드를 만들어 처리하게 된다. 하지만 버튼을 클릭해서 간단하게 접속처리한느 경우엔 메인 스레드내에서 지연시간을 주는 것만으로도 UI의 멈춤현상을 방지할 수 있다. 단순히 Threada.sleep() 메서드를 사용해서 대기상태로 있다 다시 실행할 수도 있다. 하지만 핸들러로 지연시간을 주면 순차적 실행이기때문에 UI객체들에게 영향을 주지 않으면서 지연시간을 두고 실행된다.
private void request(){
String title = "원격요청";
String message = "요청하시겠습니까?";
String titleButton = "예";
String titleButtonNo = "아뇨";
AlertDialog dialog = makeRequestDialog(title, message, titleButton, titleButtonNo);
dialog.show();
textView2.setText("대화상자 표시중 ...");
}
private AlertDialog makeRequestDialog(CharSequence title, CharSequence message, CharSequence titleButton, CharSequence titleButtonNo){
AlertDialog.Builder requestDialog = new AlertDialog.Builder(this);
requestDialog.setTitle(title);
requestDialog.setMessage(message);
requestDialog.setPositiveButton(titleButton, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
textView2.setText("5초 후..");
handler2.postDelayed(new Runnable() {
@Override
public void run() {
textView2.setText("요청완료됨");
}
}, 5000);
}
});
requestDialog.setNegativeButton(titleButtonNo, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
return requestDialog.create();
}
시간을 지정할 때는 sendMessageAtTime(Message, long)이나 sendMessageDelayed(Message, long)을 사용할 수 있다. 첫번째는 보낼때 시간을 지정할 수 있고 두번째는 일정시간 지난뒤 실행되게 설정할 수 있다. Runnable의 post()도 postAtTime()과 postDelayed()를 사용할 수 있다.
스레드로 메세지 전송하기
앞에서와 반대로 메인스레드에서 별도의 스레드로 메세지를 전달하는 방법은 메인스레드에서 변수를 선언하고 별도의 스레드가 이 값을 읽어가는 방법을 사용할 수 있다. 하지만 별도의 스레드가 관리하는 동일한 객체를 여러 스레드가 접근할 땐 별도의 스레드 안에 들어있는 메세지큐를 이용해 순서대로 접근하도록 만들어야한다.
핸들러가 처리하는 메세지큐는 루퍼로 처리되는데, 이 과정은 일반적인 이벤트 처리과정과 유사하다. 루퍼는 메세지큐에 들어오는 메세지를 지속적으로 보면서 하나씩 처리하게 된다. 메인스레드는 UI객체들을 처리하기 위해 메세지와 루퍼를 사용하고 별도의 스레드를 새로 만들었을땐 루퍼가 없다. 그래서 메인 스레드나 다른 스레드에서 메세지 전송방식으로 스레드에 데이터를 전달한 후 순차적으로 작업을 하고 싶다면 루퍼를 만들어주어야한다.
public void run(){
Looper.prepare();
Looper.loop();
}
AsyncTask 사용하기
핸들러를 사용하지 않고 좀 더 간단하게 작업하는 방법도 있다. AsyncTask 클래스를 상속해서 새로운 클래스를 만들면 이안에 스레드를 위한 코드와 UI 접근코드를 한꺼번에 넣을 수 있다. 예로, 웹서버에서 고객이름가져오는 작업, 제품이름가져오는작업을 다른코드로 분리시키고싶다면 두개의 AsyncTask 상속 클래스를 만들고 각각의 코드를 넣으면 된다. AsyncTask 객체를 만들고 execute()를 실행하면 이 객체는 정의된 백그라운드 작업을 수행하고 필요한 경우 그 결과를 메인스레드에서 실행하므로 UI객체에 접근하는데 문제가 없다.
AsyncTask 클래스를 상속하여 새로운 클래스를 정의하면 이 내부에서 필요한 경우마다 콜백메서드들이 자동으로 호출이 된다. doInBackground()에는 새로 만들어진 스레드에서 실행되어야할 코드들을 넣을 수 있고 onPreExecute() 등에선 메인스레드에서 실행될 코드들을 넣을 수 있다. 따라서 이 코드들에선 UI객체들에 자유롭게 접근이 가능하다.
-doInBackground : 새로만든 스레드에서 백그라운드 작업을 수행. execute() 호출할때 사용된 파라미터를 배열로 전달받는다.
-onPreExecute : 백그라운드 작업 수행 전 호출된다. 초기화에 사용된다.
-onProgressUpdate : 백그라운드 작업의 진행상태를 표시하기 위해 호출된다. 작업 수행 중간에 UI객체에 접근하는 경우에도 사용된다. 이 메서드가 호출되도록 하려면 백그라운드 작업중간에 publishProgress()를 호출해야한다.
-onPostExecute : 백그라운드 작업 끝난 후 호출된다. 메모리 리소스를 해제하는 등의 작업에 사용된다. 백그라운드 작업의 결과는 Result 타입의 파라미터로 전달된다.
AsyncTask의 cancle()을 호출하면 작업을 취소할 수 있는데, 그렇게되면 onCancelled()가 호출된다. 작업의 진행상황을 확인하고 싶을땐 getStatus()를 사용할 수 있다. 각 상태는 PENDING, RUNNING, FINISHED로 구분된다.
새로만든 클래스의 <>안엔 이 클래스를 상속하면서 재정의할 새로운 클래스의 메서드가 어떤 자료형의 파라미터를 가질 것인지 알려주는 역할을 한다. 순서에 따라 각 doInBackground(), onProgressUpdate(), onPostExecute()의 파라미터를 결정한다.
public class Asynctask extends AppCompatActivity {
BackgroundTask task;
int value;
ProgressBar progressBar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_asynctask);
progressBar = findViewById(R.id.progressBar2);
Button button = findViewById(R.id.button15);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
task = new BackgroundTask();
task.execute();
}
});
Button button2 = findViewById(R.id.button16);
button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
task.cancel(true);
}
});
}
class BackgroundTask extends AsyncTask <Integer , Integer , Integer> {
protected void onPreExecute(){
value = 0;
progressBar.setProgress(value);
}
protected Integer doInBackground(Integer ... values){
while (isCancelled() == false) {
value++;
if (value >= 100) {
break;
} else {
publishProgress(value);
}
try {
Thread.sleep(100);
} catch (InterruptedException ex) {}
}
return value;
}
protected void onProgressUpdate(Integer ... values) {
progressBar.setProgress(values[0].intValue());
}
protected void onPostExecute(Integer result) {
progressBar.setProgress(0);
}
protected void onCancelled() {
progressBar.setProgress(0);
}
}
}
스레드는 애니메이션을 만들때도 사용되는 경우가 많다.
'안드로이드' 카테고리의 다른 글
웹 요청, Volley 사용, Json 다뤄보기 (0) | 2021.04.28 |
---|---|
서버에 데이터 요청 후 응답받기 - 소켓 (0) | 2021.04.28 |
브로드캐스트 수신자 (0) | 2021.04.26 |
위험 권한 부여 (0) | 2021.04.23 |
서비스 Service (0) | 2021.04.23 |
댓글