getMainLooper() 함수 


getMainLooper() 함수는 Main Thread(UI Thread)가 사용하는 Looper 즉 Main Looper를 반환합니다.

이 함수는 호출하는 스레드가 메인 스레드이거나 메인 스레드가 아니어도 언제든지 Main Looper를 반환합니다.

참고 1) Main thread의 Main Looper와 Handler는 ActivityThread에서 자동으로 생성하기 때문에 개발자가 명시적으로 생성하지 않습니다.

참고 2) Looper.myLooper() 함수는 호출한 스레드의 Looper를 반환합니다.


getMainLooper() 함수는 어떤 경우에 사용하면 될까요?

크게 3가지의 경우에 사용합니다.


1. Handler를 생성할 때 Main Looper를 생성자의 인자로 전달하고 싶을 경우

즉, 작업자 스레드(UI thread가 아닌 스레드)에서 UI thread에게 "Runnabel 작업" 또는 "메시지"을 보내고 싶을 때 사용할 수 있는 방법입니다.

View.post() 또는 runOnUiThread() API를 사용하는 것도 작업자 스레드에서 UI thread로 Runnable객체를 전달하는 용도로 사용됩니다.


public class MainActivity extends AppCompatActivity {
    private String TAG = "MainActivity";
    private TestThread mTestThread;
    private Button mButton;
    static int count = 0;

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

        mButton = (Button)findViewById(R.id.button);
    }

    public void onClickButton(View view) {
        Log.d(TAG, "onClickButton");

        mTestThread = new TestThread();
        mTestThread.start();
    }

    class TestThread extends Thread {
        private Handler mHandler;

        TestThread() {
            mHandler = new Handler(Looper.getMainLooper()) {  // 핸들러에 Main Looper를 인자로 전달
                @Override
                public void handleMessage(Message msg) {  // 메인 스레드에서 호출
                    Log.d(TAG,"handleMessage : " + msg.what);

                    switch(msg.what) {
                        case 0:
                            mButton.setText("Button 0");
                            break;
                        case 1:
                            mButton.setText("Button 1");
                            break;
                        case 2:
                            mButton.setText("Button 2");
                            count = 0;
                            break;
                        default:
                            break;
                    }
                }
            };
        }

        @Override
        public void run() {
            Log.d(TAG, "Start TestThread");

            Message msg = mHandler.obtainMessage(count++);
            mHandler.sendMessage(msg);  // 메인 스레드로 메시지를 보냄
        }
    }
}

Handler를 생성할 때 인자로 Looper를 전달하면 어떤 과정을 거치는지 보도록 하겠습니다. (xref : Handler.java)


Main Looper를 인자로 넣으면 Main Looper의 Queue를 mQueue로 설정합니다. 이 부분이 중요합니다.

결론적으로 sendMessage() 또는 post()를 하게 되면 mQueue에 메시지나 작업이 들어가게됩니다. 즉 Main thread의 큐에 들어갑니다.

따라서 dequeue가 될때 Main thread에서 실행되는 것입니다.


실행 로그를 확인해 보겠습니다.

TestThread 스레드는(TID 12310) sendMessage() 함수를 호출하였고, 메인 스레드(TID 12235)에서 handleMessage() 함수가 호출되었습니다.

이 함수는 버튼을 터치할 때 전달 받은 msg.what 값에 setText("Button 0"), setText("Button 1"), setText("Button 2") 반복적으로 변경합니다. 

01-06 14:05:30.519 12235-12235/com.example.codetravel.getmainlooper D/MainActivity: onClickButton

01-06 14:05:30.562 12235-12310/com.example.codetravel.getmainlooper D/MainActivity: Start TestThread

01-06 14:05:30.586 12235-12235/com.example.codetravel.getmainlooper D/MainActivity: handleMessage : 0

01-06 14:05:32.511 12235-12235/com.example.codetravel.getmainlooper D/MainActivity: onClickButton

01-06 14:05:32.537 12235-12335/com.example.codetravel.getmainlooper D/MainActivity: Start TestThread

01-06 14:05:32.571 12235-12235/com.example.codetravel.getmainlooper D/MainActivity: handleMessage : 1

01-06 14:05:34.660 12235-12235/com.example.codetravel.getmainlooper D/MainActivity: onClickButton

01-06 14:05:34.698 12235-12366/com.example.codetravel.getmainlooper D/MainActivity: Start TestThread

01-06 14:05:34.717 12235-12235/com.example.codetravel.getmainlooper D/MainActivity: handleMessage : 2


같은 원리로 다음 예제는 Runnable 작업을 메인 스레드로 전달하는 코드입니다.


public class MainActivity extends AppCompatActivity {
    private String TAG = "MainActivity";
    private TestThread mTestThread;
    private Button mButton;
    static int count = 0;

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

        mButton = (Button)findViewById(R.id.button);
    }

    public void onClickButton(View view) {
        Log.d(TAG, "onClickButton");

        mTestThread = new TestThread();
        mTestThread.start();
    }

    class TestThread extends Thread {

        @Override
        public void run() {
            Log.d(TAG, "Start TestThread");

            Handler handler = new Handler(Looper.getMainLooper());  // 핸들러에 메인 루퍼를 인자로 전달
            handler.post(new Runnable() {  // 메인 스레드로 Runnable 객체를 보냄, runOnUiThread()함수 사용과 유사

                @Override
                public void run() {  // run()함수는 메인 스레드에서 실행 됨
                    Log.d(TAG, "Change Button text");
                    mButton.setText("Button changed");
                }
            });
        }
    }
}


2. 현재 스레드의 루퍼가 Main Looper인지 아닌지 검사하고 싶을 경우

아래 예제에는 코드와 같이 버튼 UI 텍스트를 변경하는 changeButtonText() 함수가 있습니다.

이 함수를 호출하는 스레드의 Looper가 Main Looper 인지 아닌지에 따라서 버튼 UI 텍스트를 변경하는 방법을 다르게 하고 있습니다.

UI 변경은 UI thread에서만 허용하기 때문입니다.

만약 Main Looper라면 이것은 스레드가 Main 스레드를 의미하기 때문에 바로 setText() 함수를 호출 할 수 있습니다.

하지만 Main Looper가 아니라면 runOnUiThread() 또는 View.post() 함수 등을 사용해야 합니다.


public class MainActivity extends AppCompatActivity {
    private String TAG = "MainActivity";
    private TestThread mTestThread;
    private Button mButton;

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

        mButton = (Button)findViewById(R.id.button);
    }

    public void onClickButton(View view) {
        Log.d(TAG, "onClickButton");
        changeButtonText();

        mTestThread = new TestThread();
        mTestThread.start();
    }

    class TestThread extends Thread {
        @Override
        public void run() {
            Log.d(TAG, "Start TestThread");
            changeButtonText();
        }
    }

    public void changeButtonText() {
        Log.d(TAG, "changeButtonText myLooper() " + Looper.myLooper());

        if (Looper.getMainLooper() == Looper.myLooper()) { // 현재 스레드의 루퍼와 메인 루퍼가 같은지 비교
            mButton.setText("Button 1");
            Log.d(TAG, "changeButtonText method is called from main thread");
        } else {
            (MainActivity.this).runOnUiThread(new Runnable() {

                @Override
                public void run() {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    mButton.setText("Button 2");
                    Log.d(TAG, "changeButtonText method is called from non-main thread");
                }
            });
        }
    }
}



실행 로그 입니다.

onClickButton에서 실행된 changeButtonText() 함수는 Main Looper를 사용하는 스레드에서 호출되었습니다.

그리고 별도로 생성한 스레드에서 호출된 changeButtonText() 함수는 당연히 Looper를 생성한 적이 없으므로 Looper.myLooper() 함수를 호출하면 null을 리턴합니다.

13:03:11.005 11277-11277/com.example.codetravel.getmainlooper D/MainActivity: onClickButton

01-06 13:03:11.005 11277-11277/com.example.codetravel.getmainlooper D/MainActivity: changeButtonText myLooper() Looper (main, tid 1) {f383015}

01-06 13:03:11.006 11277-11277/com.example.codetravel.getmainlooper D/MainActivity: changeButtonText method is called from main thread

01-06 13:03:11.008 11277-11364/com.example.codetravel.getmainlooper D/MainActivity: Start TestThread

01-06 13:03:11.008 11277-11364/com.example.codetravel.getmainlooper D/MainActivity: changeButtonText myLooper() null

01-06 13:03:14.034 11277-11277/com.example.codetravel.getmainlooper D/MainActivity: changeButtonText method is called from non-main thread 

Looper.myLooper() 함수를 로그로 출력하였을 때 내용은 아래와 같습니다.

Looper (main, tid 1)   <== 메인 스레드에서 호출

null   <== TestThread에서 호출

이것이 의미하는 것을 알기 위해서 Looper 클래스의 toString()함수를 보도록 하겠습니다. (xref : Looper.java)

Looper (" 스레드 이름", "스레드 ID") 임을 알 수 있습니다.



3. 현재 스레드가 Main thread(UI thread)인지 아닌지 검사하고 싶을 경우

public class MainActivity extends AppCompatActivity {
    private String TAG = "MainActivity";
    private TestThread mTestThread;
    private Button mButton;

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

        mButton = (Button)findViewById(R.id.button);
    }

    public void onClickButton(View view) {
        Log.d(TAG,"onClickButton()" + " " + Thread.currentThread() + " " + Looper.getMainLooper().getThread());

        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            Log.d(TAG,"onClickButton() : This thread is main thread!");
        }
        else {
            Log.d(TAG,"onClickButton() : This thread is not main thread!");
        }

        mTestThread = new TestThread();
        mTestThread.start();
    }

    class TestThread extends Thread {
        @Override
        public void run() {
            Log.d(TAG,"TestThread run()" + " " + Thread.currentThread() + " " + Looper.getMainLooper().getThread());
            if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
                Log.d(TAG,"TestThread run() : This thread is main thread!");
            }
            else {
                Log.d(TAG,"TestThread run() : This thread is not main thread!");
            }
            while(true) {  // ps 명령어로 스레드 ID를 보기 위해서 스레드가 종료되지 않도록 하기 위한 코드
                // 아무 동작도 안하고 그냥 살아 있는 스레드
            }
        }
    }
}


로그를 확인하기 전에 코드만으로 예측해보면 onClickButton() 함수는 UI thread에서 호출되기 때문에 "This thread is main thread!" 출력될 것이라는 것을 예측할 있습니다.

그리고 TestThread 스레드가 생성되어 run() 함수가 동작하는 스레드는 메인 스레드가 아닌 백그라운드 스레드입니다.

따라서 "This thread is not main thread!" 출력 것입니다.

실행 로그를 보도록 하겠습니다.


앞서 예측한 결과와 동일한 값이 출력 되었습니다.

01-06 06:10:49.537 32177-32177/com.example.codetravel.getmainlooper D/MainActivity: onClickButton()  Thread[main,5,main] Thread[main,5,main]

01-06 06:10:49.537 32177-32177/com.example.codetravel.getmainlooper D/MainActivity: onClickButton() : This thread is main thread!

01-06 06:10:49.620 32177-822/com.example.codetravel.getmainlooper D/MainActivity: TestThread run() Thread[Thread-175,5,main] Thread[main,5,main]

01-06 06:10:49.620 32177-822/com.example.codetravel.getmainlooper D/MainActivity: TestThread run() : This thread is not main thread!


ps 정보를 보면 아래와 같이 TestThread의 Name은 "Thread-175" 입니다.

root@generic_x86_64:/ # ps -t 32177 

USER      PID   PPID  VSIZE  RSS   WCHAN              PC  NAME

u0_a68    32177 1264  1293420 48356    ep_poll 7fc1ba06b8ca S com.example.codetravel.getmainlooper    // Main 스레드

u0_a68    32182 32177 1293420 48356 do_sigtime 7fc1ba06b7ca S Signal Catcher

u0_a68    32183 32177 1293420 48356 poll_sched 7fc1ba06b60a S JDWP

u0_a68    32184 32177 1293420 48356 futex_wait 7fc1ba000f68 S ReferenceQueueD

u0_a68    32185 32177 1293420 48356 futex_wait 7fc1ba000f68 S FinalizerDaemon

u0_a68    32186 32177 1293420 48356 futex_wait 7fc1ba000f68 S FinalizerWatchd

u0_a68    32187 32177 1293420 48356 futex_wait 7fc1ba000f68 S HeapTaskDaemon

u0_a68    32188 32177 1293420 48356 binder_thr 7fc1ba06bc67 S Binder_1

u0_a68    32189 32177 1293420 48356 binder_thr 7fc1ba06bc67 S Binder_2

u0_a68    32205 32177 1293420 48356 __skb_recv 7fc1ba06c30a S Thread-172

u0_a68    32214 32177 1293420 48356    ep_poll 7fc1ba06b8ca S RenderThread

u0_a68    32231 32177 1293420 48356 futex_wait 7fc1ba000f68 S hwuiTask1

u0_a68    822   32177 1293420 48356          0 7fc1b11ec6cc R Thread-175     // TestThread 스레드


Thread.currentThread()와 Looper.getMainLooper.getThread() 함수는 모두 Thread 클래스 객체를 반환합니다. 

출력 내용을 보면 아래와 같습니다.

Thread[main,5,main]   <== main thread

Thread[Thread-175,5,main]  <== TestThread


Thread 클래스 객체를 log로 출력했기 때문에 Thread 클래스의 toString() 함수가 호출되었습니다. 

Thread["현재 스레드 이름", "우선순위", "스레드 그룹"] 형식으로 출력됩니다.

main 스레드와 TestThread 스레드는 이름은 다르지만 우선순위와 스레드 그룹은 동일하게 출력되었습니다.

별도로 우선순위와 스레드 그룹을 설정하지 않았기 때문에 default 상태의 값이 설정되어 있습니다.


이상으로 getMainLooper() 함수의 사용에 대해서 알아 보았습니다.


지난 "runOnUiThread(Runnable) 사용하기" 포스팅을 통하여 UI 도구 키트를 UI thread에서 동작시킬 수 있는 방법을 알아 보았습니다.

이번에는 유사한 방법인 View.post(Runnable) API를 사용하여 동일한 결과를 만들어 보도록 하겠습니다.


아래 붉은색 코드는 문제가 있는 코드입니다.

이유는 별도로 생성한 스레드에서 Button UI를 변경하려고 했기 때문입니다. 

안드로이드 시스템에서는 UI 조작 및 접근은 UI thread에서만 허용합니다. (자세한 내용은 "안드로이드 메인 스레드(Android Main Thread 또는 UI Thread"포스팅 참조)

어플리케이션을 실행하여 Download 버튼을 누르면 다운로드가 완료되는 시점에 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;
        }
    }
}


로그는 아래와 같습니다.

안드로이드 시스템은 setText()함수를 실행하고 있는 스레드가 main thead인지 검사하였고

이를 위반 하였기 때문에 CalledFromWrongThreadException 런타임 에러를 발생시켰습니다.

12-06 15:31:12.849 18227-18227/com.example.codetravel.androidthread D/AndroidThread: Press Download button

12-06 15:31:13.863 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...10%

12-06 15:31:14.864 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...20%

12-06 15:31:15.865 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...30%

12-06 15:31:16.866 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...40%

12-06 15:31:17.869 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...50%

12-06 15:31:18.871 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...60%

12-06 15:31:19.873 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...70%

12-06 15:31:20.876 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...80%

12-06 15:31:21.879 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...90%

12-06 15:31:22.883 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...100%

12-06 15:31:22.883 18227-18307/com.example.codetravel.androidthread E/AndroidRuntime: FATAL EXCEPTION: Thread-204

                               Process: com.example.codetravel.androidthread, PID: 18227

                               android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

                               at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6556)

                               at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:942)

                               at android.view.ViewGroup.invalidateChild(ViewGroup.java:5081)

                               at android.view.View.invalidateInternal(View.java:12713)

                               at android.view.View.invalidate(View.java:12677)

                               at android.view.View.invalidate(View.java:12661)

                               at android.widget.TextView.checkForRelayout(TextView.java:7159)

                               at android.widget.TextView.setText(TextView.java:4342)

                               at android.widget.TextView.setText(TextView.java:4199)

                               at android.widget.TextView.setText(TextView.java:4174)

                               at com.example.codetravel.androidthread.MainActivity$Download.run(MainActivity.java:53)  


View.post(Runnable) API를 사용해 보도록 하겠습니다.

    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.post(new Runnable() {

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

            isDownloading = false;
        }
    }
}


실행 로그를 보겠습니다.

"Downloading ... %" 로그는 별도로 생성한 Download 스레드에서(TID 2886)에서 실행되고 있습니다.

다운로드가 100% 완료된 시점에 실행된 mDownloadButton.post(...) 의 인자로 전달한 Runnable action 객체의 run()함수 코드가 UI thread에서(TID 2436) 실행된 것을 확인할 수 있습니다.

12-26 13:29:28.433 2436-2436/com.example.codetravel.androidthread D/AndroidThread: Press Download button

12-26 13:29:29.448 2436-2886/com.example.codetravel.androidthread D/AndroidThread: Downloading ...10%

12-26 13:29:30.450 2436-2886/com.example.codetravel.androidthread D/AndroidThread: Downloading ...20%

12-26 13:29:31.453 2436-2886/com.example.codetravel.androidthread D/AndroidThread: Downloading ...30%

12-26 13:29:32.459 2436-2886/com.example.codetravel.androidthread D/AndroidThread: Downloading ...40%

12-26 13:29:33.463 2436-2886/com.example.codetravel.androidthread D/AndroidThread: Downloading ...50%

12-26 13:29:34.465 2436-2886/com.example.codetravel.androidthread D/AndroidThread: Downloading ...60%

12-26 13:29:35.467 2436-2886/com.example.codetravel.androidthread D/AndroidThread: Downloading ...70%

12-26 13:29:36.472 2436-2886/com.example.codetravel.androidthread D/AndroidThread: Downloading ...80%

12-26 13:29:37.473 2436-2886/com.example.codetravel.androidthread D/AndroidThread: Downloading ...90%

12-26 13:29:38.474 2436-2886/com.example.codetravel.androidthread D/AndroidThread: Downloading ...100%

12-26 13:29:38.476 2436-2436/com.example.codetravel.androidthread D/AndroidThread: Change Button text 



여기서 부터는 View.post(Runnable) API를  사용하면 어떻게 UI thread에서 Runnable 코드가 실행되는지 알아 보도록 하겠습니다.

Button 클래스는 TextView를 상속받고 있습니다. TextView는 View 클래스를 상속 받고 있구요.

post() 함수는 View 클래스의 멤버 함수입니다. 

친절하게 주석을 통하여 설명되어 있습니다.

"Runnable 작업은  메세지큐에 추가될 것입니다. 그 작업은 UI thread에서 수행될 것입니다."

주석 내용에 근거하여 Runnable action을 attachInfo.mHandler 하여 post하면 이것이 UI thread의 Looper가 관리하는 Queue에 전달될 것임을 짐작할 수 있습니다.

이 짐작이 맞는지 코드를 따라가 보겠습니다.

먼저 위 예제 코드에서 Button 객체는 아래와 같이 Layout xml로 정의하였습니다.

Layout xml에는 화면 배치 정보가 들어 있는 파일입니다.

어플리케이션이 실행되는 시점에 Layout xml 파일은 로딩되어 각 배치 정보들이 메모리에서 객체화 됩니다.

이렇게 메모리에서 배치 정보들이 객체화 되는 과정을 Inflation이라고 합니다.

명시적으로 "new Button()" 코드를 사용하지 않아도 Button은 생성됩니다.


Button 클래스의 상속관계를 살펴 보았기 때문에 Button 객체 생성 시점에 View 클래스의 생성자가 가장 먼저 호출 될것입니다.

View 클래스의 post함수에서 사용하고 있는 attachInfo.mHandler 값이 어디서 채워지는지 보겠습니다. 


안드로이드 시스템에서 어플리케이션이 실행될때  MainActivity와 MainActivity에 배치될 Button과 같은 View 들이 생성됩니다.

이 과정은 ActivityThread에서 시작됩니다.

ActivityThread는 ZygoteInit에 의해서 실행되면서 아래와 같이 main함수에서 UI thread looper를 생성합니다.


 // 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. 루퍼 동작 시작

}

}



아래 콜스택은 MainActivity의 onCreate 함수가 호출되기까지의 콜스택입니다.

"main@4450" prio=5 tid=0x1 nid=NA runnable   // main thread or ui thread
  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)  

위의 콜스택에서 파란색으로 표시된 handleLaunchActivity를 잠시 따라가 보겠습니다.


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

 public void handleMessage(Message msg) {

    switch(msg.what) {

        case LAUNCH_ACTIVITY: {

           ...

            handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");

            ...

        }

    }

}


private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {

    ...

    Activity a = performLaunchActivity(r, customIntent);

    

    if (a != null) {

        ...

        handleResumeActivity(r.token, false, r.isForward, !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);

        ...

    }

}


final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {

    ...

    ViewManager wm = a.getWindowManager();

    ...

    wm.addView(decor, l);

}


//frameworks/base/core/java/android/view/WindowManagerImpl.java

public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) { 

    applyDefaultToken(params);

    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);

}


//frameworks/base/core/java/android/view/WindowManagerGlobal.java

public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {

    ...

    root = new ViewRootImpl(view.getContext(), display);  // ActivityThread에서 Main Looper를 생성했던 스레드에서 ViewRootImpl 객체를 생성

    ...

}

위의 함수 호출과정을 통해 ActivityThread의 main 함수에서 시작된 스레드가 ViewRootImpl객체를 생성했다는 것을 알 수 있습니다.

바로 이 스레드가 앞서 보았듯이 Main Looper를 생성한 UI 스레드입니다.


다시 ViewRootImpl 객체 생성과정을 계속 따라 가겠습니다.

ViewRootImpl 객체가 생성되면서 ViewRootHandler를 생성하여 View.mAttachInfo 객체 생성시에 사용합니다.

// frameworks/base/core/java/android/view/ViewRootImpl.java

public ViewRootImpl(Context context, Display display) {   // 생성자

    ...

    // 나중에 View 객체에 넘겨줄 mAttachInfo 객체를 생성합니다

    mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this, context)  

}


final ViewRootHandler mHandler = new ViewRootHandler()   // View.AttachInfo 생성자로 넘겨지는 mHandler


final class ViewRootHandler extends Handler {   // ViewRootHandler는 handler를 상속한 클래스입니다

    ...

}

 

//frameworks/base/core/java/android/view/View.java

AttachInfo(IWindowSession session, IWindow window, Display display, ViewRootImpl viewRootImpl, 

                                                                 Handler handler, Callbacks effectPlayer, Context context) { 

    mSession = session;

    mWindow = window;

    mWindowToken = window.asBinder();

    mDisplay = display; 

    mViewRootImpl = viewRootImpl;

    mHandler = handler;      

    mRootCallbacks = effectPlayer;       

    mTreeObserver = new ViewTreeObserver(context);

}


여기까지 코드를 간단히 보았습니다. 이제 View 클래스의 post 함수에서 본 attachInfo.mHandler 값이 어디서 채워지는지 확인 할 단계입니다.

 // frameworks/base/core/java/android/view/ViewRootImpl.java

private void performTraversals() {

    // cache mView since it is used so much below...

    final View host = mView; 

    ...

    host.dispatchAttachedToWindow(mAttachInfo, 0);  // mAttachInfo  = new View.AttachInfo( ..., this, mHandler, this, context) 

}


//frameworks/base/core/java/android/view/View.java

void dispatchAttachedToWindow(AttachInfo info, int visibility) { 

    mAttachInfo = info;   // info는 ViewRootImpl 생성자에서 생성한 View.AttachInfo 객체입니다

    ...

}

결과적으로 attachInfo.mHandler.post(action) 코드는 아래와같은 과정을 거칩니다.

action은 ViewRootImpl 객체에서 생성한 핸들러를 통해서 메세지 Queue에 삽입됩니다.

이 Queue는 ActivityThread가 생성한 Main Looper를 통해서 처리됩니다.

참고로 하나의 스레드에는 여러개의 Handler가 동작할 수 있습니다. 

그리고 하나의 스레드는 단 하나의 Looper만 가질수 있고 Looper는 하나의 Queue와 연결되어 있습니다.


요약하면 post 함수는 action을 ViewRootImpl객체가 생성한 Handler로 전달하고 이 Handler에 의해서 ActivityThread가 생성한 Main Looper에 연결된  Queue에 삽입됩니다.

그리고 Main Looper는 이 Queue에서 action을 꺼내서 UI thread상에서 코드를 실행하게 됩니다. 

public boolean post(Runnable action) {

    final AttachInfo attachInfo = mAttachInfo;

    if(attachInfo != null) {

        return attachInfo.mHandler.post(action);

    }

    // Postpone the runnable until we know on which thread it needs to run.

    // Assume that the runnable will be successfully placed after attach.

    getRunQueue().post(action);

    return true;


지금까지 View.post() 함수의 사용법과 동작 원리를 간단하게 알아보았습니다.






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

그 중에서 메인 스레드가 아닌 스레드에서 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 함수의 사용에 대해서 간략하게 보았습니다.






안드로이드 어플리케이션이 실행되면 안드로이드 시스템은 하나의 실행 스레드로 어플리케이션의 프로세스를 실행합니다. 

어플리케이션의 구성 요소가 생성될 때 별도의 스레드가 생성되는 것은 아니며 앞서 말한 하나의 실행 스레드에서 실행됩니다. 이 스레드를 Main thread라고 합니다.

안드로이드 시스템에 의해 생성된 이 Main thread는 화면 구성에 관한 역할을  담당합니다.  

예를 들어 Button, CheckBox, TextView 등의 UI도구 키트 구성 요소를 생성 및 조작하였을 때 상호작용하는 스레드가 Main Thread입니다. 

그래서 Main thread를 UI thread라고도 합니다.  


Main thread에 대해서 좀더 이야기하기 전에 우리가 만든 어플리케이션에서 main thread가 존재하고 있는 모습을 잠시 보도록 하겠습니다.

안드로이드 스튜디오를 열어 Empty Activity를 추가하고 별도의 코드 추가없이 빌드 한 후 APK를 실행해 보았습니다. 

이 때 adb shell 명령어를 사용하여 실행된 APK의 스레드를 출력해보면 아래와 같습니다.(ps PID -t)

붉은 박스로 표시된 부분이 main thread입니다. 참고로 실행된 APK는 PID 26914, main thread TID 26914입니다.

그리고 프로세스간 통신을 위해 사용되는 바인더 스레드와 그외 런타임 내부 스레드들이 존재합니다. 

안드로이드 시스템은 빈 액티비티만 존재하는 어플리케이션일지라도 이렇게 여러 스레드들을 기본적으로 생성하고 있습니다.

바인더 스레드와 그외 내부 스레드라고 표시한 부분을 아는 것도 필요하겠지만 지금은 main thread에 초점을 맞추도록 하겠습니다.


Main thread에 대해서 좀더 알아보겠습니다. 

어플리케이션을 실행했을 때, 버튼을 누르거나 화면터치를 했는데 반응성(응답성)이 떨어진다면 사용자는 답답함을 느낄 것입니다.

그리고 간혹  ANR이 발생하여 어플리케이션이 강제 종료되는 것을 본 경험도 있을 것입니다.

반응성은 Main thread(UI thread)와 실제 작업이 실행되는 스레드간의 관계와 연관이 있습니다.

실제 작업이라고 하면 예를 들어 동영상 다운로드 버튼을 눌렀을 때 "네트워크를 통하여 동영상 데이터를 다운로드 하는 작업"을 의미 할 수 있을 것입니다.

사용자가 다운로드 버튼을 누르면 동영상 데이터 다운로드가 시작될 것입니다. 

만약 개발자가 데이터를 다운로드하는 작업을 Main Thread(UI thread) 에 구현하였다면 다운로드가 완료될 때까지 화면은 멈추어있는 것처럼 보일것입니다.

이런것들이 어플리케이션의 반응성을 떨어지게 만들고 사용자는 답답함을 느끼게 됩니다.

반응성 좋은 어플리케이션을 만드려면 시간이 오래걸리는 작업은 main thread에 구현하지 않고 별도의 스레드에서 동작하도록 구현하면 됩니다.


그리고 안드로이드 UI 도구 키트는 스레드로부터 세이프하지 않습니다

그렇기 때문에 UI thread가 아닌 스레드(직접 생성한 별도의 스레드)에서는 UI 구성요소를  조작하는 것은 허용되지 않습니다. ( 스레드 세이프 하다는 의미는 여러 스레드에서 동시에 사용해도 정상적으로 동작한다는 의미입니다)

그렇다면 UI 구성 요소는 모두 UI thread에서 조작되어야 하고 결국은 UI 입장에서는 보면 UI 동작은 단일 스레드로 동작하는 구조가 됩니다. 

따라서 UI thread에서 시간이 오래 걸리는 작업을 수행하는 것은 그 작업이 수행되는 동안 UI업데이트가 지연(또는 차단)되고 있다는 것을 의미합니다.

이런 이유로 시간이 오래 걸리는 작업은 별도의 스레드를 생성하여 UI thread로 부터 분리해야 반응성이 좋은 프로그램을 만들 수 있습니다.

다시 말하면 오래 걸리는 작업을 별도의 스레드를 생성한 후 그 스레드에서 동작시키면 됩니다.


위에서 이야기했던, 안드로이드 스레드 사용시 유의 사항을 정리하면 다음의 2가지 입니다.


1. 시간이 오래 걸리는 작업은 UI 스레드로 부터 분리하고 별도의 스레드에서 실행함으로써 UI 스레드 작업이 지연(또는 차단)되는 것을 방지합니다 .

2. UI 스레드가 아닌 스레드에서는 UI 구성 요소를 조작하지 않습니다.


지금부터는 1번, 2번 사항에 대해서 예제 코드로 하나씩 살펴보도록 하겠습니다.

예제 코드는 "Download" 버튼과 "Cancel" 버튼이 존재합니다. "Download" 버튼을 누르면 1초에 한번씩 다운로드 상태로그를 출력하도록 하였습니다. 

예제 코드에서는 단순히 로그를 출력하는 것이지만 실제로 데이터를 다운로드한다고 가정해도 좋습니다.(예제 코드니까요)

그리고 "Cancel" 버튼을 누르면 취소되었다는 로그를 출력합니다. 

이것 역시 실제로는 다운로드를 취소한다고 가정합니다.


그럼 예제 코드를 보겠습니다.

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) {  // Download 버튼 누르면 호출되는 함수
        Log.d(TAG, "Press Download button");
        isDownloading = true;

        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();
            }
        }

        isDownloading = false;
    }

    public void onCancel(View view) { // Cancel 버튼 누르면 호출되는 함수
        Log.d(TAG, "Press Cancel button");
        isDownloading = false;
    }
}


아래 로그는 "Download" 버튼을 한번 눌렀을 때 출력되는 로그입니다.

22509-22509를 보아 PID22509 TID22509 이므로 main thread에서 다운로드 작업이 동작하고 있음을 알 수 있습니다.

별도의 스레드 생성없이 다운로드 코드를 작성하였기 때문에 당연한 결과입니다.

12-06 14:28:02.530 22509-22509/com.example.codetravel.androidthread D/AndroidThread: Press Download button

12-06 14:28:03.537 22509-22509/com.example.codetravel.androidthread D/AndroidThread: Downloading ...10%

12-06 14:28:04.541 22509-22509/com.example.codetravel.androidthread D/AndroidThread: Downloading ...20%

12-06 14:28:05.545 22509-22509/com.example.codetravel.androidthread D/AndroidThread: Downloading ...30%

12-06 14:28:06.548 22509-22509/com.example.codetravel.androidthread D/AndroidThread: Downloading ...40%

12-06 14:28:07.549 22509-22509/com.example.codetravel.androidthread D/AndroidThread: Downloading ...50%

12-06 14:28:08.552 22509-22509/com.example.codetravel.androidthread D/AndroidThread: Downloading ...60%

12-06 14:28:09.554 22509-22509/com.example.codetravel.androidthread D/AndroidThread: Downloading ...70%

12-06 14:28:10.555 22509-22509/com.example.codetravel.androidthread D/AndroidThread: Downloading ...80%

12-06 14:28:11.557 22509-22509/com.example.codetravel.androidthread D/AndroidThread: Downloading ...90%

12-06 14:28:12.560 22509-22509/com.example.codetravel.androidthread D/AndroidThread: Downloading ...100% 

어쨌든 다운로드는 100% 완료되었습니다.


이번에는 "Download" 버튼을 누른 후, 바로 "Cancel"  버튼을 2~3번 정도 눌러 보겠습니다.

아래와 그림처럼  ANR(Application Not Responding) 다이얼로그가 발생하였습니다.

사용자가 "Cancel" 버튼을 눌렀지만 5초 이상 UI가 멈춰있었기 때문에 안드로이드 시스템에서 발생시킨 에러 다이얼로그입니다.

반응성(응답성)이 좋지못한 어플리케이션의 사례가 되겠습니다.



로그를 통해 좀더 자세히 보겠습니다.

60% 다운로드 로그 출력 이후에 ANR 로그가 생성된 것을 볼 수 있습니다.

5.7초 정도 서스펜드되었다는 로그도 함께 보입니다.

그리고 "Download" 버튼을 누른 직후 "Cancel" 버튼을 바로 눌렀지만 다운로드는 중단 되지 않았습니다.

취소 로그 출력은 다운로드 100% 완료된 후에 찍혔습니다.

이유는 앞에서도 언급했듯이 main thread에서 "Download"와 "Cancel" 버튼 처리와 다운로드 작업을 모두 처리하기 때문에 단일 스레드로 동작하는 구조입니다.

따라서 "Cancel" 버튼보다 먼저 실행된 다운로드 작업이 완료되기 전에는 취소 로그가 출력되지 않는 것입니다.

12-06 14:36:10.170 26810-26810/com.example.codetravel.androidthread D/AndroidThread: Press Download button

12-06 14:36:11.172 26810-26810/com.example.codetravel.androidthread D/AndroidThread: Downloading ...10%

12-06 14:36:12.175 26810-26810/com.example.codetravel.androidthread D/AndroidThread: Downloading ...20%

12-06 14:36:13.176 26810-26810/com.example.codetravel.androidthread D/AndroidThread: Downloading ...30%

12-06 14:36:14.179 26810-26810/com.example.codetravel.androidthread D/AndroidThread: Downloading ...40%

12-06 14:36:15.181 26810-26810/com.example.codetravel.androidthread D/AndroidThread: Downloading ...50%

12-06 14:36:16.185 26810-26810/com.example.codetravel.androidthread D/AndroidThread: Downloading ...60%

12-06 14:36:17.058 26810-26815/com.example.codetravel.androidthread W/art: Suspending all threads took: 5.770ms

12-06 14:36:17.090 26810-26815/com.example.codetravel.androidthread I/art: Wrote stack traces to '/data/anr/traces.txt'

12-06 14:36:17.187 26810-26810/com.example.codetravel.androidthread D/AndroidThread: Downloading ...70%

12-06 14:36:18.189 26810-26810/com.example.codetravel.androidthread D/AndroidThread: Downloading ...80%

12-06 14:36:19.190 26810-26810/com.example.codetravel.androidthread D/AndroidThread: Downloading ...90%

12-06 14:36:20.192 26810-26810/com.example.codetravel.androidthread D/AndroidThread: Downloading ...100%

12-06 14:36:20.228 26810-26810/com.example.codetravel.androidthread D/AndroidThread: Press Cancel button 


ANR을 회피하고 반응성 좋은 프로그램을 만들기 위해서 1번 사항인 시간이 오래 걸리는 작업은 UI thread로 부터 분리하라 는 의미를 알 수 있었습니다.

별도의 스레드를 생성하여 다운로드 작업(오래걸리는 작업)을 UI 스레드에서 분리하도록 하겠습니다.

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();  // Download 스레드 생성
        mDownload.start();  // Download 스레드 시작
    }

    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++) { // 이제 다운로드 작업은 Download 스레드에서 동작합니다.

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


이번에는 "Download"버튼을 누르고 약 3초 후에 "Cancel"버튼 3~4번  눌러보겠습니다.

취소 버튼을 누른 후에 다운로드가 중지되는 것을 볼 수 있습니다.

이렇게 취소 버튼이 바로 실행될 수 있는 이유는 다운로드 작업이 TID13679 스레드에서 동작하고 있기 때문입니다.

이제 이 프로그램은 main thread와 별도로 생성한 스레드가 멀티로 동작하는 프로그램입니다.

12-06 15:20:23.709 13571-13571/com.example.codetravel.androidthread D/AndroidThread: Press Download button

12-06 15:20:24.721 13571-13679/com.example.codetravel.androidthread D/AndroidThread: Downloading ...10%

12-06 15:20:25.723 13571-13679/com.example.codetravel.androidthread D/AndroidThread: Downloading ...20%

12-06 15:20:26.725 13571-13679/com.example.codetravel.androidthread D/AndroidThread: Downloading ...30%

12-06 15:20:27.437 13571-13571/com.example.codetravel.androidthread D/AndroidThread: Press Cancel button

12-06 15:20:27.727 13571-13679/com.example.codetravel.androidthread D/AndroidThread: Downloading ...40%

12-06 15:20:27.914 13571-13571/com.example.codetravel.androidthread D/AndroidThread: Press Cancel button

12-06 15:20:28.298 13571-13571/com.example.codetravel.androidthread D/AndroidThread: Press Cancel button

12-06 15:20:28.765 13571-13571/com.example.codetravel.androidthread D/AndroidThread: Press Cancel button

12-06 15:20:29.171 13571-13571/com.example.codetravel.androidthread D/AndroidThread: Press Cancel button 

이전보다 반응성이 좋아진 것을 확인할 수 있었습니다.

UI 편의성을 위해서 한가지 기능을 더 추가해 보겠습니다.

다운로드가 완료되면 "Download"버튼의 텍스트를 "Downloaded"으로 바꿔 보도록 하겠습니다


추가된 코드는 아래와 같습니다.

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"); // 다운로드가 완료되면 버튼의 텍스트를 Downloaded로 변경
                }
            }

            isDownloading = false;
        }
    }


이제 실행해보면 "Unfortunately, AndroidThread has stopped." 라는 에러 팝업을 발생시키면서 어플리케이션은 종료됩니다.

아래의  로그에서와 같이 Exception이 발생하였습니다.

12-06 15:31:12.849 18227-18227/com.example.codetravel.androidthread D/AndroidThread: Press Download button

12-06 15:31:13.863 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...10%

12-06 15:31:14.864 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...20%

12-06 15:31:15.865 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...30%

12-06 15:31:16.866 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...40%

12-06 15:31:17.869 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...50%

12-06 15:31:18.871 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...60%

12-06 15:31:19.873 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...70%

12-06 15:31:20.876 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...80%

12-06 15:31:21.879 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...90%

12-06 15:31:22.883 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...100%

12-06 15:31:22.883 18227-18307/com.example.codetravel.androidthread E/AndroidRuntime: FATAL EXCEPTION: Thread-204

                               Process: com.example.codetravel.androidthread, PID: 18227

                               android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

                               at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6556)

                               at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:942)

                               at android.view.ViewGroup.invalidateChild(ViewGroup.java:5081)

                               at android.view.View.invalidateInternal(View.java:12713)

                               at android.view.View.invalidate(View.java:12677)

                               at android.view.View.invalidate(View.java:12661)

                               at android.widget.TextView.checkForRelayout(TextView.java:7159)

                               at android.widget.TextView.setText(TextView.java:4342)

                               at android.widget.TextView.setText(TextView.java:4199)

                               at android.widget.TextView.setText(TextView.java:4174)

                               at com.example.codetravel.androidthread.MainActivity$Download.run(MainActivity.java:53) 


CalledFromWrongThreadException 이 발생하였습니다.

그리고 "Only the original thread that created a view hierarchy can touch its views" 라고 부연 설명을 해줍니다.

직역하면 뷰 계층 구조를 만든 원래 스레드(main thread)만 해당 뷰를 조작할 수 있다는 의미입니다.

안드로이드 시스템은 setText()함수를 실행하고 있는 스레드가 main thead인지 검사하였고

이를 위반 하였기 때문에 런타임 에러를 발생시킨 것입니다.

위 로그를 보면 TID18037 setText() 함수가 실행되었습니다. main thread인 TID18227 에서 실행되어야 합니다.

이제서야 UI 구성요소는 main thread(UI thread) 에서만 동작시켜야 한다는 2번 사항이 떠오릅니다.


그렇다면 이 문제를 해결할 수 있는 방법은 무엇일까요?

setText() 함수 호출같은 UI구성 키트 조작을 main thread에서 동작하도록 해줘야 합니다.

안드로이드에서는 다양한 방법으로 이것을 지원하고 있습니다.


1.  Activity.runOnUiThread(Runnable) 사용

2. View.post(Runnable) 사용

3. AsyncTask 사용


다음 포스팅에서 위의 3가지 방법을 하나씩 살펴봄으로써 CalledFromWrongThreadException 에러를 해결해보고

어떻게 UI구성 요소를 main thread에서 조작하는지 알아보도록 하겠습니다.



+ Recent posts