ANDROID/Android 앱 프로그래밍

[Android] 핸들러(Handler)

주 녕 2021. 6. 17. 18:52
728x90

모든 내용은 Do it! 안드로이드 앱 프로그래밍을 바탕으로 정리한 것입니다. 

 

핸들러(Handler)

새로운 프로젝트를 만들면 자동으로 생성되는 메인 액티비티는 앱이 실행될 때 하나의 프로세스에서 처리됨

→ 메인 액티비티 내에서 이벤트를 처리하거나 특정 메서드를 정의하여 기능을 구현할 때도 같은 프로세스 안에서 실행됨

☝ BUT, 대기 시간이 길어지는 네트워크 요청 등의 기능을 수행할 때는 화면에 보이는 UI 멈춤 상태가 발생하는 문제가 생길 수 있음

 

[해결방법]  멀티 스레드 방식 : 하나의 프로세스 안에서 여러 개의 작업이 동시 수행

  • 👍 같은 프로세스 안에 들어 있으면서 메모리 리소스를 공유하기 때문에 효율적인 처리 가능
  • 👎 동시에 리소스에 접근할 때 데드락(DeadLock)이 발생하여 시스템이 비정상적으로 동작할 수 있음

* 스레드(Thread) 동시 수행이 가능한 작업 단위로 현재 수행 중인 작업 이외의 기능을 동시에 처리할 때 새로운 스레드를 만들어 처리

* 데드락(DeadLock) : 동시에 두 곳 이상에서 요청이 생겼을 때 어떤 것을 먼저 처리할지 판단할 수 없어 발생하는 시스템 상의 문제(런타임 시의 예외상황)

 

 

💬 지연 시간이 길어질 수 있는 경우,

오랜 시간 작업을 수행하는 코드를 별도로 분리 & UI에 응답을 보낸 방식 을 사용함.

  1. 서비스 사용하기
    • 백그라운드 작업은 서비스로 실행하고 사용자에게는 알림 서비스로 알려주는 방식
    • 메인 액티비티로 결과 값을 전달하고 이를 이용해서 다른 작업을 수행하고자 한다면 브로드캐스팅으로 결과 값을 전달
  2. 스레드 사용하기
    • 스레드는 같은 프로세스 안에 있기 때문에 작업 수행의 결과를 바로 처리할 수 있음
    • 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. 새로운 스레드(스레드 #1)가 수행하려는 정보를 메인 스레드에 전달하기 위해 메세지 큐의 메세지 객체 하나를 참조함
    1. obtainMessage() 메서드를 이용하여 호출의 결과로 메시지 객체를 반환받음
    2. 이 메시지 객체에 필요한 정보를 넣은 후, sendMessage() 메서드를 이용해 메시지 큐에 넣음
  2. 메시지 큐에 들어간 메시지는 순서대로 핸들러가 처리함
    1. 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);
                    }
                });
            }
        }
    }
}

 

 

 

728x90