Android의 Thread는 Main Thread와 Worker Thread로 나눌 수 있다.
Main Thread는 Android Application이 실행되면 기본적으로 실행되는 Thread이고 Worker Thread는 특정 작업을 수행하기 위해 개발자가 직접 생성해서 실행되는 Thread이다.
Main Thread는 UI Thread라고도 부르며 이름에서도 알 수 있듯이 UI를 제어하며 사용자와 상호작용하는 Thread이다.
사용자와 상호작용을 하기 때문에 시간이 오래 걸리는 작업은 Main Thread에서 실행하는 것이 아닌 별도의 Worker Thread를 만들어 Worker Thread안에서 실행시켜야 한다.
예를 들어 용량이 큰 동영상을 다운 받는 작업을 Main Thread에서 실행한다면 동영상을 전부 다운 받을 때까지 사용자는 앱의 다른 기능을 이용하지 못한채 가만히 기다려야 한다. 또한 대기 시간이 길어지면 ANR(Application Not Responding)이 발생할 수도 있다.
이렇게 시간이 오래 걸리는 작업을 Main Thread에서 실행한다면 Application의 반응성이 매우 떨어진다. 이러한 문제를 해결하기 위해 시간이 오래 걸리는 작업은 별도의 Worker Thread를 만들어 Worker Thread안에서 실행시켜줘야 한다.
앞서 시간이 오래 걸리는 작업은 Worker Thread에서 실행시켜야 Application의 반응성을 높일 수 있다고 했다. 하지만 한가지 주의해야 할 점이 있다. 바로 Worker Thread에서는 UI 관련 작업을 할 수 없다는 점이다.
Android UI는 단일 Thread에서 동작한다.Main Thread에서만 동작
즉 Main Thread외의 Thread에서는 UI 관련 작업을 할 수 없다는 얘기다. Android UI가 단일 Thread에서 동작하는 이유는 만약 모든 Thread에서 UI 관련 작업을 할 수 있다 한다면, 여러 Thread가 동시에 동일한 UI 자원을 이용할 수 있고 이때 Dead lock
Race Condition
등의 문제가 발생할 수 있기 때문이다.
교착 상태(Dead lock) - 두 개 이상의 Process 또는 Thread가 서로 자원을 얻지 못해 다음 처리를 하지 못하고 무한히 다음 자원을 기다리는 상태
경쟁 상태(Race Condition) - 두 개 이상의 Process 또는 Thread가 하나의 자원에 접근하기 위해 경쟁하는 상태
하지만 Worker Thread에서도 UI 작업을 해야 할 상황이 있다. 예를 들어 네트워크 작업 중 DB에 접근해서 데이터를 가져와 TextView에 표시해야 할 경우등이 있다. 이렇게 Worker Thread에서 UI 작업을 해야 할 경우에는 Worker Thread에서 해야하는 UI 작업을 Main Thread로 넘겨줘야 하고 이를 위해 Android에서는 Looper
Handler
를 제공한다.
하나의 Thread는 하나의 Looper를 가지며, Looper 내부에는 MessageQueue가 존재한다. MessageQueue는 Handler로 전달받은 message들이 들어있으며 이를 Looper가 FIFO 방식으로 message를 꺼내 처리한다.(Looper가 FIFO 방식으로 메시지를 처리하지만, 이것이 처리 순서를 보장하는 것은 아니다. 핸들러가 메시지를 처리하는 도중에 다른 메시지나 Runnable 객체가 MessageQueue에 추가되면, 이들은 순서대로 처리되지 않을 수 있다. 이런 경우에는 핸들러 내부에서 적절한 처리를 해주어야 한다.)
Thread #1 - Main Thread
Thread #2 - Worker Thread
우선 main thread에는 Handler 객체가 생성되어 있어야 worker thread에서 작업을 넘겨 받을 수 있다. main thread에 Handler 객체가 생성되어 있고 worker thread에서 main thread로 UI작업을 넘기려 한다면 worker thread에서는 main thread의 Handler객체에 post()나 sendMessage()를 통해 UI작업을 넘겨줄 수 있다. 이렇게 작업을 넘기면 main thread의 MessageQueue에는 message 형태의 작업이 쌓이게 되고 Looper가 loop()를 통해 MessageQueue에 있는 작업을 하나씩 꺼내어 처리한다. Runnable 객체라면 Handler를 통해 처리하지 않고 run() 메서드를 호출하여 작업을 수행한다. Message 객체라면 Handler를 통해 처리하는데 handleMessage() 메서드를 호출하여 Handler가 메세지를 전달받을 수 있도록 하여 처리한다.
Handler.sendMessage(Message msg);
Message 객체를 MessageQueue에 전달
Handler.sendEmptyMessage(int what);
Message의 what 필드를 전달
Handler.post(new Runnable());
Runnable 객체를 MessageQueue에 전달
Android는 Handler의 기본생성자를 통해 Handler를 생성하면 해당 Handler를 호출한 Thread의 Looper와 자동으로 연결해준다. 즉 직접 Looper를 생성해 줄 필요 없음
runnable 사용 예제
public class MainActivity extends AppCompatActivity {
private Button button;
private TextView textView;
private Handler mHanlder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = findViewById(R.id.button);
textView = findViewById(R.id.textView);
mHanlder = new Handler();
Thread workerThread = new Thread(new Runnable() {
@Override
public void run() {
mHanlder.post(new Runnable() {
@Override
public void run() {
textView.setText("WorkerThread");
}
});
}
});
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
workerThread.start();
}
});
}
}
handler 사용 예제 - sendEmptyMessage(int what)
public class MainActivity extends AppCompatActivity {
private Button button;
private TextView textView;
private MyHandler mHanlder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = findViewById(R.id.button);
textView = findViewById(R.id.textView);
mHanlder = new MyHandler();
Thread workerThread = new Thread(new Runnable() {
@Override
public void run() {
mHanlder.sendEmptyMessage(1000);
}
});
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
workerThread.start();
}
});
}
class MyHandler extends Handler {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
if (msg.what == 1000) {
textView.setText("WorkerThread");
}
}
}
}
handler 사용 예제 - sendMessage(Message msg)
public class MainActivity extends AppCompatActivity {
private Button button;
private TextView textView;
private MyHandler mHanlder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = findViewById(R.id.button);
textView = findViewById(R.id.textView);
mHanlder = new MyHandler();
Thread workerThread = new Thread(new Runnable() {
@Override
public void run() {
Message message = mHanlder.obtainMessage();
Bundle bundle = new Bundle();
bundle.putString("value", "WorkerThread!!!");
message.setData(bundle);
mHanlder.sendMessage(message);
}
});
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
workerThread.start();
}
});
}
class MyHandler extends Handler {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
Bundle bundle = msg.getData();
String value = bundle.getString("value");
textView.setText(value);
}
}
}
Activity에서 제공하는 runOnUiThread() 메서드를 통해서도 Worker Thread에서 UI작업을 수행할 수 있으며 아래와 같이 구현되어 있다.
public final void runOnUiThread(Runnable action) {
if (Thread.currentThread() != mUiThread) {
mHandler.post(action);
} else {
action.run();
}
}
runOnUiThread의 동작은 현재 Thread가 Main Thread라면 action이 즉시 실행되고 만약 현재 Thread가 Main Thread가 아니라면 action은 MessageQueue에 들어간다.
runOnUiThread() 사용 예제
public class MainActivity extends AppCompatActivity {
private Button button;
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = findViewById(R.id.button);
textView = findViewById(R.id.textView);
Thread workerThread = new Thread(new Runnable() {
@Override
public void run() {
runOnUiThread(new Runnable() {
@Override
public void run() {
textView.setText("runOnUiThread");
}
});
}
});
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
workerThread.start();
}
});
}
}