在安卓上,调整位图大小的最节省内存的方法是什么?

118
我正在构建一个图像密集型的社交应用程序,其中图像从服务器发送到设备。当设备具有较小的屏幕分辨率时,我需要在设备上调整位图的大小以匹配其预期的显示大小。
问题是使用createScaledBitmap会导致我在调整一大堆缩略图图像后遇到很多内存不足错误。
在Android上调整位图大小的最节省内存的方法是什么?

7
你的服务器能否发送正确的文件大小,以节省客户端的内存和带宽?! - James
2
只有在我拥有服务器资源并且它具有可用的计算组件,并且在所有情况下,它可以预测尚未见过的纵横比图像的确切尺寸时,这才是有效的。因此,如果您正在从第三方CDN(如我所做的)加载资源内容,则无法正常工作 :( - Colt McAnlis
2个回答

173
这个答案总结自高效加载大位图,该文章解释了如何使用inSampleSize来加载缩小的位图版本。
特别是预缩放位图解释了各种方法的细节,如何将它们组合以及哪些是最节省内存的。
在Android上调整位图大小有三种主要方法,它们具有不同的内存属性: createScaledBitmap API 此API将接受现有位图,并创建一个新的位图,其精确尺寸为您选择的尺寸。

好的一面是,你可以得到你想要的完全尺寸的图像(无论它看起来如何)。但是不好的一面是,这个API需要一个现有的位图才能工作。这意味着在能够创建新的、更小的版本之前,必须加载、解码并创建位图。从获得精确的尺寸方面来说,这是理想的,但从额外的内存开销方面来说却很糟糕。因此,对于大多数注重内存的应用程序开发人员来说,这有点成为了一个致命问题。

inSampleSize标志

BitmapFactory.Options有一个被称为inSampleSize的属性,它将在解码图像时调整图像大小,以避免需要解码到临时位图。这里使用的整数值将以1/x的缩小比例加载图像。例如,将inSampleSize设置为2会返回一个尺寸减半的图像,将其设置为4会返回一个尺寸为原始图像的1/4。基本上,图像大小总是比源大小小二的幂。

从内存角度来看,使用inSampleSize是一项非常快速的操作。实际上,它只会将您图像中的每X个像素解码为最终位图。然而,inSampleSize存在两个主要问题:

  • 它无法给出精确的分辨率。它只能将位图的大小减小2的某个幂次。

  • 它不能产生最佳质量的调整大小效果。大多数调整大小滤镜通过读取像素块并加权处理这些像素块以产生所需的调整后的像素,从而生成外观良好的图像。 inSampleSize通过仅读取每几个像素来避免所有这些操作。结果具有相当高的性能和低内存,但质量会受到影响。

如果您只需要通过某些pow2大小缩小图像,并且过滤不是问题,则无法找到比inSampleSize更节省内存(或更高效)的方法。

inScaled、inDensity、inTargetDensity标志

如果您需要将图像缩放到不是2的幂次方的尺寸,则需要使用BitmapOptionsinScaledinDensityinTargetDensity标志。当设置了inScaled标志时,系统将通过将inTargetDensity除以inDensity值来推导应用于位图的缩放值。请注意,保留HTML标签。
mBitmapOptions.inScaled = true;
mBitmapOptions.inDensity = srcWidth;
mBitmapOptions.inTargetDensity =  dstWidth;

// will load & resize the image to be 1/inSampleSize dimensions
mCurrentBitmap = BitmapFactory.decodeResources(getResources(), 
      mImageIDs, mBitmapOptions);

使用这种方法将重新调整您的图像,并对其应用“调整大小滤镜”,即最终结果看起来更好,因为在调整大小步骤中考虑了一些额外的数学。但要注意:那个额外的滤镜步骤需要额外的处理时间,并且对于大图像可能会快速累加,导致缓慢的调整大小以及用于滤镜本身的额外内存分配。
通常不建议将此技术应用于明显大于所需大小的图像,因为存在额外的过滤开销。
神奇组合
从内存和性能的角度来看,您可以结合使用这些选项以获得最佳结果。(设置inSampleSize、inScaled、inDensity和inTargetDensity标志)
inSampleSize首先应用于图像,使其达到比目标大小大的下一个2的幂。然后,inDensity和inTargetDensity用于将结果缩放到您想要的精确尺寸,应用滤镜操作清理图像。
将这两个步骤结合起来,速度会更快,因为 inSampleSize 步骤会减少密度缩放步骤需要应用其调整大小过滤器的像素数量。
mBitmapOptions.inScaled = true;
mBitmapOptions.inSampleSize = 4;
mBitmapOptions.inDensity = srcWidth;
mBitmapOptions.inTargetDensity =  dstWidth * mBitmapOptions.inSampleSize;

// will load & resize the image to be 1/inSampleSize dimensions
mCurrentBitmap = BitmapFactory.decodeFile(fileName, mBitmapOptions);

如果您需要将图像适配到特定尺寸,并且需要一些更好的过滤效果,那么这种技术是获得正确大小的最佳方法,但是可以通过快速、低内存占用的操作来完成。
获取图像尺寸
为了调整位图大小,您需要知道输入尺寸。您可以使用inJustDecodeBounds标志来获取图像的尺寸,而无需实际解码像素数据。
// Decode just the boundaries
mBitmapOptions.inJustDecodeBounds = true;
BitmapFactory.decodeFile(fileName, mBitmapOptions);
srcWidth = mBitmapOptions.outWidth;
srcHeight = mBitmapOptions.outHeight;


//now go resize the image to the size you want

你可以使用此标志先解码大小,然后计算适合目标分辨率缩放的正确值。

1
你能告诉我们dstWidth是什么吗? - Kosh
好文!以前不知道inScaled、inDensity和inTargetDensity这些标志… - maveroid
如果我没有文件名,只有从Google照片获取的文件Uri,我该如何使用这种技术? - kolyaseg
1
请注意,使用 Bitmap 方法缩小图像会导致锯齿,因为这些方法只使用双线性插值(没有预过滤)。我写了一篇文章,其中解释了这个问题,并提出了一个解决方案,利用 RenderScript 适当地缩小图像。文章链接:https://medium.com/@petrakeas/alias-free-resize-with-renderscript-5bf15a86ce3 - Petrakeas
1
我写了一篇文章,在这篇文章中,我将这种方法与基于RenderScript的方法进行了比较,当缩小2的幂次方或任意大小时,我比较了质量和性能。 - Petrakeas
显示剩余5条评论

13
尽管这个回答很好(而且准确),但它也非常复杂。与其重新发明轮子,不如考虑使用像GlidePicassoUILIon或其他任何实现此复杂和容易出错逻辑的库。
甚至Colt自己在预缩放位图性能模式视频中建议查看Glide和Picasso。
通过使用库,您可以获得Colt回答中提到的每一点效率,但具有大大简化的API,可以在每个Android版本上保持一致。

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