이미지 줄여 읽기 한 줄이 만든 크래시: inSampleSize 는 0 에서 시작한다
메모리를 아끼려고 QR 스캐너 앱 문서함의 썸네일을 줄여 읽게 바꿨더니, 다음 버전에서 문서 목록을 열 때마다 앱이 꺼졌습니다. 원인은 BitmapFactory.Options 의 inSampleSize 기본값이 0 이라는 것이었습니다. 바꾼 코드, 크래시 기록, 고친 코드, 같은 실수를 다른 곳에서 찾은 방법을 정리합니다.
메모리 권장 사항에서 시작했다
10월 10일, Play Console 의 메모리 관련 권장 사항을 보고 운영 중인 앱에서 이미지를 원본 크기 그대로 디코딩하는 곳을 찾아 고쳤습니다. QR 스캐너 앱의 문서함 목록이 그중 하나였습니다.
문서함 목록의 한 칸에는 작은 썸네일이 있습니다. 그런데 그 썸네일 파일은 스캔한 페이지의 원본 JPEG 이었습니다. 작은 칸 하나를 그리려고 원본 해상도 이미지를 통째로 메모리에 올리고 있었던 겁니다.
바꾼 코드
먼저 크기만 읽고, 짧은 변이 320px 아래로 내려가지 않는 선까지 2배씩 줄여서 읽도록 바꿨습니다.
BitmapFactory.Options bounds = new BitmapFactory.Options();
bounds.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, bounds);
BitmapFactory.Options opts = new BitmapFactory.Options();
int minDim = Math.min(bounds.outWidth, bounds.outHeight);
while (minDim / (opts.inSampleSize * 2) >= 320) opts.inSampleSize *= 2;
Bitmap bmp = BitmapFactory.decodeFile(path, opts);
빌드는 통과했고, 이 변경은 다른 수정들과 함께 10월 11일 1.1.25 로 나갔습니다.
문서 목록을 열면 매번 꺼졌다
다음 날 Crashlytics 에 새 크래시가 올라왔습니다.
java.lang.ArithmeticException: divide by zero
at ...ui.document.DocumentAdapter.lambda$onBindViewHolder$1
10월 13일 오전 9시까지 7일 동안 27건, 사용자 4명이었고 전부 1.1.25 였습니다. 썸네일이 있는 문서가 하나라도 있으면 문서함을 여는 순간 앱이 꺼지는 상태였습니다. 썸네일 디코딩은 백그라운드 스레드에서 돌기 때문에, 거기서 난 예외를 잡는 곳이 없어 앱 전체가 종료됐습니다.
원인: 기본값이 0 이다
new BitmapFactory.Options() 로 만든 옵션의 inSampleSize 는 0 입니다. 디코더는 1 이하의 값을 1 로 취급하기 때문에 디코딩만 할 때는 0 이어도 문제가 없습니다. 그래서 평소에는 이 값이 0 이라는 걸 의식할 일이 없습니다.
문제는 그 값을 계산에 바로 썼다는 점입니다. 반복문의 첫 계산이 minDim / (0 * 2), 곧 minDim / 0 이 됩니다.
Android 문서의 예제는 계산을 지역 변수 int inSampleSize = 1; 로 시작합니다. 우리 코드는 옵션 객체의 필드를 반복문 변수로 바로 쓰면서 1 로 시작하는 줄을 빠뜨렸습니다.
테스트에서 걸러지지 않은 이유도 분명합니다. 「화면은 같고 내부만 바뀌는」 수정이라 빌드가 통과한 것만 보고, 썸네일이 있는 문서 목록을 실제로 열어 보지 않은 채 내보냈습니다.
고친 것
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inSampleSize = 1;
int minDim = Math.min(bounds.outWidth, bounds.outHeight);
while (minDim > 0 && minDim / (opts.inSampleSize * 2) >= 320) opts.inSampleSize *= 2;
- 1 에서 시작합니다.
minDim > 0도 넣었습니다. 파일이 깨져 크기를 못 읽으면outWidth·outHeight가 -1 이 되는데, 그때는 줄이지 않고 그냥 읽게 둡니다.
10월 13일 1.1.26 으로 제출했고, 이번에는 제출할 출시 빌드를 실기기에 깔아 갤러리 사진으로 문서를 만들고 문서함 목록에서 썸네일이 뜨는 것까지 확인했습니다.
같은 실수가 다른 곳에도 있나
줄여 읽기 코드는 다른 앱에도 있어서, inSampleSize * 2 로 운영 중인 앱 코드 전체를 검색했습니다.
- 진드기 촬영 앱의 다섯 곳은 모두
inSampleSize = 1을 먼저 넣고 계산하고 있었습니다. - QR 스캐너 앱의 다른 두 곳(문서 보기·페이지 목록)은 지역 변수
int sample = 1;로 계산한 뒤 마지막에 옵션에 넣는 방식이라 해당이 없었습니다.
문제가 된 곳은 하나였습니다.
정리
BitmapFactory.Options.inSampleSize의 기본값은 0 입니다. 계산에 쓰려면 1 로 먼저 넣습니다.- 크기를 못 읽으면 -1 이 나옵니다. 0 이하일 때의 처리를 같이 둡니다.
- 메모리 조치처럼 겉은 그대로이고 속만 바뀌는 수정도, 그 코드가 도는 화면을 열어 봐야 끝입니다.
- 이번에는 Crashlytics 가 들어간 버전이라 하루 만에 보였습니다. 옛 버전이었다면 Play vitals 에서 하루쯤 늦게 봤을 겁니다.
이 글의 앱은 QR코드리더: 바코드 스캐너(Google Play) 입니다.