게임을 오래 켜 두면 느려지다가 죽는다. 몇 분 켜 두는 것만으로는 안 죽고, 화면을 여러 번 오갈 때 그렇다.
Table of contents
Open Table of contents
화면을 오갈수록 죽는다
먼저 언제 죽는지부터 갈랐다. 켜자마자 죽지는 않는다. 가만히 두면 몇십 분을 켜 놔도 안 죽는다. 화면을 왔다 갔다 하면 죽는다. 몇 번 만에 죽는지 세어 보니 스무 번쯤에서 눈에 띄게 느려지고 서른 번쯤에서 죽었다. 횟수가 대략 일정하다는 건 시간이 아니라 전환 자체에 딸린 것이 쌓인다는 뜻으로 보였다.
adb shell dumpsys meminfo 를 반복해 찍으면서 다시 해 봤다. 화면을 전환할 때마다 늘어나고 원래 화면으로 돌아와도 줄지 않는다. 늘기만 하고 안 줄어드니 어딘가에서 만든 것이 안 지워지고 있다.
느는 자리가 Dalvik 힙이 아니라 네이티브 쪽이었다. 텍스처는 NDK 와 OpenGL 이 들고 있어서 Java 쪽 도구로는 안 보인다.
코드를 봤다. 화면을 만들 때 적을 새로 만들어 자식으로 붙인다. 그런데 화면을 나갈 때 지우는 코드가 없다.
Enemy* e = new Enemy();
this->addChild(e);
쌓는 화면과 버리는 화면이 있었다
지우는 코드가 없는 게 곧 잘못은 아니었다. Cocos2d-x 의 CCObject 는 참조 계수 방식이라 addChild 로 붙이면 계수가 올라간다. 부모가 정리될 때 자식도 같이 정리된다. 그러면 부모가 정리되지 않고 있다는 뜻이 된다.
화면을 바꾸는 코드를 봤다.
CCDirector::sharedDirector()->pushScene(newScene);
CCDirector.h 를 열어 보니 화면을 바꾸는 방법이 셋이었다. pushScene 은 지금 화면을 쌓아 둔 채로 새 화면을 올린다. replaceScene 은 지금 화면을 버리고 바꾼다. popScene 은 쌓아 둔 것을 꺼낸다. 예제를 보고 따라 쓴 것이라 차이를 모르고 골랐다. 오갈 때마다 CCScene 이 그대로 쌓이고 있었다.
전부 replaceScene으로 바꾸면 될 것 같았지만 그러면 안 되는 자리가 있었다. 일시정지 화면은 게임으로 돌아와야 한다. 돌아올 화면이면 쌓아 두는 것이 맞고, 대신 popScene으로 꺼내야 한다. 쌓기와 꺼내기가 짝이고 짝이 안 맞으면 쌓인다. 화면마다 돌아올 것인지를 기준으로 나눠서, 메뉴에서 게임으로 갈 때와 게임에서 결과로 갈 때는 버리고, 게임에서 일시정지로 갈 때만 쌓게 고쳤다.
직접 만든 것은 직접 지운다
고치고 나서 다시 재 보니 많이 줄었는데 여전히 조금씩 늘고 있었다. 코드를 더 보니 자식으로 붙이지 않고 만들기만 한 것이 있었다.
CCTexture2D* tex = new CCTexture2D();
new 로 만든 CCObject 는 참조 계수가 1 로 시작한다. 부모가 정리돼도 안 지워진다. 만든 쪽이 release() 를 불러 줘야 한다. 아니면 autorelease() 를 붙여 다음 프레임에 풀리게 한다.
두 경우를 놓고 보니 선이 하나 나온다. 자식으로 붙인 것은 부모가 정리하고 직접 만든 것은 직접 정리한다. 누가 소유하느냐가 정하는 것이다.
다만 코드를 봐서 어느 쪽인지 바로 안 보이는 게 문제였다. 만드는 줄과 붙이는 줄이 멀리 떨어져 있으면 붙였는지 안 붙였는지 확인하러 위아래를 오가야 한다. 그래서 만들면 바로 다음 줄에서 붙이는 것을 습관으로 두기로 했다. 떨어져 있으면 중간에 다른 경로로 빠져나갈 때 안 붙은 채로 남을 수도 있다.
이미지 캐시 비우는 시점
CCTextureCache 의 dumpCachedTextureInfo() 를 부르니 무엇이 얼마나 잡고 있는지 로그로 나왔다. 텍스처가 차지하는 몫이 컸다. CCTexture2D 를 직접 만드는 대신 addImage 로 부르면 캐시가 한 번 읽은 것을 재사용한다.
다만 캐시도 안 비우면 쌓인다. 안 쓰는 텍스처까지 계속 들고 있게 된다. removeUnusedTextures() 는 참조 계수가 1 인 것만 지운다. 어디서 부를지가 판단이었다. 자주 비우면 메모리는 적게 쓰는데 다시 읽느라 느려지고 안 비우면 빠른 대신 쌓인다.
화면이 크게 바뀔 때만 비우기로 했다. 메뉴에서 게임으로 들어갈 때처럼 쓰는 이미지가 통째로 달라지는 지점이다. 같은 계열 화면끼리 오갈 때는 어차피 다시 쓸 것이라 그냥 뒀다.
정리
- 시간이 아니라 전환 횟수에 비례해 죽으면 전환마다 쌓이는 것이 있다는 뜻이다
- 화면 전환에 쌓는 방식과 버리는 방식이 있고 예제만 보고 고르면 모르고 쌓는다
- 돌아올 화면만 쌓고 나머지는 버린다. 쌓았으면 꺼내야 짝이 맞는다
- 자식으로 붙인 것은 부모가, 직접 만든 것은 직접 정리한다. 소유가 기준이다
- 만드는 줄과 붙이는 줄을 붙여 두면 어느 쪽인지 헷갈리지 않는다
CCTextureCache는 안 비우면 쌓이고 자주 비우면 다시 읽느라 느려진다removeUnusedTextures()는 아무도 안 쓰는 것만 지운다- 쓰는 이미지가 통째로 달라지는 지점에서만 비우는 편이 균형이 맞았다