안녕하세요.
갤노트 전저에서 잘 돌아가던 어플이 ICS로 업데이트를 한후 Out Of Memory가 신나게 뜨고 있습니다.
미치겠습니다.
원인은 비트맵인데요.
이미지를 활용하는 어플이라 내부적으로 sdcard에 있는 이미지를 bitmap으로 불러다가
단말기 해상도에 맞게 사이즈를 변경하여 사용을 많이 하고 있습니다.
주로 사용하는 코드가 아래와 같습니다.
Bitmap bmp = BitmapFactory.decodeFile(filepath);
mBmp = Bitmap.createScaledBitmap(bmp, width, height, true);
bmp.recycle();
bmp = null;
onDestroy()에서
if (mBmp != null){
mBmp .recycle();
mBmp = null;
}
위와 같은 방법으로 비트맵을 불러오는데요.
썸네일의 경우 갤러리나 그리드뷰등 arraylist에 넣어서 2~3배(해상도에 따라) 정도로 키워 사용을 하는 경우도 있고
원본이미지(600*900)를 그대로 사용 하는 경우도 있습니다.
그런데 자주는 아니지만 decodeFile에서 out of memory가 테스트 중간에 "이제 없어졌나?"라고 생각 할만 하면 나오고 있습니다.
테스트폰이 갤노트(ICS)랑 갤S2(진저)인데 고사양에 속하는 갤노트에서 이러니 다른 단말기들을 생각하면 한숨만 나옵니다.
구글링을 해보아도 recycle를 잘해주는게 답인것 같은데..
답답한 마음에 글을 올립니다.
회원님들의 경험이나 조언 좀 부탁드립니다.

give & take

카베나기님 소중한 답변 감사합니다.
링크로 알려주신 내용을 보았는데요. 제 경우랑 조금 다른 부분인것 같아 추가 설명을 드립니다.
option을 사용하여 원본 이미지(200*400)보다 작게 가져와도 최종 사이즈가 300*600인 경우
작게가져와서 크게 키워주나 원본을 가져와서 크게 키워주나 최종사이즈가 같기 때문에
메모리 점유율이 비슷하지 않을까 생각이 듭니다.
그리고 아무래도 작은사이즈를 키워주는 것이 퀄리티가 더 떨어지지 않을까 염려되어 됩니다.
(결정권자가 퀄리티를 목숨보다 중요시 생각을 합니다. )
제가 잘못 생각하는 부분이 있다면 지적 부탁드립니다.
그래도 한번더 생각을 해보게 되었습니다. 감사합니다.
1. 읽어온 비트맵을 Canvas로 씌워 수정할 것이 아니라, drawBitmap으로 다른곳에 찍거나 setImageBitmap으로 이미지뷰 등에 넣을 것이라면 비트맵을 확대하기 위한 createScaledBitmap()은 일반적으로 의미없습니다. 의미없을 뿐 아니라 메모리 사용량만 늘립니다.
2. 600x900정도 비트맵이라면 한 번 읽으면 600x900x4=2MB의 메모리를 사용합니다. 10개만 되도 20MB입니다. 보통 진저브레드 OS의 힙메모리 사이즈는 많아야 48~64MB입니다. 굉장히 부담가는 용량입니다. ICS는 128MB인경우도 있지만요. 리스트뷰 등에 쓸 용도의 비트맵이라면 어떤식이든 Cache메커니즘은 반드시 필요합니다. 보통 간단히 WeakReference의 HashSet를 이용합니다 . 리스트뷰 아답터의 getView메쏘드를 잘 오버라이딩 해서 사용하세요.

레오나님 소중한 답변 감사합니다.
이해를 못하는 부분이 있어 질문 조금만 드립니다.
1.200*300인 원본 이미지들을 갤러리뷰로 보여줄때 400*600으로 보여주고 싶을 경우
제가 알고 있는 방법은
ImageView의 width, height를 400*600으로 하고 이미지를 꽉차게 하는 속성을 주고 원본이미지를 넣거나
ImageView의 width, height는 wrap_content로 잡고 원본이미지를 원하는 사이즈로 확대해서 넣거나
두가지 방법만 알고 있습니다.
그런데 이미지뷰에 넣을경우 createScaledBitmap()가 필요 없다고 하셨는데 다른 방법이 있나요?
제가 답변을 잘못 이해한건지 설명을 조금만 더..
2.바탕지식이 적어 제가 잘 이해가 안가서 bitmap, out of memory, WeakReference 를 가지고 검색하던중 아래링크와 같은 정보를 찾았습니다.
http://givenjazz.tistory.com/50
말씀하신 Cashe메커니즘이 이런걸 말씀하시는 건지?
다른거라면 좀더 설명좀 부탁드립니다.
감사합니다.
1. 이미지뷰는 굳이 소스비트맵을 확대하지 않아도 말씀하신 것과 같이 디스플레이상에 확대해서 보여줄 방법이 있습니다. 화질이나 메모리 사용에 이점이 없는 소스비트맵의 확대는 불필요하다는 것이죠. decodeFile()에서 얻은 비트맵을 소스로 쓰시면 되겠습니다.
2. 캐시의 구현방법이 중요하다기 보다 캐시의 역할이 중요합니다. 캐시는 다음 요구사항을 정확히 구현하기만 하면 됩니다.
* void setCacheSize(int capacity);
* void put(String key, Bitmap bmp); - bmp를 캐시에 저장하는데 토탈 캐시 항목이 capacity보다 큰 경우 이전 항목을 삭제(비트맵이니 recycle()하는 것이 좋겠지만 어짜피 GC퇼테니 안해도 될 겁니다
* Bitmap get(String key);
다른분들이 말씀하신 바와 같이 비트맵은 필요 없어진 경우 바로 해제해야 하는데 특히 ListView나 Gallery등에 사용할 것이라면 언제 해제해야할 지 애매한 경우가 있습니다. 이제 리스트뷰 아댑터의 getView()를 다음처럼 구현합니다.
View getView(int position, View convertView, ...)
{
ImageView iv;
If (convertView != null)
iv = (ImageView)convertView;
else {
iv = new ImageView(...);
// ....
}
Bitmap bmpForImageView = mCache.get(Integer.toString(position));
if (bmpForImageView == null) {
bmpForImageView = decodeFile(..) / createScaledBitmap(...);
mCache.put(Integer.toString(position), bmpFromImageView);
}
iv.setImageBitmap(bmpForImageView);
return iv;
}
이제 메모리에는 항상 캐시의 capacity 만큼의 비트맵만 존재할 것입니다. 캐시의 성능을 위해 LRU캐시 등을 사용하거나 풀 메모리를 사용하기 위해 WeakRefernce의 HashSet을 이용할 수 있을 것입니다.
Bitmap 객체를 단 1개 가지고 처리할수 있게 코드를 잘 구성해보세요.
위 소스같은경우 bmp는 바로 recycle 하는데, mBmp는 계속 새로 생성만 시키는것 같네요.
mBmp를 생성후 활용한 다음, 다음번 mBmp를 생성하기전에 recycle하세요.
더 좋은건 mBmp 생성 후 어딘가에 사용하고 바로 recycle 하는겁니다.
어떠한 경우에도 Bitmap 객체가 자꾸 생성되어 메모리를 갉아먹게 하지마세요.
새로운걸 생성해야한다면 이전꺼는 꼭 recycle 하시고, 가급적 사용하고 바로 recycle하세요.
흔히하는 오해중 하나가 자바는 가비지 컬렉션 기능이 있으니, 새로운 객체를 생성하여 할당하면
예전껀 자동으로 해지되겠지 하는겁니다.
Bitmap에선 그런 선입관 버려야합니다. 무조건 쓰고 최대한 빨리 recycle..

챨리권님 답변을 달아주셔서 감사합니다.
위소스는 비트맵을 불러올때 사용되는 메소드를 보여주기 위해 간단하게 적었는데요.
"mBmp 생성 후 어딘가에서 사용하고 바로 recycle 하는겁니다."라고 말씀하셨는데
갑자기 헷갈리는 부분이 있습니다.
비트맵을 생성후 이미지뷰에 넣은면 화면에 보여지는 대요 그 상태에서 mBmp를 recycle()해도 화면에 이미지는 그대로 유지가 되나요?
Bitmap을 이미지뷰에 전달한건 "다사용한"게 아닙니다. 계속 사용중인 상태로 만든거죠.
문제의 핵심은 이전에 로드한 Bitmap을 해제하지 않고, 계속 생성하는것이 문제입니다.
이미지뷰에 넘겨줬다면 당연히 바로 해제하면 안됩니다. 하지만 다음번에 새로운 이미지를 로드할때는
이전 Bitmap을 recycle 해줘야 하겠죠.
또한가지 꼭 이미지뷰을 이용해서 이미지를 표현할 필요는없습니다.
View객체를 상속받아 커스텀뷰를 하나 만들어서 onDraw함수에서 이미지로드후 Drawing하고 바로 recycle
하는것도 하나의 방법입니다.
성능과 메모리 사이에 트레이드오프를 따져서 그것이 이득이라면 그렇게 하는것도 방법입니다.
때론 선택의 여지없이 위의 방법을 사용해야 할때도 있습니다. 여려개의 이미지뷰가 있고
그것들에게 모두 Bitmap을 로드해 전달해줄수 없는 메모리 상황이라면 이방법밖에 대안이 없을겁니다.

챨리권님 계속 답변으르 달아주셔서 감사합니다.
말씀하신대로 하나의 Bitmap을 보여주다가 다른 Bitmap을 보여줄때 이전 Bitmap을 recycle()해줘야 한다는건 이해를 했습니다.
그런데 그리드뷰나 갤러리같은경우 한 화면에 여러개의 Bitmap이 보여지고 또한 스크롤링할때 빠르게 다음/이전 이미지를 보여줘야 하는데요.
이런 경우 getView()에서 어떤 로직으로 Bitmap을 로딩하고 recycle() 해줘야 할까요?
그리고 커스텀뷰에서 onDraw함수에서 이미지로드 후 Drawing하고 바로 recycle 한다는 말씀이
onDraw()에서 bitmap을 생성하고 그려주고 바로 recycle해주는 것을 반복 한다는 말씀이신가요?
안드로이드 기본 갤러리앱 소스 분석해 보시면 도움이 되실듯 합니다. ^^
당연히 읽어 보셨겠지만. 챨리권님이 말씀하시는 내용이 여기 잘 정리 되어 있는듯 합니다.
http://developer.android.com/intl/ko/training/displaying-bitmaps/index.html
아이스크림으로 버전업되면서 OOE 기준이 업격하게 바꼇다는 핑계로 저도 신나게 멘붕중입니다.
정말 한번도 안써보던 네이티브메모리 용량 메서드까지 써서 체크해가면서하는데 정말 간당간당하네요.
inSampleSize 옵션을줘서 기본적으로 용량을 작게해서 가져오는게 낫습니다.
글구 bitmap만 recycle시켜주는게아니라 이미지뷰에 붙였으면 이미지뷰의 콜백도 같이 제거해줘야될거에요.
저같은경우는 이렇게했어요.
for(ImageView iv : imageview){
Drawable d = iv.getDrawable();
if(d instanceof BitmapDrawable){
Bitmap b = ((BitmapDrawable)d).getBitmap();
b.recycle();
}
}
http://sdw8001.tistory.com/146 이게 도움이 될듯합니다