새 버전을 낸 뒤 진행 상황이 사라졌다는 문의가 왔다.
Table of contents
Open Table of contents
구조체를 통째로 쓰고 있었다
저장을 구조체 통째로 fwrite 하는 방식으로 하고 있었다. 새 버전에서 필드를 하나 추가했다.
struct SaveData { int level; int gold; int stage; int gem; };
fwrite(&data, sizeof(SaveData), 1, fp);
그러면 fwrite 가 쓰는 바이트 수가 달라진다. 옛 파일은 12바이트인데 새 구조는 16바이트를 읽으려 한다. fread 로 받아 보니 앞의 셋은 맞고 마지막이 쓰레기 값이었다. 파일에 그만큼이 없기 때문이다.
이 방식은 다른 것도 걸린다. 지금은 같은 크기 필드만 있어 딱 맞지만 크기가 다른 것이 섞이면 컴파일러가 정렬을 맞추려고 사이에 빈 바이트를 넣는다. 그 규칙은 ABI 가 정한다. 에뮬레이터의 x86 과 기기의 ARM 에서 그 값이 다르면 같은 판인데 크기가 갈린다. NDK 로 두 ABI 를 다 빌드하면 그대로 드러난다.
버전 번호를 넣었다
크기로 판단하는 방법을 먼저 생각했다. 그런데 버전마다 크기를 외워야 한다. 필드 하나를 빼고 하나를 넣으면 크기가 같은데 뜻이 다른 경우도 생긴다.
파일 앞에 버전 번호를 넣고 읽을 때 그것부터 보게 했다. 크기는 결과로 달라지는 값이고 버전은 우리가 뜻을 부여한 값이다. 근거로 쓸 수 있는 것은 뒤였다.
한 단계씩 올리게 했다
버전마다 읽는 코드를 따로 두면 버전이 늘수록 코드가 쌓인다. 한 단계씩 올리는 방식으로 바꿨다.
if (version == 1) { upgradeV1toV2(); version = 2; }
if (version == 2) { upgradeV2toV3(); version = 3; }
각 단계만 알면 되고 아주 옛 파일도 여러 번 거쳐 최신이 된다. 읽고 나서 새 형식으로 다시 저장하니 다음부터는 한 번에 읽힌다.
각 버전의 파일을 하나씩 만들어 두고 전부 돌려 봤다. 1에서 3으로 가는 경로가 두 단계를 거치는지 눈으로 확인해야 했다. 새로 만든 파일만으로 시험하면 옛 경로는 한 번도 안 돈다.
이름과 값으로 바꾼 뒤
더 근본적으로는 구조체를 통째로 쓰는 것이 문제였다. 이름과 값으로 저장하게 바꾸니 항목이 늘어도 옛 파일에 없으면 기본값을 쓰고 순서가 바뀌어도 상관없다.
CCUserDefault 가 이미 그런 모양이라 그걸 쓸까 했는데 안 썼다. XML 로 평평하게만 저장해서 목록이나 중첩이 안 들어간다. 우리 저장에는 스테이지별 기록이 배열로 있었다.
이름을 같이 쓰니 파일이 커진다. 통째로 쓰는 쪽은 작고 빠른 대신 깨지기 쉽다. 이름별로 쓰는 쪽은 크고 느린 대신 안 깨진다. 저장 파일이 몇 킬로바이트라 안 깨지는 쪽을 골랐다.
정리
- 구조체를
fwrite로 통째로 저장하면 구조가 바뀔 때 못 읽는다 - 구조체 크기는 정렬 때문에
x86과ARM의ABI에서 갈릴 수 있다 - 크기로 판단하면 크기가 같은데 뜻이 다른 경우에 막힌다
- 버전 번호를 앞에 넣는다
- 버전마다 읽는 코드를 두면 쌓이므로 한 단계씩 올린다
- 옛 버전 파일을 만들어 두고 경로를 전부 돌려 본다
- 이름과 값으로 저장하면 항목이 늘어도 안 깨진다
CCUserDefault는 평평한 값만 담아서 배열이 있으면 못 쓴다