加载图像到Bitmap对象时出现奇怪的OutOfMemory问题

1386

我有一个ListView,每行都有几个图像按钮。当用户点击列表行时,它会启动一个新的活动。由于相机布局的问题,我不得不构建自己的选项卡。被启动的用于结果的活动是一个地图。如果我点击我的按钮来启动图像预览(从SD卡加载图像),应用程序将从活动返回到ListView活动,然后返回结果处理程序以重新启动我的新活动,该活动仅是一个图像小部件。

ListView上的图像预览是使用游标和ListAdapter完成的。这使得它非常简单,但我不确定如何在飞行中放置调整大小的图像(即较小的位大小而不是像素作为图像按钮的src)。因此,我只调整了从手机相机拍摄的图像的大小。

问题是当它尝试返回并重新启动第二个活动时,会出现OutOfMemoryError

  • 有没有一种简单的方法可以逐行构建列表适配器,其中我可以在飞行中调整大小(按位)?

这将是首选的,因为我还需要更改每行中小部件/元素的属性,因为由于焦点问题,我无法使用触摸屏选择行。(我可以使用滚轮球。

  • 我知道我可以进行带外调整大小并保存我的图像,但这并不是我想做的事情,但一些关于那方面的示例代码会很好。

一旦我禁用了ListView上的图像,它就可以正常工作了。

顺便说一句:这就是我做的方式:

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 

当显示图片时,我也遇到了一个新的错误:

22:13:18.594: DEBUG/skia(4204): xxxxxxxxxxx jpeg error 20 Improper call to JPEG library in state %d
22:13:18.604: INFO/System.out(4204): resolveUri failed on bad bitmap uri: 
22:13:18.694: ERROR/dalvikvm-heap(4204): 6291456-byte external allocation too large for this process.
22:13:18.694: ERROR/(4204): VM won't let us allocate 6291456 bytes
22:13:18.694: DEBUG/skia(4204): xxxxxxxxxxxxxxxxxxxx allocPixelRef failed

9
我通过避免使用Bitmap.decodeStream或decodeFile方法,改用BitmapFactory.decodeFileDescriptor方法来解决这个问题。 - Fraggle
2
我几周前也遇到了类似的问题,通过将图像缩小到最佳点来解决它。我在我的博客http://codingjunkiesforum.wordpress.com/2014/06/12/outofmemory-due-to-large-bitmap-handling-in-android/中写了完整的方法,并上传了完整的示例项目,其中包含OOM易感代码与OOM证明代码https://github.com/shailendra123/BitmapHandlingDemo。 - Shailendra Singh Rajawat
6
这个问题的被接受答案正在元社区上讨论。 - rene
2
阅读这篇博客文章:http://codingaffairs.blogspot.com/2016/07/processing-bitmap-and-memory-management.html - Developine
5
这是由于糟糕的Android架构造成的。它应该像iOS和UWP一样自动调整图像大小。我不需要亲自做这些事情。Android开发人员习惯了那种困难,认为它可以正常工作。 - Access Denied
显示剩余3条评论
44个回答

920

为了解决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;
}

32
请注意,尽管文档建议使用 2 的幂次方,但 10 可能不是 inSampleSize 的最佳值。 - Mirko N.
71
我面临的问题和Chrispix一样,但我不认为这里的解决方案真正解决了问题,而是避开了它。改变样本大小可以减少内存使用量(以图像质量为代价,对于图像预览来说可能还好),但如果解码足够大的图像流或多个图像流,则无法防止出现异常情况。如果我找到更好的解决方案(也许没有更好的),我会在这里发布答案。 - Flynn81
5
您只需选取与屏幕像素密度相匹配的适当尺寸,如需缩放等功能,则可使用更高密度的采样图像。 - stealthcopter
5
"REQUIRED_SIZE" 是你想要缩放到的新尺寸。 - Fedor
10
这个解决方案对我有帮助,但图像质量很差。我正在使用ViewFlipper来显示图片,请问有什么建议吗? - user1106888
显示剩余5条评论

692
Android Training 中的"高效显示位图"课程提供了一些关于理解和处理在加载位图时出现`java.lang.OutOfMemoryError: bitmap size exceeds VM budget`异常的好信息。

读取位图尺寸和类型

BitmapFactory类提供几种解码方法(decodeByteArray(), decodeFile(), decodeResource()等)从不同的来源创建一个Bitmap。根据您的图像数据源选择最合适的解码方法。这些方法试图为构建的位图分配内存,因此很容易导致OutOfMemory异常。每种类型的解码方法都有额外的签名,让您通过BitmapFactory.Options类指定解码选项。在解码时将inJustDecodeBounds属性设置为true可以避免内存分配,并返回null的位图对象,但设置outWidthoutHeightoutMimeType。这种技术允许您在构建(和内存分配)位图之前读取图像数据的尺寸和类型。
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 或 UI 组件的尺寸。
  • 当前设备的屏幕尺寸和密度。

例如,如果最终将在 ImageView 中显示 128x96 像素的缩略图,则没有必要将 1024x768 像素的图像加载到内存中。

要告诉解码器对图像进行子采样,即将较小版本加载到内存中,请在BitmapFactory.Options对象中将inSampleSize设置为true。例如,使用inSampleSize为4解码分辨率为2048x1536的图像会产生大约512x384的位图。将其加载到内存中只使用0.75MB,而不是完整图像的12MB(假设位图配置为ARGB_8888)。以下是根据目标宽度和高度计算2的幂次方采样大小值的方法:

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

注意: 计算出的是2的幂次方值,因为解码器会按照inSampleSize文档所述向下舍入到最近的2的幂次方。

要使用此方法,首先将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* 方法进行替换。


24
这个答案正在元社区上讨论。 - rene
10
除了通过链接获得的信息外,这个回答并没有提供太多解决方案。链接中的重要部分应该合并到问题中。 - FallenAngel
8
这个回答和问题以及其他回答都是社区维基,因此社区可以通过编辑来解决这个问题,这不需要版主介入。 - Martijn Pieters
非常有帮助的2022年。试图在旧的HTC One S上运行一个应用程序。一个31MB的图像在内存流中太大了,哈哈。 - TwoFingerRightClick

383

我对Fedor的代码进行了小改进。它基本上做了相同的事情,但没有使用(在我看来)丑陋的while循环,并且它始终得到2的幂次方。感谢Fedor的原创解决方案,我一直卡住了,直到找到他的代码后,我才能够制作这个版本 :)

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

42
是的,你说得对,虽然这不是很美观。我只是试着让每个人都能明白。谢谢你的代码。 - Fedor
12
那段代码存在一个大问题。^符号并不是求2的幂,它实际上是将2与结果进行异或。你需要使用Math.pow(2.0, ...)来求2的幂。除此之外,代码看起来很好。 - DougW
7
哦,这真是一个很好的建议!我的错,我会立即更正它,感谢您的回复! - Thomas Vervest
9
你正在创建两个新的FileInputStream,每次调用 BitmapFactory.decodeStream() 都会创建一个。难道你不需要保存对它们的引用,以便在 finally 块中关闭它们吗? - matsev
2
@Babibu 文档没有说明流已经关闭,因此我认为它应该仍然被关闭。在这里可以找到一个有趣且相关的讨论(https://dev59.com/Fm865IYBdhLWcg3wFqnY)。请注意Adrian Smith的评论,它直接关系到我们的争论。 - Thomas Vervest
1
2022年很有帮助。试图在HTC One S上运行应用程序。无法加载31MB的流,哈哈,这个方法非常棒,因为我已经压缩了要上传到ML API的图像。 - TwoFingerRightClick

238

我具有 iOS 开发经验,但发现一个非常基础的问题:加载和显示图片。尽管这个问题困扰着许多人,他们都试图显示大小合理的图片。无论如何,以下两个更改解决了我的问题(并且使我的应用十分灵敏)。

1)每次使用 BitmapFactory.decodeXYZ() 时,请确保传入一个 BitmapFactory.Options,并将 inPurgeable 设为 true(最好还设 inInputShareabletrue)。

2)绝不要使用 Bitmap.createBitmap(width, height, Config.ARGB_8888)。我是说永远不要!在进行几次操作后,我从未遇到过该方法不引发内存错误的情况。无论你如何调用 recycle()System.gc(),或其他任何方法,它总会抛出异常。另一个实际可行的方法是在 drawables 中放置一个虚拟图片(或者使用上文中步骤 1 解码的 Bitmap),将其缩放到所需大小,然后操纵生成的 Bitmap(例如将其传递给 Canvas 进一步处理)。因此,你应该使用如下方法代替:Bitmap.createScaledBitmap(srcBitmap, width, height, false)。如果你无论如何都必须使用粗暴的 create 方法,那么至少要传入 Config.ARGB_4444

这几乎可以确保为你节省数小时甚至数天的时间。所有关于缩放图像等的讨论都不是真正的解决方案(除非你认为获取错误大小或降级图像是一种解决方案)。


23
BitmapFactory.Options options = new BitmapFactory.Options(); options.inPurgeable = true;Bitmap.createScaledBitmap(srcBitmap, width, height, false); 解决了我在 Android 4.0.0 上遇到的内存溢出异常问题。谢谢! - Jan-Terje Sørensen
6
在调用Bitmap.createScaledBitmap()方法时,建议将标志参数设置为true。否则,在放大图像时,图像质量不会平滑。请查看此线程https://dev59.com/SHE85IYBdhLWcg3wMQlm - rOrlig
12
那真是太棒的建议了。希望我能给你多加一个赞,因为你对谷歌这个非常瑕疵的错误提出了批评。如果这不是一个bug,那么文档就需要有一些极其醒目的提示说“这是如何处理照片的”,因为我已经苦苦挣扎了两年,直到现在才发现这篇文章。真是个好发现。 - Yevgeny Simkin
11
从Lollipop版本开始,BitmapFactory.Options.inPurgeableBitmapFactory.Options.inInputShareable已被标记为过时。http://developer.android.com/reference/android/graphics/BitmapFactory.Options.html#inPurgeable - Denys Kniazhev-Support Ukraine

98

这是一个已知的错误,并不是由于文件过大引起的。由于Android缓存了可绘制对象,在使用几张图片后会导致内存不足。但我发现了一种替代方法,即跳过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);
}

83

我曾经遇到过这个问题,通过避免使用 BitmapFactory.decodeStream 或 decodeFile 函数,改用 BitmapFactory.decodeFileDescriptor 解决了这个问题。

decodeFileDescriptor 调用的本地方法与 decodeStream/decodeFile 不同。

总之,以下代码是有效的(注意我添加了一些选项,但这并不是重点。关键是要调用 BitmapFactory.decodeFileDescriptor 而不是 decodeStreamdecodeFile):

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方式分配的,而是通过本地调用;这些分配是在虚拟堆之外完成的,但是它们 会计入其中!"


2
同样的结果会导致内存不足,实际上使用哪种方法并不重要,它取决于你所持有的字节数,用于读取数据从而导致内存不足。 - PiyushMishra

75

我认为避免OutOfMemoryError的最佳方法是正视它并理解它。

我制作了一个应用程序,有意引起OutOfMemoryError,并监视内存使用情况。

在使用此应用程序进行了大量实验后,我得出以下结论:

首先,我要谈一下蜂窝之前的SDK版本。

  1. 位图存储在本地堆中,但会自动进行垃圾回收,调用recycle()是不必要的。

  2. 如果{VM堆大小} + {分配的本地堆内存} ≥ {设备的VM堆大小限制},并且您正在尝试创建位图,则将抛出OOM。

    注意:计算的是VM HEAP SIZE而不是VM ALLOCATED MEMORY。

  3. VM Heap大小将永远不会在增长后缩小,即使分配的VM内存缩小了。

  4. 因此,您必须尽可能保持峰值VM内存较低,以防止VM堆大小过大而为位图节省可用内存。

  5. 手动调用System.gc()没有意义,在尝试增加堆大小之前系统将首先调用它。

  6. 本地堆大小也永远不会缩小,但它不计入OOM,因此不必担心它。

然后,让我们谈谈从Honey Comb开始的SDK。

  1. 位图存储在VM堆中,本地内存不计入OOM。

  2. OOM的条件更简单:{VM堆大小} ≥ {设备的VM堆大小限制}。

  3. 因此,您可以使用相同的堆大小限制创建更多可用内存的位图,OOM的可能性较小。

以下是我关于垃圾收集和内存泄漏的一些观察。

您可以在应用程序中自行查看。如果一个Activity执行的AsyncTask在Activity被销毁后仍在运行,则该Activity将不会被垃圾回收,直到AsyncTask完成。

这是因为AsyncTask是匿名内部类的一个实例,它持有Activity的引用。

调用AsyncTask.cancel(true)将无法停止执行,如果任务在后台线程中被阻塞在IO操作中。

回调也是匿名内部类,因此,如果项目中的静态实例持有它们并且不释放它们,内存就会泄漏。

如果您安排了一个重复或延迟的任务,例如Timer,并且您在onPause()中没有调用cancel()和purge(),那么内存将被泄漏。


68
我最近看到了很多关于OOM异常和缓存的问题。开发者指南中有一篇非常好的文章,但有些人在实施方面仍然存在困难。
因此,我编写了一个示例应用程序,演示了在Android环境下的缓存实现。这个实现还没有出现过OOM。
请查看本答案末尾的链接以获取源代码。

要求:

  • Android API 2.1或更高版本(我无法在API 1.6中获取应用程序的可用内存 - 这是唯一在API 1.6中不起作用的代码片段)
  • Android支持包

Screenshot

特点:

  • 使用单例,在方向更改时保留缓存
  • 将分配的应用程序内存的1/8用于缓存(如果需要,可进行修改)
  • 对大位图进行缩放(您可以定义允许的最大像素)
  • 在下载位图之前,控制是否有可用的互联网连接
  • 确保每行只实例化一个任务
  • 如果您正在快速滑动 ListView,则不会下载其间的位图

不包括以下内容:

  • 磁盘缓存。无论如何,这都很容易实现 - 只需指向从磁盘获取位图的不同任务即可

示例代码:

正在下载的图像是来自Flickr的图像(75x75)。但是,您可以放置任何要处理的图像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。唯一的瓶颈是下载需要更长的时间。

只有一种可能的情况会导致OOM出现,那就是如果我们下载许多真正大的图像,并且在它们被缩放并放入缓存之前,将同时占用更多的内存并导致OOM。但是这甚至不是一个理想的情况,而且很可能无法以更可行的方式解决。

请在评论中报告错误! :-)


46

我按照以下方式获取图像并动态调整大小。希望这可以帮到你。

Bitmap bm;
bm = Bitmap.createScaledBitmap(BitmapFactory.decodeFile(filepath), 100, 100, true);
mPicture = new ImageView(context);
mPicture.setImageBitmap(bm);    

27
这种方法可以对位图进行缩放,但它并不能解决内存不足的问题,因为完整的位图仍然会被解码。 - Fedor
6
我会看看我的旧代码,但我想它确实解决了我的内存不足问题。我会再次检查一下我的旧代码。 - Chrispix
3
在这个例子中,看起来你没有保留对整个位图的引用,因此节省了内存。 - NoBugs

39

不幸的是,如果以上方法都无效,则将此添加到您的清单文件中。在应用程序标签内部。

 <application
         android:largeHeap="true"

2
你能解释一下这实际上是做什么的吗?仅仅告诉别人添加这个并没有帮助。 - Stealth Rabbi
4
这是一个非常糟糕的解决方案。基本上,你并没有试图修复问题,而是要求安卓系统为你的应用程序分配更多的堆空间。这将对你的应用程序产生非常不良的影响,比如你的应用程序会消耗大量电池电量,因为垃圾回收器必须在大堆空间中运行以清理内存,而且你的应用程序性能会变慢。 - Prakash
3
为什么Android允许我们在清单文件中添加android:largeHeap="true"?现在你在质疑Android。 - Himanshu Mori
@HimanshuMori,你可能需要重新考虑使用android:largeHeap="true"的决定。请参考此答案https://dev59.com/DF4c5IYBdhLWcg3w6N07#30930239或该线程中的任何其他答案。这可能有助于您了解您所做的错误。 - abhiTronix

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