[Android] 핸들러(Handler)
모든 내용은 Do it! 안드로이드 앱 프로그래밍을 바탕으로 정리한 것입니다.
핸들러(Handler)
새로운 프로젝트를 만들면 자동으로 생성되는 메인 액티비티는 앱이 실행될 때 하나의 프로세스에서 처리됨
→ 메인 액티비티 내에서 이벤트를 처리하거나 특정 메서드를 정의하여 기능을 구현할 때도 같은 프로세스 안에서 실행됨
☝ BUT, 대기 시간이 길어지는 네트워크 요청 등의 기능을 수행할 때는 화면에 보이는 UI 멈춤 상태가 발생하는 문제가 생길 수 있음
[해결방법] 멀티 스레드 방식 : 하나의 프로세스 안에서 여러 개의 작업이 동시 수행
- 👍 같은 프로세스 안에 들어 있으면서 메모리 리소스를 공유하기 때문에 효율적인 처리 가능
- 👎 동시에 리소스에 접근할 때 데드락(DeadLock)이 발생하여 시스템이 비정상적으로 동작할 수 있음
* 스레드(Thread) : 동시 수행이 가능한 작업 단위로 현재 수행 중인 작업 이외의 기능을 동시에 처리할 때 새로운 스레드를 만들어 처리
* 데드락(DeadLock) : 동시에 두 곳 이상에서 요청이 생겼을 때 어떤 것을 먼저 처리할지 판단할 수 없어 발생하는 시스템 상의 문제(런타임 시의 예외상황)
💬 지연 시간이 길어질 수 있는 경우,
오랜 시간 작업을 수행하는 코드를 별도로 분리 & UI에 응답을 보낸 방식 을 사용함.
- 서비스 사용하기
- 백그라운드 작업은 서비스로 실행하고 사용자에게는 알림 서비스로 알려주는 방식
- 메인 액티비티로 결과 값을 전달하고 이를 이용해서 다른 작업을 수행하고자 한다면 브로드캐스팅으로 결과 값을 전달
- 스레드 사용하기
- 스레드는 같은 프로세스 안에 있기 때문에 작업 수행의 결과를 바로 처리할 수 있음
- BUT, UI 객체는 직접 접근할 수 없으므로 핸들러(Handler) 객체를 사용함
- 메인 스레드 : 안드로이드에서 UI를 처리할 때 사용되는 기본 스레드
- 메인 스레드에서 이미 UI에 접근하고 있으므로 새로 생성한 다른 스레드에서 핸들러 객체를 사용해서 메시지를 전달함으로써 메인 스레드에서 처리하도록 만드는 것
스레드 사용하기
- 안드로이드에서 표준 자바의 스레드를 그대로 사용할 수 있음
- 스레드는 new 연산자로 객체를 생성한 후 start() 메서드를 호출하면 시작할 수 있음
- Thread 클래스에 정의된 생성자는 크게 파라미터가 없는 경우와 Runnable 객체를 갖는 경우가 있음
- 일반적으로 Thread 클래스를 상속한 새로운 클래스를 정의한 후 객체를 만들어 시작하는 방법을 사용함
public class MainActivity extends AppCompatActivity {
int value = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
BackgroundThread thread = new BackgroundThread();
thread.start(); // start()를 호출하여 스레드를 시작
}
});
}
class BackgroundThread extends Thread {
// 스레드를 시작하면 run() 메서드가 실행됨
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(1000);
} catch (Exception e) {}
value += 1;
Log.d("Thread", "value : "+value);
}
}
}
}
❓ 위의 예제에서 화면에는 TextView를 추가하고, 해당 위젯에 value 값을 출력하도록 한다면?
~ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
Thread 클래스를 상속받은 BackgroundThread 객체에서 UI 객체를 직접 접근했다는 의미의 에러가 발생함
→ 메인 스레드에서 관리하는 UI 객체는 개발자가 직접 만든 스레드 객체에서는 접근할 수 없다는 의미
<Thread 생성자에 파라미터가 없는 경우>
핸들러로 메시지 전송하기
- 앱을 실행할 때 프로세스가 만들어지면 그 안에 메인 스레드가 함께 만들어짐
- 최상위에서 관리되는 앱 구성 요소인 액티비티, 브로드캐스트 수신자 등과 새로 만들어지는 윈도우를 관리하기 위한 메시지 큐(Message Queue)를 실행함
- 메시지 큐를 사용하면 순차적으로 코드를 수행할 수 있음
- 핸들러 클래스는 메시지 큐로 메인 스레드에서 처리할 메시지를 전달하는 역할을 담당함
- 핸들러를 이용하면 특정 메시지가 미래의 어떤 시점에 실행되도록 스케줄링 할 수도 있음
< 메인 스레드에 접근하기 위해 핸들러를 사용할 때의 과정 >
- 새로운 스레드(스레드 #1)가 수행하려는 정보를 메인 스레드에 전달하기 위해 메세지 큐의 메세지 객체 하나를 참조함
- obtainMessage() 메서드를 이용하여 호출의 결과로 메시지 객체를 반환받음
- 이 메시지 객체에 필요한 정보를 넣은 후, sendMessage() 메서드를 이용해 메시지 큐에 넣음
- 메시지 큐에 들어간 메시지는 순서대로 핸들러가 처리함
- handleMesage() 메서드가 실행되는데, 실행 위치는 메인 스레드
public class MainActivity extends AppCompatActivity {
TextView textView;
MainHandler handler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.textview);
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
BackgroundThread thread = new BackgroundThread();
thread.start();
}
});
handler = new MainHandler();
}
class BackgroundThread extends Thread {
int value = 0;
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(1000);
} catch (Exception e) {}
value += 1;
Log.d("Thread", "value : "+value);
Message message = handler.obtainMessage(); // 1번, 메시지 객체 반환 받음
// 메시지 객체 안에 Bundle 객체가 들어 있어
// putOOO()로 데이터를 넣고 getOOO()으로 데이터를 가져올 수 있음
Bundle bundle = new Bundle();
bundle.putInt("value", value);
message.setData(bundle); // 메시지 객체에 데이터 넣음
handler.sendMessage(message); // 2번, 메시지 객체를 메시지 큐에 전달
}
}
}
class MainHandler extends Handler {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
// 3번, 핸들러 처리
Bundle bundle = msg.getData();
int value = bundle.getInt("value");
textView.setText("value 값 : "+value);
}
}
}
<Thread 생성자에 Runnable 객체를 갖는 경우>
Runnable 객체 실행하기
- 핸들러를 사용하는 방식은 가장 일반적이지만 코드가 복잡해보이는 단점이 있음
- Runnable 객체를 핸들러의 post() 메서드로 전달해주면 이 객체에 정의된 run() 메서드 안의 코드들은 메인 스레드에서 실행됨
- post() 메서드로 전달되는 Runnable 객체는 스레드의 작업 결과물로 만들어지는 데이터를 처리해야 함
- 결과물을 화면에 보여주어야 하는 경우, new 연산자로 Runnable 인터페이스를 구현하는 새로운 객체를 만들어 사용하는 것이 일반적인 방법
- post() 메서드를 호출하는 것이 훨씬 간단하기 때문에 실제 앱을 만들 때는 post() 메서드를 더 많이 사용함
public class MainActivity extends AppCompatActivity {
TextView textView;
Handler handler = new Handler();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.textview);
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
BackgroundThread thread = new BackgroundThread();
thread.start();
}
});
}
class BackgroundThread extends Thread {
int value = 0;
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(1000);
} catch (Exception e) {}
value += 1;
Log.d("Thread", "value : "+value);
// 핸들러 객체에 post() 메서드로 Runnable 객체 전달
handler.post(new Runnable() {
@Override
public void run() {
textView.setText("value 값 : "+value);
}
});
}
}
}
}