디벨로퍼 블로그에 있는 메모리 릭을 발생시키는 잘못된 코드의 예제다.

private static Drawable sBackground;

@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
  
TextView label = new TextView(this);
label.setText("Leaks are bad");
  
if (sBackground == null) {
sBackground = getDrawable(R.drawable.large_bitmap);
}
label.setBackgroundDrawable(sBackground);

setContentView(label);
}

Drawable 은 TextView 를 거쳐 Activity 의 Context 를 참조하게 되므로 액티비티의 첫 생성시 메모리 공간은 릭이 발생한다고 하는것 같은데...
비트맵도 마찬가지 아닌가... 어이쿠!! 구글아저씨들~~ 우리 저런 코드 엄청 많이 쓰거든요!! 잘못된 예제를 바로 잡는 예제도 주셔야죠!! 약올리는것도 아니고... 권장사항에 있는데로라면 new TextView(this) 대신 (getApplicationContext()) 를 하면 해결될거라고 하는것 같은데...
글쎄요.. 내경우는 문제가 좀 달라요.

난 Activity도 View 도 죽지 않는다. View 에 올리는 Bitmap 만 죽어라고 로테이션 할 뿐이다. 이경우 어떻게 해야 하는가...??
Context 를 참조하는 View 가 Bitmap 의 Callback 으로 등록되기 때문에 View 가 죽기전엔 Bitmap 이 죽지 않을까 그럼...??
이런것 때문에 Bitmap.recycle() 메서드가 있는것 아닌가? 왜 그럼 Callback 을 해제할수 없는가?? 왜 가비지 컬럭터는 이딴식으로 작동하나..??
이럴 바에야 malloc/free 하는게 더 좋지 않은가..??

제공되는 메소드의 특성상 Bitmap 은 static 이 될 수 밖에 없고.. 그런고로 recycle() 메소드가 제공되는것이겠지만...
작동이 되지 않는것은 분명 버그다.

실험1.
자 요렇게 자료형태를 구성했다.

1. Main 에서 View1, View2 로 참조시켜도 둘다 표현이 된다.
2. 셋중 어느 한군데서 recycle() 을 호출하면 모두 표현불가능해진다. ( invalidtate() 를 호출하기 전에는 표시되어있던건 그냥 있다 )
3. View 로 참조후 Main 에서 bitmap = null 을 줘도 View에서 사용하는데 아무 지장이 없다.
4. View 에서 참조후 한군데서 null 을 줘도 다른 View 나 메인에서는 아무 이상이 없다. 다른 View 로 다시 참조도 가능하다.
5. 두개의 이미지를 번갈아 수십 수백번 하나의 bitmap 참조에 할당해도 문제가 없다.

결론.
Bitmap 클래스는 비트맵의 메모리상의 위치와 크기, 밀도, 등의 각종 정모만 가질뿐.. 실제 Bitmap 의 내용을 변수로 갖지 않는다. 메모리상에서 참조해야할 위치만 가지고 있다고 생각된다. recycle 을 하면 메모리는 해제되지만 getWidth(), getDensity() 같은걸 해보면 정보는 다 그대로 가지고 있다. 메모리를 해제해도 invalidate()를 호출하기 전에 화면이 그대로 있으니 비디오 장치는 따로있다는 말이렸다.
메인에서는 View 로 생성한 Bitmap 을 넘긴후 즉시 null 처리해 버려도 전혀 문제가 되지 않는다.

실험2.
이번에는 조금 무식한 실험을 했다.
인터넷에서 배경화면류 (1024 * 768 ~ 1900 * 1200 다양함..) 정도의 이미지를 300개 받아서.. sd 카드에 넣고 하나의 이미지뷰에 로테이션으로 전부 보여주려고 시도했다.

힙 메모리는 기기마다 할당크기가 다 다르다고 한다. 누군가 나와 똑같은 실험을 진행한다면 다른 결과를 얻을 수 있다. 내폰은 넥서스다.

추가.
최초로 이 삽질을 했던것은 실패다. 잘못된 실험이다. 같은크기의 이미지로 했어야 했다. 그거 변경하는게 귀찮아서 그냥 했던 나의 불찰이다. 23번에서 자꾸 죽은 이유는 23번이미지가(24개째) 너무 큰 사이즈(4256 x 2848) 짜리여서 그랬다. 실험 내용은 수정했다.
실험 순서로는 네번째 실험이 된 셈이다. 3번째 실험에서 이미지 사이즈가 3200 x 3200 = 10240000  이상은 무사히 통과된다는 사실을 알았다.

1. 16개 표시 후 다이. ( 노말 )
bitmap = BitmapFactory.decodeFile(filepath);
img.setImageBitmap(bitmap);

2. 91개 표시 후 다이. 92번째 이미지가 살짝 크긴 한데(3330x2929) 동일사이즈 이미지를 테스트 했을때는 더 큰 크기도 통과했었다. ( gc() 호출 - 호출한다고 gc가 바로 수행되는건 아니다. gc 에 작업을 예약할 뿐.. )
bitmap = BitmapFactory.decodeFile(filepath);
img.setImageBitmap(bitmap);
System.gc();

3. 91개 표시후 다이. 92번째 이미지가 의심시러워 진다. ( 지역변수로 사용 )
Bitmap tmp;
tmp = BitmapFactory.decodeFile(filepath);
img.setImageBitmap(tmp);
tmp = null;
System.gc();

4. 1200개 무사히 표시. - 이 코드는 92 번째 이미지를 무사히 통과했다. ( recycle() 메소드만 호출 )
if(bitmap != null) { bitmap.recycle(); }
bitmap = BitmapFactory.decodeFile(filepath);
img.setImageBitmap(bitmap);

5. 1200개 무사히 표시.  ( recycle() 과 gc() 호출 )
if(bitmap != null) { bitmap.recycle(); bitmap = null; System.gc(); }
bitmap = BitmapFactory.decodeFile(filepath);
img.setImageBitmap(bitmap);

6. 그럼 지역변수를 선언한다해도 static 이니까 해제해 볼까..
Bitmap tmp;
tmp = BitmapFactory.decodeFile(filepath);
img.setImageBitmap(tmp);
tmp = null;
System.gc();

5. 23개 표시 후 다이. ( ImageView 자체를 지역변수로 동적 할당 )
if(stage.getChildCount() > 0){
for(int i=0; i<stage.getChildCount(); i++){
ImageView rtmp = (ImageView)stage.getChildAt(i);
rtmp = null;
}
stage.removeAllViews();
System.gc();
}
ImageView tmpImg = new ImageView(getApplicationContext());
tmpImg.setAdjustViewBounds(true);
tmpImg.setScaleType(ScaleType.CENTER_INSIDE);
bitmap = BitmapFactory.decodeFile(filepath);
tmpImg.setImageBitmap(bitmap);
stage.addView(tmpImg);

6. 23개 후 다이. ( bitmap 할당만 )
bitmap = BitmapFactory.decodeFile(filepath);
//img.setImageBitmap(bitmap);

6. 23개 후 다이. ( bitmap 을 static 으로 선언 )
private static Bitmap bitmap;
bitmap = BitmapFactory.decodeFile(filepath);
img.setImageBitmap(bitmap);

결론.
로그를 보면 거의 매번 gc가 호출되어 수행되는걸 볼수있다.(항상은 아니다. 이넘은 확실히 지 하고싶은데로 한다) 
if(bitmap != null) { bitmap.recycle(); bitmap = null; }  이정도면 메모리 릭은 충분히 잡아지는듯하다. gc도 일일이 예약할 필요는 없다. 수행속도만 느리게 할 뿐이다. 어차피 지 오고싶으면 오는 녀석이다.


힙 메모리 총량의 문제

하지만 문제는 다른곳에서 발생한다. 현제 개발중인 앱은 bitmap 을 화면에 계속 올린다. 결론부터 이야기하면 메모리 총량의 한계에 의한 문제가 발생한다. 화면에 올라간 bitmap 을 ArrayList 로 가지고 있으면서 View 에서 참조하고 사용자의 입력에 따라 작업을 하는데 이 과정에서 힙메모리 총량 부족으로 앱이 죽는현상이 자꾸 발생한다. 내가 겪었던 문제는 메모리 누수에 의한 문제가 아니었다. 총량의 문제였다.

기본적으로 메모리 총량에 의한 문제는..이런류의 루틴이 필요하다고 생각한다.

if (프리메모리 > 필요메모리){
할당/작업
}else{
경고/안쓰는것 해제..
}

하지만 프리메모리와 필요한 메모리의 양을 알아낼 방법이 없다. 내가 자바초보라서 모르는건지도 모르겠지만.. bitmap 의 구조를 분석해서 필요한 메모리를 계산해 낼수는 있을것이다. bitmap 구성방식이야 어차피 다 나와있는거니까...그런데 프리메모리를 알 수가 없다. 몇가지 코드를 찾아냈지만 다르다... 프리메모리가 한참 남았다는데 앱은 자꾸 죽는다. 실제로 올라가는것도 계산이 조금 이상하다.

문제는 힙 메모리 한계때문에 앱이 죽는현상을 막을 방법이 없다. 최소한 앱이 죽지는 말아야 할것아닌가.. 그런데 막을 방법이 없다.
try - catch 문으로도 잡히질 않는다. 이부분은 Exception 이 아니라 런타임 에러에 해당하기 때문이라나...
이런 사소한 부분에 대해 확실히 안드로이드의 부족함을 느낀다. 지금 머릿속에는 비트맵을 sd 카드에서 필요할때마다 한번씩만 로딩하는 루틴을 생각하고 있다. 물론 속도는 굉장히 느려지겠지... 정 안되면 ndk 로 가던지...

(혹시 해결법을 아시는분 계시면 알려주시기 바랍니다.)

안펍에서 한분은 비슷한 문제를 겪었는데 자기는 비트맵수의 제한을 두어서 죽는걸 방지한다고 하더라. 물론 그리 나쁜 생각은 아니지만 근본적은 해결책도 아니다. 장치마다 화면 사이즈가 다 다르니 혹시 갤탭에서 이용하는 유저가 있다면 비트맵을 화면 사이즈에 맞춰 로딩하더라도 상당한 메모리를 먹을 테고 넥서스에서 20개 올려지던 비트맵이 갤탭에선 5개도 못올리는 현상이 생길 수 있다. 그럼 limit 은 얼마를 줘야 하나...??
어림 짐작으로 해야하는 이런 주먹구구식 코딩은 하고 싶지가 않다...ㅠ.ㅠ 모르겠다. 정 안되면 할지도.. 어느 기기에서 죽는다는 피드백이 오면 limit 을 조금씩 낮춰가는... 그런 조잡한 짓을 하게 될지도 모르겠다. 하지만 앤디 루빈 아저씨....ㅜ.ㅜ 이건 아니 잖아요 !!!!!!

답을 못찾으니 그저 한숨만 나올 뿐이로군요..

+ Recent posts