Зареждане на големи изображения без OutOfMemoryError

Имам изображение с размери 5000 x 4000 px, което искам да нарисувам върху платно.

Първо се опитах да го заредя от ресурси. Сложих го в /res/drawable.

Използвах следния метод:

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

Работи като чар.

В този случай InputStream е AssetManager.AssetInputStream.

Така че сега искам да го заредя от sdcard.

Ето какво се опитах да направя:

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 е, 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(). Тегленето се създава от InputStream. Можете да намерите пълния източник тук. - person Benito Bertoli; 14.08.2012
comment
Проектът ми е готов и работи. Зареждането по ресурс не работи на моя Galaxy Nexus. Но мисля, че зареждането по ресурс прави точно същото, мащабирайки растерното изображение до плътността на екрана. Мисля, че като се направи setBounds() чертежът се мащабира, за да пасне на региона. - person n3utrino; 14.08.2012

Според мен най-добрият вариант е да разделите изображението на по-малки части. Това ще попречи на приложението да даде OutOfMemoryException. Ето моя примерен код

Първо вземете изображението от SD картата и го поставете в Bitmap

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. Кодът за поставяне на изображенията върху Canvas мисля, че не е много различен.

// 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
Благодаря Ви за отговора. Разработвам персонализиран изглед с разпознаване на жестове и WebView просто не е достатъчен. - person Benito Bertoli; 13.08.2012

Ако работи, когато зареждате от ресурси и сега не работи, когато зареждате ръчно, тогава подозирам, че имате някакъв проблем с управлението на ресурсите, вероятно някакво изтичане на памет.

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

person Marek R    schedule 14.08.2012
comment
OutOfMemoryError се появява на този ред Drawable d = Drawable.createFromStream(input, "image");, когато зареждам от Uri. Не използвам никакви статични полета. Можете да разгледате пълния клас тук. Обаждам се на setImageUri(). - person Benito Bertoli; 14.08.2012
comment
тази връзка е за работещ код (изображение, заредено от ресурси). Очевидно не сте наложили проблемни промени (нов клон?). - person Marek R; 14.08.2012
comment
Моля, проверете отново сега. Добавих меню. От Res и От файл. Грешката възниква при използване на От файл. Трябва да копирате huge_image.png от res/drawable на вашата sdcard и да замените пътя във вашия код. - person Benito Bertoli; 14.08.2012