java.lang.OutOfMemoryError : 비트 맵 크기가 VM 예산을 초과합니다-Android


159

Android에서 많은 이미지를 사용하는 응용 프로그램을 개발했습니다.

이 앱은 (화면에 정보를 채우고, 한 번 실행 Layouts, Listviews,Textviews , ImageViews, 등) 사용자는 정보를 읽습니다.

애니메이션, 특수 효과 또는 메모리를 채울 수있는 것은 없습니다. 때로는 드로어 블이 변경 될 수 있습니다. 일부는 안드로이드 리소스이고 일부는 SDCARD의 폴더에 저장된 파일입니다.

그런 다음 사용자가 종료합니다 ( onDestroy 메소드가 메소드를 실행하고 앱이 메모리에 남아 있음) 어느 시점에서 사용자가 다시 입력합니다.

사용자가 앱에 들어갈 때마다 사용자가 앱을 얻을 때까지 메모리가 점점 커지는 것을 볼 수 있습니다. java.lang.OutOfMemoryError .

그렇다면 많은 이미지를 처리하는 가장 좋은 방법은 무엇입니까?

항상로드되지 않도록 정적 메소드에 넣어야합니까? 특별한 방법으로 레이아웃이나 레이아웃에 사용 된 이미지를 청소해야합니까?


4
드로어 블을 많이 변경하는 경우 도움이 될 수 있습니다. 그것은 프로그램을 직접 한 이후로 작동합니다 :) androidactivity.wordpress.com/2011/09/24/…

답변:


72

메모리 누수가있는 것 같습니다. 문제가 많은 이미지를 처리하지 않고, 활동이 파괴 될 때 이미지가 할당 해제되지 않는 것입니다.

이것이 코드를 보지 않고 왜 그런지 말하기는 어렵습니다. 그러나이 기사에는 다음과 같은 몇 가지 팁이 있습니다.

http://android-developers.blogspot.de/2009/01/avoiding-memory-leaks.html

특히 정적 변수를 사용하면 상황이 나빠질 수 있습니다. 애플리케이션이 다시 그려 질 때 콜백을 제거하는 코드를 추가해야 할 수도 있지만, 여기에는 확실히 말할 정보가 충분하지 않습니다.


8
이것은 사실이며 많은 이미지 (메모리 누수가 아님)가 있으면 많은 다른 앱이 실행 중이거나 메모리 조각화 등과 같은 여러 가지 이유로 outOfMemory가 발생할 수 있습니다. outOfMemory를 얻는 경우 많은 이미지로 작업 할 때 배웠습니다. 체계적으로, 항상 똑같이하고 누수를합니다. 하루에 한 번 또는 무언가를 얻는다면 한계에 너무 가깝기 때문입니다. 이 경우 내 제안은 물건을 제거하고 다시 시도하는 것입니다. 또한 이미지 메모리가 힙 메모리 외부에 있으므로 두 가지를 모두 찾으십시오.
Daniel Benedykt

15
메모리 누수가 의심되는 경우 힙 사용량을 모니터하는 빠른 방법은 adb shell dumpsys meminfo 명령을 사용하는 것입니다. 몇 gc주기 (logcat의 GC_ * 로그 행)에 걸쳐 힙 사용량이 증가하는 경우 누출이있을 수 있습니다. 그런 다음 adb 또는 DDMS를 통해 힙 덤프 (또는 다른 시간에 여러 개)를 작성하고 Eclipse MAT의 도미네이터 트리 도구를 통해이를 분석하십시오. 시간이 지남에 따라 증가하는 보유 힙이있는 오브젝트를 곧 찾아야합니다. 그것은 당신의 메모리 누수입니다. 나는 여기에 좀 더 세부 기사를 썼다 : macgyverdev.blogspot.com/2011/11/...
요한 노렌

98

Android 앱을 개발할 때 발견 한 가장 일반적인 오류 중 하나는 "java.lang.OutOfMemoryError : Bitmap Size Exceeds VM Budget"오류입니다. 방향을 변경 한 후 많은 비트 맵을 사용하는 활동 에서이 오류가 자주 발견되었습니다. 활동이 파괴되고 다시 생성되고 XML에서 비트 맵에 사용할 수있는 VM 메모리를 사용하여 레이아웃이 "팽창"됩니다.

이전 활동 레이아웃의 비트 맵은 활동에 대한 참조를 교차했기 때문에 가비지 수집기에 의해 올바르게 할당 해제되지 않습니다. 많은 실험을 한 후이 문제에 대한 훌륭한 해결책을 찾았습니다.

먼저 XML 레이아웃의 상위 뷰에서 "id"속성을 설정하십시오.

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="fill_parent"
     android:layout_height="fill_parent"
     android:id="@+id/RootView"
     >
     ...

그런 다음 onDestroy() 활동의 unbindDrawables()메소드에서 상위보기에 대한 참조를 전달하는 메소드를 호출 한 다음을 수행하십시오 System.gc().

    @Override
    protected void onDestroy() {
    super.onDestroy();

    unbindDrawables(findViewById(R.id.RootView));
    System.gc();
    }

    private void unbindDrawables(View view) {
        if (view.getBackground() != null) {
        view.getBackground().setCallback(null);
        }
        if (view instanceof ViewGroup) {
            for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
            unbindDrawables(((ViewGroup) view).getChildAt(i));
            }
        ((ViewGroup) view).removeAllViews();
        }
    }

unbindDrawables()방법은 뷰 트리를 재귀 적으로 탐색하고 다음을 수행합니다.

  1. 모든 백그라운드 드로어 블에서 콜백을 제거합니다
  2. 모든 뷰 그룹에서 자식을 제거합니다

10
예외 ... java.lang.UnsupportedOperationException : removeAllViews ()는 AdapterView에서 지원되지 않습니다

고마워 이것은 많은 드로어 블이 있기 때문에 힙 크기를 크게 줄이는 데 도움이됩니다 ... @GJTorikian ... removeAllViews ()에서 catch를 시도하면 대부분의 ViewGroup 서브 클래스가 제대로 작동합니다.
Eric Chen

5
아니면 그냥 조건을 변경하는 경우 (&& 뷰 그룹 instanceof를보기 (어댑터 뷰 AdapterView instanceof를보기)!)
의 Anke

2
@Adam Varhegyi의 onDestroy에서 갤러리보기에 대해 동일한 함수를 명시 적으로 호출 할 수 있습니다. unbindDrawables (galleryView)와 같습니다. 이것이 당신을 위해 작동하기를 바랍니다 ...
hp.android

1
이 두 가지
gotcha

11

이 문제를 방지하려면 기본 방법을 사용할 수 있습니다 Bitmap.recycle()전에 null-ing Bitmap개체 (또는 다른 설정 값을). 예:

public final void setMyBitmap(Bitmap bitmap) {
  if (this.myBitmap != null) {
    this.myBitmap.recycle();
  }
  this.myBitmap = bitmap;
}

그리고 다음 과 같이 myBitmap전화하지 않고 변경할 수 있습니다 System.gc().

setMyBitmap(null);    
setMyBitmap(anotherBitmap);

1
해당 요소를 목록보기에 추가하려고하면 작동하지 않습니다. 그것에 대한 제안이 있습니까?
Rafael Sanches

@ddmytrenko 이미지를 할당하기 전에 이미지를 재활용하고 있습니다. 픽셀 렌더링을 방해하지 않습니까?
IgorGanapolsky

8

나는이 정확한 문제에 부딪쳤다. 힙은 매우 작기 때문에 이러한 이미지는 메모리와 관련하여 오히려 빠르게 제어 할 수 없습니다. 한 가지 방법은 가비지 수집기에게 recycle 메서드 를 호출하여 비트 맵에서 메모리를 수집하는 힌트를 제공하는 입니다.

또한 onDestroy 메소드가 호출되지는 않습니다. 이 로직 / 정리를 onPause 활동으로 옮기고 싶을 수도 있습니다. 자세한 정보 는이 페이지 의 활동 라이프 사이클 다이어그램 / 표 확인하십시오 .


정보 주셔서 감사합니다. 비트 맵이 아닌 모든 Drawables를 관리하고 있으므로 Recycle을 호출 할 수 없습니다. 드로어 블에 대한 다른 제안이 있습니까? 감사합니다 Daniel
Daniel Benedykt

onPause제안 해 주셔서 감사합니다 . Bitmap이미지가 모두 포함 된 4 개의 탭을 사용 하고 있으므로 활동을 파괴하는 것이 너무 빨리 일어나지 않을 수 있습니다 (사용자가 모든 탭을 탐색하는 경우).
Azurespot

7

이 설명이 도움이 될 수 있습니다. http://code.google.com/p/android/issues/detail?id=8488#c80

"빠른 팁 :

1) 절대 System.gc ()를 직접 호출하지 마십시오. 이것은 여기서 수정으로 전파되었으며 작동하지 않습니다. 하지마. 내 설명에서 OutOfMemoryError가 발생하기 전에 JVM이 이미 가비지 콜렉션을 실행하므로 다시 수행 할 이유가 없습니다 (프로그램 속도가 느려짐). 활동이 끝날 때 하나를 수행하면 문제가 해결됩니다. 이로 인해 비트 맵이 파이널 라이저 큐에 더 빨리 배치 될 수 있지만 각 비트 맵에서 단순히 recycle을 호출 할 수 없었습니다.

2) 더 이상 필요하지 않은 비트 맵에서는 항상 recycle ()을 호출하십시오. 최소한 활동의 ​​onDestroy에서 사용중인 모든 비트 맵을 살펴보고 재활용하십시오. 또한 비트 맵 인스턴스를 dalvik 힙에서 더 빨리 수집하려는 경우 비트 맵에 대한 참조를 지우는 것이 아프지 않습니다.

3) recycle ()을 호출 한 다음 System.gc ()는 여전히 Dalvik 힙에서 비트 맵을 제거하지 못할 수 있습니다. 이것에 대해 걱정하지 마십시오. recycle ()이 작업을 수행하고 기본 메모리를 해제했습니다. Dalvik 힙에서 실제로 비트 맵을 제거하려면 앞에서 설명한 단계를 수행하는 데 약간의 시간이 걸립니다. 네이티브 메모리의 큰 덩어리가 이미 비어 있기 때문에 이것은 큰 문제가 아닙니다!

4) 항상 프레임 워크에 버그가 있다고 가정하십시오. Dalvik은 정확히해야 할 일을하고 있습니다. 그것은 당신이 기대하거나 원하는 것이 아니라 작동 방식입니다. "


5

나는 똑같은 문제가 있었다. 몇 가지 테스트 후 큰 이미지 스케일링 에서이 오류가 나타나는 것으로 나타났습니다. 이미지 스케일링을 줄이고 문제가 사라졌습니다.

PS 처음에는 이미지를 축소하지 않고 이미지 크기를 줄이려고했습니다. 그것은 오류를 멈추지 않았습니다.


4
이미지 스케일링을 수행 한 방법에 대한 코드를 게시 할 수 있습니까? 동일한 문제에 직면하고 있으며 이것이 해결할 수 있다고 생각합니다. 감사!
Gligor

@Gix-그는 이미지를 프로젝트 리소스로 가져 오기 전에 이미지 크기를 줄여서 드로어 블의 메모리 공간을 줄여야한다는 것을 의미합니다. 이를 위해 선택한 사진 편집기를 사용해야합니다. pixlr.com을 좋아합니다
Kyle Clegg

5

다음 요점은 정말 많은 도움이되었습니다. 다른 점들도있을 수 있지만, 이것들은 매우 중요합니다 :

  1. 가능하면 활동 대신에 응용 프로그램 컨텍스트를 사용하십시오.
  2. onPause () 활동 방식에서 스레드 중지 및 해제
  3. onDestroy () 활동 방식으로 뷰 / 콜백을 해제하십시오.

4

이 문제를 해결하는 편리한 방법을 제안합니다. 오류가 발생한 활동에 대해 Mainfest.xml에 따라 "android : configChanges"속성 값을 지정하십시오. 이처럼 :

<activity android:name=".main.MainActivity"
              android:label="mainActivity"
              android:configChanges="orientation|keyboardHidden|navigation">
</activity>

내가 준 첫 번째 솔루션은 실제로 OOM 오류의 빈도를 낮은 수준으로 줄였습니다. 그러나 문제를 완전히 해결하지 못했습니다. 그런 다음 두 번째 해결책을 알려 드리겠습니다.

OOM이 자세히 설명했듯이 런타임 메모리를 너무 많이 사용했습니다. 따라서 프로젝트의 ~ / res / drawable에서 그림 크기를 줄입니다. 128X128의 해상도를 가진 과잉 사진과 같은, 내 응용 프로그램에 적합 64x64로 크기를 조정할 수 있습니다. 그리고 사진 더미로 그렇게 한 후에는 OOM 오류가 다시 발생하지 않습니다.


1
앱을 다시 시작하지 않기 때문에 작동합니다. 따라서 회피하는 것만 큼 해결책이 아닙니다. 그러나 때때로 그것은 당신이 필요한 전부입니다. 그리고 Activity의 기본 onConfigurationChanged () 메소드가 방향을 바꾸므로 다른 방향에 실제로 적응하기 위해 UI 변경이 필요하지 않으면 결과에 완전히 만족할 수 있습니다. 그래도 다시 시작할 수있는 다른 것들에주의하십시오. android : configChanges = "keyboardHidden | orientation | keyboard | locale | mcc | mnc | touchscreen | screenLayout | fontScale"
Carl

3

나도 메모리 부족 버그에 좌절하고 있습니다. 그리고 네, 이미지 스케일링 시이 오류가 많이 발생한다는 것을 알았습니다. 처음에는 모든 밀도에 대한 이미지 크기를 만들려고 시도했지만 이것이 앱의 크기가 크게 증가한 것을 발견했습니다. 이제 모든 밀도에 대해 하나의 이미지를 사용하고 이미지 크기를 조정하고 있습니다.

사용자가 한 활동에서 다른 활동으로 이동할 때마다 내 응용 프로그램에서 메모리 부족 오류가 발생합니다. 드로어 블을 null로 설정하고 System.gc () 호출이 작동하지 않았으며 getBitMap (). recycle ()을 사용하여 bitmapDrawables를 재활용하지 않았습니다. Android는 첫 번째 방법으로 메모리 부족 오류를 계속 발생시키고 두 번째 방법으로 재활용 된 비트 맵을 사용하려고 할 때마다 캔버스 오류 메시지를 발생시킵니다.

나는 세 번째 접근 방식을 취했습니다. 모든보기를 null로 설정하고 배경을 검은 색으로 설정했습니다. 나는 onStop () 메소드 에서이 정리를 수행합니다. 활동이 더 이상 표시되지 않는 즉시 호출되는 메소드입니다. onPause () 메서드에서이 작업을 수행하면 검정색 배경이 나타납니다. 이상적이지 않습니다. onDestroy () 메소드에서 수행하는 것에 대해서는 호출 될 것이라는 보장이 없습니다.

사용자가 장치에서 뒤로 버튼을 눌렀을 때 검은 화면이 발생하지 않도록 startActivity (getIntent ()) 및 finish () 메서드를 호출하여 onRestart () 메서드에서 활동을 다시로드합니다.

참고 : 배경을 검은 색으로 변경할 필요는 없습니다.


1

디스크 또는 네트워크 위치 (또는 실제로 메모리 이외의 다른 소스)에서 소스 데이터를 읽는 경우 기본 큰 비트 맵로드 레슨 에서 설명한 BitmapFactory.decode * 메소드를 기본 UI 스레드에서 실행하면 안됩니다. 이 데이터를로드하는 데 걸리는 시간은 예측할 수 없으며 다양한 요소 (디스크 또는 네트워크에서 읽기 속도, 이미지 크기, CPU 성능 등)에 따라 다릅니다. 이러한 작업 중 하나가 UI 스레드를 차단하면 시스템은 응용 프로그램에 응답하지 않는 것으로 표시하고 사용자는 해당 응용 프로그램을 닫을 수 있습니다 (자세한 내용은 응답을위한 디자인 참조).


0

나는 인터넷에서 찾은 모든 것을 시도했지만 그중 아무것도 작동하지 않았습니다. System.gc ()를 호출하면 앱 속도 만 드래그됩니다. onDestroy에서 비트 맵을 재활용해도 나에게 도움이되지 않았습니다.

이제 작동하는 유일한 방법은 모든 비트 맵의 ​​정적 목록을 작성하여 다시 시작한 후에 비트 맵이 유지되도록하는 것입니다. 그리고 활동을 다시 시작할 때마다 새로운 비트 맵을 생성하는 대신 저장된 비트 맵을 사용하십시오.

필자의 경우 코드는 다음과 같습니다.

private static BitmapDrawable currentBGDrawable;

if (new File(uriString).exists()) {
    if (!uriString.equals(currentBGUri)) {
        freeBackground();
        bg = BitmapFactory.decodeFile(uriString);

        currentBGUri = uriString;
        bgDrawable = new BitmapDrawable(bg);
        currentBGDrawable = bgDrawable;
    } else {
        bgDrawable = currentBGDrawable;
    }
}

이것이 당신의 컨텍스트를 유출시키지 않습니까? (활동 데이터)
Rafael Sanches

0

합리적인 크기의 배경 이미지를 전환하는 것과 동일한 문제가있었습니다. 새 그림을 만들기 전에 ImageView를 null로 설정하면 더 나은 결과를 얻었습니다.

ImageView ivBg = (ImageView) findViewById(R.id.main_backgroundImage);
ivBg.setImageDrawable(null);
ivBg.setImageDrawable(getResources().getDrawable(R.drawable.new_picture));

0

FWIW, 여기 내가 코딩하고 몇 달 동안 사용한 가벼운 비트 맵 캐시가 있습니다. 그것은 종소리가 아니라 코드를 사용하기 전에 코드를 읽으십시오.

/**
 * Lightweight cache for Bitmap objects. 
 * 
 * There is no thread-safety built into this class. 
 * 
 * Note: you may wish to create bitmaps using the application-context, rather than the activity-context. 
 * I believe the activity-context has a reference to the Activity object. 
 * So for as long as the bitmap exists, it will have an indirect link to the activity, 
 * and prevent the garbaage collector from disposing the activity object, leading to memory leaks. 
 */
public class BitmapCache { 

    private Hashtable<String,ArrayList<Bitmap>> hashtable = new Hashtable<String, ArrayList<Bitmap>>();  

    private StringBuilder sb = new StringBuilder(); 

    public BitmapCache() { 
    } 

    /**
     * A Bitmap with the given width and height will be returned. 
     * It is removed from the cache. 
     * 
     * An attempt is made to return the correct config, but for unusual configs (as at 30may13) this might not happen.  
     * 
     * Note that thread-safety is the caller's responsibility. 
     */
    public Bitmap get(int width, int height, Bitmap.Config config) { 
        String key = getKey(width, height, config); 
        ArrayList<Bitmap> list = getList(key); 
        int listSize = list.size();
        if (listSize>0) { 
            return list.remove(listSize-1); 
        } else { 
            try { 
                return Bitmap.createBitmap(width, height, config);
            } catch (RuntimeException e) { 
                // TODO: Test appendHockeyApp() works. 
                App.appendHockeyApp("BitmapCache has "+hashtable.size()+":"+listSize+" request "+width+"x"+height); 
                throw e ; 
            }
        }
    }

    /**
     * Puts a Bitmap object into the cache. 
     * 
     * Note that thread-safety is the caller's responsibility. 
     */
    public void put(Bitmap bitmap) { 
        if (bitmap==null) return ; 
        String key = getKey(bitmap); 
        ArrayList<Bitmap> list = getList(key); 
        list.add(bitmap); 
    }

    private ArrayList<Bitmap> getList(String key) {
        ArrayList<Bitmap> list = hashtable.get(key);
        if (list==null) { 
            list = new ArrayList<Bitmap>(); 
            hashtable.put(key, list); 
        }
        return list;
    } 

    private String getKey(Bitmap bitmap) {
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        Config config = bitmap.getConfig();
        return getKey(width, height, config);
    }

    private String getKey(int width, int height, Config config) {
        sb.setLength(0); 
        sb.append(width); 
        sb.append("x"); 
        sb.append(height); 
        sb.append(" "); 
        switch (config) {
        case ALPHA_8:
            sb.append("ALPHA_8"); 
            break;
        case ARGB_4444:
            sb.append("ARGB_4444"); 
            break;
        case ARGB_8888:
            sb.append("ARGB_8888"); 
            break;
        case RGB_565:
            sb.append("RGB_565"); 
            break;
        default:
            sb.append("unknown"); 
            break; 
        }
        return sb.toString();
    }

}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.