앞서 안드로이드 "안드로이드 메인 스레드 포스팅"을 통하여 메인스레드의 특징에 대해서 알아보았습니다. 

그 중에서 메인 스레드가 아닌 스레드에서 Button UI를 조작하다가 CalledFromWrongThreadException를 경험하기도 하였습니다.

이런 문제를 해결할 수 있는 방법에 대해서 알아보겠습니다.


Activity.runOnUiThread(Runnable) 사용

Developer Android 사이트에서 찾아보면 다음과 같이 설명이 있습니다.

설명 중 다음 부분이 우리가 처한 상황을 설명해 주고 있습니다.

"특정 동작을 UI 스레드에서 동작하도록 합니다. 만약 현재 스레드가 UI 스레드이면 그 동작은 즉시 수행됩니다."

하지만 "현재 스레드가 UI 스레드가 아니면, 필요한 동작을 UI 스레드의 이벤트 큐로 전달한다" <== 이부분 입니다.


우선 설명을 믿고 코드를 만들어 보겠습니다. 먼저 CalledFromWrongThreadException를 발생시켰던 코드는 아래와 같습니다.

 
    public class MainActivity extends Activity {
    private String TAG = "AndroidThread";
    private Button mDownloadButton;
    private Button mCancelButton;
    private Download mDownload;
    private boolean isDownloading = false;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mDownloadButton = (Button)findViewById(R.id.button_download);
        mCancelButton = (Button)findViewById(R.id.button_cancel);
    }

    public void onDownload(View view) {
        Log.d(TAG, "Press Download button");
        isDownloading = true;
        mDownload = new Download();
        mDownload.start();
    }

    public void onCancel(View view) {
        Log.d(TAG, "Press Cancel button");
        isDownloading = false;
    }

    public class Download extends Thread {

        @Override
        public void run() {
            for(int i=1;i<=10;i++) {

                if (!isDownloading) {
                    break;
                }
                try {
                    Thread.sleep(1000);
                    Log.d(TAG, "Downloading ..." + i*10 + "%");

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                if(i == 10) {
                    mDownloadButton.setText("Downloaded"); // CalledFromWrongThreadException 유발 코드!!
                }
            }
            isDownloading = false;
        }
    }
}

public class MainActivity extends Activity {
    private String TAG = "AndroidThread";
    private Button mDownloadButton;
    private Button mCancelButton;
    private Download mDownload;
    private boolean isDownloading = false;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mDownloadButton = (Button)findViewById(R.id.button_download);
        mCancelButton = (Button)findViewById(R.id.button_cancel);
    }

    public void onDownload(View view) {
        Log.d(TAG, "Press Download button");
        isDownloading = true;
        mDownload = new Download();
        mDownload.start();
    }

    public void onCancel(View view) {
        Log.d(TAG, "Press Cancel button");
        isDownloading = false;
    }

    public class Download extends Thread {

        @Override
        public void run() {
            for(int i=1;i<=10;i++) {

                if (!isDownloading) {
                    break;
                }
                try {
                    Thread.sleep(1000);
                    Log.d(TAG, "Downloading ..." + i*10 + "%");

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                if(i == 10) {

                    (MainActivity.this).runOnUiThread(new Runnable(){

                        @Override
                        public void run() {
                            Log.d(TAG, "Change Button text");
                            mDownloadButton.setText("Downloaded");
                        }
                    });
                }
            }

            isDownloading = false;
        }
    }
}


다음은 runOnUiThread 함수를 사용한 코드입니다.

public class MainActivity extends Activity {
    private String TAG = "AndroidThread";
    private Button mDownloadButton;
    private Button mCancelButton;
    private Download mDownload;
    private boolean isDownloading = false;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mDownloadButton = (Button)findViewById(R.id.button_download);
        mCancelButton = (Button)findViewById(R.id.button_cancel);
    }

    public void onDownload(View view) {
        Log.d(TAG, "Press Download button");
        isDownloading = true;
        mDownload = new Download();
        mDownload.start();
    }

    public void onCancel(View view) {
        Log.d(TAG, "Press Cancel button");
        isDownloading = false;
    }

    public class Download extends Thread {

        @Override
        public void run() {
            for(int i=1;i<=10;i++) {

                if (!isDownloading) {
                    break;
                }
                try {
                    Thread.sleep(1000);
                    Log.d(TAG, "Downloading ..." + i*10 + "%");

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                if(i == 10) {
                       (MainActivity.this).runOnUiThread(new Runnable(){

                        @Override
                        public void run() {
                            Log.d(TAG, "Change Button text");
                            mDownloadButton.setText("Downloaded");
                        }
                    });
                }
            }

            isDownloading = false;
        }
    }
}


run()함수를 오버라이드하여 Runnable 인터페이스를 구현하였습니다. 

이제 run() 함수에 있는 작업은(여기서는 setText함수 실행이겠죠) main thread(UI thread) 에서 실행 됩니다. 

로그로 확인 해보겠습니다.   

12-08 16:05:22.968 3443-3443/com.example.codetravel.androidthread D/AndroidThread: Press Download button

12-08 16:05:23.986 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...10%

12-08 16:05:24.987 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...20%

12-08 16:05:25.990 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...30%

12-08 16:05:26.994 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...40%

12-08 16:05:27.998 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...50%

12-08 16:05:29.000 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...60%

12-08 16:05:30.003 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...70%

12-08 16:05:31.006 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...80%

12-08 16:05:32.011 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...90%

12-08 16:05:33.013 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...100%

12-08 16:05:33.014 3443-3443/com.example.codetravel.androidthread D/AndroidThread: Change Button text 

다운로드 진행 상태를 출력하는 로그는 TID3467 thread에서 동작합니다. 

의도한대로 다운로드 완료 후,  setText 함수를 수행하는 run 함수가 UI thread에서 실행된 것을 확인 할 수 있습니다.

다음 그림은 다운로드 버튼의 텍스트가 정상적으로 변경된 모습입니다.



runOnUiThread 함수는 어떻게 이런 일을 할까요? xref에서 runOnUiThread 함수를 살펴 보겠습니다.

"특정 동작을 UI 스레드에서 동작하도록 합니다. 만약 현재 스레드가 UI 스레드이면 그 동작은 즉시 수행됩니다."

하지만 "현재 스레드가 UI 스레드가 아니면, 필요한 동작을 UI 스레드의 이벤트 큐로 전달합니다 설명이 그대로 코드화 되어 있네요


runOnUiThread가 실행된 스레드는 TID3467이므로 즉 UI thread가 아니므로 mHandler.post(action) 코드가 실행됩니다.

mHandler.post(action)을 실행하면 아래와 같이 순서대로 함수가 호출됩니다.(1 -4)

결과적으로 Runnable r은 UI thread의 queue에 메시지 형식으로 enqueue가 됩니다. 

후에 queue에 있는 메세지를 꺼내서 UI thread에서 동작을 수행할 것입니다.

1. post(Runnable r)

2. sendMessageDelayed(Message msg, long delayMillis)

3. sendMessageAtTime(Message msg, long uptimeMillis)

 - MessageQueue queue = mQueue;

4. enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis)


간략하게 말씀드리면 처음 startActivity() 함수를 통해서 MainActivity가 생성되는 과정은 zygoteInit에 의해서 ActivityThread가 생성됩니다.

ActivityThread는 아래와 같이 Main Looper와 Handler를 생성합니다. 

 // frameworks/base/core/java/android/app/ActivityThread.java

public final class ActivityThread {

...

public static void main(String[] args) { 

...

Looper.prepareMainLooper(); // 1. UI thread의 루퍼를 생성 

ActivityThread thread = new ActivityThread(); 

thread.attach();


if(sMainThreadHandler == null) {     

    sMainThreadHandler = thread.getHandler(); // 2. 핸들러 설정

 } 

Looper.loop(); // 3. 루퍼 동작 시작

}

}


이것이 안드로이드 시스템에서 자동으로 생성되는 UI thread의  Looper와  Handler 입니다. 

그리고 ActivityThread는 ActivityManagerService 와 바인더 통신을 하여 MainActivity를 생성하게 됩니다. 

따라서 ActivityThread가 생성한 MainActivity는 ActivityThread에서 생성한 Looper와 Handler를 사용하게 됩니다.

즉 위의 3번에서 mQueue는 UI thread의 Looper가 관리하는 queue를 가리킵니다.

결국 queue에 메세지 형태로 들어간 Runnable 작업은 UI Thread에서 dequeue 되어 UI Thread상에서 수행됩니다.

 UI thread에서 Button의 Text 변경 작업을 했기 때문에 CalledFromWrongThreadException와 같은 문제가 발생하지 않는 것입니다.


아래 콜스택은 MainActivity 액티비티의 onCreate가 불리는 함수 호출 과정입니다.

"main@4450" prio=5 tid=0x1 nid=NA runnable
  java.lang.Thread.State: RUNNABLE
      at com.example.codetravel.androidthread.MainActivity.onCreate(MainActivity.java:17)
      at android.app.Activity.performCreate(Activity.java:6757)
      at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1119)
      at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2703)
      at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2811)
      at android.app.ActivityThread.-wrap12(ActivityThread.java:-1)
      at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1528)
      at android.os.Handler.dispatchMessage(Handler.java:102)
      at android.os.Looper.loop(Looper.java:154)
      at android.app.ActivityThread.main(ActivityThread.java:6316)
      at java.lang.reflect.Method.invoke(Method.java:-1)
      at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:872)

      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:762) 


ActivityThread의 main 함수에서 실행된 Looper.loop() 함수에 의해서 메세지가 처리되고 있는것을 볼 수 있습니다.

그리고 MainActivity가 생성되면서 onCreate가 호출됩니다.

위의 과정은 붉은색으로 표시된 "main"이라는 표시로 main thread 즉 UI thread의  콜스택임을 알 수 있습니다.  

이상으로 runOnUiThread 함수의 사용에 대해서 간략하게 보았습니다.






+ Recent posts