Programming/Language Tip

안드로이드 - 외부 스레드에서 UI 스레드로

awesometic 2017. 3. 11. 16:16
반응형

 최근 취업준비를 하면서 개인 안드로이드 프로젝트를 연습삼아 진행하고 있습니다. 하면서 계속 깨닫는 건 멀티스레딩의 중요성입니다. 아무리 튼튼한 자료구조가 있고 복잡한 알고리즘의 빅오가 기가막혀도 기본 스레드(UI 스레드)에서만 작동한다면 ANR이 발생하기 마련입니다. 

 기본 스레드에선 UI만 다루고, 사용자 눈에 보이지 않는 모든 작업은 뒤에서 적절히 스레드를 줘서 돌려야 스무스하게 아무 일 없는 듯 돌아갈 겁니다. 비록 폰은 뜨거워져도 보기엔 부드러워야 좋다고 생각합니다.

 그래서 개발을 진행하기 보다, 머릿속으로 먼저 정리할 겸 포스팅하고 있습니다......




 [들어가기 앞서...]

 안드로이드는 리눅스 기반 운영체제입니다. 따라서 안드로이드는 리눅스의 정책을 따라갈 것이고, 리눅스의 프로세스와 스레드에 대한 개념을.. 알면 좋습니다.
-> http://awesometic.tistory.com/15


 안드로이드 개발자 사이트에서 안드로이드 입장에서의 프로세스와 스레드에 대한 설명이 한글로 자세히 나와 있습니다.
-> https://developer.android.com/guide/components/processes-and-threads.html

 이 중 스레드에 대해서 간단히 요약한다면..

1. 기본 스레드(UI 스레드): UI 관련 작업은 모두 UI 스레드에서 해야만 함
2. 작업자 스레드(백그라운드 스레드): UI 스레드를 차단(네트워크 액세스나 데이터베이스 쿼리 등 긴 작업)하지 않기 위해 사용


 본 포스팅에선 위의 개발자 페이지 중 작업자 스레드에 대해 핵심 정리 및 내용을 추가했습니다.




 [작업자 스레드]

 개발자 페이지에서 볼 수 있듯 작업자 스레드는 UI 스레드를 차단하지 않기 위해 사용합니다.

 예를 들어, 버튼을 누르면 네트워크에서 이미지를 가져와 화면에 나타내는 작업에 대한 코드를 보겠습니다.

public void onClick(View v) {
    BItmap bitmap = loadImageFromNetwork("http://example.com/image.png");
    mImageView.setImageBitmap(bitmap);
}

 위의 코드대로 실행할 경우, 버튼을 누르면서

1. http://example.com/image.png 를 다운 받아 bitmap 변수에 넣음
2. mImageView 위젯에 bitmap를 설정해 사용자에게 보이게 함

 순서로 잘 실행되겠지 ㅎㅎ 하고 마음 놓고 있으면 큰일납니다.

 만약 다운 받아야 할 이미지 파일이 너무 크거나, 현재 스마트폰의 인터넷 속도가 너무 느리면 한참을 받고 앉아 있을 겁니다. 그럼 UI 스레드가 이미지 파일을 받는 데만 혈안이니 사용자가 뭔 짓을 해도 어플리케이션은 묵묵부답입니다. 5초 이상 지속되면 ANR이 발생하여 어플리케이션 자체가 강제로 닫힐 것입니다.

 UI 스레드가 차단되는 건, 이런 현상을 말하는 겁니다.


  그럼 이제 저대로 가다간 UI 스레드가 차단될 수 있다는 위험성을 알았으니 분위기있게 작업자 스레드를 사용해봅시다.

public void onClick(View v) {
    new Thread(new Runnable() {
        public void run() {
            Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png");
            mImageView.setImageBitmap(bitmap);
        }
    }).start();
}

 코드가 이제 좀 볼 만합니다. new Thread 와 new Runnable을 통해 인터페이스를 구현했습니다. 

 이제는 버튼을 누르면서

1. 스레드를 새로 생성
2. 새로운 스레드에서 이미지 파일을 받아 이미지뷰에 이미지를 뿌림

 순서로 잘 실행되겠지 ㅎㅎ 싶으시겠지만 여기에도 문제가 있습니다. 바로 작업자 스레드에서 UI를 조작하려고 하는 것입니다.

 UI 는 UI 스레드에서만 조작할 수 있으며, 만약 다른 스레드에서 UI를 조작하려 하면 일종의 동기화 문제가 발생하기 때문에 하면 안 될 행동입니다.



 [작업자 스레드에서 UI 스레드로]

 따라서, 외부 스레드에서 UI 를 조작하기 위해 안드로이드에선 다양한 몇 가지 메서드를 제공해줍니다.

- Activity.runOnUiThread(Runnable)
- View.post(Runnable)
- View.postDelayed(Runnable)


 * Runnable?
여기까지 읽으신 분들은 자꾸 나오는 이 러너블이 뭔지 궁금하실 겁니다. 다음 포스트에서 간단히 설명했습니다.
-> http://awesometic.tistory.com/16


 이 중 View.post(Runnable) 메서드를 사용해 위의 코드를 정상적으로 실행되게끔 바꿔보겠습니다.

public void onClick(View v) {
    new Thread(new Runnable() {
        public void run() {
            final Bitmap bitmap =
                    loadImageFromNetwork("http://example.com/image.png");
            mImageView.post(new Runnable() {
                public void run() {
                    mImageView.setImageBitmap(bitmap);
                }
            });
        }
    }).start();
}

 이젠 여러분의 의도대로 실행될 수 있습니다.

1. 새로운 스레드를 만들고
2. 해당 스레드에서 이미지를 다운받으며
3. 다운이 완료되면 post() 메서드를 통해 실행되길 원하는 코드(러너블의 run() 메서드)를 UI 스레드로 전달합니다.

 하지만 이런 종류의 코드는 작업이 복잡해질수록 관리가 까다로워집니다. 개발자 페이지에서도 복잡한 상호작용을 위해선 Handler를 사용해 처리하라고 합니다. 개발자 페이지에선 나와있지 않지만 Handler를 사용해봅시다.

public void onClick(View v) {
    new Thread(new Runnable() {
         public void run() {
            final Bitmap bitmap =
                loadImageFromNetwork("http://example.com/image.png");
            mHandler.postDelayed(new Runnable() {
                public void run() {
                    mImageView.setImageBitmap(bitmap);
                }
            }, 0);
        }
    }).start();
}

 물론 mHandler의 객체는 미리 생성해놔야 합니다.

 핸들러는 간단하게 외부 스레드로부터의 메세지(Message)와 러너블을 UI 해당 루퍼에 대한 스레드에서 실행될 수 있도록 해주는 인터페이스라고 생각하시면 됩니다. 핸들러 인터페이스의 handleMessage(Message)를 구현할 때 메시지의 내용에 따라 다른 행동을 할 수 있도록 해줄 수 있습니다.

 이렇게 외부 스레드에서 UI 스레드로 UI 조작을 요청하는 방법은 여러 가지가 있겠지만, 가장 권장되고 간편한 방법은 AsyncTask를 이용한 방법입니다.

 Message, Handler의 추가 설명과 AsyncTask를 이용한 방법은 다른 포스트에서 정리됩니다.

반응형