Cocos2d-x 쪽에서 기기 정보가 필요해 JNI 로 Java 를 부르는 코드를 만들었다. 잘 동작했는데 오래 켜 두면 느려졌다.
Table of contents
Open Table of contents
받기만 하고 있었다
문자열을 얻는 부분이 이랬다.
const char* c = env->GetStringUTFChars(js, NULL);
std::string result(c);
return result;
GetStringUTFChars 는 Java 힙에서 버퍼를 받아 오는 것이다. 짝인 ReleaseStringUTFChars 가 빠져 있었다. 부를 때마다 버퍼가 쌓이고 아무 오류도 안 난다.
지역 참조도 쌓인다
문자열 버퍼만 문제가 아니었다. FindClass 와 CallObjectMethod 가 돌려주는 jobject 도 지역 참조로 남는다.
지역 참조는 네이티브 함수가 Java 로 돌아갈 때 정리된다. 그 안에서 반복문을 돌면 그때까지 계속 쌓인다.
Dalvik 의 지역 참조 표는 512 칸이다. 넘기면 이렇게 죽는다.
JNI ERROR (app bug): local reference table overflow (max=512)
Failed adding to JNI local ref table (has 512 entries)
목록을 그리면서 반복해 부르다 실제로 만났다.
정리를 소멸자로 옮겼다
정리 호출을 넣으니 함수가 눈에 띄게 길어졌다. 중간에 빠져나가는 경로마다 정리를 해야 하기 때문인데 그렇게 여러 곳에 흩어 놓으면 언젠가 하나를 빠뜨린다.
class LocalRef {
public:
LocalRef(JNIEnv* e, jobject o) : env(e), obj(o) {}
~LocalRef() { if (obj) env->DeleteLocalRef(obj); }
jobject get() { return obj; }
private:
JNIEnv* env;
jobject obj;
};
JNI 에 PushLocalFrame 과 PopLocalFrame 도 있었다. 그 사이에서 만든 지역 참조를 한 번에 놓는다. 다만 감싸는 쪽이 소멸자라 어느 경로로 나가도 불린다는 점이 나아서 위쪽으로 갔다.
범위를 벗어나면 소멸자가 DeleteLocalRef 를 부른다. 중간 어느 지점에서 돌아가도 빠뜨릴 수 없다. GetStringUTFChars 쪽도 같은 방식으로 감쌌다.
사람이 기억해서 부르는 것과 구조가 대신 부르는 것은 다르다. 앞의 것은 경로가 늘어날수록 빠뜨릴 확률이 올라가고 뒤의 것은 경로가 늘어도 그대로다.
예외 확인과 반복 호출
Java 쪽에서 예외가 나도 네이티브 코드는 그냥 진행한다. 예외가 걸린 상태로 다른 JNI 호출을 하면 안 된다. 확인하고 지우는 처리를 넣었다.
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
return "";
}
adb shell setprop debug.checkjni 1 을 켜니 JNI 실수마다 로그가 붙었다. 그리고 같은 함수를 천 번 반복해서 불러 봤다. 정리를 넣기 전에는 500 회쯤에서 죽었고 넣은 뒤에는 천 회를 넘겼다. 반복해서 불러 보는 것이 이런 문제를 가장 빨리 드러냈다. 한 번 부르면 아무 문제가 없어 보인다.
정리
- 경계를 넘어 받은 문자열 버퍼는 놓아 주는 호출이 짝으로 있어야 한다
- 지역 참조도 쌓인다. Dalvik 은 512 칸이고 넘기면 그 자리에서 죽는다
PushLocalFrame·PopLocalFrame로 묶는 방법도 있다- 빠져나가는 경로마다 정리를 하면 하나를 빠뜨린다
- 정리를 소멸자로 옮기면 빠뜨릴 수 없다
- 사람이 기억해서 부르는 것과 구조가 대신 부르는 것은 다르다
- 자바 쪽 예외는 네이티브가 모른다. 확인하고 지워야 다음 호출이 정상이다
debug.checkjni를 켜면 JNI 실수가 로그로 나온다- 반복해서 불러 보면 이런 문제가 가장 빨리 드러난다