본문 바로가기

Android

Thread handler를 이용한 스탑워치

Thread handler를 이용한 스탑워치

프로세스란 컴퓨터에서 연속적으로 실행되고있는 컴퓨터 프로그램을 말하며, 
쓰레드란 하나의 컴퓨터 프로그램 내에서 둘 이상의 작업을 동시에 진행하는(것처럼 보이는) 프로세스보다 작은 단위를 뜻한다.
실제로는 동시에 진행하는 것이 아니지만, 매우 작은 시간으로 쪼개서 실행하기에 동시에 실행된 것처럼 보인다.

이 포스팅에서 스탑워치를 만든 후 다음 포스팅에 이어질 스탑워치를 응용한 두더지게임을 만들어보며 안드로이드에서의 Thread와 Thread handler에 대해 알아보자.


쓰레드를 만드는 방법에는 2가지 방법이 있다.

Thread를 상속받는 방법

Thread 클래스를 상속(extends) 하여 구현하는 방법이다.
뒤의 방법보다 사용하는 방법이 간단하다.
상속 후 run() 메소드를 오버라이드하여 구현하고, 쓰레드 객체를 생성 후 start() 메소드로 실행한다.

Android Studio에서 오버라이딩은 ALT+INSERT 단축키로 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.example.pc_20.stopwatch;
 
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
 
public class MainActivity extends AppCompatActivity {
    TextView tv_count ;
 
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    tv_count = (TextView) findViewById(R.id.tv_count);
    
    timeThread t = new timeThread();
    t.start();
 
}
 
public class timeThread extends Thread{
    int i = 0;
    @Override
    public void run() {
        while(true){
            tv_count.setText(i++ +"");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
}
cs

1초마다 i를 1씩 증가시키며 textView에 보여준다.
하지만 실행시키면 error가 발생하며 앱이 종료될 것이다.

Thread Handler가 추가로 필요하기 때문이다.
안드로이드에서 UI업데이트는 Main Thread에서만 가능하고, 
내가 생성한 외부 Thread에서 UI를 업데이트하기 위해서는 Handler가 따로 필요하다.
onCreate() 밖에 아래 코드를 추가한다.
1
2
3
4
5
6
Handler handler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
        tv_count.setText(msg.arg1 +"");
    }
};
cs

Handler를 구현했으면 handleMessage() 메소드가 파라미터로 받는 Message 클래스에 대한 이해가 필요하다.
Message 클래스에서 알아야 할 필드는 arg1(int), arg2(int), obj(Object) 3개 필드다.
셋 다 public이며, 따로 setter() 메소드가 없어 msg.arg1 = 1; 과 같은 방식으로 값을 넣을 수 있다.

아래 코드에서는 쓰레드가 돌면서 Message 객체의 arg1 필드에 i의 값을 1씩 증가시키며 넣고
sendMessage() 메소드를 이용해 Message객체를 전달하면 Handler가 textView의 값을 변경한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.example.pc_20.stopwatch;
 
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
 
public class MainActivity extends AppCompatActivity {
    TextView tv_count ;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        tv_count = (TextView) findViewById(R.id.tv_count);
 
        timeThread t = new timeThread();
        t.start();
 
    }
 
    Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            tv_count.setText(msg.arg1 +"");
        }
    };
 
    public class timeThread extends Thread{
        int i = 0;
        @Override
        public void run() {
            while(true){
                Message msg = new Message();
                msg.arg1 = i++;
                handler.sendMessage(msg);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
 
        }
    }
}
cs

Runnable 인터페이스를 implements받는 방법

또 하나의 방법은 Runnable 인터페이스를 implements 받는 것이다.
자바에서는 다중상속이 불가하므로 일반적으로 더 권장되는 방법이다.
(사실 Thread 클래스도 Runnableimplements 받고 있다.)

Runnable 인터페이스는 추상메소드인 run() 하나만을 갖고 있으며, Thread 클래스와는 달리 start() 메소드가 존재하지 않는다.
따라서 Runnable 인터페이스를 이용할 때는 
Thread 객체를 생성할 때 생성자로 Runnable 인터페이스를 implements한 클래스를 넣어주고 start() 한다.
1
2
3
4
5
6
7
8
9
10
//Runnable 안에는 start()가 없으므로
Thread thread = new Thread(new timeThread());
thread.start();
 
    public class timeThread implements Runnable{
        @Override
        public void run() {
        .....
        }
    }
cs

쓰레드의 종료

스탑워치에 측정시작이 있으면 측정종료도 필요할 것이다.
쓰레드를 멈추는 방법에는 stop()interrupt() 2가지 방법이 있다.
그러나 stop()은 더 이상 권장되지 않는 방법이며 사용하면 빨간 취소선과 함께 경고가 뜰 것이다.
interrupt() 메소드를 사용하면 진행중인 쓰레드가 즉시 멈추고 해당 쓰레드에서는 아래와 같은 일이 발생한다:

1
thread.interrupt();
cs
  • 현재 스레드가 대상 스레드를 수정할 수 있는 권한이 없으면 SecurityException이 발생한다.
  • 대상 스레드가 Object.wait()Thread.sleep()Thread.join() 메서드에 의해 블로킹된 경우, interrupt state가 사라지고 InterruptedException이 발생한다.
  • 대상 스레드가 InterruptibleChannel을 이용한 I/O 작업에 의해 블로킹된 경우, interrupt state가 설정되고 ClosedByInterruptException이 발생한다.
  • 대상 스레드가 Selector에서 블로킹된 경우, interrupt state가 설정되고 selection 작업에서 리턴된다.
  • 이외의 경우에는 interrupt state가 설정된다.
이에 따라 쓰레드를 멈추고자 할 때는 interrupt()와 try-catch를 이용해 예외가 발생하면 return 등을 이용해 빠져나오면 된다.

쓰레드의 재시작

일단 한 번 죽은 Thread는 재시작이 불가하다.
그러므로 stop() 혹은 interrupt() 당한 Thread는 다시 이용할 수 없다.
따라서 stop 후 다시 start를 눌러 새로 측정을 시작할 때는 Thread 객체를 매번 새로 생성해야 한다.

구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package com.example.pc_20.stopwatch2;
 
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
 
public class MainActivity extends AppCompatActivity {
 
    private Button btn_start, btn_stop, btn_record, btn_pause;
    private TextView time, recordView;
    
    private Thread thread=null;
    private int minute = 0;
    private String record = "";
    private Boolean isRunning = true;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        //findViewById
        btn_start = (Button) findViewById(R.id.btn_start);
        btn_stop = (Button) findViewById(R.id.btn_stop);
        btn_record = (Button) findViewById(R.id.btn_record);
        btn_pause = (Button) findViewById(R.id.btn_pause);
        time = (TextView) findViewById(R.id.timeView);
        recordView = (TextView) findViewById(R.id.recordView);
 
        time.setText("000:00:00");
        recordView.setText("");
 
        btn_start.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // START 버튼을 누르면 START 버튼이 사라지고, 나머지 버튼이 보인다
                v.setVisibility(View.GONE);
                btn_stop.setVisibility(View.VISIBLE);
                btn_record.setVisibility(View.VISIBLE);
                btn_pause.setVisibility(View.VISIBLE);
                
                //START 버튼을 누를 때마다 쓰레드 객체를 새로 만들어 시작한다
                thread = new Thread(new timeThread());
                thread.start();
            }
        });
 
        btn_stop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //STOP 버튼을 누르면 STOP버튼과 PAUSE버튼, RECORD 버튼이 사라지고 START버튼이 다시 생긴다
                v.setVisibility(View.GONE);
                btn_record.setVisibility(View.GONE);
                btn_start.setVisibility(View.VISIBLE);
                btn_pause.setVisibility(View.GONE);
 
                //쓰레드를 인터럽트해 멈춘다
                thread.interrupt(); 
                time.setText("000:00:00");
                record="";
                recordView.setText(record);
 
            }
        });
 
        btn_record.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //RECORD 버튼을 누르면 시간을 찍어 출력한다
                record+=String.valueOf(time.getText())+"\n";
                recordView.setText(record);
 
            }
        });
 
        btn_pause.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //일시정지를 위한 boolean 변수
                isRunning = !isRunning;
                if(isRunning){
                    btn_pause.setText("일시정지");
                }else{
                    btn_pause.setText("계속");
                }
            }
        });
    }
 
    Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            // 시간 format : 
            int mSec = msg.arg1 % 100;
            int sec = (msg.arg1 / 100) % 60;
            int min = (msg.arg1 /100/ 60;
            int hour = (msg.arg1 % 3600 ) % 24;
            String result = String.format("%03d:%02d:%02d", min,sec,mSec);
            time.setText(result);
        }
    };
 
    public class timeThread implements Runnable{
        @Override
        public void run() {
            int i = 0;
 
            while(true){
                while(isRunning){ //일시정지를 누르면 멈추도록
                    Message msg = new Message();
                    msg.arg1=i++;
                    handler.sendMessage(msg);
 
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        return// 인터럽트 받을 경우 return됨
                    }
                }
            }
        }
    }
}
cs

완성


다음 포스팅에 이어집니다.