Проблема с нехваткой памяти при загрузке изображения в растровый объект - PullRequest
1203 голосов
/ 25 января 2009

У меня есть представление списка с парой кнопок изображений в каждой строке. Когда вы щелкаете по строке списка, запускается новое действие. Мне пришлось создавать свои собственные вкладки из-за проблемы с макетом камеры. Действие, которое запускается для результата, является картой. Если я нажму на кнопку, чтобы запустить предварительный просмотр изображения (загрузить изображение с SD-карты), приложение вернется из действия обратно в действие listview к обработчику результатов, чтобы повторно запустить мое новое действие, которое является не более чем виджетом изображения ,

Предварительный просмотр изображения в виде списка выполняется с помощью курсора и ListAdapter. Это делает это довольно просто, но я не уверен, как я могу поместить измененное изображение (т.е. меньший размер бит, а не пиксель, как src для кнопки изображения на лету. Так что я просто изменил размер изображения, полученного с камеры телефона .

Проблема в том, что я получаю сообщение об ошибке «Недостаточно памяти» при попытке вернуться назад и перезапустить второе действие.

  • Есть ли способ, с помощью которого можно легко построчно построить адаптер списка, где я могу изменить размер на лету ( в битах )?

Это было бы предпочтительным, поскольку мне также необходимо внести некоторые изменения в свойства виджетов / элементов в каждой строке, поскольку я не могу выбрать строку на сенсорном экране из-за проблемы с фокусировкой. ( Я могу использовать роликовый шарик. )

  • Я знаю, что могу сделать внешнее изменение размера и сохранить свое изображение, но это не совсем то, что я хочу сделать, но некоторый пример кода для этого был бы неплох.

Как только я отключил изображение в списке, оно снова заработало.

К вашему сведению: Вот как я это делал:

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.imagefilename является ButtonImage.

Вот мой 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

Ответы [ 42 ]

876 голосов
/ 05 мая 2009

Чтобы исправить ошибку 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;
}
606 голосов
/ 12 апреля 2012

Класс Android Training , " Эффективное отображение растровых изображений ", предоставляет полезную информацию для понимания и обработки исключения java.lang.OutOfMemoryError: bitmap size exceeds VM budget при загрузке растровых изображений.


Чтение размеров и типа растрового изображения

Класс BitmapFactory предоставляет несколько методов декодирования (decodeByteArray(), decodeFile(), decodeResource() и т. Д.) Для создания Bitmap из различных источников. Выберите наиболее подходящий метод декодирования в зависимости от источника данных вашего изображения. Эти методы пытаются выделить память для созданного растрового изображения и поэтому могут легко привести к исключению OutOfMemory. Каждый тип метода декодирования имеет дополнительные подписи, которые позволяют указывать параметры декодирования через класс BitmapFactory.Options. Установка свойства inJustDecodeBounds на true во время декодирования позволяет избежать выделения памяти, возвращая null для растрового объекта, но устанавливая outWidth, outHeight и outMimeType. Этот метод позволяет считывать размеры и тип данных изображения до создания (и выделения памяти) растрового изображения.

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 или пользовательского интерфейса, в который должно быть загружено изображение.
  • Размер экрана и плотность текущего устройства.

Например, не стоит загружать изображение 1024x768 пикселей в память, если оно в конечном итоге будет отображаться в виде миниатюры 128x96 пикселей в ImageView.

Чтобы сказать декодеру, чтобы он выполнял выборку изображения, загружая уменьшенную версию в память, установите inSampleSize в true в вашем BitmapFactory.Options объекте. Например, изображение с разрешением 2048x1536, которое декодируется с inSampleSize, равным 4, создает растровое изображение приблизительно 512x384. Загрузка этого в память использует 0,75 МБ, а не 12 МБ для полного изображения (при условии, что конфигурация растрового изображения ARGB_8888). Вот метод для вычисления значения размера выборки, которое является степенью двойки, на основе целевой ширины и высоты:

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 документация.

Чтобы использовать этот метод, сначала выполните декодирование с inJustDecodeBounds, установленным на true, передайте параметры и затем снова декодируйте, используя новое значение inSampleSize и inJustDecodeBounds, установив false:

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

Этот метод позволяет легко загружать растровое изображение произвольно большого размера в ImageView, который отображает миниатюру 100x100 пикселей, как показано в следующем примере кода:

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

Вы можете выполнить аналогичный процесс для декодирования растровых изображений из других источников, заменив соответствующий метод BitmapFactory.decode* при необходимости.

359 голосов
/ 23 августа 2010

Я сделал небольшое улучшение в коде Федора. Он в основном делает то же самое, но без (на мой взгляд) безобразного цикла while, и это всегда приводит к степени двойки. Слава Федору за принятие оригинального решения, я застрял, пока не нашел его, а потом смог сделать это :))

 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;
}
224 голосов
/ 16 декабря 2011

Я пришел из опыта iOS, и я был разочарован, обнаружив проблему с чем-то настолько простым, как загрузка и показ изображения. В конце концов, каждый, кто имеет эту проблему, пытается отобразить изображения разумного размера. В любом случае, вот два изменения, которые исправили мою проблему (и сделали мое приложение очень отзывчивым).

1) Каждый раз, когда вы делаете BitmapFactory.decodeXYZ(), обязательно передайте BitmapFactory.Options с inPurgeable, установленным на true (и предпочтительно с inInputShareable, также установленным на true).

2) НИКОГДА не используйте Bitmap.createBitmap(width, height, Config.ARGB_8888). Я имею в виду НИКОГДА! У меня никогда не было, чтобы это не вызывало ошибку памяти после нескольких проходов. Никаких сумм recycle(), System.gc(), что бы ни помогло. Это всегда вызывало исключение. Еще один способ, который на самом деле работает, заключается в том, чтобы иметь фиктивное изображение в ваших чертежах (или другое растровое изображение, которое вы декодировали с помощью шага 1, описанного выше), измените его масштаб на любое значение, а затем манипулируйте полученным растровым изображением (например, передав его на холст для большего удовольствия). Итак, что вы должны использовать вместо этого: Bitmap.createScaledBitmap(srcBitmap, width, height, false). Если по какой-либо причине вы ДОЛЖНЫ использовать метод создания грубой силы, тогда, по крайней мере, передайте Config.ARGB_4444.

Это почти гарантированно сэкономит вам часы, если не дни. Все, что говорит о масштабировании изображения и т. Д., На самом деле не работает (если только вы не решите получить неправильный размер или ухудшенное изображение).

87 голосов
/ 25 мая 2011

Это известная ошибка , это не из-за больших файлов. Поскольку Android кэширует Drawables, он исчерпывает память после использования нескольких изображений. Но я нашел альтернативный способ, пропустив систему кеширования по умолчанию для Android.

Решение : Переместите изображения в папку «assets» и используйте следующую функцию для получения BitmapDrawable:

public static Drawable getAssetImage(Context context, String filename) throws IOException {
    AssetManager assets = context.getResources().getAssets();
    InputStream buffer = new BufferedInputStream((assets.open("drawable/" + filename + ".png")));
    Bitmap bitmap = BitmapFactory.decodeStream(buffer);
    return new BitmapDrawable(context.getResources(), bitmap);
}
72 голосов
/ 19 августа 2011

У меня была та же проблема, и я решил ее, избегая функций BitmapFactory.decodeStream или decodeFile, и вместо этого использовал BitmapFactory.decodeFileDescriptor

decodeFileDescriptor похоже, что он вызывает собственные методы, отличные от decodeStream / decodeFile.

В любом случае, это сработало (обратите внимание, что я добавил некоторые параметры, как некоторые из них выше, но это не то, что имело значение. Что важно, так это вызов BitmapFactory.decodeFileDescriptor вместо decodeStream или decodeFile ):

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, а с помощью собственных вызовов; выделения выполняются вне виртуальной кучи, но против этого! "

66 голосов
/ 01 декабря 2012

Я думаю, что лучший способ избежать OutOfMemoryError - это смириться с этим и понять его.

Я создал приложение , чтобы преднамеренно вызывать OutOfMemoryError и отслеживать использование памяти.

После того, как я провел много экспериментов с этим приложением, я сделал следующие выводы:

Сначала я расскажу о версиях SDK перед Honey Comb.

  1. Растровое изображение хранится в собственной куче, но оно будет собирать мусор автоматически, вызывая recycle () не нужно.

  2. Если {размер кучи виртуальной машины} + {выделенная собственная память кучи}> = {ограничение размера кучи виртуальной машины для устройства}, и вы пытаетесь создать растровое изображение, OOM будет выброшено.

    УВЕДОМЛЕНИЕ: РАЗМЕР КАРТЫ ВМ учитывается, а не ВСПОМОГАТЕЛЬНАЯ ПАМЯТЬ ВМ.

  3. Размер кучи виртуальной машины никогда не будет уменьшаться после увеличения, даже если выделенная память виртуальной машины уменьшается.

  4. Таким образом, вы должны поддерживать максимальный объем памяти виртуальной машины как можно меньшим, чтобы размер кучи виртуальных машин не становился слишком большим для сохранения доступной памяти для растровых изображений.

  5. Вручную вызывать System.gc () бессмысленно, система сначала вызовет его, прежде чем пытаться увеличить размер кучи.

  6. Собственный размер кучи тоже никогда не сократится, но это не считается OOM, поэтому не стоит об этом беспокоиться.

Тогда давайте поговорим о SDK Starts от Honey Comb.

  1. Растровое изображение хранится в куче виртуальной машины, собственная память не учитывается для OOM.

  2. Условие для OOM намного проще: {размер кучи виртуальной машины}> = {ограничение размера кучи виртуальной машины для устройства}.

  3. Таким образом, у вас есть больше доступной памяти для создания растрового изображения с тем же ограничением размера кучи, OOM с меньшей вероятностью будет выброшено.

Вот некоторые из моих наблюдений о сборке мусора и утечке памяти.

Вы сами можете увидеть это в приложении. Если Activity выполнил AsyncTask, которая все еще выполнялась после того, как Activity была уничтожена, Activity не будет собирать мусор, пока AsyncTask не завершит работу.

Это потому, что AsyncTask является экземпляром анонимного внутреннего класса, он содержит ссылку на Activity.

Вызов AsyncTask.cancel (true) не остановит выполнение, если задача заблокирована в операции ввода-вывода в фоновом потоке.

Обратные вызовы также являются анонимными внутренними классами, поэтому, если статический экземпляр в вашем проекте удерживает их и не освобождает их, в памяти будет утечка.

Если вы запланировали повторяющуюся или отложенную задачу, например, Timer, и не вызывали cancel () и purge () в onPause (), память просочилась.

59 голосов
/ 24 августа 2012

В последнее время я видел много вопросов об исключениях OOM и кэшировании. В руководстве разработчика действительно хорошая статья по этому вопросу, но некоторые, как правило, терпят неудачу при реализации его подходящим способом.

Из-за этого я написал пример приложения, демонстрирующего кеширование в среде Android. Эта реализация еще не получила OOM.

Посмотрите в конце этого ответа ссылку на исходный код.

Требования:

  • Android API 2.1 или выше (мне просто не удалось получить доступную память для приложения в API 1.6 - это единственный фрагмент кода, который не работает в API 1.6)
  • Пакет поддержки Android

Screenshot

Особенности:

  • Сохраняет кэш при изменении ориентации , используя синглтон
  • Используйте одну восьмую назначенной памяти приложения для кэша (измените, если хотите)
  • Большие растровые изображения масштабируется (вы можете определить максимальное количество пикселей, которое вы хотите разрешить)
  • Управляет доступным подключением к Интернету перед загрузкой растровых изображений
  • Гарантирует, что вы создаете только одну задачу на строку
  • Если вы отбрасываете на ListView, он просто не будет загружать растровые изображения между

Это не включает:

  • Кэширование диска. В любом случае это должно быть легко реализовано - просто укажите на другую задачу, которая получает растровые изображения с диска

Пример кода:

Загружаемые изображения - это изображения (75x75) с Flickr. Однако поместите любые URL-адреса изображений, которые вы хотите обработать, и приложение уменьшит их, если они превысят максимум. В этом приложении URL-адреса просто находятся в массиве String.

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 в задаче, и если она есть, она не запускает другую. Поэтому мы, скорее всего, не превышаем ограничение рабочей очереди из пула AsyncTask.

Скачать:

Вы можете скачать исходный код с https://www.dropbox.com/s/pvr9zyl811tfeem/ListViewImageCache.zip.


Последние слова:

Я проверял это уже несколько недель, пока не получил ни одного исключения OOM. Я проверил это на эмуляторе, на своем Nexus One и на Nexus S. Я протестировал URL-адреса изображений, которые содержат изображения в качестве HD. Единственным узким местом является то, что загрузка занимает больше времени.

Существует только один возможный сценарий, в котором я могу представить, что OOM появится, и это если мы загрузим много действительно больших изображений, и до того, как они масштабируются и помещаются в кэш, они одновременно занимают больше памяти и вызывают ООМ. Но в любом случае это даже не идеальная ситуация, и, скорее всего, ее невозможно будет решить более приемлемым способом.

Сообщить об ошибках в комментариях! : -)

38 голосов
/ 16 февраля 2009

Я сделал следующее, чтобы сделать изображение и изменить его размер на лету. Надеюсь, это поможет

Bitmap bm;
bm = Bitmap.createScaledBitmap(BitmapFactory.decodeFile(filepath), 100, 100, true);
mPicture = new ImageView(context);
mPicture.setImageBitmap(bm);    
32 голосов
/ 19 мая 2015

к сожалению если ничего из вышеперечисленного не работает, добавьте это в файл Manifest Внутри приложение тег

 <application
         android:largeHeap="true"
...