android - 최적화 - 안드로이드 큰 이미지 로딩




Bitmap 객체에 이미지를로드하는 중 이상한 메모리 부족 문제 (20)

각 행에 두 개의 이미지 버튼이있는 목록보기가 있습니다. 목록 행을 클릭하면 새 활동이 실행됩니다. 카메라 레이아웃과 관련된 문제 때문에 자체 탭을 만들어야했습니다. 결과에 대해 실행되는 활동은지도입니다. 버튼을 클릭하여 이미지 미리보기를 시작하면 (SD 카드에서 이미지로드) 애플리케이션이 활동에서 listview 활동으로 결과 핸들러로 돌아가서 이미지 위젯 이상의 새로운 활동을 다시 시작합니다.

목록 뷰의 이미지 미리보기는 커서 및 ListAdapter 사용하여 수행됩니다. 이것은 매우 간단하지만 크기 조정 된 이미지를 어떻게 넣을 수 있는지 잘 모르겠습니다 (즉, 작은 픽셀 크기가 픽셀이 아니라 이미지 버튼의 픽셀로 사용됩니다.) 그래서 저는 전화 카메라에서 가져온 이미지의 크기를 조정했습니다.

문제는 되돌아 가서 두 번째 활동을 다시 시작하려고 할 때 메모리 부족 오류가 발생한다는 것입니다.

  • 필자는 행별로 목록 어댑터를 쉽게 만들 수있는 방법이 있습니까? 즉, 즉석에서 크기를 조정할 수 있습니까?

포커스 문제로 인해 터치 스크린이있는 행을 선택할 수 없기 때문에 각 행의 위젯 / 요소 속성을 일부 변경해야하므로이 방법이 바람직합니다. ( 롤러 볼을 사용할 수 있습니다. )

  • 대역 밖의 크기 조정 및 이미지 저장을 할 수 있다는 것을 알고 있습니다. 그러나 그 일은 실제로하고 싶은 것이 아닙니다. 그러나이를위한 샘플 코드가 좋을 것입니다.

목록보기에서 이미지를 비활성화하자마자 다시 정상적으로 작동했습니다.

참고로 : 이것은 내가 그것을하고있는 방법이다 :

String[] from = new String[] { DBHelper.KEY_BUSINESSNAME,DBHelper.KEY_ADDRESS,DBHelper.KEY_CITY,DBHelper.KEY_GPSLONG,DBHelper.KEY_GPSLAT,DBHelper.KEY_IMAGEFILENAME  + ""};
int[] to = new int[] {R.id.businessname,R.id.address,R.id.city,R.id.gpslong,R.id.gpslat,R.id.imagefilename };
notes = new SimpleCursorAdapter(this, R.layout.notes_row, c, from, to);
setListAdapter(notes);

R.id.imagefilenameButtonImage 입니다.

다음은 LogCat입니다.

01-25 05:05:49.877: ERROR/dalvikvm-heap(3896): 6291456-byte external allocation too large for this process.
01-25 05:05:49.877: ERROR/(3896): VM wont let us allocate 6291456 bytes
01-25 05:05:49.877: ERROR/AndroidRuntime(3896): Uncaught handler: thread main exiting due to uncaught exception
01-25 05:05:49.917: ERROR/AndroidRuntime(3896): java.lang.OutOfMemoryError: bitmap size exceeds VM budget
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.BitmapFactory.nativeDecodeStream(Native Method)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:304)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.BitmapFactory.decodeFile(BitmapFactory.java:149)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.BitmapFactory.decodeFile(BitmapFactory.java:174)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.drawable.Drawable.createFromPath(Drawable.java:729)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ImageView.resolveUri(ImageView.java:484)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ImageView.setImageURI(ImageView.java:281)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.SimpleCursorAdapter.setViewImage(SimpleCursorAdapter.java:183)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.SimpleCursorAdapter.bindView(SimpleCursorAdapter.java:129)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.CursorAdapter.getView(CursorAdapter.java:150)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.AbsListView.obtainView(AbsListView.java:1057)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ListView.makeAndAddView(ListView.java:1616)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ListView.fillSpecific(ListView.java:1177)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ListView.layoutChildren(ListView.java:1454)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.AbsListView.onLayout(AbsListView.java:937)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1119)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.layoutHorizontal(LinearLayout.java:1108)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.onLayout(LinearLayout.java:922)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.FrameLayout.onLayout(FrameLayout.java:294)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1119)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.layoutVertical(LinearLayout.java:999)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.onLayout(LinearLayout.java:920)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.FrameLayout.onLayout(FrameLayout.java:294)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.ViewRoot.performTraversals(ViewRoot.java:771)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.ViewRoot.handleMessage(ViewRoot.java:1103)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.os.Handler.dispatchMessage(Handler.java:88)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.os.Looper.loop(Looper.java:123)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.app.ActivityThread.main(ActivityThread.java:3742)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at java.lang.reflect.Method.invokeNative(Native Method)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at java.lang.reflect.Method.invoke(Method.java:515)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:739)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:497)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at dalvik.system.NativeStart.main(Native Method)
01-25 05:10:01.127: ERROR/AndroidRuntime(3943): ERROR: thread attach failed 

이미지를 표시 할 때 새로운 오류가 있습니다.

01-25 22:13:18.594: DEBUG/skia(4204): xxxxxxxxxxx jpeg error 20 Improper call to JPEG library in state %d
01-25 22:13:18.604: INFO/System.out(4204): resolveUri failed on bad bitmap uri: 
01-25 22:13:18.694: ERROR/dalvikvm-heap(4204): 6291456-byte external allocation too large for this process.
01-25 22:13:18.694: ERROR/(4204): VM won't let us allocate 6291456 bytes
01-25 22:13:18.694: DEBUG/skia(4204): xxxxxxxxxxxxxxxxxxxx allocPixelRef failed

BitmapFactory.decodeStream 또는 decodeFile 함수를 사용하지 않고 동일한 문제를 해결하고 대신 BitmapFactory.decodeFileDescriptor 를 사용 BitmapFactory.decodeFileDescriptor

decodeFileDescriptor 는 decodeStream / decodeFile과 다른 원시 메소드를 호출하는 것처럼 보입니다.

어쨌든, 이것이 효과적이었습니다 (일부는 위에 언급했듯이 몇 가지 옵션을 추가했지만, 차이점을 만들지는 않았습니다. 중요한 것은 decodeStream 또는 decodeFile 대신 BitmapFactory.decodeFileDescriptor 를 호출하는 것입니다).

private void showImage(String path)   {
    Log.i("showImage","loading:"+path);
    BitmapFactory.Options bfOptions=new BitmapFactory.Options();
    bfOptions.inDither=false;                     //Disable Dithering mode
    bfOptions.inPurgeable=true;                   //Tell to gc that whether it needs free memory, the Bitmap can be cleared
    bfOptions.inInputShareable=true;              //Which kind of reference will be used to recover the Bitmap data after being clear, when it will be used in the future
    bfOptions.inTempStorage=new byte[32 * 1024]; 


    File file=new File(path);
    FileInputStream fs=null;
    try {
        fs = new FileInputStream(file);
    } catch (FileNotFoundException e) {
        //TODO do something intelligent
        e.printStackTrace();
    }

    try {
        if(fs!=null) bm=BitmapFactory.decodeFileDescriptor(fs.getFD(), null, bfOptions);
    } catch (IOException e) {
        //TODO do something intelligent
        e.printStackTrace();
    } finally{ 
        if(fs!=null) {
            try {
                fs.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
    //bm=BitmapFactory.decodeFile(path, bfOptions); This one causes error: java.lang.OutOfMemoryError: bitmap size exceeds VM budget

    im.setImageBitmap(bm);
    //bm.recycle();
    bm=null;



}

decodeStream / decodeFile에서 사용 된 네이티브 함수에 문제가 있다고 생각합니다. decodeFileDescriptor를 사용할 때 다른 원시 메소드가 호출되었음을 확인했습니다. 또한 내가 읽은 것은 "이미지 (비트 맵)는 표준 Java 방식으로 할당되지 않고 기본 호출을 통해 할당되며, 할당은 가상 힙 외부에서 이루어 지지만 계산됩니다. "


OutOfMemory 오류를 수정하려면 다음과 같이해야합니다.

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 8;
Bitmap preview_bitmap = BitmapFactory.decodeStream(is, null, options);

inSampleSize 옵션은 메모리 소비를 줄입니다.

여기에 완벽한 방법이 있습니다. 먼저 콘텐츠 자체를 디코딩하지 않고 이미지 크기를 읽습니다. 그런 다음 가장 좋은 inSampleSize 값을 찾고 2의 거듭 제곱이어야하며 마지막으로 이미지가 디코딩됩니다.

// Decodes image and scales it to reduce memory consumption
private Bitmap decodeFile(File f) {
    try {
        // Decode image size
        BitmapFactory.Options o = new BitmapFactory.Options();
        o.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(new FileInputStream(f), null, o);

        // The new size we want to scale to
        final int REQUIRED_SIZE=70;

        // Find the correct scale value. It should be the power of 2.
        int scale = 1;
        while(o.outWidth / scale / 2 >= REQUIRED_SIZE && 
              o.outHeight / scale / 2 >= REQUIRED_SIZE) {
            scale *= 2;
        }

        // Decode with inSampleSize
        BitmapFactory.Options o2 = new BitmapFactory.Options();
        o2.inSampleSize = scale;
        return BitmapFactory.decodeStream(new FileInputStream(f), null, o2);
    } catch (FileNotFoundException e) {}
    return null;
}

나는 Fedor의 코드를 조금 개선했다. 그것은 기본적으로 동일하지만, (제 의견으로는) 추악한 while 회 돌이없이 항상 2의 거듭 제로가됩니다. 원래 솔루션을 만들기위한 Fedor에게 명성을 얻으 려하면서, 나는 그의 것을 발견 할 때까지 붙어 있었고, 다음으로 이것을 만들 수있었습니다 :)

 private Bitmap decodeFile(File f){
    Bitmap b = null;

        //Decode image size
    BitmapFactory.Options o = new BitmapFactory.Options();
    o.inJustDecodeBounds = true;

    FileInputStream fis = new FileInputStream(f);
    BitmapFactory.decodeStream(fis, null, o);
    fis.close();

    int scale = 1;
    if (o.outHeight > IMAGE_MAX_SIZE || o.outWidth > IMAGE_MAX_SIZE) {
        scale = (int)Math.pow(2, (int) Math.ceil(Math.log(IMAGE_MAX_SIZE / 
           (double) Math.max(o.outHeight, o.outWidth)) / Math.log(0.5)));
    }

    //Decode with inSampleSize
    BitmapFactory.Options o2 = new BitmapFactory.Options();
    o2.inSampleSize = scale;
    fis = new FileInputStream(f);
    b = BitmapFactory.decodeStream(fis, null, o2);
    fis.close();

    return b;
}

나는 iOS 경험에서 왔고 이미지를로드하고 표시하는 것과 같은 기본적인 문제를 발견하기를 좌절 시켰습니다. 결국이 문제를 겪고있는 모든 사람이 합리적인 크기의 이미지를 표시하려고합니다. 어쨌든, 내 문제를 해결 한 두 가지 변경 사항은 다음과 같습니다 (내 응용 프로그램의 반응이 좋았습니다).

1) BitmapFactory.decodeXYZ() 를 수행 할 때마다 inPurgeabletrue 설정하여 BitmapFactory.Options 를 전달해야 true (물론 inInputShareabletrue 설정하는 것이 바람직 함).

2) Bitmap.createBitmap(width, height, Config.ARGB_8888) )을 절대로 사용하지 마십시오. 나는 결코 의미하지 않는다! 몇 번 지나면 메모리 오류가 발생하지 않습니다. recycle() , System.gc() , 아무리 많은 도움이 System.gc() 상관 없습니다. 항상 예외가 발생했습니다. 실제로 작동하는 또 다른 방법은 drawables (또는 위의 1 단계를 사용하여 디코딩 한 다른 Bitmap)에 더미 이미지를 넣고, 원하는대로 다시 스케일링 한 다음 결과 Bitmap을 조작하는 것입니다 (예 : Canvas 더 많은 재미를 위해). 그래서, 대신 사용해야 할 것은 : Bitmap.createScaledBitmap(srcBitmap, width, height, false) . 어떤 이유로 든 무차별 대입 (brute force) 생성 메소드를 사용해야한다면, 적어도 Config.ARGB_4444 전달 Config.ARGB_4444 .

며칠이 아니라면 거의 시간을 절약 할 수 있습니다. 이미지 크기를 조정하는 것에 대해 이야기하는 모든 것들은 실제로 작동하지 않습니다 (잘못된 크기 나 해상도가 떨어지는 이미지를 고려하지 않는 한).


최근에 OOM 예외 및 캐싱에 관해 많은 질문을 보았습니다. 개발자 가이드는 이것에 관한 정말 좋은 기사 를 가지고 있지만 일부는 적절한 방식으로 구현하는 데 실패하는 경향이 있습니다.

이 때문에 나는 안드로이드 환경에서 캐싱을 보여주는 예제 애플리케이션을 작성했다. 이 구현은 아직 OOM을 얻지 못했습니다.

소스 코드에 대한 링크를 보려면이 대답의 끝 부분을보십시오.

요구 사항 :

  • Android API 2.1 이상 (API 1.6에서 응용 프로그램의 사용 가능한 메모리를 가져올 수 없었습니다. 이는 API 1.6에서 작동하지 않는 유일한 코드입니다)
  • Android 지원 패키지

풍모:

  • 싱글 톤을 사용하여 방향 변경이있을 경우 캐시를 유지합니다.
  • 할당 된 응용 프로그램 메모리의 1/8 을 캐시에 사용하십시오 (원하는 경우 수정하십시오)
  • 큰 비트 맵 은 크기가 조정됩니다 (허용 할 최대 픽셀을 정의 할 수 있음).
  • 비트 맵을 다운로드하기 전에 인터넷 연결이 가능한지 제어 합니다.
  • 행당 하나의 작업 만 인스턴스화하는지 확인합니다.
  • ListView 멀리 떨어 뜨리고 있다면 단순히 비트 맵을 다운로드하지 않습니다.

여기에는 다음 내용이 포함되지 않습니다.

  • 디스크 캐싱. 어쨌든 쉽게 구현할 수 있어야합니다. 디스크에서 비트 맵을 가져 오는 다른 작업을 가리 키기 만하면됩니다.

샘플 코드 :

다운로드중인 이미지는 Flickr의 이미지 (75x75)입니다. 그러나 처리 할 이미지 URL을 입력하십시오. 응용 프로그램은 최대 값을 초과하면 크기를 줄입니다. 이 응용 프로그램에서 url은 String 배열에 있습니다.

LruCache 는 비트 맵을 처리하는 좋은 방법이 있습니다. 그러나이 응용 프로그램에서는 LruCache 의 인스턴스를 응용 프로그램을보다 쉽게 ​​구현할 수 있도록 다른 캐시 클래스에 LruCache 습니다.

Cache.java의 중요한 요소 ( loadBitmap() 메소드가 가장 중요 함) :

public Cache(int size, int maxWidth, int maxHeight) {
    // Into the constructor you add the maximum pixels
    // that you want to allow in order to not scale images.
    mMaxWidth = maxWidth;
    mMaxHeight = maxHeight;

    mBitmapCache = new LruCache<String, Bitmap>(size) {
        protected int sizeOf(String key, Bitmap b) {
            // Assuming that one pixel contains four bytes.
            return b.getHeight() * b.getWidth() * 4;
        }
    };

    mCurrentTasks = new ArrayList<String>();    
}

/**
 * Gets a bitmap from cache. 
 * If it is not in cache, this method will:
 * 
 * 1: check if the bitmap url is currently being processed in the
 * BitmapLoaderTask and cancel if it is already in a task (a control to see
 * if it's inside the currentTasks list).
 * 
 * 2: check if an internet connection is available and continue if so.
 * 
 * 3: download the bitmap, scale the bitmap if necessary and put it into
 * the memory cache.
 * 
 * 4: Remove the bitmap url from the currentTasks list.
 * 
 * 5: Notify the ListAdapter.
 * 
 * @param mainActivity - Reference to activity object, in order to
 * call notifyDataSetChanged() on the ListAdapter.
 * @param imageKey - The bitmap url (will be the key).
 * @param imageView - The ImageView that should get an
 * available bitmap or a placeholder image.
 * @param isScrolling - If set to true, we skip executing more tasks since
 * the user probably has flinged away the view.
 */
public void loadBitmap(MainActivity mainActivity, 
        String imageKey, ImageView imageView,
        boolean isScrolling) {
    final Bitmap bitmap = getBitmapFromCache(imageKey); 

    if (bitmap != null) {
        imageView.setImageBitmap(bitmap);
    } else {
        imageView.setImageResource(R.drawable.ic_launcher);
        if (!isScrolling && !mCurrentTasks.contains(imageKey) && 
                mainActivity.internetIsAvailable()) {
            BitmapLoaderTask task = new BitmapLoaderTask(imageKey,
                    mainActivity.getAdapter());
            task.execute();
        }
    } 
}

디스크 캐싱을 구현하고 싶지 않으면 Cache.java 파일의 내용을 편집 할 필요가 없습니다.

MainActivity.java의 중요한 것들 :

public void onScrollStateChanged(AbsListView view, int scrollState) {
    if (view.getId() == android.R.id.list) {
        // Set scrolling to true only if the user has flinged the       
        // ListView away, hence we skip downloading a series
        // of unnecessary bitmaps that the user probably
        // just want to skip anyways. If we scroll slowly it
        // will still download bitmaps - that means
        // that the application won't wait for the user
        // to lift its finger off the screen in order to
        // download.
        if (scrollState == SCROLL_STATE_FLING) {
            mIsScrolling = true;
        } else {
            mIsScrolling = false;
            mListAdapter.notifyDataSetChanged();
        }
    } 
}

// Inside ListAdapter...
@Override
public View getView(final int position, View convertView, ViewGroup parent) {           
    View row = convertView;
    final ViewHolder holder;

    if (row == null) {
        LayoutInflater inflater = getLayoutInflater();
        row = inflater.inflate(R.layout.main_listview_row, parent, false);  
        holder = new ViewHolder(row);
        row.setTag(holder);
    } else {
        holder = (ViewHolder) row.getTag();
    }   

    final Row rowObject = getItem(position);

    // Look at the loadBitmap() method description...
    holder.mTextView.setText(rowObject.mText);      
    mCache.loadBitmap(MainActivity.this,
            rowObject.mBitmapUrl, holder.mImageView,
            mIsScrolling);  

    return row;
}

getView() 는 매우 자주 호출됩니다. 우리가 행당 무한대의 쓰레드를 시작하지 못하도록하는 체크를 구현하지 않았다면 일반적으로 이미지를 다운로드하는 것이 좋지 않습니다. Cache.java는 rowObject.mBitmapUrl 이미 태스크에 있는지 여부를 검사하고, rowObject.mBitmapUrl 하는 경우 다른 rowObject.mBitmapUrl 시작하지 않습니다. 따라서 AsyncTask 풀의 작업 큐 제한을 초과하지 않을 가능성이 큽니다.

다운로드 :

https://www.dropbox.com/s/pvr9zyl811tfeem/ListViewImageCache.zip 에서 소스 코드를 다운로드 할 수 있습니다.

마지막 말:

지금 몇 주 동안 이것을 테스트했지만 아직 OOM 예외가 하나도 없습니다. 에뮬레이터, Nexus One 및 Nexus S에서이 기능을 테스트했습니다. HD 품질의 이미지가 포함 된 이미지 URL을 테스트했습니다. 유일한 병목은 다운로드하는 데 더 많은 시간이 걸린다는 것입니다.

OOM이 나타날 것이라고 상상할 수있는 가능한 시나리오는 하나뿐입니다. 즉, 실제로 많은 큰 이미지를 다운로드하고 크기를 조정하여 캐시에 넣기 전에 동시에 더 많은 메모리를 차지하고 OOM을 발생시킵니다. 그러나 어쨌든 그것은 이상적인 상황조차도 아니며, 더 실현 가능한 방법으로 해결할 가능성이 거의 없습니다.

코멘트에 오류를보고하십시오! :-)


2 센트 : 비트 맵으로 OOM 오류를 해결했습니다.

a) 이미지를 2 배 스케일링

b) ListView에 대한 내 사용자 지정 어댑터의 Picasso 라이브러리를 사용하여 다음과 같이 getView에서 one-call을 사용합니다.Picasso.with(context).load(R.id.myImage).into(R.id.myImageView);


Android Training 클래스 인 " 비트 맵을 효율적으로 표시 "에서는 java.lang.OutOfMemoryError: bitmap size exceeds VM budget 예외 java.lang.OutOfMemoryError: bitmap size exceeds VM budget 처리하고 이해하는 데 유용한 정보를 제공합니다 java.lang.OutOfMemoryError: bitmap size exceeds VM budget 로드 할 때 java.lang.OutOfMemoryError: bitmap size exceeds VM budget 합니다.

비트 맵 치수 및 유형 읽기

BitmapFactory 클래스는 다양한 소스에서 Bitmap 을 만들기위한 여러 가지 디코딩 메서드 ( decodeByteArray() , decodeFile() , decodeResource() 등)를 제공합니다. 이미지 데이터 소스를 기반으로 가장 적합한 디코드 방법을 선택하십시오. 이러한 메서드는 구성된 비트 맵에 메모리를 할당하려고 시도하므로 OutOfMemory 예외가 발생할 수 있습니다. 각 유형의 디코드 메서드에는 BitmapFactory.Options 클래스를 통해 디코딩 옵션을 지정할 수있는 추가 시그니처가 있습니다. 디코딩 중에 inJustDecodeBounds 속성을 true 설정하면 비트 맵 객체에 null 이 반환되지만 outWidth , outHeightoutMimeType 설정하면 메모리 할당이 방지됩니다. 이 기법을 사용하면 비트 맵의 ​​구성 (및 메모리 할당) 이전에 이미지 데이터의 크기와 유형을 읽을 수 있습니다.

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;

java.lang.OutOfMemory 예외를 피하려면 사용 가능한 메모리 내에서 편안하게 맞는 크기의 예상 이미지 데이터를 제공하도록 소스를 절대적으로 신뢰하지 않는 한, 비트 맵을 디코딩하기 전에 크기를 확인하십시오.

축소 된 버전을 메모리에로드하십시오.

이미지 크기가 알려졌으므로 전체 이미지를 메모리에로드할지 또는 서브 샘플링 된 버전을 대신로드할지 결정하는 데 사용할 수 있습니다. 다음은 고려해야 할 몇 가지 요소입니다.

  • 전체 이미지를 메모리에로드 할 때 예상되는 메모리 사용량입니다.
  • 응용 프로그램의 다른 메모리 요구 사항에 따라이 이미지를로드하기 위해 기꺼이 수행 할 메모리 양입니다.
  • 이미지를로드 할 대상 ImageView 또는 UI 구성 요소의 크기입니다.
  • 현재 장치의 화면 크기 및 밀도.

예를 들어 ImageView 의 128x96 픽셀 미리보기 이미지에 결국 1024x768 픽셀 이미지를로드하면 메모리에로드 할 가치가 없습니다.

이미지를 서브 샘플링하고 메모리에 작은 버전을로드하도록 디코더에 지시하려면 BitmapFactory.Options 객체에서 inSampleSizetrue 로 설정하십시오. 예를 들어, 4의 inSampleSize 로 디코딩 된 해상도 2048x1536의 이미지는 약 512x384의 비트 맵을 생성합니다. 이것을 메모리에로드하는 것은 전체 이미지에 대해 12MB가 아닌 0.75MB를 사용합니다 (ARGB_8888의 비트 맵 구성으로 ARGB_8888 ). 목표 너비와 높이를 기준으로 2의 거듭 제곱 인 샘플 크기 값을 계산하는 방법은 다음과 같습니다.

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;
}

: 디코더가 inSampleSize 문서에 따라 가장 가까운 2의 제곱으로 반올림하여 최종 값을 사용하기 때문에 두 값의 거듭 제곱이 계산됩니다.

이 메서드를 사용하려면 먼저 inJustDecodeBoundstrue 설정하여 디코딩 한 다음 옵션을 통과시킨 다음 새 inSampleSize 값과 inJustDecodeBoundsfalse 설정하여 다시 디코딩하십시오.

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);
}

이 메서드를 사용하면 다음 예제 코드와 같이 100x100 픽셀 축소판을 표시하는 ImageView 에 임의로 큰 크기의 비트 맵을 쉽게로드 할 수 있습니다.

mImageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

필요에 따라 적절한 BitmapFactory.decode* 메서드를 대체하여 유사한 프로세스를 따라 다른 소스의 비트 맵을 디코딩 할 수 있습니다.


불행히도 위의 중 아무 것도 작동하지 않으면 Manifest 파일에 추가 하십시오. 내부 애플리케이션 태그

 <application
         android:largeHeap="true"

나는 다음과 같은 방식으로 같은 문제를 해결했다.

Bitmap b = null;
Drawable d;
ImageView i = new ImageView(mContext);
try {
    b = Bitmap.createBitmap(320,424,Bitmap.Config.RGB_565);
    b.eraseColor(0xFFFFFFFF);
    Rect r = new Rect(0, 0,320 , 424);
    Canvas c = new Canvas(b);
    Paint p = new Paint();
    p.setColor(0xFFC0C0C0);
    c.drawRect(r, p);
    d = mContext.getResources().getDrawable(mImageIds[position]);
    d.setBounds(r);
    d.draw(c);

    /*   
        BitmapFactory.Options o2 = new BitmapFactory.Options();
        o2.inTempStorage = new byte[128*1024];
        b = BitmapFactory.decodeStream(mContext.getResources().openRawResource(mImageIds[position]), null, o2);
        o2.inSampleSize=16;
        o2.inPurgeable = true;
    */
} catch (Exception e) {

}
i.setImageBitmap(b);

나는 이미지를 찍고 즉시 크기를 조정하기 위해 다음을 수행했다. 희망이 도움이

Bitmap bm;
bm = Bitmap.createScaledBitmap(BitmapFactory.decodeFile(filepath), 100, 100, true);
mPicture = new ImageView(context);
mPicture.setImageBitmap(bm);    

이 코드는 드로어 블에서 큰 비트 맵을로드하는 데 도움이됩니다.

public class BitmapUtilsTask extends AsyncTask<Object, Void, Bitmap> {

    Context context;

    public BitmapUtilsTask(Context context) {
        this.context = context;
    }

    /**
     * Loads a bitmap from the specified url.
     * 
     * @param url The location of the bitmap asset
     * @return The bitmap, or null if it could not be loaded
     * @throws IOException
     * @throws MalformedURLException
     */
    public Bitmap getBitmap() throws MalformedURLException, IOException {       

        // Get the source image's dimensions
        int desiredWidth = 1000;
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;

        BitmapFactory.decodeResource(context.getResources(), R.drawable.green_background , options);

        int srcWidth = options.outWidth;
        int srcHeight = options.outHeight;

        // Only scale if the source is big enough. This code is just trying
        // to fit a image into a certain width.
        if (desiredWidth > srcWidth)
            desiredWidth = srcWidth;

        // Calculate the correct inSampleSize/scale value. This helps reduce
        // memory use. It should be a power of 2
        int inSampleSize = 1;
        while (srcWidth / 2 > desiredWidth) {
            srcWidth /= 2;
            srcHeight /= 2;
            inSampleSize *= 2;
        }
        // Decode with inSampleSize
        options.inJustDecodeBounds = false;
        options.inDither = false;
        options.inSampleSize = inSampleSize;
        options.inScaled = false;
        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
        options.inPurgeable = true;
        Bitmap sampledSrcBitmap;

        sampledSrcBitmap =  BitmapFactory.decodeResource(context.getResources(), R.drawable.green_background , options);

        return sampledSrcBitmap;
    }

    /**
     * The system calls this to perform work in a worker thread and delivers
     * it the parameters given to AsyncTask.execute()
     */
    @Override
    protected Bitmap doInBackground(Object... item) {
        try { 
          return getBitmap();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

이것은 나를 위해 일했습니다!

public Bitmap readAssetsBitmap(String filename) throws IOException {
    try {
        BitmapFactory.Options options = new BitmapFactory.Options(); 
        options.inPurgeable = true;
        Bitmap bitmap = BitmapFactory.decodeStream(assets.open(filename), null, options);
        if(bitmap == null) {
            throw new IOException("File cannot be opened: It's value is null");
        } else {
            return bitmap;
        }
    } catch (IOException e) {
        throw new IOException("File cannot be opened: " + e.getMessage());
    }
}

그 같은 OutofMemoryException것을 부름으로써 완전히 해결 될 수는 없습니다 System.gc().

활동 라이프 사이클 참조

활동 상태는 각 프로세스의 메모리 사용과 각 프로세스의 우선 순위에 따라 OS 자체에 의해 결정됩니다.

사용 된 각 비트 맵 그림의 크기와 해상도를 고려할 수 있습니다. 크기를 줄이고 낮은 해상도로 다시 샘플링하고 갤러리 디자인을 참조하는 것이 좋습니다 (하나의 작은 그림 PNG 및 하나의 원본 그림).


나는 모든 종류의 스케일링을 필요로하지 않는 훨씬 더 효과적인 솔루션을 가지고있다. 간단히 비트 맵을 한 번만 디코딩 한 다음 맵에 이름과 비교하여 캐시하십시오. 그런 다음 이름에 대해 비트 맵을 검색하고 ImageView에서 설정합니다. 할 일이 더 이상 필요 없습니다.

이는 디코딩 된 비트 맵의 ​​실제 2 진 데이터가 dalvik VM 힙 내에 저장되지 않기 때문에 작동합니다. 그것은 외부에 저장됩니다. 따라서 비트 맵을 디코딩 할 때마다 GC에서 회수하지 않은 VM 힙 외부에 메모리를 할당합니다.

이 점을 더 잘 이해할 수 있도록 드로어 블 폴더에 이미지를 보관하고 있다고 상상해보십시오. getResources (). getDrwable (R.drawable.)을 수행하여 이미지를 얻을 수 있습니다. 이렇게하면 매번 이미지를 디코딩하지 않지만 호출 할 때마다 이미 디코딩 된 인스턴스를 다시 사용하게됩니다. 따라서 본질적으로 캐시됩니다.

이제 이미지가 어딘가에 파일에 있거나 외부 서버에서 가져온 것일 수도 있으므로 디코딩 된 비트 맵 인스턴스를 필요에 따라 재사용 할 수 있도록 캐시하는 것은 사용자의 책임입니다.

희망이 도움이됩니다.


내 응용 프로그램 중 하나에서 사진을 찍어야합니다 Camera/Gallery. 카메라에서 이미지를 클릭하면 (2MP, 5MP 또는 8MP 일 수 있음) 이미지 크기는 kBs 에서 s까지 다양 MB합니다. 이미지 크기가 작은 경우 (또는 최대 1-2MB) 코드가 정상적으로 작동하지만 4MB 또는 5MB를 초과하는 크기의 이미지 OOM가있는 경우 프레임에 포함됩니다.

그럼 내가이 문제를 해결하기 위해 노력하고 마침내 나는 아래의 개선을 만들었습니다 (좋은 신용 솔루션을 만들기위한 Fedor의 모든 신용) 코드 :)

private Bitmap decodeFile(String fPath) {
    // Decode image size
    BitmapFactory.Options opts = new BitmapFactory.Options();
    /*
     * If set to true, the decoder will return null (no bitmap), but the
     * out... fields will still be set, allowing the caller to query the
     * bitmap without having to allocate the memory for its pixels.
     */
    opts.inJustDecodeBounds = true;
    opts.inDither = false; // Disable Dithering mode
    opts.inPurgeable = true; // Tell to gc that whether it needs free
                                // memory, the Bitmap can be cleared
    opts.inInputShareable = true; // Which kind of reference will be used to
                                    // recover the Bitmap data after being
                                    // clear, when it will be used in the
                                    // future

    BitmapFactory.decodeFile(fPath, opts);

    // The new size we want to scale to
    final int REQUIRED_SIZE = 70;

    // Find the correct scale value. 
    int scale = 1;

    if (opts.outHeight > REQUIRED_SIZE || opts.outWidth > REQUIRED_SIZE) {

        // Calculate ratios of height and width to requested height and width
        final int heightRatio = Math.round((float) opts.outHeight
                / (float) REQUIRED_SIZE);
        final int widthRatio = Math.round((float) opts.outWidth
                / (float) REQUIRED_SIZE);

        // Choose the smallest ratio as inSampleSize value, this will guarantee
        // a final image with both dimensions larger than or equal to the
        // requested height and width.
        scale = heightRatio < widthRatio ? heightRatio : widthRatio;//
    }

    // Decode bitmap with inSampleSize set
    opts.inJustDecodeBounds = false;

    opts.inSampleSize = scale;

    Bitmap bm = BitmapFactory.decodeFile(fPath, opts).copy(
            Bitmap.Config.RGB_565, false);

    return bm;

}

나는 이것이 동일한 문제에 직면하는 친구를 도울 것이라는 점을 희망한다!

자세한 내용은 this 참조 this


방금 몇 분 전에이 문제에 관해서 만났습니다. 필자는 listview 어댑터를 관리 할 때 더 나은 작업을 수행하여 문제를 해결했습니다. 50x50 픽셀 이미지를 수백 장 사용하여 문제가 있다고 생각했는데 행이 표시 될 때마다 내 맞춤보기를 부풀려했습니다. 단순히 행이 비정상적으로 증가했는지 확인하기 위해 테스트하여이 오류를 제거했으며 수백 비트 맵을 사용하고 있습니다. 이것은 실제로 Spinner 용이지만 기본 어댑터는 ListView에서 모두 동일하게 작동합니다. 이 간단한 수정으로 어댑터의 성능도 크게 향상되었습니다.

@Override
public View getView(final int position, View convertView, final ViewGroup parent) {

    if(convertView == null){
        LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        convertView = inflater.inflate(R.layout.spinner_row, null);
    }
...

위대한 해답은 있지만 이 문제를 해결할 있는 완전히 유용한 수업 이 필요했습니다. 그래서 한 가지를했습니다.

다음은 OutOfMemoryError라는 내 BitmapHelper 클래스 입니다.

import java.io.File;
import java.io.FileInputStream;

import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;

public class BitmapHelper
{

    //decodes image and scales it to reduce memory consumption
    public static Bitmap decodeFile(File bitmapFile, int requiredWidth, int requiredHeight, boolean quickAndDirty)
    {
        try
        {
            //Decode image size
            BitmapFactory.Options bitmapSizeOptions = new BitmapFactory.Options();
            bitmapSizeOptions.inJustDecodeBounds = true;
            BitmapFactory.decodeStream(new FileInputStream(bitmapFile), null, bitmapSizeOptions);

            // load image using inSampleSize adapted to required image size
            BitmapFactory.Options bitmapDecodeOptions = new BitmapFactory.Options();
            bitmapDecodeOptions.inTempStorage = new byte[16 * 1024];
            bitmapDecodeOptions.inSampleSize = computeInSampleSize(bitmapSizeOptions, requiredWidth, requiredHeight, false);
            bitmapDecodeOptions.inPurgeable = true;
            bitmapDecodeOptions.inDither = !quickAndDirty;
            bitmapDecodeOptions.inPreferredConfig = quickAndDirty ? Bitmap.Config.RGB_565 : Bitmap.Config.ARGB_8888;

            Bitmap decodedBitmap = BitmapFactory.decodeStream(new FileInputStream(bitmapFile), null, bitmapDecodeOptions);

            // scale bitmap to mathc required size (and keep aspect ratio)

            float srcWidth = (float) bitmapDecodeOptions.outWidth;
            float srcHeight = (float) bitmapDecodeOptions.outHeight;

            float dstWidth = (float) requiredWidth;
            float dstHeight = (float) requiredHeight;

            float srcAspectRatio = srcWidth / srcHeight;
            float dstAspectRatio = dstWidth / dstHeight;

            // recycleDecodedBitmap is used to know if we must recycle intermediary 'decodedBitmap'
            // (DO NOT recycle it right away: wait for end of bitmap manipulation process to avoid
            // java.lang.RuntimeException: Canvas: trying to use a recycled bitmap [email protected]
            // I do not excatly understand why, but this way it's OK

            boolean recycleDecodedBitmap = false;

            Bitmap scaledBitmap = decodedBitmap;
            if (srcAspectRatio < dstAspectRatio)
            {
                scaledBitmap = getScaledBitmap(decodedBitmap, (int) dstWidth, (int) (srcHeight * (dstWidth / srcWidth)));
                // will recycle recycleDecodedBitmap
                recycleDecodedBitmap = true;
            }
            else if (srcAspectRatio > dstAspectRatio)
            {
                scaledBitmap = getScaledBitmap(decodedBitmap, (int) (srcWidth * (dstHeight / srcHeight)), (int) dstHeight);
                recycleDecodedBitmap = true;
            }

            // crop image to match required image size

            int scaledBitmapWidth = scaledBitmap.getWidth();
            int scaledBitmapHeight = scaledBitmap.getHeight();

            Bitmap croppedBitmap = scaledBitmap;

            if (scaledBitmapWidth > requiredWidth)
            {
                int xOffset = (scaledBitmapWidth - requiredWidth) / 2;
                croppedBitmap = Bitmap.createBitmap(scaledBitmap, xOffset, 0, requiredWidth, requiredHeight);
                scaledBitmap.recycle();
            }
            else if (scaledBitmapHeight > requiredHeight)
            {
                int yOffset = (scaledBitmapHeight - requiredHeight) / 2;
                croppedBitmap = Bitmap.createBitmap(scaledBitmap, 0, yOffset, requiredWidth, requiredHeight);
                scaledBitmap.recycle();
            }

            if (recycleDecodedBitmap)
            {
                decodedBitmap.recycle();
            }
            decodedBitmap = null;

            scaledBitmap = null;
            return croppedBitmap;
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
        return null;
    }

    /**
     * compute powerOf2 or exact scale to be used as {@link BitmapFactory.Options#inSampleSize} value (for subSampling)
     * 
     * @param requiredWidth
     * @param requiredHeight
     * @param powerOf2
     *            weither we want a power of 2 sclae or not
     * @return
     */
    public static int computeInSampleSize(BitmapFactory.Options options, int dstWidth, int dstHeight, boolean powerOf2)
    {
        int inSampleSize = 1;

        // Raw height and width of image
        final int srcHeight = options.outHeight;
        final int srcWidth = options.outWidth;

        if (powerOf2)
        {
            //Find the correct scale value. It should be the power of 2.

            int tmpWidth = srcWidth, tmpHeight = srcHeight;
            while (true)
            {
                if (tmpWidth / 2 < dstWidth || tmpHeight / 2 < dstHeight)
                    break;
                tmpWidth /= 2;
                tmpHeight /= 2;
                inSampleSize *= 2;
            }
        }
        else
        {
            // Calculate ratios of height and width to requested height and width
            final int heightRatio = Math.round((float) srcHeight / (float) dstHeight);
            final int widthRatio = Math.round((float) srcWidth / (float) dstWidth);

            // Choose the smallest ratio as inSampleSize value, this will guarantee
            // a final image with both dimensions larger than or equal to the
            // requested height and width.
            inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
        }

        return inSampleSize;
    }

    public static Bitmap drawableToBitmap(Drawable drawable)
    {
        if (drawable instanceof BitmapDrawable)
        {
            return ((BitmapDrawable) drawable).getBitmap();
        }

        Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);

        return bitmap;
    }

    public static Bitmap getScaledBitmap(Bitmap bitmap, int newWidth, int newHeight)
    {
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        float scaleWidth = ((float) newWidth) / width;
        float scaleHeight = ((float) newHeight) / height;

        // CREATE A MATRIX FOR THE MANIPULATION
        Matrix matrix = new Matrix();
        // RESIZE THE BIT MAP
        matrix.postScale(scaleWidth, scaleHeight);

        // RECREATE THE NEW BITMAP
        Bitmap resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, false);
        return resizedBitmap;
    }

}

위의 답변 중 아무 것도 나를 위해 일한,하지만 난 문제를 해결 끔찍한 못생긴 해결 방법을 생각해 냈어. 리소스로 프로젝트에 매우 작은 1x1 픽셀 이미지를 추가하고 가비지 수집을 호출하기 전에 ImageView에 이미지를로드했습니다. ImageView가 비트 맵을 공개하지 않아서 GC가 그것을 픽업하지 않았을 수도 있습니다. 못 생겼지 만 지금은 효과가있는 것 같습니다.

if (bitmap != null)
{
  bitmap.recycle();
  bitmap = null;
}
if (imageView != null)
{
  imageView.setImageResource(R.drawable.tiny); // This is my 1x1 png.
}
System.gc();

imageView.setImageBitmap(...); // Do whatever you need to do to load the image you want.

이것은 다른 설명이 많이 포함 된 매우 긴 실행 문제 인 것으로 보입니다. 여기서는 가장 일반적인 두 가지 답변에 대한 조언을 들었지만, VM의 내 문제를 해결하지 않아서 프로세스 의 디코딩 부분 을 수행 할 바이트를 제공 할 여유가 없다고 주장했습니다 . 일부 파기 후에 나는 여기서 진짜 문제가 네이티브 힙 에서 벗어나는 디코딩 프로세스라는 것을 알게되었습니다 .

BitmapFactory OOM을 통해 나를 견뎌 낼 수 있습니다.

이 문제에 대한 몇 가지 해결책을 찾은 다른 토론 스레드로 안내합니다. 하나는 System.gc();이미지가 표시된 후에 수동으로 호출하는 것 입니다. 하지만 실제로 네이티브 힙을 줄이기 위해 앱이 더 많은 메모리를 사용하게 만듭니다. 2.0 (Donut) 릴리스의 더 좋은 해결책은 BitmapFactory 옵션 "inPurgeable"을 사용하는 것입니다. 그래서 나는 o2.inPurgeable=true;단지 후에 추가 했다.o2.inSampleSize=scale; .

그 주제에 대한 자세한 내용은 다음과 같습니다. 메모리 힙의 한계는 6M입니까?

자,이 모든 것을 말하면서, 저는 자바와 안드로이드에 대한 완전한 dunce입니다. 따라서 이것이이 문제를 해결하기위한 끔찍한 방법이라고 생각한다면 아마 맞을 것입니다. ;-) 그러나 이것은 나를 위해 놀라운 일을했습니다. 그리고 VM을 힙 캐시에서 실행할 수 없다는 것을 알게되었습니다. 내가 찾을 수있는 유일한 단점은 캐시 된 그림을 휴지통에 버려야한다는 것입니다. 다시 말하면 이미지로 바로 돌아 가면 매번 다시 그려 넣을 것입니다. 내 응용 프로그램이 어떻게 작동하는지는 문제가되지 않습니다. 귀하의 마일리지가 다를 수 있습니다.


이것을 사용하면 bitmap.recycle();이미지 품질 문제없이 도움이됩니다.







android-bitmap