Загрузка больших изображений без OutOfMemoryError

У меня есть изображение размером 5000 x 4000 пикселей, которое я хочу нарисовать на холсте.

Сначала я попытался загрузить его из ресурсов. Я положил его в /res/drawable.

Я использовал следующий метод:

InputStream input = getResources().openRawResource(R.drawable.huge_image);
Drawable d = Drawable.createFromStream(input, "image");
d.setBounds(...);
d.draw(canvas);

Оно работало завораживающе.

В этом случае InputStream является AssetManager.AssetInputStream.

Итак, теперь я хочу загрузить его с SD-карты.

Вот что я пытался сделать:

File f = new File(path);
Uri uri = Uri.fromFile(f);
InputStream input = mContext.getContentResolver().openInputStream(uri);
Drawable d = Drawable.createFromStream(input, "image");

В данном случае InputStream — это FileInputStream, а я получил OutOfMemoryError при создании Drawable.

Поэтому мне интересно:

Есть ли способ загрузить изображение без этой ошибки? Или есть способ преобразовать FileInputStream в AssetInputStream?

Примечание:

Я не хочу изменять размер изображения, потому что реализую функции масштабирования/панорамирования. Пожалуйста, не говорите мне читать Эффективная загрузка больших растровых изображений.

Вы можете ознакомиться с полным курсом здесь. Ошибка возникает при использовании setImageUri().

Вот мой журнал ошибок:

08-13 11:57:54.180: E/AndroidRuntime(23763): FATAL EXCEPTION: main
08-13 11:57:54.180: E/AndroidRuntime(23763): java.lang.OutOfMemoryError: bitmap size exceeds VM budget
08-13 11:57:54.180: E/AndroidRuntime(23763):    at android.graphics.BitmapFactory.nativeDecodeStream(Native Method)
08-13 11:57:54.180: E/AndroidRuntime(23763):    at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:468)
08-13 11:57:54.180: E/AndroidRuntime(23763):    at android.graphics.BitmapFactory.decodeResourceStream(BitmapFactory.java:332)
08-13 11:57:54.180: E/AndroidRuntime(23763):    at android.graphics.drawable.Drawable.createFromResourceStream(Drawable.java:697)
08-13 11:57:54.180: E/AndroidRuntime(23763):    at android.graphics.drawable.Drawable.createFromStream(Drawable.java:657)
08-13 11:57:54.180: E/AndroidRuntime(23763):    at com.benitobertoli.largeimagezoom.ZoomImageView.setDrawablefromUri(ZoomImageView.java:187)
08-13 11:57:54.180: E/AndroidRuntime(23763):    at com.benitobertoli.largeimagezoom.ZoomImageView.setImageUri(ZoomImageView.java:588)
08-13 11:57:54.180: E/AndroidRuntime(23763):    at com.benitobertoli.largeimagezoom.TestActivity.onKeyDown(TestActivity.java:30)
08-13 11:57:54.180: E/AndroidRuntime(23763):    at android.view.KeyEvent.dispatch(KeyEvent.java:1257)
08-13 11:57:54.180: E/AndroidRuntime(23763):    at android.app.Activity.dispatchKeyEvent(Activity.java:2075)
08-13 11:57:54.180: E/AndroidRuntime(23763):    at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchKeyEvent(PhoneWindow.java:1673)
08-13 11:57:54.180: E/AndroidRuntime(23763):    at android.view.ViewRoot.deliverKeyEventToViewHierarchy(ViewRoot.java:2493)
08-13 11:57:54.180: E/AndroidRuntime(23763):    at android.view.ViewRoot.handleFinishedEvent(ViewRoot.java:2463)
08-13 11:57:54.180: E/AndroidRuntime(23763):    at android.view.ViewRoot.handleMessage(ViewRoot.java:1752)
08-13 11:57:54.180: E/AndroidRuntime(23763):    at android.os.Handler.dispatchMessage(Handler.java:99)
08-13 11:57:54.180: E/AndroidRuntime(23763):    at android.os.Looper.loop(Looper.java:144)
08-13 11:57:54.180: E/AndroidRuntime(23763):    at android.app.ActivityThread.main(ActivityThread.java:4937)
08-13 11:57:54.180: E/AndroidRuntime(23763):    at java.lang.reflect.Method.invokeNative(Native Method)
08-13 11:57:54.180: E/AndroidRuntime(23763):    at java.lang.reflect.Method.invoke(Method.java:521)
08-13 11:57:54.180: E/AndroidRuntime(23763):    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:868)
08-13 11:57:54.180: E/AndroidRuntime(23763):    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:626)
08-13 11:57:54.180: E/AndroidRuntime(23763):    at dalvik.system.NativeStart.main(Native Method)

ИЗМЕНИТЬ:

Я тестировал свой код на HTC Desire A8181. После того, как мне сказали, что первый фрагмент кода не работает на некоторых других устройствах, я протестировал его на Samsung Galaxy S2 и на эмуляторе.

Результаты: при загрузке из ресурсов эмулятор выдал OutOfMemoryError, Galaxy S2 не выдал исключение, но возвращенное Drawable было нулевым.

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


person Benito Bertoli    schedule 13.08.2012    source источник
comment
Я попытался запустить ваш код на эмуляторе m и получаю ошибку OutOfMemory даже в методе From Res :) Какие настройки устройства и/или эмулятора вы используете, чтобы запустить его без ошибок?   -  person Joe    schedule 15.08.2012


Ответы (4)


Взгляни на:

http://developer.android.com/reference/android/graphics/BitmapFactory.html

decodeStream (InputStream is, Rect outPadding, BitmapFactory.Options opts)

or

decodeFile (String pathName, BitmapFactory.Options opts)

таким образом вы можете загрузить все растровое изображение и показать его на экране.

Вам нужно установить правильные параметры.

http://developer.android.com/reference/android/graphics/BitmapFactory.Options.html

inSampleSize

Я попробовал это с вашим кодом:

 BitmapFactory.Options options = new BitmapFactory.Options();

 options.inSampleSize = 4;

 Bitmap myBitmap = BitmapFactory.decodeFile(mUri.getPath(),options);
 Drawable d = new BitmapDrawable(Resources.getSystem(),myBitmap);
 updateDrawable(d);

работает на меня.

person n3utrino    schedule 14.08.2012
comment
Как я уже сказал в своем вопросе, у меня нет проблем с загрузкой изображения из ресурсов. Кроме того, я не хочу изменять размер изображения. - person Benito Bertoli; 14.08.2012
comment
извините, я пропустил это, тогда попробуйте метод decodeStream (InputStream is, Rect outPadding, BitmapFactory.Options opts) - person n3utrino; 14.08.2012
comment
Я повторяю я не хочу изменять размер изображения. - person Benito Bertoli; 14.08.2012
comment
вам нужно масштабировать и размещать мозаику, вы не можете хранить растровое изображение размером 5000x4000px в памяти. для масштабирования и панорамирования вам нужно выяснить, какая часть исходного изображения будет видна, и загрузить соответствующие плитки, масштабированные в соответствии с плотностью экрана. - person n3utrino; 14.08.2012
comment
кстати: код для загрузки изображения из ресурса даст вам OutOfMemory на Galaxy Nexus, если включен аппаратный рендеринг - person n3utrino; 14.08.2012
comment
Я никогда ничего не говорил о хранении Bitmap в памяти. На самом деле, если вы читаете мой вопрос, вы должны заметить, что я использую Drawable. Я выполняю панорамирование и масштабирование с помощью Drawable.setBounds(). Drawable создается из InputStream. Вы можете найти полный исходный код здесь. - person Benito Bertoli; 14.08.2012
comment
У меня проект запущен. Загрузка по ресурсу не работает на моем Galaxy Nexus. Но я думаю, что загрузка по ресурсу делает то же самое, масштабируя растровое изображение до плотности экрана. Я думаю, что, выполнив setBounds(), масштабируемое изображение будет увеличено, чтобы соответствовать региону. - person n3utrino; 14.08.2012

На мой взгляд, лучший вариант — разбить изображение на более мелкие части. Это предотвратит выдачу приложением исключения OutOfMemoryException. Вот мой пример кода

Сначала получите изображение с SD-карты и поместите его в растровое изображение.

Bitmap b = null;
        try {
            File sd = new File(Environment.getExternalStorageDirectory() + link_to_image.jpg);
            FileInputStream fis;

            fis = new FileInputStream(sd);

            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inPurgeable = true;
            options.inScaled = false;

            b = BitmapFactory.decodeStream(fis, null, options);
            fis.close();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

Разделите растровое изображение (b) на более мелкие части. В этом случае SPLIT_HEIGHT равно 400.

double rest = (image_height + SPLIT_HEIGHT);
        int i = 0;
        Bitmap[] imgs = new Bitmap[50];

        do {
            rest -= SPLIT_HEIGHT;
            if (rest < 0) {
                // Do nothing
            } else if (rest < SPLIT_HEIGHT) {
                imgs[i] = Bitmap.createBitmap(b, 0, (i * SPLIT_HEIGHT), image_width, (int) rest);
            } else {
                imgs[i] = Bitmap.createBitmap(b, 0, i * SPLIT_HEIGHT, image_width, SPLIT_HEIGHT);
            }

            i++;

        } while (rest > SPLIT_HEIGHT);

Теперь у вас есть коллекция изображений меньшего размера. Если вы хотите сделать это идеально, вы можете переработать это растровое изображение:

// Not needed anymore, free up some memory
        if (b != null)
            b.recycle();

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

// Add images to scrollview
        for (int j = 0; j < imgs.length; j++) {
            Bitmap tmp = imgs[j];
            if (tmp != null) {
                // Add to scrollview
                ImageView iv = new ImageView(activity);

                String id = j + "" + articlenumber;
                iv.setId(Integer.parseInt(id));
                iv.setTag(i);

                iv.setImageBitmap(tmp);

                RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(globalwidth, tmp.getHeight());
                lp.addRule(RelativeLayout.BELOW, previousLongPageImageId);
                lp.addRule(RelativeLayout.ALIGN_PARENT_LEFT);

                container.addView(iv, lp);
                previousLongPageImageId = iv.getId();
            }
        }

Я надеюсь, что это поможет вам.

ПРИМЕЧАНИЕ. Максимальное разрешение для изображений составляет 2048*2048 (ограничения OpenGL или что-то в этом роде), поэтому вам всегда придется разбивать изображения на более мелкие части.

person Ceetn    schedule 14.08.2012
comment
Спасибо за ваш ответ. Прежде чем даже попытаться разделить растровое изображение, b = BitmapFactory.decodeStream(fis, null, options); уже выдает ошибку OutOfMemoryError. Кроме того, вы разбиваете растровое изображение на 50 меньших, но все равно загружаете их все одновременно в память, что дает тот же результат. - person Benito Bertoli; 14.08.2012
comment
Если вы установите android:largeHeap=true, а затем выполните приведенный выше код? Как я уже сказал в своей заметке, вам придется разделить изображение на части, потому что максимальное разрешение 2048*2048. Конечно, в моем решении вам также придется разделить изображение на вертикальные части (ширина выше 2048). - person Ceetn; 14.08.2012

Я не уверен, что это правильный способ сделать это, но все же это может решить вашу проблему.

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

Чтобы открыть изображение в веб-просмотре из ресурсов:

    WebView  wv = (WebView) findViewById(R.id.webView1);  
    wv.loadUrl("file:///android_asset/abc.png");  

Вы можете WebSettings разрешить управление масштабированием.

Надеюсь, что это поможет вам!!

person mudit    schedule 13.08.2012
comment
Спасибо за ваш ответ. Я разрабатываю Custom View с обнаружением жестов, а WebView недостаточно. - person Benito Bertoli; 13.08.2012

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

Всегда ли OutOfMemoryError возникает в начале приложения или позже? Это при изменении конфигурации? Используете ли вы статические поля (при неправильном использовании это часто приводит к утечкам памяти в приложении Android, пример с объяснением это подходит для вашего случая)?
Попробуйте использовать некоторые инструменты профилирования (google может помочь найти что-то полезное).

person Marek R    schedule 14.08.2012