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

지난번에 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등을 좀 더 깊이있게 다루면 좋겠지만, 일단 블로그에 강좌식이 아닌 정리목적으로 포스팅을 하는것을 우선으로 두고 있기에 여기서 마무리해봅니다.

댓글

  1. 작성자가 댓글을 삭제했습니다.

    답글삭제
  2. "Native Code(C/C++) < - > Java 양방향 호출해보자" 글에서의 프로젝트로... ThreadSafe를 테스트 해보고 있는데요..잘 안되어서 이렇게 글을 남기게 되었습니다.
    아래와 같이 했는데요..실행하자마자 죽어버리더라구요 ㅠ멀 잘못한걸까요ㅠㅠ
    public class testPj extends Cocos2dxActivity{

    private static Cocos2dxGLSurfaceView mCocos2dxGLSurfaceView;
    //private static Handler sHandler = new Handler();

    protected void onCreate(Bundle savedInstanceState){
    super.onCreate(savedInstanceState);


    mCocos2dxGLSurfaceView.queueEvent(new Runnable() {
    @Override
    public void run() {
    nativeCppFunc(); // 자바에서 C++의 함수를 호출함.
    }
    });


    }
    private native void nativeCppFunc();//네이티브 코드 선언

    답글삭제
    답글
    1. 음... c++ native딴 함수 정의부 오타 정도의 문제로 보이네요... 죽을때 나오는 에러로그가 있다면 더 확실할듯싶어요. 아마 Unknown 어쩌고 이런단어가 있다면 함수명 오타일듯 합니다.

      삭제
    2. 작성자가 댓글을 삭제했습니다.

      삭제
  3. 염치불구하고 에러 메세지를 올려봅니다 ㅠ
    FATAL EXCEPTION: main
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.app.testPj/com.app.testPj.testPj}: java.lang.NullPointerException
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2100)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2125)
    at android.app.ActivityThread.access$600(ActivityThread.java:140)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1227)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loop(Looper.java:137)
    at android.app.ActivityThread.main(ActivityThread.java:4898)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:511)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1006)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:773)
    at dalvik.system.NativeStart.main(Native Method)
    Caused by: java.lang.NullPointerException
    at com.app.testPj.testPj.onCreate(testPj.java:46)
    at android.app.Activity.performCreate(Activity.java:5206)
    at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1083)
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2064)
    11 more

    답글삭제
    답글
    1. Unable to start activity ComponentInfo 에러.. 저도 있던거네요..
      제 블로그에서 왼쪽 최상단이나 오른쪽에 검색바에서 검색해보세요.
      Unable to start activity ComponentInfo
      이걸로요.. 해결이 되시길...

      삭제
    2. 감사합니다 덕분에 해결했습니다^^

      삭제

댓글 쓰기

이 블로그의 인기 게시물

CMake Windows에 설치하기

'xxx.exe' 프로그램을 시작할 수 없습니다. 지정된 파일을 찾을 수 없습니다.

크로스 스레드 작업이 잘못되었습니다. xxx 컨트롤이 자신이 만들어진 스레드가 아닌 스레드에서 액세스되었습니다