在 Android 应用中加载位图很棘手主要有以下几个原因:
-
位图很容易耗尽应用程序的内存。例如,Pixel 手机上的相机拍摄的照片最高可达 4048 x 3036 像素(1200 万像素)。如果使用的位图配置是ARGB_8888,则默认为 Android 2.3(API 级别 9)及更高版本,加载单张照片进入内存需要大约 48 MB 的内存(4048 * 3036 * 4 字节)。如此大的内存需求可以立即耗尽应用程序可用的所有内存。
-
在 UI 线程上加载位图会降低应用程序的性能,导致响应速度慢甚至 ANR。因此,在使用位图时适当地管理线程非常重要。
-
如果你的应用程序正在将多个位图加载到内存中,则需要有技巧地管理内存和磁盘缓存。否则,应用程序 UI 的响应性和流畅性可能会受到影响。
多数情况下建议使用 Glide 库来获取,解码和显示应用中的位图。其他受欢迎的图像加载库还有 Square 的 Picasso 和 Facebook 的 Fresco。这些库简化了与 Android 上的位图和其他类型图像相关的大多数复杂任务。你还可以选择直接使用 Android 框架中内置的低级 API。有关执行此操作的详细信息,请参阅 Loading Large Bitmaps Efficiently,Caching Bitmaps,和 Managing Bitmap Memory。
一、高效加载大位图 — Loading Large Bitmaps Efficiently
下面介绍如何通过在内存中加载较小的子采样版本来解码大型位图,而不会超出每个应用程序的内存限制。
1.1 读取位图尺寸和类型 — Read Bitmap Dimensions and Type
BitmapFactory 类提供了几种解码方法(decodeByteArray()
,decodeFile()
,decodeResource()
等),用于从各种源创建位图。根据图像数据源选择最合适的解码方法。这些方法尝试为构造的位图分配内存,因此很容易导致 OutOfMemory 异常。每种类型的解码方法都有其他签名,可让你通过 BitmapFactory.Options
类指定解码选项。解码时将 inJustDecodeBounds
属性设置为 true 可避免内存分配,为位图对象返回 null 但设置 outWidth,outHeight 和 outMimeType。此技术允许你在构造(和内存分配)位图之前读取图像数据的尺寸和类型。
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
异常,请在解码之前检查位图的尺寸,除非你完全信任该源为你提供可预测大小的图像数据,这些数据可以轻松地放入可用内存中。
1.2 将缩小版本加载到内存中 — Load a Scaled Down Version into Memory
既然图像尺寸已知,它们可用于决定是否应将完整图像加载到内存中,或者是否应加载子采样版本。以下是需要考虑的一些因素:
-
估计在内存中加载完整图像的内存使用情况。
-
根据应用程序的其他内存要求,你愿意加载此图片的内存量。
-
要加载图像的目标 ImageView 或 UI 组件的尺寸。
-
当前设备的屏幕尺寸和密度。
例如,如果最终将在 ImageView 中以 128 x 96 像素的缩略图显示,则不值得将 1024 x 768 像素图像加载到内存中。
要告诉解码器对图像进行子采样,将较小的版本加载到内存中,请在 BitmapFactory.Options
对象中将 inSampleSize
设置为 true。例如,使用 inSampleSize
为 4 解码的分辨率为 2048 x 1536 的图像会产生大约 512 x 384 的位图。将其加载到内存中对于完整图像使用 0.75 MB 而不是 12 MB(假设 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 的幂是因为解码器使用最终值,通过向下舍入到最接近的 2 的幂,根据 inSampleSize 文档。
要使用此方法,首先将 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);
}
此方法可以轻松地将任意大尺寸的位图加载到显示 100 x 100 像素缩略图的 ImageView 中,如以下示例代码所示:
mImageView.setImageBitmap(
decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
你可以按照类似的过程解码来自其他来源的位图,方法是根据需要替换相应的 BitmapFactory.decode*
方法。
二、管理位图内存 — Managing Bitmap Memory
除了缓存位图中描述的步骤之外,你还可以执行一些特定操作来促进垃圾回收和位图重用。推荐的策略取决于你的目标 Android 版本。以下内容将向你展示如何设计应用程序以在不同版本的 Android 中高效工作。
为了之后更好的说明,我们先看看 Android 对位图内存管理的演变过程:
-
在 Android 2.2(API 级别 8)及更低版本中,当垃圾回收发生时,你的应用程序的线程会停止。这会导致延迟,从而降低性能。Android 2.3 添加了并发垃圾回收,这意味着当不再引用位图时很快就会回收内存。
-
在 Android 2.3.3(API 级别 10)及更低版本中,位图的像素数据存储在 native 内存中。它与位图本身是分开的,位图本身存储在 Dalvik 堆中。native 内存中的像素数据不会以可预测的方式释放,这可能导致应用程序短暂超出其内存限制并崩溃。从 Android 3.0(API 级别 11)到 Android 7.1(API 级别 25),像素数据与相关位图一起存储在 Dalvik 堆上。在 Android 8.0(API 级别 26)及更高版本中,位图像素数据存储在 native 堆中。
以下部分介绍如何针对不同的 Android 版本优化位图内存管理。
2.1 在 Android 2.3.3 和更低版本上管理内存 — Manage Memory on Android 2.3.3 and Lower
在 Android 2.3.3(API 级别 10)及更低版本上,建议使用 recycle()
。如果你在应用中显示大量位图数据,则可能会遇到 OutOfMemoryError 错误。recycle()
方法允许应用程序尽快回收内存。
警告:只有在确定不再使用位图时才应使用
recycle()
。如果你调用recycle()
并稍后尝试绘制该位图,将收到错误:"Canvas: trying to use a recycled bitmap"
。
以下代码片段给出了调用 recycle()
的示例。它使用引用计数(在变量 mDisplayRefCount 和 mCacheRefCount 中)来跟踪当前位图是正在显示还是在缓存中。当满足以下条件时,代码会回收位图:
-
mDisplayRefCount 和 mCacheRefCount 的引用计数均为 0。
-
位图不为 null 且尚未回收。
private int mCacheRefCount = 0;
private int mDisplayRefCount = 0;
...
// Notify the drawable that the displayed state has changed.
// Keep a count to determine when the drawable is no longer displayed.
public void setIsDisplayed(boolean isDisplayed) {
synchronized (this) {
if (isDisplayed) {
mDisplayRefCount++;
mHasBeenDisplayed = true;
} else {
mDisplayRefCount--;
}
}
// Check to see if recycle() can be called.
checkState();
}
// Notify the drawable that the cache state has changed.
// Keep a count to determine when the drawable is no longer being cached.
public void setIsCached(boolean isCached) {
synchronized (this) {
if (isCached) {
mCacheRefCount++;
} else {
mCacheRefCount--;
}
}
// Check to see if recycle() can be called.
checkState();
}
private synchronized void checkState() {
// If the drawable cache and display ref counts = 0, and this drawable
// has been displayed, then recycle.
if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed
&& hasValidBitmap()) {
getBitmap().recycle();
}
}
private synchronized boolean hasValidBitmap() {
Bitmap bitmap = getBitmap();
return bitmap != null && !bitmap.isRecycled();
}
2.2 在 Android 3.0 及更高版本上管理内存 — Manage Memory on Android 3.0 and Higher
Android 3.0(API 级别 11)引入了 BitmapFactory.Options.inBitmap
字段。如果设置了此选项,则使用 Options 对象的解码方法将在加载内容时尝试重用现有位图。这意味着重用了位图的内存,从而提高了性能,并省去了内存分配和回收的步骤。但是,使用 inBitmap 有一些限制。比如说,在 Android 4.4(API 级别 19)之前,仅支持相同大小的位图。
2.2.1 保存位图供以后使用 — Save a bitmap for later use
以下代码段演示了如何存储现有位图以便以后使用。当应用程序在 Android 3.0 或更高版本上运行并且位图从 LruCache 中移出时,可以把位图的软引用放置在 HashSet 中,以便稍后可以在 inBitmap 中重用:
Set<SoftReference<Bitmap>> mReusableBitmaps;
private LruCache<String, BitmapDrawable> mMemoryCache;
// If you're running on Honeycomb or newer, create a
// synchronized HashSet of references to reusable bitmaps.
if (Utils.hasHoneycomb()) {
mReusableBitmaps =
Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
}
mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {
// Notify the removed entry that is no longer being cached.
@Override
protected void entryRemoved(boolean evicted, String key,
BitmapDrawable oldValue, BitmapDrawable newValue) {
if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
// The removed entry is a recycling drawable, so notify it
// that it has been removed from the memory cache.
((RecyclingBitmapDrawable) oldValue).setIsCached(false);
} else {
// The removed entry is a standard BitmapDrawable.
if (Utils.hasHoneycomb()) {
// We're running on Honeycomb or later, so add the bitmap
// to a SoftReference set for possible use with inBitmap later.
mReusableBitmaps.add
(new SoftReference<Bitmap>(oldValue.getBitmap()));
}
}
}
....
}
2.2.2 使用现有位图 — Use an existing bitmap
在正在运行的应用程序中,decoder 方法检查是否存在可以使用的现有位图。例如:
public static Bitmap decodeSampledBitmapFromFile(String filename,
int reqWidth, int reqHeight, ImageCache cache) {
final BitmapFactory.Options options = new BitmapFactory.Options();
...
BitmapFactory.decodeFile(filename, options);
...
// If we're running on Honeycomb or newer, try to use inBitmap.
if (Utils.hasHoneycomb()) {
addInBitmapOptions(options, cache);
}
...
return BitmapFactory.decodeFile(filename, options);
}
以下代码段显示了上述代码段中调用的 addInBitmapOptions()
方法。它查找现有位图以设置为 inBitmap 的值。请注意,如果找到合适的匹配项,此方法仅为 inBitmap 设置一个值(你永远不应该假设一定能找到匹配项):
private static void addInBitmapOptions(BitmapFactory.Options options,
ImageCache cache) {
// inBitmap only works with mutable bitmaps, so force the decoder to
// return mutable bitmaps.
options.inMutable = true;
if (cache != null) {
// Try to find a bitmap to use for inBitmap.
Bitmap inBitmap = cache.getBitmapFromReusableSet(options);
if (inBitmap != null) {
// If a suitable bitmap has been found, set it as the value of
// inBitmap.
options.inBitmap = inBitmap;
}
}
}
// This method iterates through the reusable bitmaps, looking for one
// to use for inBitmap:
protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
Bitmap bitmap = null;
if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {
synchronized (mReusableBitmaps) {
final Iterator<SoftReference<Bitmap>> iterator
= mReusableBitmaps.iterator();
Bitmap item;
while (iterator.hasNext()) {
item = iterator.next().get();
if (null != item && item.isMutable()) {
// Check to see it the item can be used for inBitmap.
if (canUseForInBitmap(item, options)) {
bitmap = item;
// Remove from reusable set so it can't be used again.
iterator.remove();
break;
}
} else {
// Remove from the set if the reference has been cleared.
iterator.remove();
}
}
}
}
return bitmap;
}
最后,使用以下方法确定候选位图是否满足用于 inBitmap 的大小标准:
static boolean canUseForInBitmap(
Bitmap candidate, BitmapFactory.Options targetOptions) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// From Android 4.4 (KitKat) onward we can re-use if the byte size of
// the new bitmap is smaller than the reusable bitmap candidate
// allocation byte count.
int width = targetOptions.outWidth / targetOptions.inSampleSize;
int height = targetOptions.outHeight / targetOptions.inSampleSize;
int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
return byteCount <= candidate.getAllocationByteCount();
}
// On earlier versions, the dimensions must match exactly and the inSampleSize must be 1
return candidate.getWidth() == targetOptions.outWidth
&& candidate.getHeight() == targetOptions.outHeight
&& targetOptions.inSampleSize == 1;
}
/**
* A helper function to return the byte usage per pixel of a bitmap based on its configuration.
*/
static int getBytesPerPixel(Config config) {
if (config == Config.ARGB_8888) {
return 4;
} else if (config == Config.RGB_565) {
return 2;
} else if (config == Config.ARGB_4444) {
return 2;
} else if (config == Config.ALPHA_8) {
return 1;
}
return 1;
}
网友评论