[Android] 스레드와 핸들러

Android/Android · 2020. 7. 29. 03:51
반응형

https://survivalcoding.com/p/android_basic

 

될 때까지 안드로이드

될 때까지 안드로이드에 수록된 예제의 라이브 코딩 해설

survivalcoding.com

위 서적을 참고하였습니다.

 

 

 이번 시간에는 비동기 처리의 대표적인 예인 ThreadHandler에 대해서 알아보겠습니다.

 

 먼저 비동기 처리란, 여러 작업을 한 번에 처리하는 것을 뜻합니다. 예를 들어, A가 B에게 일을 시켰을 때, B는 일을 하고 있고, A는 쉬고 있으면 동기, A가 같이 일을 하고 있으면 이것을 비동기라고 합니다.

 

 안드로이드에서는 대표적으로 두 가지의 스레드가 존재합니다. 먼저 일반적인 코드 처리, 화면을 갱신하는 처리 등을 하는 메인 스레드 or UI 스레드라고 하고, Thread 클래스를 사용하여 백그라운드에서 작업을 처리하는 스레드를 작업 스레드 or 백그라운드 스레드라고 합니다. 보통 작업 스레드의 대표적인 예가 다운로드 서비스라고 할 수 있죠.

 

 그래서 정리를 하자면,

 

 메인 스레드 : UI 갱신, 눈에 보이는 처리

 작업 스레드 : 오래 걸리는 작업, 눈에 보이지 않는 처리

 

 로 간단히 정리할 수 있습니다.

 

 그런데 만약에 다운로드를 하면서 화면에 얼마만큼 진행되었는지 진행도를 표시하려면 어떻게 해야 할까요? 작업 스레드로 다운로드를 하면서 메인  스레드가 진행률을 갱신하도록 해야겠죠. 두 가지의 스레드를 연결하기 위해 Handler 클래스가 필요합니다.

 

 작업 스레드는 UI를 수정할 수 없는 제약이 있어 UI를 변경하려면 메인 스레드를 사용해야 하는데, 이를 사용하기 위해선 Handler 클래스를 사용합니다.

 

 예제로 확인해 보겠습니다.

 

 

 activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="진행률 : "/>

    <ProgressBar
        android:id="@+id/pb"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:max="100"
        style="@style/Widget.AppCompat.ProgressBar.Horizontal"/>

    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="다운로드 시작"
        android:onClick="startDownload"/>
</LinearLayout>

 

 

 MainActivity.java

public class MainActivity extends AppCompatActivity {

    private TextView textView;
    private ProgressBar progressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = (TextView)findViewById(R.id.tv);
        progressBar = (ProgressBar)findViewById(R.id.pb);

    }

    public void startDownload(View view) {
        for(int i=0; i<=100; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            textView.setText(i+ "%");
            progressBar.setProgress(i);
        }
    }
}

 

 백그라운드 작업으로 1초마다 1퍼센트가 올라가는 코드를 구현해봤습니다. 이것의 실행 결과는 강제 종료입니다. 오류 코드는 ANR(Application Not Response).

 안드로이드는 UI가 10초 동안 반응하지 않으면 ANR 에러를 발생시키고 앱을 종료할 건지, 기다릴 건지 정하도록 메뉴를 하나 띄웁니다. 이런 오류가 발생하는 이유는 Thread.sleep(1000)이 UI 갱신을 방해하고 있기 때문에 앱이 멈춘 것처럼 보이는 겁니다. 이를 해결하기 위해서 Handler를 사용해야 합니다.

 

 

public class MainActivity extends AppCompatActivity {

    private TextView textView;
    private ProgressBar progressBar;
    private Handler handler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = (TextView)findViewById(R.id.tv);
        progressBar = (ProgressBar)findViewById(R.id.pb);
        handler = new Handler();
    }

    public void startDownload(View view) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=0; i<=100; i++) {
                    try {
                        Thread.sleep(1000);
                    }catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // inner 클래스라서 따로 final 변수 생성
                    final int percent = i;
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            textView.setText(percent+ "%");
                            progressBar.setProgress(percent);
                        }
                    });
                }
            }
        }).start();
    }
}

 

 이렇게 핸들러를 사용해서 코드를 작성했습니다. 결과는 당연히 잘 나옵니다.

 

 

 이렇게 핸들러가 UI를 갱신하는 부분은 post() 메서드로 수행합니다. 이렇게 백그라운드 작동은 스레드로 시키고, UI 갱신 작업은 핸들러를 통해 메인 스레드에서 작동시키게 하는 코드를 작성해봤습니다.

 

 핸들러에는 post() 메서드만 있는 것이 아닙니다. 다음은 핸들러의 대표적인 메서드입니다.

 

post(Runnable) 바로 수행
postAtTime(Runnable, long) 지정 시간에 수행
postDelayed(Runnable, long) 지정 시간 이후에 수행

 

 그리고 send로 시작하는 메서드들은 Message 객체를 전달하는 기능을 갖고 있습니다.

 

sendEmptyMessage(int) 빈 메시지로 수행
sendMessage(Message) Message 객체 전달
sendMessageAtTime(Message, long) Message 객체 전달 및 지정 시간에 수행
sendMessageDelayed(Message, long) Message 객체 전달 및 지정 시간 이후에 수행

 

        Message message = Message.obtain();
        message.what = 1;
        message.arg1 = 10;
        message.arg2 = 20;
        message.obj = "hello";

 

 Message 클래스는 인스턴스를 생성하고 what, arg1, arg2, obj 등과 같이 값을 담아서 사용할 수 있습니다. obj는 Object 형이라서 모든 데이터 타입을 받을 수 있습니다. 다음은 Message 값을 사용한 예제입니다.

 

 

public class MainActivity extends AppCompatActivity {

    private TextView textView;
    private ProgressBar progressBar;
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(@NonNull Message msg) {
            int percent = msg.arg1;
            textView.setText(percent+ "%");
            progressBar.setProgress(percent);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = (TextView)findViewById(R.id.tv);
        progressBar = (ProgressBar)findViewById(R.id.pb);
    }

    public void startDownload(View view) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=0; i<=100; i++) {
                    try {
                        Thread.sleep(1000);
                    }catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // inner 클래스라서 따로 final 변수 생성
                    final int percent = i;
                    Message message = Message.obtain();
                    message.arg1 = percent;
                    handler.sendMessage(message);
                }
            }
        }).start();
    }
}

 

 sendMessage() 메서드를 실행하게 되면 Handler의 handleMessage() 콜백 메서드가 호출되고, 파라미터를 통해 값을 전달받을 수 있습니다. 다만, 이 방법은 잘못된 방법이라 메모리 누수가 일어날 수 있습니다.

 

 

 다음은 runOnUiThread()를 사용한 방법입니다. runOnUiThread() 메서드는 post() 메서드와 동일하게 메인 스레드에서 동작하도록 하는 메서드입니다. 따로 Handler 객체를 생성하지 않아도 되는 방법입니다.

 

public class MainActivity extends AppCompatActivity {

    private TextView textView;
    private ProgressBar progressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = (TextView)findViewById(R.id.tv);
        progressBar = (ProgressBar)findViewById(R.id.pb);
    }

    public void startDownload(View view) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=0; i<=100; i++) {
                    try {
                        Thread.sleep(1000);
                    }catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // inner 클래스라서 따로 final 변수 생성
                    final int percent = i;
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            textView.setText(percent+ "%");
                            progressBar.setProgress(percent);
                        }
                    });
                }
            }
        }).start();
    }
}

 

 

 다음은 뷰에 내장되어 있는 핸들러를 사용한 방법입니다.

 

public class MainActivity extends AppCompatActivity {

    private TextView textView;
    private ProgressBar progressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = (TextView)findViewById(R.id.tv);
        progressBar = (ProgressBar)findViewById(R.id.pb);
    }

    public void startDownload(View view) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=0; i<=100; i++) {
                    try {
                        Thread.sleep(1000);
                    }catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // inner 클래스라서 따로 final 변수 생성
                    final int percent = i;
                    textView.post(new Runnable() {
                        @Override
                        public void run() {
                            textView.setText(percent+ "%");
                            progressBar.setProgress(percent);
                        }
                    });
                }
            }
        }).start();
    }
}

 

 모든 뷰는 핸들러를 포함하고 있습니다. 각 뷰의 내장 핸들러를 사용하면 다양한 순간에 UI를 갱신할 수 있습니다.

 

 

 핸들러는 무조건 작업 스레드 내부에서 사용해야 되는 건 아닙니다. 특정 시간 이후에 어떤 기능을 수행하려고 할 때에도 유용한 기능입니다. 아래는 시작 시 한 번, 5초 뒤에 한 번 토스트를 띄우는 코드입니다.

 

public class MainActivity extends AppCompatActivity {

    private TextView textView;
    private ProgressBar progressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = (TextView)findViewById(R.id.tv);
        progressBar = (ProgressBar)findViewById(R.id.pb);
    }

    public void startDownload(View view) {
        Toast.makeText(this, "Notification after 5 second", Toast.LENGTH_SHORT).show();
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(MainActivity.this, "after 5 second", Toast.LENGTH_SHORT).show();
            }
        }, 5000);
    }
}

 

 

 이렇게 이번 포스트에서는 스레드와 핸들러에 대해 알아봤습니다. 백그라운드 처리와 UI 갱신하는 방법은 정말 다양하게 있지만, 계속 이렇게 스레드와 핸들러를 사용하다 보면 언젠간 코드 블록이 겹쳐지면서 점점 복잡해지는 것을 볼 수 있을 것입니다. 이를 방지하기 위해 안드로이드는 AsyncTask라는 것을 제공합니다. 이는 앞서 설명한 스레드와 핸들러를 하나의 작업으로 취급하기 때문에 코드 작성에 있어서 더욱 간편하게 사용이 가능합니다.

 

 다음 포스트에선 AsyncTask에 대해 다뤄보겠습니다.

 

 감사합니다.

반응형