IT_Games/Cocos2D

[cocos2d-x] JNI작업시 Activity UI Thread와 GLSurfaceView의 GLThread간 ThreadSafe하게 메세지 통신하기

JJun ™ 2013. 8. 6. 02:00


 출처: http://westwoodforever.blogspot.kr/2012/10/cocos2d-x-jni-activity-ui-thread.html




 

지난번에 AD fresca Android용을 cocos2d-x에 붙이는 것 cocos2d-x에 JNI를 사용하는 간단한 샘플을 정리했었습니다. 

이제는 샘플 단계를 넘어 현재 작업중인 C++ Framework에 통합을 해야했는데 아래와 같은 문제가 저를 괴롭혔습니다.

 

 Fatal signal 11 (SIGSEGV) at 0x00000232 (code=1)

FATAL EXCEPTION: GLThread 593
java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()

threadid=13: thread exiting with uncaught exception (group=0x40a511f8)
FATAL EXCEPTION: GLThread 461
android.view.ViewRootImpl$CalledFromWrongThreadException: only the original thread that created a view hierarchy can touch its views.
 at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:4039)
 at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:709)
 at android.view.View.requestLayout(View.java:12675)
 at android.view.View.requestLayout(View.java:12675)
 at android.view.View.requestLayout(View.java:12675)
 at android.view.ViewGroup.addView(ViewGroup.java:3206)
 at android.view.ViewGroup.addView(ViewGroup.java:3188)
 at com.android.internal.policy.impl.PhoneWindow.addContentView(PhoneWindow.java:282)
 at android.app.Activity.addContentView(Activity.java:1883)
 at com.adfresca.ads.AdFrescaView.showAd(AdFrescaView.java:686)
 at com.adfresca.ads.AdFrescaView.showAd(AdFrescaView.java:633)
 at org..game..ShowADFresca(.java:145)
 at org.cocos2dx.lib.Cocos2dxRenderer.nativeRender(Native Method)
 at org.cocos2dx.lib.Cocos2dxRenderer.onDrawFrame(Cocos2dxRenderer.java:59)
 at android.opengl.GLSurfaceView$GLThread.guardedRun(GLSurfaceView.java:1462)
 at android.opengl.GLSurfaceView$GLThread.run(GLSurfaceView.java:1216)

 

모두 이클립스 LogCat내용으로 제가 아직은 부족한 Java, Android관련 에러입니다. 구글링 들어갔죠.

 Fatal signal 11 (SIGSEGV) at



먼저 위와 같은 키워드로 검색해봤을 때는 여러가지 원인으로 에러가 발생하는 것 같더군요.
그래서 해결책도 여러가지였는데, 확인을 위해서는 에러 주소값을 ndk-gdb에서 list로 확인하면
문제가 된 소스가 나온다고는 하는데 일단 gdb 사용할지 모르니 패스했습니다.
gdb 관련 사용법은 다음에 해보고 정리를 따로 해봐야겠네요.

 FATAL EXCEPTION: GLThread

java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare

CalledFromWrongThreadException: only the original thread that created a view hierarchy can touch its views.



 다음으로 위와 같은 로그에서 공통된 키워드가 있는데요, 바로 Thread입니다.

 android.view.ViewRootImpl.checkThread(ViewRootImpl.java:4039)



이 로그를 통해 Thread쪽 문제라는 것이 확실해졌습니다. 로그를 자세히 살펴봤습니다.

 android.view.ViewRootImpl.checkThread(ViewRootImpl.java:4039)
...
android.view.View.requestLayout(View.java:12675)
...
android.view.ViewGroup.addView(ViewGroup.java:3188)
...
android.app.Activity.addContentView(Activity.java:1883)
com.adfresca.ads.AdFrescaView.showAd(AdFrescaView.java:633)
org..game..ShowADFresca(.java:145)
org.cocos2dx.lib.Cocos2dxRenderer.nativeRender(Native Method)
org.cocos2dx.lib.Cocos2dxRenderer.onDrawFrame(Cocos2dxRenderer.java:59)
...
android.opengl.GLSurfaceView$GLThread.run(GLSurfaceView.java:1216)


 

제일 아래가 먼저 출력된 로그죠. 대략적인 호출스택은 android의 GLSufaceView의 GLThread -> Cocos2dxRenderer의 java단 -> Cocos2dxRenderer의 C++단인 

nativeRender -> 제가 만든  java 함수인 ShowADFresca -> ADfresca의 showAd -> android Activity의 addContentView -> view의 addView -> requestLayout을 

거쳐 최종 checkThread입니다.

처음보기도 하고 그래서 눈에 띄기도 한 GLSufaceView, GLThread, Activity 등으로 구글링을 해봤습니다. 

간단히 정리하면 android에는 Activity 생명주기라는 것이 있는데 보통 하나의 Process나 Thread가 Active 또는 Deactive에 따라 App이 돌아가고 안 돌아가는 

뭐 그런 개념정도로 보면 되는 것 같더군요.

 

그리고 android에서 OpenGL ES를 사용하기 위해서는 GLSufaceView를 만들어 따로 GLThread를 통해 Randering을 수행한다고 합니다. 

그러니까 App의 최상단 Activity가 Main Thread고 추가로 GLThread가 하나 더 돌게 되는 것이죠. 자세한 것은 추후 정리할 수 있을 때 더 정리하기로 해볼게요.


// Cocos2dxRenderer.java
public void onDrawFrame(GL10 gl) {
...
nativeRender();    
....
}
//MessageJni.cpp
void Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeRender(JNIEnv* env)
{
    cocos2d::CCDirector::sharedDirector()->mainLoop();
}




cocos2d-x를 통해 C++로 작업된 Native Code들은 모두 GLThread를 통해 처리되고 있던것입니다. 

Cocos2dxRenderer.java와 MessageJni.cpp를 확인해보시면 관련 된 함수를 보실 수 있습니다.


이제 문제 해결에 대한 내용을 정리해보겠습니다. 원인이 잘 못된 Thread 참조에 의한것이었죠.
그럼 android에서 JNI를 통해 Java < - > C++에 ThreadSafe하게 메세지를 주고 받는 것을 찾아야하겠죠. 

즉, android Activity( Java )에서 발생한 사용자 입력 이벤트등을 native인 cocos2d-x( C++ )에 보낼 수 있어야 하고 

반대로 cocos2d-x에서 android로의 메세지 처리 모두 ThreadSafe하게 해야 합니다.

우선 Android -> C++을 먼저 살펴봅니다. 아래는 Cocos2dxGLSurfaceView.java의 소스중 터치 이벤트 부분을 가져왔습니다.



public boolean onTouchEvent(final MotionEvent event) {
     ...
        switch (event.getAction() & MotionEvent.ACTION_MASK) {
        ...
        case MotionEvent.ACTION_DOWN:
         // there are only one finger on the screen
         final int idDown = event.getPointerId(0);
            final float xDown = xs[0];
            final float yDown = ys[0];
            queueEvent(new Runnable() {
                @Override
                public void run() {
                    mRenderer.handleActionDown(idDown, xDown, yDown);
                }
            });
            break;
        case MotionEvent.ACTION_MOVE:
            queueEvent(new Runnable() {
                @Override
                public void run() {
                    mRenderer.handleActionMove(ids, xs, ys);
                }
            });
            break;
...
}



 

터치 다운이든 이동이든 간에 중요한 것은 queueEvent(new Runnable() { public void run() { ... } }
입니다. 이것이 Activity의 UI쓰레드에서 Renderer의 GLThread로의 ThreadSafe하게 해주는 놈입니다.



 public void handleActionDown(int id, float x, float y)
 {
     nativeTouchesBegin(id, x, y);
 }


 

위와같이 Cocos2dxRenderer.java 에 handleActionDown등이 정의되어 있습니다.



 private static native void nativeTouchesBegin(int id, float x, float y);


 

또한 이렇게 선언되어 있구요.



 // handle touch event  
 void Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeTouchesBegin(JNIEnv*  env, jobject thiz, jint id, jfloat x, jfloat y)
 {
     cocos2d::CCDirector::sharedDirector()->getOpenGLView()->handleTouchesBegin(1, &id, &x, &y);
 }


 

최종적으로 c++딴인 paltform/android/jni/TouchesJni.cpp에 위와 같이 구현되어 있습니다.
이제 반대로 C++ -> Android를 살펴봅니다. 간단히 MessageBox를 호출하는 부분을 보겠습니다.



 void showMessageBoxJNI(const char * pszMsg, const char * pszTitle)



cocos2dx\platform\android\jni\MessageJni.cpp에서 위 함수가 Android MessageBox를 호출하는 native 코드 부분입니다. 

Java는 Cocos2dxActivity.java를 보시면 되는데요,



public class Cocos2dxActivity extends Activity{ ... private static Handler handler; private final static int HANDLER_SHOW_DIALOG = 1; ... protected void onCreate(Bundle savedInstanceState) { ... ... handler = new Handler(){ public void handleMessage(Message msg){ switch(msg.what){ case HANDLER_SHOW_DIALOG: showDialog(((DialogMessage)msg.obj).title, ((DialogMessage)msg.obj).message); break; } } }; } // cocos2d-x native에 의해 호출 되어지는 함수 public static void showMessageBox(String title, String message){ Message msg = new Message(); msg.what = HANDLER_SHOW_DIALOG; msg.obj = new DialogMessage(title, message); handler.sendMessage(msg); }



C++ -> Android로 ThreadSafe하게 메세지 핸들링을 하려면 Handler라는 객체를 통해서 Message를 보내서 처리하면 됩니다.


JNI로 Android < - > C++간 ThreadSafe한 메세지 통신이 다른 해결책이 더 있을지도 모르지만
일단 전 위와같이 cocos2d-x가 해놓은 방법으로 Android용 ADfresca를 제 C++ Framework에 성공적으로 추가했습니다.


이래저래 에러내용으로 서론이 길고(로그 때문이긴 하지만) 결과적으로 해결부분은 짧게 나왔네요. 
GLSufaceView, Activity등을 좀 더 깊이있게 다루면 좋겠지만, 일단 블로그에 강좌식이 아닌 정리목적으로 포스팅을 하는것을 우선으로 두고 있기에 

여기서 마무리해봅니다.