Android Bitmap 高效加载以及内存管理

Handling bitmaps

https://youtu.be/HY9aaXHx8yA

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

猜你喜欢

转载自blog.csdn.net/weixin_34026484/article/details/86995648