ProgressBar로 AsyncTask 이해하기

이번 포스팅에서는 ProgressBar와 AsyncTask의 사용방법에 대해 알아보도록 하겠습니다.

# 들어가기

ProgressBar는 안드로이드가 어떤 작업을 진행할때 그 작업의 진행상황을 보여주는 모듈입니다. 시간이 걸리는 처리가 필요할 때 표시해주면 기다리는 유저의 거부감을 줄일 수 있습니다. 이 기능은 기존에는 ProgressDialog를 이용해서 구현했는데 다이얼로그가 표시될 때 유저의 조작을 제한한다는 문제가 있어 API level 26에서 deprecated 되고 ProgressBar로 대체되었습니다.

메인 스레드의 관점에서 보면 ProgressBar는 처리에 긴 시간이 걸리기 때문에 메인 스레드와는 별개의 스레드에서 처리를 수행해야 할 필요가 있습니다. 별개의 스레드에서 처리를 수행하는데는 여러가지 방법이 있을 수 있는데 여기서는 AsyncTask 클래스를 이용해서 ProgressBar를 구현해 보도록 하겠습니다.

# AsyncTask란

스레드와 핸들러에 대한 구체적인 설명과 사용법은 제가 했던 다른 강의를 참조하시면 좋을 것 같습니다.

AsyncTask는 주로 짧은 시간동안 운용되는 스레드를 더 사용하기 편하도록 만든 헬퍼 클래스입니다. 실제 사용시에는 메모리 리크등 문제가 있었기에 Android 11(API level 30) 부터 deprecated 되었으나 그래도 아직은 현역이라 할 수 있습니다.

# AsyncTask 구조

AsyncTask를 상속받는 클래스는 다음과 같이 구성합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class MyAsyncTask : AsyncTask<String, Int, Boolean>() {
    override fun onPreExecute() {
        super.onPreExecute()
    }

    override fun doInBackground(vararg p0: String?): Boolean {
        return true
    }

    override fun onProgressUpdate(vararg values: Int?) {
        super.onProgressUpdate(*values)
    }

    override fun onPostExecute(result: Boolean?) {
        super.onPostExecute(result)
    }
    
    override fun onCancelled() {
        super.onCancelled()
    }
}

AsyncTask<A, B, C> 안에는 세가지 데이터 타입을 정의하게 됩니다. 내용은 순서대로 다음과 같고, 이 때 값은 가변인자 형태이므로 배열 형태의 값도 사용할 수 있습니다. 또한 데이터를 전달할 필요가 없을 경우 Void로 정의를 생략할 수 있습니다.

  • A : doInBackground가 메인스레드 execute에서 전달받는 값
  • B : doInBackground가 실행중일 때 onProgressUpdate로 전달하는 값
  • C : doInBackground가 종료되었을 때 onPostExecute로 전달하는 값

# AsyncTask의 동작

AsyncTask의 동작은 다음과 같이 이루어지게 됩니다.

출처 : https://academy.realm.io/kr/posts/android-thread-looper-handler/

  1. 메인 스레드에서 AsyncTask의 인스턴스를 생성하고 execute 로 실행.
  2. AsyncTask 스레드가 시작되기 직전에 onPreExcuted가 실행됨. 스레드 실행전 필요한 컴포넌트들을 로딩하는데 사용.
  3. execute 호출시 입력된 파라미터가 AsyncTask 스레드로 전달됨.
  4. doInBackground에서 AsyncTask 작업을 수행.
  5. doInBackground에서 publishProgress를 호출하면 메인스레드에서 onProgressUpdate가 호출됨.
  6. doInBackground 처리가 끝나면 메인스레드의 onPostExcute로 결과값 result를 리턴하면서 AsyncTask 스레드가 종료됨.

# 코드 작성

# 화면 구성

진행상황을 표시하는 텍스트뷰와 ProgressBar를 추가하고, AsyncTask를 시작하는 다운로드 버튼과 취소버튼을 추가합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingStart="30dp"
    android:paddingEnd="30dp"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:text="Progress : 0"
        android:textSize="20sp" />

    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:scaleY="1"
        android:progressBackgroundTint="@color/design_default_color_primary_dark"
        android:progressTint="@color/design_default_color_primary"
        android:indeterminate="false"
        android:progress="0"
        android:max="100" />

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >

        <Button
            android:id="@+id/downloadButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:layout_marginEnd="20dp"
            android:layout_weight="1"
            android:text="다운로드"
            android:textSize="16sp" />
        <Button
            android:id="@+id/cancelButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_marginTop="20dp"
            android:text="취소"
            android:textSize="16sp" />
    </LinearLayout>

</LinearLayout>

# DownloadTask 클래스 준비

AsyncTask를 상속받는 DownloadTask 클래스를 작성합니다. 이 때 doInBackground에서는 0.1초 마다 진행률을 max값에 대해 10퍼센트씩 증가시키고 이 값을 publishProgress로 메인스레드에 전달하여 onProgressUpdate로 UI를 변경할 수 있도록 합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class DownloadTask(private val binding: ActivityMainBinding) : AsyncTask<String, Int, String>() {

    override fun doInBackground(vararg tasks: String): String {
        val count = tasks.size

        for (i in 0 until count) {
            try {
                Thread.sleep(100)
            } catch (e: InterruptedException) {
                e.printStackTrace()
            }
            publishProgress(((i + 1) / count.toFloat()  - 100).toInt())
            if (isCancelled) break
        }

        return "Download Completed"
    }

    override fun onProgressUpdate(vararg values: Int?) {

    }

    override fun onPostExecute(result: String?) {

    }
}

# 액티비티 처리

작성한 DownloadTask를 메인액티비티에서 사용하기 위한 코드를 작성해 줍니다. 다운로드 버튼을 클릭하면 AsyncTask 인스턴스를 만들어 실행하는데 excute에서는 vararg 입력 확인을 위해 굳이 10개의 스트링으로 이루어진 값을 전달해 주었습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class MainActivity : AppCompatActivity() {
    lateinit var binding: ActivityMainBinding
    private var downloadTask: DownloadTask? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.downloadButton.setOnClickListener {
            downloadTask = DownloadTask(this)
            downloadTask?.execute("1", "2", "3", "4", "5", "6", "7", "8", "9", "10")
        }
    }
}

# 작업 중 취소 처리

[doInBackground]에서 작업중인 스레드는 메인스레드에서 cancel을 호출하여 취소할 수 있습니다. 이 경우 백그라운드 스레드 내부의 isCancelledtrue를 반환하게 되며 doInBackground가 종료된 후, onPostExecute 대신 onCancelled가 호출됩니다.

이때 스레드 실행중에 중단해도 상관없는 작업이라면 cancel 실행시 mayInterruptIfRunning 파라메터로 true를, 스레드를 끝까지 처리하고 종료해야 할 경우라면 false를 전달해줍니다.

이제 메인액티비티에서 취소버튼을 클릭하면 AsyncTask 작업이 취소됩니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// MainActivity
    override fun onCreate(savedInstanceState: Bundle?) {
        binding.cancelButton.setOnClickListener {
            downloadTask?.cancel(false)
        }
    }

    override fun onPause() {
        super.onPause()
        downloadTask?.cancel(false)
    }
    
// DownloadTask
override fun onCancelled() {
    binding.textView.text = "Progress : 0"
    binding.progressBar.progress = 0
}

# 메모리 누수 방어하기

# AsyncTask의 특징

자바에서는 객체간의 관계를 다음과 같이 구분하는데, 메모리 확보를 위해 가비지 컬렉터가 작동하면 강한참조는 무시되고 약한참조를 가진 객체만 삭제되게 됩니다.

  • Strong reference
  • Soft reference
  • Weak reference
  • Phantom reference

여기서 만들어진 AsyncTask 객체가 액티비티를 참조하면 강한 참조를 가지게 됩니다. 그러면 가비지 컬렉터(Garbage Collector, GC)가 인스턴스를 제거하지 않게 되어 메모리 누수가 발생하게 되죠.

또한 AsyncTask는 하나의 객체만 생성할 수 있으며 액티비티가 종료되어도 스레드 작업이 끝나지 않았을 경우 종료되지 않는다는 특징이 있습니다.

그런데 이런 특징때문에 액티비티가 종료되고 재시작되면 스레드 객체가 액티비티 참조를 잃어버리게 되서 액티비티에 영향을 줄 수 없는 스레드만 좀비처럼 살아남아버리는 문제가 발생하게 됩니다.

# Weakreference화 하기

이러한 문제를 방지하기 위해 WeakReference를 이용해서 AsyncTask클래스가 액티비티에 대해 약한참조를 가지도록 합니다. 그러면 액티비티가 종료되었을 때 가비지 컬렉터가 AsyncTask 인스턴스를 메모리에서 해제할 수 있게 됩니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class DownloadTask(activity: MainActivity) : AsyncTask<String, Int, String>() {
    private val weakReference: WeakReference<MainActivity> = WeakReference(activity)

    override fun doInBackground(vararg tasks: String): String {
        val count = tasks.size

        for (i in 0 until count) {
            try {
                Thread.sleep(100)
            } catch (e: InterruptedException) {
                e.printStackTrace()
            }
            publishProgress(((i + 1) / count.toFloat()  - 100).toInt())
            if (isCancelled) break
        }

        return "Download Completed"
    }

    override fun onProgressUpdate(vararg values: Int?) {
        val activity = weakReference.get()
        if (activity == null || activity.isFinishing) return

        activity.binding.textView.text = "Progress : ${values[0]}"
        activity.binding.progressBar.progress = values[0]!!
    }

    override fun onPostExecute(result: String?) {
        val activity = weakReference.get()
        if (activity == null || activity.isFinishing) return

        Toast.makeText(activity, result, Toast.LENGTH_SHORT).show()
    }

    override fun onCancelled() {
        val activity = weakReference.get()
        if (activity == null || activity.isFinishing) return

        activity.binding.textView.text = "Progress : 0"
        activity.binding.progressBar.progress = 0
    }
}

이렇게 해서 AsyncTask를 사용하는 법에 대해 알아보았습니다.

Built with Hugo
Theme Stack designed by Jimmy