본문 바로가기
Android

안드로이드 스튜디오, JAVA] 스레드

by 김마리님 2020. 7. 28.

프로그램은 하나의 일만을 할 수 없다. 그렇게 되면 대기시간이 길어지면서 프로그램의 효율이 많이 떨어지기 때문이다.

그렇기 때문에 우리는 멀티 스레드 방식으로 작업이 동시수행 되도록 해야한다.

멀티 스레드 방식은 자바와 같다. 스레드를 선언하고 내부에 타겟을 만드는 것도 같다.

문제는, UI를 만드는 스레드가 메인 스레드인데, 다른 스레드가 UI를 변경하려고 하는 것 부터 시작한다.

안드로이드는 메인스레드 이외의 스레드가 UI를 변경하는 것을 원칙적으로 허용하지 않는다. 왜냐하면 UI가 그려지는 동안 기타 스레드가 동작하여 UI를 변경해버리면 메인 스레드의 코드가 꼬이면서 오류가 발생하기 때문이다.

그렇기 때문에 안드로이드에서는 외부 스레드가 UI를 변경하려고 하면 반드시 이벤트 핸들러를 거쳐야만 한다.

(빨간 선은 오류가 나는 동선이다.)

 

그럼, 핸들러를 이용해서 외부 스레드에서 UI를 변경하는 코드를 보자.

먼저 프로그래스 바를 이용해서 만들자.

 

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">

    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="73dp"
       />

    <Button
        android:id="@+id/btn_execute"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="실행" />

    <Button
        android:id="@+id/btn_stop"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="중지" />

</LinearLayout>

 

자바 코드에서 스레드를 걸어준다.

 

MainActivity.java

package com.mary.myapplication;

import androidx.appcompat.app.AppCompatActivity;

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

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "Main_Activity";

    private Button btnExecute, btnStop;
    private ProgressBar progressBar;
    private int value=0;
    private Handler handler=new Handler();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mInit();
        progressBar.setProgress(value);
        mListener();
    }

    private void mInit(){
        btnExecute=findViewById(R.id.btn_execute);
        btnStop=findViewById(R.id.btn_stop);
        progressBar=findViewById(R.id.progressBar);
    }

    private void mListener(){
        btnExecute.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        handler.post(new Runnable() {
                            @Override
                            public void run() {
                                while (true){
                                    Log.d(TAG, "run: "+value);
                                    value=value+10;
                                    progressBar.setProgress(value);
                                    try {
                                        Thread.sleep(500);
                                    } catch (InterruptedException e) {
                                        e.printStackTrace();
                                    }
                                    if (value>100){
                                        break;
                                    }
                                }
                            }
                        });
                    }
                }).start();
            }
        });
    }
}

 

코드는 0.5초마다 value를 10씩 증가시키고, 증가된 값이 100이 넘어가면 스레드가 중단되는 형태이다.

코드 결과는 다음과 같다.

(0.5초마다 늘어나는게 아니라 한번에 훅 늘어난다)

이렇게 되는 이유는 다른게 아니라, 핸들러는 시스템의 과부하를 방지하기 위해 이벤트 핸들러를 스레드가 종료될 때 실행한다. 따라서, 

value 자체는 착실하게 시간에 따라 늘어나는걸 볼 수 있지만 핸들러가 변경한 UI는 브레이킹 된 value=100의 상태만 출력하게 된 것이다. 예상했던 동작이 아니다.

 

그래서 우리는 AsyncTask를 사용한다. 

package com.mary.myapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "Main_Activity";

    private Button btnExecute, btnStop;
    private ProgressBar progressBar;
    private int value=0;
    private BackgroundTask task;
    private Boolean threadStatus;

    class BackgroundTask extends AsyncTask<Integer, Integer, Integer>{
        //타겟 호출 직전 실행
        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            progressBar.setProgress(value);
            threadStatus=true;
            Log.d(TAG, "onPreExecute: ");
        }

        //Target Run
        @Override
        protected Integer doInBackground(Integer... integers) { //스레드 실행 시 인수 받음
            Log.d(TAG, "doInBackground: ");
          // publishProgress(10); //onProgressUpdate를 호출
            while (threadStatus){
                value=value+5;
                publishProgress(value);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    return -1;
                }
                if(value>100){
                    return 1;
                }
            }
            //정상적으로 수행됐으면 1, 아니면 -1 리턴 식으로 사용
            return -1;
        }

        //UI 스레드 그림을 그리는 메서드
        @Override
        protected void onProgressUpdate(Integer... values) { //publish 인수 받음
            Log.d(TAG, "onProgressUpdate: ");
            super.onProgressUpdate(values);
            progressBar.setProgress(values[0]);
        }

        //타겟 호출 이후 실행
        @Override
        protected void onPostExecute(Integer integer) { //doInBackground 리턴 값 받음
            Log.d(TAG, "onPostExecute: ");
            super.onPostExecute(integer);
            if (integer == 1) {
                Toast.makeText(MainActivity.this, "다운로드 완료", Toast.LENGTH_SHORT).show();
            }
        }
    }

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

        mInit();
        task=new BackgroundTask();
        mListener();

    }

    private void mInit(){
        btnExecute=findViewById(R.id.btn_execute);
        btnStop=findViewById(R.id.btn_stop);
        progressBar=findViewById(R.id.progressBar);
    }

    private void mListener(){
        btnExecute.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                task=new BackgroundTask();
                task.execute();
            }
        });
     btnStop.setOnClickListener(new View.OnClickListener() {
         @Override
         public void onClick(View v) {
             threadStatus=false;
         }
     });
    }

}

먼저 pre는 스레드 시작 전 동작한다. 

doInBackgrpound는 스레드 실행 시 인수를 받아 실행한다. 이것이 마치 스레드처럼 동작한다.

여기서 넘어간 변수가 progressUpdate를 통해 변경한다. 이 때, 넘어간 인수는 배열의 형태를 가지기 때문에 반드시 [0]을 넣어서 값이 들어간 곳을 설정한다.

마지막으로 스레드가 끝나면 onPostExecute를 통해 마지막 값이 도출된다.

 

주의 :

stop를 할 때 두 가지 방법이 있는데,

매서드를 아예 중지시키는 방법이 있고, 매서드 내부의 루프를 false로 바꾸는 방법이 있다.

아예 매서드를 중지시키는 방향 task.cancel(true)으로 하면 스레드가 완전 종료되며 일시정지가 되지 않는다. 따라서, 매서드 내부 값을 false로 바꾸는 형태를 사용하자.

(하지만 이 방법은 매서드를 여러개 만들긴 한다..)

 

결과 :

반응형