한참 하다 보면 앱이 꺼진다는 보고가 왔다. 언제 꺼지는지 물으니 정해진 것이 없다고 했다. 어떤 날은 20분이고 어떤 날은 한 시간이었다. 내 폰에서는 재현이 안 돼서 그 기기만의 문제로 봤다.
Table of contents
Open Table of contents
재현이 안 됐다
같은 보고가 여러 기기에서 오면서 기기 문제가 아니게 됐다. 공통점을 찾아봤다. 특정 화면이나 조작에서 꺼지는 것이 아니었다. 오래 할수록 잘 꺼진다는 것 하나만 남았다.
시간이 지날수록 무언가 쌓인다는 뜻이다. logcat 에는 잘못된 주소에 접근했다는 것만 있었다. 어디서 접근했는지가 없었다.
재현 대신 측정으로 바꿨다
죽는 지점이 매번 다르면 그 지점이 원인이 아닐 수도 있다. 주소를 쫓는 것을 접었다. adb shell dumpsys meminfo 로 얼마나 쓰이는지부터 봤다.
주기적으로 찍어 보니 그림이 명확했다. 10분과 30분과 50분에 계속 늘었다. 한 번도 줄지 않았다. 게임 화면을 나갔다 들어와도 그대로였다.
언제 죽는지는 알 수 없다. 죽기 전에 무엇이 변하는지는 잴 수 있다. 재현을 시도할 때는 진전이 없었고 측정으로 바꾸고 나서 움직였다.
어디서 새는지 좁혔다
전체가 는다는 것은 알았지만 어디서인지는 몰랐다. 화면을 하나씩 오가며 쟀다. 로비만 오가면 안 늘고 게임에 들어갔다 나오면 늘었다.
그 화면은 C++로 되어 있고 Java 에서 불러 띄우는 구조였다. 그래서 JNI 경계를 의심하고 adb shell setprop debug.checkjni 1 로 CheckJNI 를 켰다. 켜자마자 전역 참조가 계속 는다는 경고가 나왔다. 처음부터 켜 두었으면 측정하는 동안 이미 알았을 것이다.
두 언어가 만나는 자리
Java 객체를 넘겨받아 들고 있었는데 화면을 나갈 때 놓아 주지 않았다.
void setCallback(JNIEnv* env, jobject obj) {
callbackObj = env->NewGlobalRef(obj); // 붙잡는다
}
void onExit() {
// 놓는 코드가 없다
}
NewGlobalRef 로 붙잡으면 Dalvik 의 회수 대상에서 빠진다. 화면에 들어갈 때마다 하나씩 붙잡았고 나올 때 안 놓으니 계속 쌓였다. 이 표는 기본 상한이 2000이라 그것을 넘으면 앱이 그대로 죽는다. DeleteGlobalRef 를 넣으니 그래프가 평평해졌다.
여기서 이 객체는 누가 책임지는가라는 물음이 생겼다. C++에서는 내가 만든 것을 내가 지운다. Java 에서는 안 쓰면 알아서 정리된다. 두 세계가 만나는 자리에서는 한쪽이 붙잡고 있는 동안 정리되지 않는다.
정리
- 오래 할수록 잘 꺼진다는 것은 시간에 비례해 무언가 쌓인다는 뜻이다
- 죽는 순간을 재현하려 하지 말고 죽기 전에 변하는 것을 측정한다
dumpsys meminfo를 주기적으로 찍으면 그래프가 나온다- 화면을 오가며 측정하면 어느 구간에서 늘어나는지 좁혀진다
JNI경계가 의심되면CheckJNI를 켠다. 전역 참조 누수를 잡아 준다NewGlobalRef로 잡은 것은Dalvik의 회수 대상에서 빠진다- 그 표의 기본 상한이 2000이고 넘으면 앱이 죽는다
- 두 언어가 만나는 자리에서는 붙잡고 있는 동안 정리되지 않는다