ImageView에 대용량 Bitmap 효과적으로 로딩하기
하나의 안드로이드 앱에서 다룰 수 있는 메모리의 한계 때문에 이미지를 처리하다보면 OutofMemory exception이나 퍼포먼스 문제에 부딪치기 쉽다. 또한 이미지를 처리하는 도중 상대적으로 작은 뷰의 크기에 맞지 않는 고해상도의 이미지를 불러오는 것은 괜한 리소스를 낭비하게 만든다.
이 포스팅은 Google 개발자 튜토리얼(여기)을 참조하여 효과적으로 비트맵을 핸들링 하는 방법에 대해 정리해 보았다.
1. inJustDecodeBounds 설정하기
BitmapFactory class는 여러가지 리소스로부터 Bitmap 이미지를 만들어내기 위한 여러 decoding 메소드를 제공한다.
이중 options.inJustDecodeBounds를 true로 설정하면 이미지를 decoding할때 이미지의 크기만을 먼저 불러와 OutofMemory Exception을 일으킬만한 큰 이미지를 불러오더라도 선처리를 가능하도록 해준다.
BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(getResources(), R.id.myimage, options); // inJustDecodeBounds 설정을 해주지 않으면 이부분에서 큰 이미지가 들어올 경우 outofmemory Exception이 발생한다. int imageHeight = options.outHeight; int imageWidth = options.outWidth; String imageType = options.outMimeType;
2. Scaled Down된 버전의 이미지 메모리에 로딩하기
1번의 과정을 통해 이미지의 크기를 알게되었다면 이제 적절한 크기로 이미지를 리사이징할 차례이다.
inSampleSize 설정을 통해 이미지를 작은 사이즈로 리사이즈 할 수 있다.
inSampleSize는 픽셀수 기준으로 1/inSampleSize 크기로 이미지를 줄여준다. (가로,세로 기준)
리사이징을 위한 decoder가 inSampleSize를 2의 배수에 가까운 수로 버림하여 계산하기 때문에 2의 배수로 값을 계산하도록 하고, 선처리된 높이와 폭을 바탕으로 이미지를 표시할 뷰의 크기보다 작지 않은 크기로 가장 큰 inSampleSize값을 산출한다.
public static int calculateInSampleSize( BitmapFactory.Options options, int reqWidth, int reqHeight) { // Raw height and width of image final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { final int halfHeight = height / 2; final int halfWidth = width / 2; // Calculate the largest inSampleSize value that is a power of 2 and keeps both // height and width larger than the requested height and width. while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) { inSampleSize *= 2; } } return inSampleSize; }
3.이미지 전처리 메소드 생성하기
이제 1, 2번 과정을 통합하여 출력하고자 하는 이미지 뷰에 표시할 차례이다.
1번 과정과 2번 메소드를 통합한 decodeSampledBitmapFromResource 메소드를 생성한다.
해당 메소드는 어떠한 크기의 이미지가 소스로 들어오더라도 outofmemory exception 을 발생시키지 않는다.
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { // First decode with inJustDecodeBounds=true to check dimensions final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); // Calculate inSampleSize options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // Decode bitmap with inSampleSize set options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); }
4. ImageView에 출력하기
이제 3번에서 만든 메소드를 이용해 이미지 비트맵을 출력해주면 된다.
mImageView.setImageBitmap( decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
바로 setImageBitmap을 사용했을 때 outofmemory exception이 발생하는 이미지에 대해서도
decodeSampledBitmapFromResource 메소드를 통해서는 적절하게 리사이징해서 출력함을 확인할 수 있다.
public Bitmap decodeSampledBitmapFromIs(InputStream is,
int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
Log.i(“decodeFromIs: “, “width ” + reqWidth + ” height ” + reqHeight + ” inSampleSize : ” + options.inSampleSize);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
Bitmap b = BitmapFactory.decodeStream(is, null, options);
return b;
}
리소스가 아니라 외부서버에서 인풋스트림으로 받아서 처리하는데 메모리를 잡아먹어서 해결방법 찾다가 오게되었습니다! 설명 되게 잘해주셔서 감사합니다 ㅋㅋ
근데 위에 처럼 인풋스트림으로 하고있는데 Log.i를 찍어도 잘나오고 하는데 이미지가 안그려져서 질문드립니다.
CalculateinSampleSize 메서드를 부르기 전에
BitmapFactory.decodeStream(is, null, options);
이 한줄때문에 안그려지는거 같습니다.. 이거 지우면 잘되더라구요. 물론 CalculateinSamepleSize 안부르구요..
디코드스트림을 두번 부르면 안되는거같은데 왜그런지모르겟네요 ㅠㅠ
익명
2015/07/16 at 1:13 pm
decodeStream을 두번 부르게되면, 스트림의 위치가 첫번째 읽은 데이터의 다음으로 넘겨져, 두번째 호출 시에 빈 스트림을 읽게되어 이미지가 출력되지 않는것으로 보이네요. 해당 내용이 api에도 나와있네요.
https://developer.android.com/reference/android/graphics/BitmapFactory.html#decodeStream(java.io.InputStream, android.graphics.Rect, android.graphics.BitmapFactory.Options)
Ringster
2015/07/17 at 7:03 pm
is.reset() 하면 된다고 들었던 거 같아요
진영
2016/09/02 at 4:50 pm
감사합니다. 덕분에 메모리가 적게 드네요 ㅎㅎ
그런데 액티비티를 전환할 때 메모리를 해제시키려면 어떻게 해야 되나요??
액티비티 전환 시에도 메모리가 계속 누적이 되네요..
HandyKim
2015/12/23 at 3:29 am
액티비티에서 사용하지 않는 이미지라면 다음과 같이 ImageView를 명시적으로 background를 null로 주고 GC를 호출하시면 될 것 같습니다.
Ringster
2015/12/23 at 3:02 pm
답장을 이제서야 봤네요 ㅎㅎ.. 감사합니다
제가 다이얼로그로 팝업창을 여러 개 띄우는 액티비티가 있는데 팝업창에도 이미지가 있습니다.
그럼 onDestroy와 dismiss 할 때 팝업창에 사용된 모든 이미지들도 background를 null로 주는게 맞을까요??
p.s : 이미지가 background로 설정된 것만 가능한거죠?? src로 되어있는건 setImageDrawable(null);가 맞나요?
Handykim
2016/01/02 at 1:33 am