본문 바로가기

안드로이드

스레드와 핸들러, Runnable, runOnUiThread

반응형

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

 

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

같은 프로세스 안에서 일련의 기능이 순서대로 실행될 때 대부분은 큰 문제가 없지만,

대기 시간이 길어지는 네트워크 요청 등의 기능을 수행할 때는 화면에 보이는 UI도 멈춤 상태로 있게 되는 문제가 생길 수 있다

 

이런 문제를 해결하려면 하나의 프로세스 안에서 여러 개의 작업이 동시 수행되는 멀티 스레드 방식을 사용하게 된다

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

 

이런 멀티 스레드 방식은 같은 프로세스 안에 들어 있으면서 메모리 리스를 공유하므로 효율적인 처리가 가능하다

하지만 동시에 리소스에 접근할 때 데드락이 발생하여 시스템이 비정상적으로 동작할 수도 있다

 

여러 개의 스레드가 동시에 공통 메모리 리소스에 접근할 때 데드락이 발생한다

데드락이랑 동시에 두 곳 이상에서 요청이 생겼을 때 어떤 것을 먼저 처리할지 판단할 수 없어 발생하는 시스템상의 문제이다

이런 경우는 런타임 시의 예외 상황이므로 디버깅하기 쉽지 않은 경우가 많다

 

지연시간이 길어질 수 있는 앱이라면 오랜 시간 작업을 수행하는 코드를 별도로 분리한 다음, UI에 응답을 보내는 방식을 사용한다

이를 위해 안드로이드가 제공하는 두 가지 시나리오가 있다

 

1. 서비스 이용하기 - 백그라운드 작업은 서비스로 실행하고, 사용자에게는 알림 서비스로 알려준다

만약 메인 액티비티로 결과 값을 전달하고 이를 이용해서 다른 작업을 수행하고자 한다면 브로드캐스팅으로 결과 값을 전달할 수 있다

 

2. 스레드 사용하기 - 스레드는 같은 프로세스 안에 있기 때문에 작업 수행의 결과를 바로 처리할 수 있다

그러나 UI 객체는 직접 접근할 수 없으므로 핸들러 객체를 사용한다

 

안드로이드에서 UI처리할 때 사용되는 기본 스레드를 메인 스레드라고 부른다

메인 스레드에서 이미 UI에 접근하고 있으므로 새로 생성한 다른 스레드에서는 핸들러 객체를 사용해서 메세지를 전달함으로써 메인 스레드에서 처리할 수 있도록 만든다

 

 

activity_main

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="스레드 시작"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

 

 

MainActivity

package com.example.a52_thread;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

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 v) {
                BackgroundThread thread = new BackgroundThread();
                thread.start();
            }
        });
    }

    class BackgroundThread extends Thread {
        public void run() {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {

                }

                value += 1;
                Log.d("Thread", "value : " + value);
            }
        }
    }
}

 

 

 

 

여기까지만 하면 로그에 value 값이 출력되면서 문제 없이 잘된다

하지만 화면에 value 값을 출력하게 하려면 문제가 생긴다

 

activity_main

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="스레드 시작"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="240dp"
        android:hint="value"
        android:textSize="30sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

 

 

MainActivity

package com.example.a52_thread;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    int value = 0;

    TextView textView;

    @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 v) {
                BackgroundThread thread = new BackgroundThread();
                thread.start();
            }
        });
    }

    class BackgroundThread extends Thread {
        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.setText("value 값 : " + value);
            }
        }
    }
}

 

 

직접 만든 BackgroundThread 객체에서 UI 객체를 직접 접근했다는 것을 알려주는 오류가 뜬다

W/System: A resource failed to call close. 
D/Thread: value : 1
E/AndroidRuntime: FATAL EXCEPTION: Thread-2
    Process: com.example.a52_thread, PID: 23076
    android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8798)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1606)
        at android.view.View.requestLayout(View.java:25390)
        at android.view.View.requestLayout(View.java:25390)
        at android.view.View.requestLayout(View.java:25390)
        at android.view.View.requestLayout(View.java:25390)
        at android.view.View.requestLayout(View.java:25390)
        at android.view.View.requestLayout(View.java:25390)
        at androidx.constraintlayout.widget.ConstraintLayout.requestLayout(ConstraintLayout.java:3593)
        at android.view.View.requestLayout(View.java:25390)
        at android.widget.TextView.checkForRelayout(TextView.java:9719)
        at android.widget.TextView.setText(TextView.java:6311)
        at android.widget.TextView.setText(TextView.java:6139)
        at android.widget.TextView.setText(TextView.java:6091)
        at com.example.a52_thread.MainActivity$BackgroundThread.run(MainActivity.java:46)
I/Process: Sending signal. PID: 23076 SIG: 9

 

메인 스레드에서 관리하는 UI 객체는 직접 만든 스레드 객체에서는 접근 할수 없다는 의미이다

이를 해결하려면 핸들러를 사용해야 한다

 

앱을 실행할 때 프로세스가 만들어지면, 그 안에 메인 스레드가 함께 만들어진다

그리고 최상위에서 관리되는 앱 구성 요소인 액티비티, 브로드캐스트 수신자 등과 새로 만들어지는 윈도우를 관리하기 위한 메세지 큐(Message Queue)를 실행한다

메세지 큐를 사용하면 순차적으로 코드를 수행할 수 있는데, 이렇게 메세지 큐로 메인 스레드에서 처리할 메세지를 전달하는 역할을 핸들러 클래스가 담당한다

핸들러는 실행하려는 특정 기능이 있을 때 핸들러가 포함되어있는 스레드에서 순차적으로 실행시킬 때 사용된다

핸들러를 이용하면 특정 메세지가 미래의 어던 시점에 실행되도록 스케줄링할 수도 있다

 

새로 만든 스레드(스레드 #1)가 수행하려는 정보를 메인 스레드로 전달하기 위해서는,

먼저 핸들러가 관리하는 메시지 큐에서 처리할 수 있는 메세지 객체 하나를 참조해야 한다

이 첫 번째 과정에서는 obtainMessage 메소드를 이용할 수 있으며, 호출의 결과로 메세지 객체를 반환받게 된다

이 메세지 객체에 필요한 정보를 넣은 후 sendMessage 메소드를 이용해 메세지 큐에 넣을 수 있다

메세지 큐에 들어간 메세지는 순서대로 핸들러가 처리하게 되며, 이때 handleMessage 메소드에 정의된 기능이 수행된다

이 때 handleMessage에 들어 있는 코드가 수행되는 위치는 새로 만든 스레드가 아닌 메인 스레드가 된다

 

MainActivity

package com.example.a52_thread;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    int value = 0;

    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 v) {
                BackgroundThread thread = new BackgroundThread();
                thread.start();
            }
        });

        handler = new MainHandler();
    }

    class BackgroundThread extends Thread {
        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();
                Bundle bundle = new Bundle();
                bundle.putInt("value", value);
                message.setData(bundle);

//                핸들러로 메세지 객체 보내기
                handler.sendMessage(message);
            }
        }
    }

    class MainHandler extends Handler {

        //        핸들러 안에서 전달받은 메세지 객체 처리하기
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);

            Bundle bundle = msg.getData();
            int value = bundle.getInt("value");
            textView.setText("value 값 : " + value);
        }
    }
}

 

 

 

좀 더 간단한 방법으로 메인 스레드에서 실행시킬 수 있도록, 핸들러 클래스는 메세지 전송 방법 이외에

Runnable 객체를 실행시킬 수 있는 방법을 제공한다

새로 만든 Runnable 객체를 핸들러의 post 메소드로 전달해주면 이 객체에 정의된 run 메소드 안의 코드들은 메인 스레드에서 실행된다

 

MainActivity

package com.example.a52_thread;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

//    int value = 0;

    TextView textView;

//    MainHandler handler;

    //    API의 기본 핸들러 객체 생성
    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 v) {
                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);

//                핸들러의 post 메소드 호출
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        textView.setText("value 값 : " + value);
                    }
                });

//                Message message = handler.obtainMessage();
//                Bundle bundle = new Bundle();
//                bundle.putInt("value", value);
//                message.setData(bundle);
//
////                핸들러로 메세지 객체 보내기
//                handler.sendMessage(message);
            }
        }
    }

//    class MainHandler extends Handler {
//
//        //        핸들러 안에서 전달받은 메세지 객체 처리하기
//        @Override
//        public void handleMessage(Message msg) {
//            super.handleMessage(msg);
//
//            Bundle bundle = msg.getData();
//            int value = bundle.getInt("value");
//            textView.setText("value 값 : " + value);
//        }
//    }
}

 

 

 

 

runOnUiThread 메소드를 사용하면 핸들러 객체를 만들지 않고도 메인 스레드에서 동작하게 만들 수 있다

 

MainActivity

package com.example.a52_thread;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

//    int value = 0;

    TextView textView;

//    MainHandler handler;

    //    API의 기본 핸들러 객체 생성
//    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 v) {
                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);

                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        textView.setText("value 값 : " + value);
                    }
                });

////                핸들러의 post 메소드 호출
//                handler.post(new Runnable() {
//                    @Override
//                    public void run() {
//                        textView.setText("value 값 : " + value);
//                    }
//                });

//                Message message = handler.obtainMessage();
//                Bundle bundle = new Bundle();
//                bundle.putInt("value", value);
//                message.setData(bundle);
//
////                핸들러로 메세지 객체 보내기
//                handler.sendMessage(message);
            }
        }
    }

//    class MainHandler extends Handler {
//
//        //        핸들러 안에서 전달받은 메세지 객체 처리하기
//        @Override
//        public void handleMessage(Message msg) {
//            super.handleMessage(msg);
//
//            Bundle bundle = msg.getData();
//            int value = bundle.getInt("value");
//            textView.setText("value 값 : " + value);
//        }
//    }
}
반응형

'안드로이드' 카테고리의 다른 글

스레드로 애니메이션 만들기  (0) 2021.10.27
일정 시간 후에 실행하기  (0) 2021.10.27
키패드 제어  (0) 2021.10.27
시크바  (0) 2021.10.27
웹뷰  (0) 2021.10.27