티스토리 뷰

여기에서 정리하는 것을 요약해서 말하자면 별도의 Thread를 생성하고 사용 할 경우, 그 안에서는 UI 관련 객체(Button, TextView 등)에 대해서는 변경을 가할 수 없기 때문에 이를 해결 할 수 있는 방법을 정리하는 것이다.




  •  Thread 사용시 Handler의 필요성



안드로이드 어플리케이션이 실행되면 안드로이드에선 UI의 처리를 위해 사용되는 기본 쓰레드를 생성하는데 이것을 '메인쓰레드'라고 한다.


그런데 어떤 작업을 백그라운드로 실행 시키면서 그 작업의 상황을 중간중간 보고 받을 필요가 있는데, 백그라운드를 만드는 작업은 새로운 Thread를 만들어서 할 수 있다.


이를테면 <ProgressBar>의 진행상황을 TextView에서 표현할 상황이 생길 수 있다.

새로운 Thread를 이용하여 ProgressBar의 진행률을 시간 별로 캐치하여 변경할 수 있지만, 그 새로운 Thread에서 TextView 진행률을 표시해야 하기 때문에 이 객체에 접근해야 하는데.. 결과적으로 이 UI객체인 TextView에 접근할 수가 없다.


이유는,

안드로이드에서는 UI객체는 '메인쓰레드'가 아닌 쓰레드에서는 변경을 가할 수 없도록 하기 때문이다.



새로운 Thread를 이용해 ProgressBar의 진행률을 표시하면서 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
49
50
51
52
53
54
55
56
57
public class TestActivity extends AppCompatActivity {
 
    ProgressBar bar;
    boolean isRunning;
    Button btn;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
 
        //레이아웃 XML에 선언한 뷰 객체를 가져온다.
        bar = (ProgressBar) findViewById(R.id.testProgressBar);
        btn = (Button) findViewById(R.id.testBtn);
 
        //ProgressThread 생성
        Runnable r = new ProgressThread();
        Thread t = new Thread(r);
        //ProgressThread 시작
        t.start();
 
 
        btn.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View view) {
                btn.setText("작업량"+ bar.getProgress());
               // Toast.makeText(TestActivity.this, , Toast.LENGTH_SHORT).show();
            }
        });
 
    }
 
 
 
 
    public class ProgressThread implements Runnable{
        @Override
        public void run() {
 
            while(!isRunning){
                try {
                    Thread.sleep(100);
                    bar.incrementProgressBy(1);
                    btn.setText("작업량.."+ bar.getProgress() + "%");
                    if(bar.getProgress() == 100){
                        break;
                    }
                    
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
 
        }
    }
 
}
cs

위 코드는 44라인에서 다음과 같은 에러를 내뱉는다.

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

잘못된 쓰레드에서 이 객체를 호출 하였다는 에러다. 

더 상세히 읽어보면 이 뷰(버튼)가 만들어진 계층의 쓰레드에서만 이 뷰를 건들 수 있다는 것이다. 

안드로이드에서 UI관련 객체는 '메인쓰레드'에서만 접근이 가능하다고 했다. 그러니까 에러 문구에서 말하는 original Thread는 메인쓰레드를 말하고, 메인쓰레드에서만이 이 뷰(버튼)에  접근할 수 있다는 얘기다.




이 같은 문제를 해결하기 위해 Handler가 있다.

안드로이드 애플리케이션이 실행될 때 '메인쓰레드'는 메세지 큐(Message Queue)라는 것을 실행하게 되는데, 이 메세지 큐를 이용해서 사용자 정의 쓰레드에서 처리할 작업을 메인쓰레드에서 처리할 수 있도록 할 수 있다.


Hadler의 sendMessage(Message msg)메서드를 이용하여 메세지 큐에 Message를 넣을 수 있다. 매개변수 파라미터로 들어가는 Message타입은 Handler타입의 메서드인 obtainMessge()를 호출하면 참조할 수 있다.

sendMessage(..)로 인해 메세지 큐에 들어간 Message는 차례대로 Handler가 처리하게 되며, 처리는 Handler를 확장한 클래스의 오버라이딩 된 handleMessage()가 담당한다.



+ 레이아웃 XML

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
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.choonie.threadapp.MainActivity">
 
    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="작업중..." />
 
    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyleHorizontal"
        android:progress="0"
        android:max="100"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="5dp" />
 
</LinearLayout>
 
cs


+ MainActivity

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
public class MainActivity extends AppCompatActivity {
 
    Handler handler;
    ProgressBar bar;
    TextView tv;
    boolean running;
    ProgressThread t;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        bar = (ProgressBar) findViewById(R.id.progressBar);
        tv = (TextView) findViewById(R.id.textView);
        
        //Handler의 handleMessage(Message msg)를 오버라이딩한 인스턴스를 생성
        handler = new Handler() {
            //이 곳에서 UI와 관련된 객체의 작업을 호출
            //Thread안에서 UI 관련된 객체에 변경을 가할 때는 에러를 내뱉었지만 여기서는 정상적으로 된다.
            @Override
            public void handleMessage(Message msg) {
                //프로그래스바의 값을 1증가 시킨다.
                bar.incrementProgressBy(1);
                
                //프로그래스 값이 100이라면 작업완료
                if(bar.getProgress() == 100){
                    running = true;
                    tv.setText("작업완료!");
                }else {
                    tv.setText("작업중... " + bar.getProgress() + "%");
                }
            }
        };
 
 
    }
 
 
    @Override
    protected void onStart() {
        super.onStart();
        
        bar.setProgress(0);
        t = new ProgressThread();
        t.start();
 
    }
 
    @Override
    protected void onPause() {
        super.onPause();
        
        t = null;
        running = false;
    }
 
    public class ProgressThread extends Thread {
        Message msg;
 
        @Override
        public void run() {
            try {
                while (!running) {
                    Thread.sleep(100);
                    //메세지 큐에 작업을 보내는 sendMessage(..)를 호출하기 전에 obtainMessage()로 Message객체를 가져온다.
                    msg = handler.obtainMessage();
                    //메세지 큐에 작업을 보낸다. 보낸 작업은 큐에서 순서대로 해당 handler의 handleMessage(..)를 호출하게 된다.
                    handler.sendMessage(msg);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                tv.setText("작업중에 에러가 발생했습니다!");
            }
        }
    }
 
}
cs

위와 같이 Handler를 이용하게 되면 사용자 정의 쓰레드에서 sendMessage(..)를 호출만 하면 해당 Handler의 handleMessage(..)가 호출되어 그 안에서 UI객체에 대한 접근을 할 수 있다.





  • Handler를 사용하면서 시간차를 두고 작업 실행하기



Handler클래스는 또한 해당 메세지 큐에 들어간 작업을 몇초 후에 실행할 건지, 또는 언제 실행할 건지를 결정할 수 있는 메서드를 제공하고 있다.


  • boolean sendMessageAtTime(Message msg, long timeMillis) : 언제 큐에 들어간 작업을 실행할 건지?
  • boolean sendMessageDelayed(Message msg, long delayMillis) : 몇 초 후에 큐의 작업을 실행할 건지?

테스트 해본 결과 최초로 호출했을 때만 적용이 되고, 이 후에는 일반적인 sendMessage(..)를 호출했을 때와 같이 딜레이 없이 실행된다.






  • 사용자 정의 쓰레드로 




Comments
최근에 올라온 글
최근에 달린 댓글
TAG
more
Total
Today
Yesterday