在 android 上调整位图大小的最有效内存方式?

Most memory efficient way to resize bitmaps on android?

我正在构建一个图像密集型社交应用程序,图像从服务器发送到设备。当设备的屏幕分辨率较小时,我需要在设备上调整位图的大小,以匹配其预期的显示尺寸。

问题是使用 createScaledBitmap 导致我 运行 在调整大量缩略图的大小后出现大量内存不足错误.

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

This answer is summarised from Loading large bitmaps Efficiently which explains how to use inSampleSize to load a down-scaled bitmap version.

In particular Pre-scaling bitmaps explains the details of various methods, how to combine them, and which are the most memory efficient.

在 Android 上调整位图大小的主要方法有三种,它们具有不同的内存属性:

createScaledBitmap API

此 API 将采用现有位图,并创建一个具有您选择的确切尺寸的新位图。

有利的一面是,您可以获得所需的准确图像尺寸(无论其外观如何)。但缺点是,这个API需要一个现有的位图才能工作。这意味着必须先加载、解码图像并创建位图,然后才能创建新的、更小的版本。这在获取精确尺寸方面是理想的,但在额外的内存开销方面却很糟糕。因此,对于大多数倾向于记忆的应用程序开发人员来说,这有点像交易破坏者

inSampleSize flag

BitmapFactory.Options 有一个 属性 标记为 inSampleSize,它将在解码时调整图像大小,以避免解码为临时位图的需要。此处使用的这个整数值将以 1/x 缩小的尺寸加载图像。例如,将 inSampleSize 设置为 2 returns 图像大小的一半,并将其设置为 4 returns 图像大小的 1/ 4。基本上图像大小总是比源大小小一些二次方。

从内存的角度来看,使用 inSampleSize 是一个非常快的操作。实际上,它只会将图像的每个 Xth 像素解码为生成的位图。 inSampleSize 有两个主要问题:

  • 它没有给你确切的分辨率。它只会将位图的大小减小 2 的某个幂。

  • 它不会产生最佳质量的调整大小。大多数调整大小的过滤器通过读取像素块来生成好看的图像,然后对它们进行加权以生成有问题的调整大小的像素。 inSampleSize 通过每隔几个像素读取一次就避免了这一切。结果相当高效,内存不足,但质量受到影响。

如果您只是处理将图像缩小一些 pow2 大小,并且过滤不是问题,那么您找不到比 inSampleSize 更节省内存(或性能)的方法了.

inScaled, inDensity, inTargetDensity flags

如果您需要将图像缩放到不等于 2 的幂的尺寸,那么您将需要 inScaledinDensityinTargetDensity 标志 BitmapOptions。设置 inScaled 标志后,系统将通过将 inTargetDensity 除以 inDensity 值来导出要应用于位图的缩放值。

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

使用此方法将调整图像大小,并对其应用“调整大小过滤器”,也就是说,最终结果看起来会更好,因为在调整大小步骤中考虑了一些额外的数学运算。但请注意:额外的过滤步骤需要额外的处理时间,并且可以快速添加大图像,导致调整大小缓慢,并为过滤器本身分配额外的内存。

由于额外的过滤开销,将此技术应用于明显大于所需尺寸的图像通常不是一个好主意。

魔法组合

从内存和性能的角度来看,您可以结合使用这些选项以获得最佳效果。 (设置 inSampleSizeinScaledinDensityinTargetDensity 标志)

inSampleSize 将首先应用于图像,使其达到比您的目标尺寸大的下一个二次方。然后,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 标志来帮助您获取图像的尺寸,w/o 需要实际解码像素数据。

// 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

您可以先使用此标志解码大小,然后计算缩放到目标分辨率的适当值。

这个答案很好(而且很准确),但也很复杂。与其重新发明轮子,不如考虑像 Glide, Picasso, UIL, Ion 这样的库,或任何其他为您实现这种复杂且容易出错的逻辑的库。

Colt 本人甚至建议在 Pre-scaling Bitmaps Performance Patterns Video.

中看看 Glide 和 Picasso

通过使用库,您可以获得 Colt 的回答中提到的每一点效率,但使用在 Android 的每个版本中一致工作的非常简单的 API。

最好的方法是使用 coil 异步执行所有操作

Android 的图像加载库,由 Kotlin Coroutines 支持。线圈是:

Fast:Coil 执行多项优化,包括内存和磁盘缓存、对内存中的图像进行下采样、重新使用位图、自动 pausing/canceling 请求等. 轻量级:Coil 为您的 APK 添加了约 2000 种方法(对于已经使用 OkHttp 和协程的应用程序),这与 Picasso 相当,但明显少于 Glide 和 Fresco。 易于使用:Coil 的 API 利用 Kotlin 的语言功能实现简单性和最小样板。 现代:Coil 是 Kotlin-first 并使用现代库,包括 Coroutines、OkHttp、Okio 和 AndroidX Lifecycles。

Coil 是以下缩写:Coroutine Image Loader