文章目录

  • 高效加载大图
  • 读取位图大小及类型
  • 加载比例缩小版
  • 缓存位图
  • 内存缓存
  • 磁盘缓存
  • 管理图片内存



大多情况下,一般推荐使用Glide来获取,解码,显示位图。Glide抽象了Android上图片处理这些和其他与位图和其他图像相关的任务的复杂性。

下边整理的几点是Bitmap加载过程中的基础知识点,也会是Glide等图片库中需要解决的问题。

高效加载大图

图片以各种形状,大小展示。多数情况下,图片比用户UI更大。例如,相机拍着的照片像素数一般都比屏幕的分辨率更高。

内存有限的情况下,理想情况只能是在内存中加载加载低分辨率的图片。低分辨率图片应该适合UI。

读取位图大小及类型

BitmapFactory类提供了decodeByteArray(),decodeFile(),decodeResource()等方法来处理来源不同的Bitmap。根据数据源选择合适的decode方法。这些decode方法尝试给结构化Bitmap分配内存,因此容易导致OutOfMemory异常。每个类型的decode方法签名可以通过BitmapFactory.Options来明确解码参数。例如,设置 inJustDecodeBounds 属性值为true来避免deocde时分配内存,方法返回的Bitmap为null,只设置了 outWidthoutHeightoutMimeType。这样的方式使得可以通过在不构造Bitmap(不分配内存)前提下读取Bitmap的大小及类型。

val options = BitmapFactory.Options().apply {
    inJustDecodeBounds = true
}
BitmapFactory.decodeResource(resources, R.id.myimage, options)
val imageHeight: Int = options.outHeight
val imageWidth: Int = options.outWidth
val imageType: String = options.outMimeType

例:一张 2560*1440,位深度32的图片,加载进入内存需要14.0625MB。

加载比例缩小版

大小知道后,就可以确定应该直接加载图片进入内存或应该二次抽样。需要考虑的因素:

  • 评估加载整张图片需要的内存的大小;
  • 考虑到应用的其他内存需求,愿意为加载此次图片所承担的内存大小;
  • 加载Image的目标组件大小;
  • 设备的屏幕大小及密度;

例如:要在一个120*120的ImageView显示上面加载的2560×1440的图片是是合适的。

这样就需要使用 BitmapFactory.Options 中的 inSampleSize 设置项,设置 inSampleSize 为int的k值,产生原来k分之一大小的图片加载到内存。

例如:2560*1440(ARGB_8888方式存储)的图片加载,设置了 inSampleSize=4,这样加载到内存的大小就是原来的1/4,即 640×360 的图片,内存的占用也变为了0.879MB,与原图加载占用的 14.0625MB 减少了不止一点。

尝试如下代码,考虑到目标大小计算inSampleSize值:

fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
        val (height: Int, width: Int) = options.run { outHeight to outWidth }
        var sampleSize = 1

        if (height > reqHeight || width > reqWidth) {
            val halfHeight = height / 2
            val halfWidth = width / 2

            while (halfHeight / sampleSize >= reqHeight && halfWidth / sampleSize >= reqWidth) {
                sampleSize *= 2
            }
        }

        return sampleSize;
    }

还是使用2560*1440(ARGB_8888方式存储) 的目标大小是 113×113 的图片计算 inSampleSize 值为8,在使用 calculateInSampleSize() 方法计算时,inJustDecodeBounds 设置值是true。

在实际使用 inSampleSize 进行图片加载时,需要将 inJustDecodeBounds 设置false。

android createBitmap 单位 android studio bitmap_android

缓存位图

开发中,加载当单张图片可以直截了当。但很多情况下是一次需要加载多张图片,甚至可能是不限制数量的加载,如:ListView等组件中加载图片。

考虑到屏幕滑动,图片被移出屏幕后会被回收。这里需要考虑缓存。

内存缓存

内存缓存是占用应用内存来快速访问图片。LruCache 类很适合这种缓存场景。

在缓存场景中没有明确的缓存大小标准,根据使用场景及来选择使用合适的大小。若缓存太小,加载大图时可能导致OOM错误。

如下代码是可以用以建立一个内存缓存:

private lateinit var mMemoryCache: LruCache<String, Bitmap>

override fun onCreate(savedInstanceState: Bundle?) {
    // memory返回的是字节单位,转换为KB单位
    val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()

    // 使用可用内存的1/8作用图片缓存
    val cacheSize = maxMemory / 8

    mMemoryCache = object : LruCache<String, Bitmap>(cacheSize) {

        override fun sizeOf(key: String, bitmap: Bitmap): Int {
            // 返回实体数据Bitmap大小,KB单位,与cacheSize一直。
            return bitmap.byteCount / 1024
        }
    }
}

磁盘缓存

内存缓存可以加速对访问过的Bitmap的再次访问,但在需要展示众多图片的场景下,内存缓存是不可靠的,因为垃圾回收会不定时进行回收内存资源。例如: GridView中展示Bitmap,众多图片很快就会占据内存。

因此这种情况下,可以使用磁盘缓存,将处理的图片进行缓存。

如下的代码是从Android Source中提取的。

private const val DISK_CACHE_SIZE = 1024 * 1024 * 10 // 10MB
private const val DISK_CACHE_SUBDIR = "thumbnails"
...
private var diskLruCache: DiskLruCache? = null
private val diskCacheLock = ReentrantLock()
private val diskCacheLockCondition: Condition = diskCacheLock.newCondition()
private var diskCacheStarting = true

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    // Initialize memory cache
    ...
    // Initialize disk cache on background thread
    val cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR)
    InitDiskCacheTask().execute(cacheDir)
    ...
}

internal inner class InitDiskCacheTask : AsyncTask<File, Void, Void>() {
    override fun doInBackground(vararg params: File): Void? {
        diskCacheLock.withLock {
            val cacheDir = params[0]
            diskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE)
            diskCacheStarting = false // Finished initialization
            diskCacheLockCondition.signalAll() // Wake any waiting threads
        }
        return null
    }
}

internal inner class  BitmapWorkerTask : AsyncTask<Int, Unit, Bitmap>() {
    ...

    // Decode image in background.
    override fun doInBackground(vararg params: Int?): Bitmap? {
        val imageKey = params[0].toString()

        // Check disk cache in background thread
        return getBitmapFromDiskCache(imageKey) ?:
                // Not found in disk cache
                decodeSampledBitmapFromResource(resources, params[0], 100, 100)
                        ?.also {
                            // Add final bitmap to caches
                            addBitmapToCache(imageKey, it)
                        }
    }
}

fun addBitmapToCache(key: String, bitmap: Bitmap) {
    // Add to memory cache as before
    if (getBitmapFromMemCache(key) == null) {
        memoryCache.put(key, bitmap)
    }

    // Also add to disk cache
    synchronized(diskCacheLock) {
        diskLruCache?.apply {
            if (!containsKey(key)) {
                put(key, bitmap)
            }
        }
    }
}

fun getBitmapFromDiskCache(key: String): Bitmap? =
        diskCacheLock.withLock {
            // Wait while disk cache is started from background thread
            while (diskCacheStarting) {
                try {
                    diskCacheLockCondition.await()
                } catch (e: InterruptedException) {
                }

            }
            return diskLruCache?.get(key)
        }

// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
fun getDiskCacheDir(context: Context, uniqueName: String): File {
    // Check if media is mounted or storage is built-in, if so, try and use external cache dir
    // otherwise use internal cache dir
    val cachePath =
            if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()
                    || !isExternalStorageRemovable()) {
                context.externalCacheDir.path
            } else {
                context.cacheDir.path
            }

    return File(cachePath + File.separator + uniqueName)
}

管理图片内存

不同Android版本上对bitmap的内存管理是不同的。

  • 在Android 2.2(API 8)或更低版本上,垃圾回收开始时,线程会被停止。这导致性能的降低。在Android 2.3中加入了并发垃圾回收机制,这就意味着bitmap不再被引用时内存就会被回收。
  • 在Android 2.3.3(API 10)或更低版本上,bitmap的像素数据存储在native内存中。数据与bitmap本身是分离的,bitmap本身存储在Dalvik堆中。native内存中的像素数据是可预见的不会被释放,潜在的问题即是引起应用内存溢出而crash。
  • 在Android 3.0(API 11)后至Android 7.1(API 25),像素数据与bitmap一同存储于堆内存中。
  • 在Android 8(API 26)及更高版本,bitmap像素数据存储在native堆中。

对sample BitmapFun有兴趣可以阅读BitmapFun源码。