ProgressDialog로 Thread와 Handler 이해하기

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

# ProgressDialog란

ProgressDialog는 안드로이드가 어떤 작업을 진행할때 그 작업의 진행상황과 부가적인 문장을 팝업창으로 보여주는 모듈입니다. 시간이 걸리는 파일 다운로드나 복사를 수행할 때 이런창을 띄워주면 기다리는데 유저의 거부감을 줄일 수 있습니다.

ProgressDialog는 다이얼로그가 표시되는 동안 유저가 다른 조작을 못하게 된다는 이유로 Android 8.0(API level 26)부터는 deprecated 되고 대신 ProgressBar를 사용하도록 권장하고 있습니다.

…만, ProgressDialog는 Thread를 설명하는데 좋은 예제이기 때문에 여기서는 ProgressDialog를 구현하면서 Thread에 대해 이해해 보도록 하겠습니다

# 스레드란

컴퓨터 하드에 저장된 프로그램을 실행하면 운영체제는 프로그램을 메모리에 로딩하고 실행시킵니다. 이 때 메모리로 올라가 실행되고 있는 프로그램을 프로세스라고 합니다. 그리고 프로세스 안에서 실행되는 여러가지 작업 흐름을 각각 스레드라고 합니다.

안드로이드로 예를 들면 스마트폰 안에 저장된 유튜브앱은 프로그램이고 앱을 실행시키면 메모리에 유튜브 프로세스가 새로 하나 생성됩니다. 유튜브에는 앱을 보고있는 동안 계속 메뉴화면을 표시하는 스레드가 생기고 동영상을 선택했을 때 유튜브 서버에서 동영상을 다운로드하는 또 하나의 스레드가 생기게 되는 식입니다.

안드로이드에서 앱을 실행했을 때 처음으로 시작되는 스레드를 메인 스레드라고 합니다. 메인 스레드는 화면을 표시하고 버튼이나 텍스트뷰에 대한 조작이 이루어졌을 때 그 처리를 담당합니다.

이 때 예를들어 ProgressDialog로 1초에 10%씩 증가하는 다운로드 진행상황을 표시한다고 합시다. 0% 를 표시하고 1초 후에 10%, 2초후에 20%.. 를 표시해야 하는거죠.

그런데 메인 스레드에서 이 처리를 하게되면 진행상황을 표시하지 않는 1초의 인터벌시간동안 메인스레드가 멈춰서 1초동안 기다리고 있어야 합니다. 그 동안 UI처리는 수행할 수 없게 되죠.

그래서 ProgressDialog의 처리는 메인스레드와는 별개로 동작하는 백그라운드 스레드를 추가로 만들어서 구현하게 됩니다.

# 프로그램 작성

그럼 버튼을 터치하면 진행율이 0.2초당 10%씩 증가하는 ProgressDialog를 만들어 보면서 Thread를 구현해 보겠습니다.

# View binding 처리

View Binding 처리를 해 줍니다.

1
2
3
4
5
build.gradle

android {
    buildFeatures.viewBinding true
}

# ProgressDialog 준비

메인액티비티 안에 ProgressDialog용 전역변수를 만들어줍니다.

1
private lateinit var progressDialog: ProgressDialog

버튼을 터치했을때 다이얼로그가 표시되게 할 것이므로 버튼의 클릭리스너 안에서 ProgressDialog를 설정합니다.

1
2
3
4
5
6
7
progressDialog = ProgressDialog(this)
progressDialog.max = 100
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
progressDialog.setTitle("Download Task")
progressDialog.setMessage("Please wait, we are downloading your files...")
progressDialog.setCancelable(false)
progressDialog.show()

# 스레드와 핸들러

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

한 프로세스 안에서 여러개의 스레드를 사용하게 되면 서로다른 스레드가 버튼이나 텍스트뷰 등의 UI 컴포넌트에 동시에 접근하는 문제가 생길 수 있습니다. 안드로이드에서는 이것을 막기 위해 처리흐름 중간에 LooperHandler를 끼워 넣었습니다.

스레드는 수행해야 할 UI 작업이 발생하면 Message 또는 Runnable을 만들어내고 이걸 Message queue에 담습니다. Looper는 무한히 루프를 돌며 Message queue속의 객체를 핸들러에 전달합니다. Handler는 메시지를 받으면 운영체제가 정해주는 타이밍에 따라 UI조작을 수행하게 되므로 UI요소에 대한 동시접근을 막을 수 있게 됩니다.

# 핸들러 구현

스레드에서 사용할 핸들러 객체는 다음과 같이 만들어 줍니다. handleMessage 메소드를 오버라이드하여 그 안에 수행할 UI작업을 작성하면 됩니다.

1
2
3
4
5
6
val handler: Handler = object : Handler() {
    override fun handleMessage(msg: Message) {
        super.handleMessage(msg)
        progressDialog.incrementProgressBy(10)
    }
}

# 코틀린에서 스레드 사용

코틀린에서 스레드를 사용하는 방법은 Thread 클래스를 상속받아 스레드를 직접 구현하는 법, 그리고 Runnable 인터페이스로 구현하는 법이 있습니다. 여기서는 스레드 클래스를 변경할 필요가 없기 때문에 간편하게 구현할 수 있는 Runnable 인터페이스를 사용하겠습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Thread {
    try {
        while (progressDialog.progress <= progressDialog.max) {
            Thread.sleep(200)
            handler.sendMessage(handler.obtainMessage())
            if (progressDialog.progress == progressDialog.max) {
                progressDialog.dismiss()
            }
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
}.start()

쓰레드에서는 매 루프마다 현재 progress 값을 체크하고 그 값이 최대값보다 작으면 핸들러에 메시지를 보냅니다. 이 때 Thread.sleep을 이용해 0.2초 간격으로 스레드를 정지시킵니다.

# 메모리 리크 방어하기

# 메인 루퍼 이용하기

이렇게 해서 프로그레스다이얼로그를 구현하였습니다. 그런데 핸들러 객체에 하이라이트 된 부분을 보면 This Handler class should be static or leaks might occur 라는 경고가 표시됩니다. 앞에서 설명했듯이 모든 스레드는 각자의 개별 Looper를 가지게 됩니다. 그런데 핸들러가 백그라운드 스레드에 의해 만들어진 루퍼를 사용할 경우, 메시지큐에 메시지가 남아있으면 액티비티가 종료되도 죽지않고 살아있게 됩니다. 그래서 메모리누수가 일어날 수 있다는 경고가 발생하는 것입니다.

그래서 핸들러가 백그라운드 스레드의 Looper가 아닌 메인 스레드의 Looper를 사용하게 하면 핸들러의 수명은 액티비티가 종료될 때 같이 종료되게 되므로 경고메시지가 사라지게 됩니다. 그렇게 하기 위해서는 핸들러를 만들 때 메인 스레드의 루퍼를 전달해주면 됩니다.

1
2
3
4
5
6
val handler: Handler = object : Handler(Looper.getMainLooper()) {
    override fun handleMessage(msg: Message) {
        super.handleMessage(msg)
        progressDialog.incrementProgressBy(10)
    }
}

그러면 메모리 리크 경고가 사라진 것을 알 수 있습니다.

# 약한참조 이용하기

메모리 리크는 핸들러 클래스를 새로 만들어 주는 방식을 통해 방지할 수도 있습니다.

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

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

현재 구현한 코드대로라면 핸들러가 메인액티비티에 대해 강한참조를 가지게 되므로 액티비티가 종료되어도 핸들러 객체에 대해 가비지 컬렉터가 작동하지 않게 되어 메모리 리크가 발생하게 됩니다.

따라서 메모리 리크를 방지하기 위해서는 핸들러를 메인 액티비티에 대한 참조를 가지지 않는 정적 클래스로 만들면 됩니다.

# 핸들러 정적 클래스화

코틀린에서 만들어지는 클래스는 기본적으로 static 이므로 핸들러 클래스를 다음과 같이 만들어서 사용하면 됩니다.

1
2
3
4
5
6
7
val handler = MyHandler()

private class MyHandler() : Handler() {
    override fun handleMessage(msg: Message) {
   
    }
}

# 클래스 약한참조화하기

그런데 정적인 핸들러 클래스를 만들었지만 우리가 사용할 핸들러는 액티비티의 progressDialog를 참조하여야 하므로 다시 참조가 생기게 됩니다, 따라서 WeakReference 를 이용해서 이를 약한참조로 만들어 줍니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
private class MyHandler(activity: MainActivity?) : Handler() {

    private val weakReference: WeakReference<MainActivity> = WeakReference(activity)

    override fun handleMessage(msg: Message) {
        val activity = weakReference.get()
        if (activity == null || activity.isFinishing) return

        activity.progressDialog.incrementProgressBy(10)
    }
}

이렇게 하면 핸들러를 약한 참조로 하면서 액티비티의 참조를 가져올 수 있습니다. 이렇게 ProgressDialog를 구현해 보면서 Thread에 대해 이해해 보았습니다.

Built with Hugo
Theme Stack designed by Jimmy