Loading large images without OutOfMemoryError

2019-01-23 14:17发布

问题:

I have a 5000 x 4000 px image which I want to draw onto a canvas.

First I tried to load it from resources. I put it in /res/drawable.

I used the following method:

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

It worked like a charm.

In this case the InputStream is an AssetManager.AssetInputStream.

So now I want to load it from the sdcard.

Here's what I tried to do:

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

In this case the InputStream is a FileInputStream and I got an OutOfMemoryError when creating the Drawable.

So I'm wondering:

Is there a way to load the image without getting that error? Or is there a way to convert a FileInputStream to an AssetInputStream ?

Note:

I don't want to resize the image because I'm implementing zoom/pan functionality. Please don't tell me to read Loading Large Bitmaps Efficiently.

You can check out the full class here. The error occurs when using setImageUri().

Here's my Error Log:

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)

EDIT:

I was testing my code on an HTC Desire A8181. After being told that the first code snippet wasn't working on some other devices, I tested on a Samsung Galaxy S2 and on the Emulator.

Results: When loading from resources, the emulator gave an OutOfMemoryError, the Galaxy S2 didn't throw an exception but the returned Drawable was null.

So I guess for the time being the only solution is to downsample the image.

回答1:

Take a look at:

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

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

or

decodeFile (String pathName, BitmapFactory.Options opts)

this way you can load your whole bitmap and show it on screen.

You need to set the correct options.

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

inSampleSize

I tried it with your code:

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

works for me.



回答2:

In my opinion, the best option is to split the image into smaller pieces. This will prevent the application to give an OutOfMemoryException. Here is my example code

First get the image from the SD Card an put it into an 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();
        }

Split the bitmap (b) into smaller pieces. In this case SPLIT_HEIGHT is 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);

Now you have a collection of smaller images. If you want to do it perfecly, you can recycle this bitmap:

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

From this point you can put the smaller images onto the canvas. In my next code I put the images into an ScrollView. The code for putting the images on a Canvas is I think not very different.

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

I hope this will help you.

NOTE: The maximum resolution for images is 2048*2048 (OpenGL restrictions or something), so you will always have to split images into smaller pieces.



回答3:

I am not sure whether it is the correct of way of doing this but still this can solve your problem.

Try opening your image in a webview. This will provide you in-built zoom-in/out and panning feature and you dont have to worry about opening any input stream or anything else.

To open an image in webview from assets:

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

You can WebSettings for allowing zoom controls.

Hope this will help you!!



回答4:

If it worked when you loaded from resources and now it doesn't work when you load manually then I suspect that you have some problem with resource management, probably some memory leak.

Does OutOfMemoryError occur always at the start of application or it happens in later time? Is it on configuration change? Do you use static fields (if used incorrectly this often leads to memory leaks in Android application, example with explanation this fits to your case)?
Try use some profiling tools (google can help to find something useful).