管理位图内存

注意:在大多数情况下,我们建议您使用 Glide 库获取、解码和显示应用中的位图。在处理这些任务以及与位图和 Android 上的其他图片相关的其他任务时,Glide 会将大部分的复杂工作抽象出来。如需了解如何使用和下载 Glide,请访问 GitHub 上的 Glilt 代码库

除了缓存位图中介绍的步骤外,您可以采取一些具体措施来促进垃圾回收和位图重复使用。建议的策略取决于您的目标 Android 版本。本节课中附带的 BitmapFun 示例应用向您展示了如何通过设计让您的应用可在不同的 Android 版本上高效运行。

为给本节课奠定基础,下面介绍了 Android 位图内存管理的演变过程:

  • 在 Android Android 2.2(API 级别 8)及更低版本上,当发生垃圾回收时,应用的线程会停止。这会导致延迟,从而降低性能。Android 2.3 添加了并发垃圾回收功能,这意味着系统不再引用位图后,很快就会回收内存。
  • 在 Android 2.3.3(API 级别 10)及更低版本上,位图的后备像素数据存储在本地内存中。它与存储在 Dalvik 堆中的位图本身是分开的。本地内存中的像素数据并不以可预测的方式释放,可能会导致应用短暂超出其内存限制并崩溃。从 Android 3.0(API 级别 11)到 Android 7.1(API 级别 25),像素数据会与关联的位图一起存储在 Dalvik 堆上。在 Android 8.0(API 级别 26)及更高版本中,位图像素数据存储在原生堆中。

以下各部分介绍了如何针对不同 Android 版本优化位图内存管理。

在 Android 2.3.3 及更低版本上管理内存

在 Android 2.3.3(API 级别 10)及更低版本上,建议使用 recycle()。如果您在应用中显示大量位图数据,则可能会遇到 OutOfMemoryError 错误。利用 recycle() 方法,应用可以尽快回收内存。

注意:只有当您确定位图已不再使用时才应该使用 recycle()。如果您调用 recycle() 并在稍后尝试绘制位图,则会收到错误:"Canvas: trying to use a recycled bitmap"

以下代码段给出了调用 recycle() 的示例。它使用引用计数(在变量 mDisplayRefCountmCacheRefCount 中)来跟踪位图当前是在显示还是在缓存中。当满足以下条件时,代码便会回收位图:

  • mDisplayRefCountmCacheRefCount 的引用计数均为 0。
  • 位图不是 null,并且尚未被回收。

Kotlin

    private var cacheRefCount: Int = 0
    private var displayRefCount: Int = 0
    ...
    // Notify the drawable that the displayed state has changed.
    // Keep a count to determine when the drawable is no longer displayed.
    fun setIsDisplayed(isDisplayed: Boolean) {
        synchronized(this) {
            if (isDisplayed) {
                displayRefCount++
                hasBeenDisplayed = true
            } else {
                displayRefCount--
            }
        }
        // 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.
    fun setIsCached(isCached: Boolean) {
        synchronized(this) {
            if (isCached) {
                cacheRefCount++
            } else {
                cacheRefCount--
            }
        }
        // Check to see if recycle() can be called.
        checkState()
    }

    @Synchronized
    private fun checkState() {
        // If the drawable cache and display ref counts = 0, and this drawable
        // has been displayed, then recycle.
        if (cacheRefCount <= 0
                && displayRefCount <= 0
                && hasBeenDisplayed
                && hasValidBitmap()
        ) {
            getBitmap()?.recycle()
        }
    }

    @Synchronized
    private fun hasValidBitmap(): Boolean =
            getBitmap()?.run {
                !isRecycled
            } ?: false
    

Java

    private int cacheRefCount = 0;
    private int displayRefCount = 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) {
                displayRefCount++;
                hasBeenDisplayed = true;
            } else {
                displayRefCount--;
            }
        }
        // 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) {
                cacheRefCount++;
            } else {
                cacheRefCount--;
            }
        }
        // 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 (cacheRefCount <= 0 && displayRefCount <= 0 && hasBeenDisplayed
                && hasValidBitmap()) {
            getBitmap().recycle();
        }
    }

    private synchronized boolean hasValidBitmap() {
        Bitmap bitmap = getBitmap();
        return bitmap != null && !bitmap.isRecycled();
    }
    

在 Android 3.0 及更高版本上管理内存

Android 3.0(API 级别 11)引入了 BitmapFactory.Options.inBitmap 字段。如果设置了此选项,那么采用 Options 对象的解码方法会在加载内容时尝试重复使用现有位图。这意味着位图的内存得到了重复使用,从而提高了性能,同时移除了内存分配和取消分配。不过,inBitmap 的使用方式存在某些限制。特别是在 Android 4.4(API 级别 19)之前,系统仅支持大小相同的位图。如需了解详情,请参阅 inBitmap 文档。

保存位图供稍后使用

以下代码段演示了如何存储现有位图,以供稍后在示例应用中使用。当应用在 Android 3.0 或更高版本上运行并且位图从 LruCache 删除时,对位图的软引用会放置在 HashSet 中,以供稍后通过 inBitmap 重复使用:

Kotlin

    var reusableBitmaps: MutableSet<SoftReference<Bitmap>>? = null
    private lateinit var memoryCache: LruCache<String, BitmapDrawable>
    // If you're running on Honeycomb or newer, create a
    // synchronized HashSet of references to reusable bitmaps.
    if (Utils.hasHoneycomb()) {
        reusableBitmaps = Collections.synchronizedSet(HashSet<SoftReference<Bitmap>>())
    }

    memoryCache = object : LruCache<String, BitmapDrawable>(cacheParams.memCacheSize) {

        // Notify the removed entry that is no longer being cached.
        override fun entryRemoved(
                evicted: Boolean,
                key: String,
                oldValue: BitmapDrawable,
                newValue: BitmapDrawable
        ) {
            if (oldValue is RecyclingBitmapDrawable) {
                // The removed entry is a recycling drawable, so notify it
                // that it has been removed from the memory cache.
                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.
                    reusableBitmaps?.add(SoftReference(oldValue.bitmap))
                }
            }
        }
    }
    

Java

    Set<SoftReference<Bitmap>> reusableBitmaps;
    private LruCache<String, BitmapDrawable> memoryCache;

    // If you're running on Honeycomb or newer, create a
    // synchronized HashSet of references to reusable bitmaps.
    if (Utils.hasHoneycomb()) {
        reusableBitmaps =
                Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
    }

    memoryCache = new LruCache<String, BitmapDrawable>(cacheParams.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.
                    reusableBitmaps.add
                            (new SoftReference<Bitmap>(oldValue.getBitmap()));
                }
            }
        }
    ....
    }
    

使用现有位图

在正在运行的应用中,解码器方法会检查是否存在可以使用的现有位图。例如:

Kotlin

    fun decodeSampledBitmapFromFile(
            filename: String,
            reqWidth: Int,
            reqHeight: Int,
            cache: ImageCache
    ): Bitmap {

        val options: BitmapFactory.Options = 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)
    }
    

Java

    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 设置一个值(您的代码绝不应假定会找到匹配项):

Kotlin

    private fun addInBitmapOptions(options: BitmapFactory.Options, cache: ImageCache?) {
        // inBitmap only works with mutable bitmaps, so force the decoder to
        // return mutable bitmaps.
        options.inMutable = true

        // Try to find a bitmap to use for inBitmap.
        cache?.getBitmapFromReusableSet(options)?.also { inBitmap ->
            // 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:
    fun getBitmapFromReusableSet(options: BitmapFactory.Options): Bitmap? {
        mReusableBitmaps?.takeIf { it.isNotEmpty() }?.let { reusableBitmaps ->
            synchronized(reusableBitmaps) {
                val iterator: MutableIterator<SoftReference<Bitmap>> = reusableBitmaps.iterator()
                while (iterator.hasNext()) {
                    iterator.next().get()?.let { item ->
                        if (item.isMutable) {
                            // Check to see it the item can be used for inBitmap.
                            if (canUseForInBitmap(item, options)) {
                                // Remove from reusable set so it can't be used again.
                                iterator.remove()
                                return item
                            }
                        } else {
                            // Remove from the set if the reference has been cleared.
                            iterator.remove()
                        }
                    }
                }
            }
        }
        return null
    }
    

Java

    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 (reusableBitmaps != null && !reusableBitmaps.isEmpty()) {
            synchronized (reusableBitmaps) {
                final Iterator<SoftReference<Bitmap>> iterator
                        = reusableBitmaps.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 的候选位图是否满足相应的大小条件:

Kotlin

    private fun canUseForInBitmap(candidate: Bitmap, targetOptions: BitmapFactory.Options): Boolean {
        return 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.
            val width: Int = targetOptions.outWidth / targetOptions.inSampleSize
            val height: Int = targetOptions.outHeight / targetOptions.inSampleSize
            val byteCount: Int = width * height * getBytesPerPixel(candidate.config)
            byteCount <= candidate.allocationByteCount
        } else {
            // On earlier versions, the dimensions must match exactly and the inSampleSize must be 1
            candidate.width == targetOptions.outWidth
                    && candidate.height == targetOptions.outHeight
                    && targetOptions.inSampleSize == 1
        }
    }

    /**
     * A helper function to return the byte usage per pixel of a bitmap based on its configuration.
     */
    private fun getBytesPerPixel(config: Bitmap.Config): Int {
        return when (config) {
            Bitmap.Config.ARGB_8888 -> 4
            Bitmap.Config.RGB_565, Bitmap.Config.ARGB_4444 -> 2
            Bitmap.Config.ALPHA_8 -> 1
            else -> 1
        }
    }
    

Java

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