在ViewPager中加载大型位图 - 内存不足

6
我有一个ViewPager,用来展示可缩放的图片(使用ImageViewTouch)。 我需要从互联网(http)加载大型位图。所谓大型是指2000x1000像素的图片。这些图片需要如此之大,因为它们是可缩放的,并且需要显示细节。服务器上的图片格式为.jpg,但这不是问题——我可以更改它。
我如何在不出现内存问题的情况下将如此大的图片加载到ImageViewTouch(ImageView)中?
目前,我只是使用以下方法(AsyncTask):
    ImageView currentView; //ImageView where to place image loaded from Internet
    String ImageUrl; //URL of image to load

    protected Bitmap doInBackground(ArrayList... params) {

            Bitmap bitmap;
            InputStream in = null;
            imageUrl = (String)params[0].get(1);

            try{
                HttpClient httpclient = new DefaultHttpClient();
                HttpResponse response = httpclient.execute(new HttpGet(imageUrl));
                in = response.getEntity().getContent();
            } catch(Exception e){
                e.printStackTrace();
            }
            try {
                bitmapa = BitmapFactory.decodeStream(in);
                in.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }

            return bitmap;
        }


        @Override
        protected void onPostExecute(Bitmap bitmap) {

            currentView.setImageBitmap(bitmap);
        }

而且它会导致许多内存问题:

E/dalvikvm-heap(369): 69560740-byte external allocation too large for this process.
E/GraphicsJNI(369): VM won't let us allocate 69560740 bytes

或者

E/AndroidRuntime(369): java.lang.RuntimeException: An error occured while executing doInBackground()
E/AndroidRuntime(369):  at android.os.AsyncTask$3.done(AsyncTask.java:200)
E/AndroidRuntime(369):  at java.util.concurrent.FutureTask$Sync.innerSetException(FutureTask.java:274)
E/AndroidRuntime(369):  at java.util.concurrent.FutureTask.setException(FutureTask.java:125)
E/AndroidRuntime(369):  at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:308)
E/AndroidRuntime(369):  at java.util.concurrent.FutureTask.run(FutureTask.java:138)
E/AndroidRuntime(369):  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1088)
E/AndroidRuntime(369):  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:581)
E/AndroidRuntime(369):  at java.lang.Thread.run(Thread.java:1019)
E/AndroidRuntime(369): Caused by: java.lang.OutOfMemoryError: bitmap size exceeds VM budget
E/AndroidRuntime(369):  at android.graphics.BitmapFactory.nativeDecodeStream(Native Method)
E/AndroidRuntime(369):  at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:470)
E/AndroidRuntime(369):  at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:525)
E/AndroidRuntime(369):  at com.package.app.ImageDownloader.doInBackground(ImageDownloader.java:78)
E/AndroidRuntime(369):  at com.package.app.ImageDownloader.doInBackground(ImageDownloader.java:1)
E/AndroidRuntime(369):  at android.os.AsyncTask$2.call(AsyncTask.java:185)
E/AndroidRuntime(369):  at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:306)
E/AndroidRuntime(369):  ... 4 more

http://stackoverflow.com/a/24135283/294884 - Fattie
2个回答

2
你测试这个的操作系统版本是什么?在早期版本中,可用的堆空间比后期版本要低得多。
我会认真考虑缩小位图的大小以避免此问题。如果你为 mdpi 手机下载了一个 2000x1000 的位图,那么这可能是一个糟糕的主意。
我还建议阅读 this article 以获取有关如何根据其尺寸精确加载适当的图像到特定的 ImageView 的更多信息。
最后,你应该始终在 finally 块中关闭 InputStream
try {
        bitmap = BitmapFactory.decodeStream(in);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (in != null) { in.close(); }
    }

还有一个android:largeHeap="true",你可以考虑使用,它是专门为处理大位图的应用程序设计的,比如照片编辑应用程序。

感谢您的回答。我的目标SDK版本是15,最低版本为4。我已经阅读了那篇文章,但它并没有帮助我——问题在于我无法调整或缩放我的图像,因为它必须保持可缩放性,并在缩放时显示细节。 - michalsol
我不会骗你,你不可能找到一个适合2000x1000像素图像的内存解决方案。想一想,无论你做什么,都会超出可用堆空间。也许你可以找到一种类似Chrome网页浏览器的不同策略,只在内存中渲染图像的一部分,当用户移动视口时,你可以快速重新加载一个基于当前视口的新窗口。 - dnkoutso
我考虑过这个问题——重新加载图像的缩放部分——但实现起来似乎相当困难。你有任何相关教程吗?谢谢! - michalsol
你可以查看画廊应用程序的源代码,看看它们是如何处理的。这些图片来自手机相机,所以可能是一个不错的起点! - dnkoutso

2

一张2000x1000像素的图片很大,但并不是那么大。

我看到,尽管这个过程在死亡之前试图分配69560740字节,大约为66兆字节!

您的2000x1000像素,在最坏的情况下,大约是2000x1000x4 = 8兆字节,远小于66兆字节。还有其他问题。

此外,使用位图作为AsyncTasks的返回/结果值存在问题。已完成的AsyncTasks仍然保留其结果,直到它们被垃圾回收。由于您无法控制AsyncTask实例的垃圾回收,因此不要将位图用作返回/结果值。相反,将位图放入字段中,并在doInBackground即将结束时将其分配,并使用此字段在onPostExecute中获取位图,并(!)将此字段设置为null:

Bitmap bitmap;
protected Void doInBackground(ArrayList... params) {

        InputStream in = null;
        imageUrl = (String)params[0].get(1);

        try{
            HttpClient httpclient = new DefaultHttpClient();
            HttpResponse response = httpclient.execute(new HttpGet(imageUrl));
            in = response.getEntity().getContent();
        } catch(Exception e){
            e.printStackTrace();
        }
        try {
            bitmap = BitmapFactory.decodeStream(in);
            in.close();
        } catch (IOException e1) {
            e1.printStackTrace();
        }

        return null;
    }


    @Override
    protected void onPostExecute(Void unused) {
        if (bitmap != null) {
            currentView.setImageBitmap(bitmap);
        }

        // And don't forget to null the bitmap field!
        bitmap = null; 
    }

作为对我之前回答的额外评论。 我认为您的服务器返回非常非常大的图像,因为VM尝试分配69,500,000字节。这将是一个接近3000x3000像素的ARGB_8888图像。 - Streets Of Boston
这个解决方案真的改善了我的内存使用情况。我只是按照你说的做,从“另外,我发现在AsyncTasks中使用位图作为返回/结果值存在问题......”开始。我按照你的建议修改了我的代码,现在可以显示大约两倍的图像而不会崩溃。谢谢。 - Yunus Yurtturk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接