ui를 구성하는 bitmap 파일에 관한 질문입니다.
bitmap 파일들은 따로 추가적인 이미지도 아니고 ui를 구성하는데 필요한 꼭 필요한 이미지이고
모든 bitmap파일의 크기를 합쳐도 620kb 밖에 되지않습니다.
bitmap파일 하나하나는 1~5kb고 제일큰 배경이 9kb정도 입니다.
하지만 이 이미지들을 한번에 모두 올려서 사용하면 out of memory 에러가 나더군요.
보통 어플리케이션이 16메가의 힙메모리정도를 사용해도 무난하다고 알 고 있는데, 이유를 알 수 가 없습니다.
혹시 엑티비티나 하나의 클래스에서 사용할 수 잇는 힙메모리가 제한되어있나요?
이유야 어쨋든 위의 문제를 해결하기 위해서 bitmap을 recycle해가며 사용하기로 했습니다.
하지만 bitmap을 recycle 할 때는 해당 bitmap을 더이상 사용하지 않을 때만 하는걸로 알고 있습니다.
몇번 테스트해본 결과 역시 recycle한 bitmap을 다시 사용하려는 경우 에러가 발생했습니다.
글을 읽어보면 깨달으셨겠지만 딜레마 입니다. ui를 구성하는 기본적인 bitmap 파일이기 때문에
activity를 왔다갔다하는 어플리케이션의 특성상 recycle과 다시 사용할 때 메모리에 올려서 사용하는 bitmap 파일의 주기를
만들어내기가 꽤 어렵더군요.
정리하면,
1. 대체 총 620kb의 이미지를 메모리에 올렸을 뿐인데 왜 뻗어버리는 걸까요? 혹시 정답을 아시는분 계신가요.
2. 이 문제를 해결하기 위한 방법이 정말 recycle 밖에없는건가요.
3. recycle로 이 문제를 해결한다고 하면 bitmap주기를 어떻게 잡아야 할까요? ㅠㅠ
고수님들의 답변 부탁드립니다 ㅠㅠ
이거 떄문에 3일째 끙끙앓고 있습니다.
친절한 답변 감사드립니다 ㅠㅠ
imageView.setImageDrawable(R.drawable.test_img);
이렇게 짠놈은 recycle을 하면 안되는 거군요..
제 문제에 대해 좀더 부연설명을 하자면
제가 레이아웃 자체를 동적으로 구현하고 있습니다. (안드로이드의 엄청나게 많은 단말기 때문이죠...)
xml 코드로 짜지않고서
ImageVeiw imageview = new ImageView(getApplicationContext());
imageView.setImageDrawable(R.drawable.test_img);
이런식으로 말이죠...
메모리릭 관련하여 구글링을 해보니 Activity Context를 많이 쓰면 Activity의 Reference count에 문제가 생긴다고 하더군요
그래서 ApplicationContext를 사용하는 쪽으로 모든 코드를 검사했습니다.
문제는
imageView.setImageDrawable(R.drawable.test_img);
요놈에서 발생합니다...ㅠㅠ
메모리검사를 해본결과 onCreate 과정에서 저 이미지를 계속 메모리에 올려서 Activity를 켰다 껏다를 반복하면
그 누적된 메모리 때문에 OOM이 나는듯 합니다.
flag를 통해서 Activity가 중복되서 만들어지지 않도록 조절도 하고, context 관리도 나름 해주고 있는데도
계속 OOM이 납니다.
갤럭시 넥서스가 힙메모리가 매우 작아 그렇다고 하는데...ㅠㅠ
이 OOM을 해결할 방법이없습니다.
맨마지막에 달아주신 방법대로 onDestroy에서
View를 Unbind하는 트릭을 구현해서 쓰면 리소스에서 직접 이미지뷰에
할당한 메모리는 회수가 되는건가요?
imageView.setImageDrawable(null);
이렇게요?
구글링을 통해서 긁어온 Unbind 트릭을 써본결과 메모리회수에 별 도움이 안되는거 같아서 그냥 안쓰고있습니다 ㅠㅠ
혹시 몰라 UnbindHelper 코드 올려봅니다.
public class ViewUnBindHelper {
public static void unbindReferences(View view) {
try {
if (view != null) {
unbindViewReferences(view);
if (view instanceof ViewGroup) {
unbindViewGroupReferences((ViewGroup) view);
}
}
} catch (Exception ignore) {
}
}
public static void unbindReferences(Activity activity, int viewId) {
try {
View view = activity.findViewById(viewId);
if (view != null) {
unbindViewReferences(view);
if (view instanceof ViewGroup) {
unbindViewGroupReferences((ViewGroup) view);
}
}
} catch (Exception ignore) {
}
}
private static void unbindViewReferences(View view) {
try {
view.setOnClickListener(null);
} catch (Exception ignore) {
}
try {
view.setOnCreateContextMenuListener(null);
} catch (Exception ignore) {
}
try {
view.setOnFocusChangeListener(null);
} catch (Exception ignore) {
}
try {
view.setOnKeyListener(null);
} catch (Exception ignore) {
}
try {
view.setOnLongClickListener(null);
} catch (Exception ignore) {
}
try {
view.setOnTouchListener(null);
} catch (Exception ignore) {
}
try {
view.setTouchDelegate(null);
} catch (Exception ignore) {
}
Drawable d = view.getBackground();
if (d != null) {
try {
d.setCallback(null);
} catch (Exception ignore) {
}
}
if (view instanceof ImageView) {
ImageView imageview = (ImageView) view;
d = imageview.getDrawable();
if (d != null) {
d.setCallback(null);
}
if (d instanceof BitmapDrawable) {
Bitmap bm = ((BitmapDrawable) d).getBitmap();
bm.recycle();
}
imageview.setImageDrawable(null);
} else if (view instanceof WebView) {
((WebView) view).destroyDrawingCache();
((WebView) view).destroy();
}
try {
view.setBackgroundDrawable(null);
} catch (Exception ignore) {
}
try {
view.setAnimation(null);
} catch (Exception ignore) {
}
try {
view.setContentDescription(null);
} catch (Exception ignore) {
}
try {
view.setTag(null);
} catch (Exception ignore) {
}
}
private static void unbindViewGroupReferences(ViewGroup viewgroup) {
int numOfChild = viewgroup.getChildCount();
for(int i = 0; i < numOfChild; i++) {
View view = viewgroup.getChildAt(i);
unbindViewReferences(view);
if (view instanceof ViewGroup) {
unbindViewGroupReferences((ViewGroup) view);
}
}
try {
viewgroup.removeAllViews();
} catch (Exception ignore) {
}
}
}예... 일단은 위에 첨부하신 코드중
bm.recycle() 하는 코드는 자체 bitmap 캐시클래스를 운용하시는 경우라면 주석 처리하시는게 나을 것 같구요. (혹은 캐시된 이미지가 recycle되었는지를 확인하는 루틴을 자체 캐시클래스에 추가)
의심되신다는 부분 확인을 위해서는 해당 액티비티 onResume등에서 루프를 통해 반복적으로 해당 코드를 실행해보세요
정말 그 부분이 문제가 된다면 바로 OOM이 뜨겠죠.
그게 아니라면 해당 액티비티에서 문제가 될만한 코드들을 주석을 걸어가며 어떤 경우에 메모리가 릴리즈되지 않고, 계속 중가되는지를 확인하셔야겠네요.




일단 짧은 지식으로 답변드리면,
질문자 분께서 말하신 620kb 이미지라는것은 사실 압축데이터이죠 (png, jpeg등등)
bitmap은 압축 데이터가 아니구요.
그래서 1kb 의 이미지를 bitmap데이터로 메모리에 올리는게 1kb는 아니라는거죠.
두번째.. bitmap.recycle() 이 필요한 부분은 제생각엔 이렇습니다.
* 리스트를 통해서 많은 이미지가 빠르게 스위칭 되며 보여지는 화면 *
* 큰 해상도의, 고품질의 이미지 (갤러리같은곳에서) 가 보여지는 화면 *
gc 타이밍이라는게 우리가 가늠할 수 없기에 메모리가 충분하지 않은 상황에서 메모리에서 해제되기 전 새로운 bitmap 데이터를 메모리에 올리려하는 순간이 된다면 out of memory 가 뜨겠죠.
또한 recycle 같은 경우는 BitmapFactory를 통해서 bitmap을 만들지 않은 경우에는 사용하시면 안됩니다.
예를 들어...
imageView.setImageDrawable(R.drawable.test_img);
와 같이 리소스 데이터를 셋해주고
((BitmapDrawable)imageView.getDrawable()).getBitmap().recycle();
이렇게 한다면..
다음번 해당 drawable 리소스를 사용할때 이런 에러메시지를 보시게 될겁니다.
java.lang.RuntimeException: Canvas: trying to use a recycled bitmap android.graphics.Bitmap@428ab420
위 결과가 말해주는것은 resource로부터 가져오는 이미지 데이터는 os차원에서 캐싱 관리를 한다는 얘기죠. (매번 file -> bitmap변환이 아니란말)
캐싱되어있지만 recycle 가능하고, 하지만 다음번 사용할때 recycle 되어있는지 체크하는 루틴이 없다는 말.
사실 정상적으로는 뷰가 릴리즈 되면서 bitmap도 gc에 의해 release 되어야 합니다. (위에서 말한것처럼 리소스는 제외겠죠, os가 알아서 처리할것 같네요. 메모리 부족같은 특수 상황에)
그러나 좀만 검색해보면 아시겠지만, BitmapFactory를 통해 bitmap변한되어 뷰에 drawable객체로 할당된 경우는, 이런 문제점이 있다더군요.
drawable 의 callback이란 녀석때문에 메모리 누수가 생긴다는 얘기가 있죠. drawable 은 null이 되지만 이 콜백 프로퍼티가 bitmap을 레퍼런싱 한다. 그래서 뷰가 해제 되어도 bitmap이 누수된다? 였나요?
(저는 테스트 안해봐서 확신할 수가 없네요. 테스트 해보신분들이 얘기해주실것임)
그외에도 이유는 메모리가 누수되는 있습니다. Bitmap이 누수되는 원인이, Activity가 실제론 release되지 않는경우라던지 (잘못된 static 객체의 사용 등 잘못된 코딩의 예.. 이경우 액비티티의 레퍼런스가 남아있기에 gc대상이 아니게 된다는...)
그래서 onDestroy 같은 곳에서 액티비티의 멤버 변수들을 null 로 assign 해주는 코드들과 View의 hierachy를 돌며 view들의 이벤트 리스너를 null로 세팅해주고, 뷰를 부모뷰로부터 remove 해주며, view가 ImageView 나 ImageButton 등 일 경우에는 해당 뷰가 가진 drawavble의 callback을 null로, drawable 자체도 null 로 해주는 등의 코드를 넣어서 시스템상의 결함 또는 코드상의 결함으로 인한 메모리 누수를 최소화 하는 방법이 있겠습니다.
도움이 되셨으면 좋겠군요~
사실, 좋은 코딩 습관은 위와 같은 행동들의 대부분을 할 필요가 없게 해줍니다... 진정 버그가 아니라면!! :)
미흡한 답변 죄송하네요 ㅠ