지난 "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() 함수의 사용법과 동작 원리를 간단하게 알아보았습니다.
'Android > 기본 개념' 카테고리의 다른 글
MediaRecorder 를 이용한 캠코딩 예제 (4) | 2018.01.11 |
---|---|
getMainLooper 사용하기 (0) | 2018.01.06 |
MediaRecorder API 를 사용한 레코딩 예제 (0) | 2017.12.27 |
MediaRecorder API 를 사용한 레코딩 방법 (0) | 2017.12.20 |
runOnUiThread 사용하기 (0) | 2017.12.19 |