Bitmap深入分析(一)

getAllocationByteCount 探索

我们可以通过 Bitmap.getAllocationByteCount() 方法获取 Bitmap 占用的字节大小,比如以下代码:

kotlin 复制代码
fun testXhdpi(){
    BitmapFactory.decodeResource(resources,R.mipmap.xhdpi).also {
        Log.e("$TAG", "test bitmap size ${it.allocationByteCount.byteToM()}")
    }
}

上图中 rodman 是保存在 res/mipmap-xhdpi 目录下的一张 960*600,大小为 54.88Kb 的图片。打印结果如下:

11-25 11:25:14.280 5791-5791/com.youdao.bitmaptest E/MainActivity: test bitmap size 2.1972656

解释

默认情况下 BitmapFactory 使用 Bitmap.Config.ARGB_8888 的存储方式来加载图片内容,而在这种存储模式下,每一个像素需要占用 4 个字节。因此上面图片 rodman 的内存大小可以使用如下公式来计算:

宽 * 高 * 4 = 960 * 600 * 4 = 2304000

屏幕自适应

但是如果我们在保证代码不修改的前提下,将图片 rodman 移动到(注意是移动,不是拷贝)res/drawable 目录下,重新运行代码,则打印日志如下:

11-25 11:25:14.269 5791-5791/com.youdao.bitmaptest E/MainActivity: test bitmap size 8.7890625

可以看出我们只是移动了图片的位置,Bitmap 所占用的空间竟然上涨了 4倍。这是为什么呢?

实际上 BitmapFactory 在解析图片的过程中,会根据当前设备屏幕密度和图片所在的 drawable 目录来做一个对比,根据这个对比值进行缩放操作。具体公式为如下所示:

  1. 缩放比例 scale = 当前设备屏幕密度 / 图片所在 drawable 目录对应屏幕密度
  2. Bitmap 实际大小 = 宽 * scale * 高 * scale * Config 对应存储像素数

在 Android 中,各个 drawable 目录对应的屏幕密度分别为下:

目录 mdpi hdpi xhdpi xxhdpi xxxhdpi
density 1 1.5 2 3 4
densityDpi 160 240 320 480 640

我运行的设备是 Nexus 4,屏幕密度为 320。如果将 rodman 放到 drawable-mdpi 目录下,最终的计算公式如下:

实际占用内存大小 = 960 *(320/160) * 600 * (320/160) * 4 内存增大了4倍

assets 中的图片大小

我们知道,Android 中的图片不仅可以保存在 drawable 目录中,还可以保存在 assets 目录下,然后通过 AssetManager 获取图片的输入流。那这种方式加载生成的 Bitmap 是多大呢?同样是上面的 rodman.png,这次将它放到 assets 目录中,使用如下代码加载:

kotlin 复制代码
    fun testAssets(){
        var inputStream: InputStream? = null
        try {
            inputStream = assets.open("assets.jpg")
            BitmapFactory.decodeStream(inputStream).also {
                Log.e("$TAG", "testAssets bitmap size ${it.allocationByteCount.byteToM()}")
            }
        }catch (e: Exception){
            e.printStackTrace()
        }finally {
            inputStream?.close()
        }
    }

最终打印结果如下:

11-25 12:06:35.777 11171-11171/com.youdao.bitmaptest E/MainActivity: testAssets bitmap size 2.1972656

可以看出,加载 assets 目录中的图片,系统并不会对其进行缩放操作。

Bitmap 加载优化

修改图片加载的 Config

修改占用空间少的存储方式可以快速有效降低图片占用内存。比如通过 BitmapFactory.Options 的 inPreferredConfig 选项,将存储方式设置为 Bitmap.Config.RGB_565。这种存储方式一个像素占用 2 个字节,所以最终占用内存直接减半.如下

kotlin 复制代码
  fun testBitmapCompressConfig(){
        val options = BitmapFactory.Options().apply {
            inPreferredConfig = Bitmap.Config.RGB_565
        }
        BitmapFactory.decodeResource(resources,R.mipmap.xhdpi,options).also {
            Log.e("$TAG", "testBitmapCompressConfig bitmap size ${it.allocationByteCount.byteToM()}")
        }
    }

打印日志如下:

11-25 12:14:05.017 11317-11317/com.youdao.bitmaptest E/MainActivity: testXhdpi bitmap size 1.0986328

另外 Options 中还有一个 inSampleSize 参数,可以实现 Bitmap 采样压缩,这个参数的含义是宽高维度上每隔 inSampleSize 个像素进行一次采集。比如以下代码:

kotlin 复制代码
fun testBitmapCompressConfig(){
    val options = BitmapFactory.Options().apply {
        inPreferredConfig = Bitmap.Config.RGB_565
        // 宽和高每2个像素进行一次采集
        inSampleSize = 2
    }
    BitmapFactory.decodeResource(resources,R.mipmap.xhdpi,options).also {
        Log.e("$TAG", "testBitmapCompressConfig bitmap size ${it.allocationByteCount.byteToM()}")
    }
}

因为宽高都会进行采样,所以最终图片会被缩略 4 倍,最终打印效果如下

11-25 12:16:50.242 11432-11432/com.youdao.bitmaptest E/MainActivity: testBitmapCompressConfig bitmap size 0.2746582

Bitmap 复用

通过下方的例子看出,如果我们频繁的切换图片,会造成内存抖动

kotlin 复制代码
findViewById<View>(R.id.bt_iamge3).setOnClickListener {
    val smallBitmap = getBitmap()
    ivPreview.setImageBitmap(smallBitmap)
}
    
private fun getBitmap(): Bitmap{
    return BitmapFactory.decodeResource(resources, resIds[resIndex++ % 2])
}

使用 Options.inBitmap 优化

实际上经过第一次显示之后,内存中已经存在了一个 Bitmap 对象。每次切换图片只是显示的内容不一样,我们可以重复利用已经占用内存的 Bitmap 空间,具体做法就是使用 Options.inBitmap 参数。将 getBitmap 方法修改如下:

kotlin 复制代码
// code 1
    val options = BitmapFactory.Options()
    // 必须设置该属性,否则会重用失败
    options.inMutable = true
    val reuseBitmap = BitmapFactory.decodeResource(resources, R.mipmap.xhdpi,options)


   findViewById<View>(R.id.bt_iamge3).setOnClickListener {
        val smallBitmap = getBitmap()
        Log.e(TAG, "reuseBitmap before size ${reuseBitmap?.allocationByteCount?.byteToM()}")
        Log.e(TAG, "smallBitmap size ${smallBitmap?.allocationByteCount?.byteToM()}")
        Log.e(TAG, "reuseBitmap after size ${reuseBitmap?.allocationByteCount?.byteToM()}")
        ivPreview.setImageBitmap(smallBitmap)
    }


    private fun getReuseBitmap(): Bitmap? {
        val options = BitmapFactory.Options()
        options.inJustDecodeBounds = true
        BitmapFactory.decodeResource(resources, resIds[resIndex % 2], options)
        if (canUseForInBitmap(reuseBitmap!!, options)) {
            Log.e(TAG, "reuseBitmap is reusable")
            options.inMutable = true
            // code 2
            options.inBitmap = reuseBitmap
        }
        options.inJustDecodeBounds = false
        return BitmapFactory.decodeResource(resources, resIds[resIndex++ % 2], options)
    }


    fun canUseForInBitmap(candidate: Bitmap, targetOptions: BitmapFactory.Options): Boolean {
        val width = targetOptions.outWidth / Math.max(targetOptions.inSampleSize, 1)
        val height = targetOptions.outHeight / Math.max(targetOptions.inSampleSize, 1)
        val byteCount = width * height * getBytesPerPixel(candidate.config)
        return byteCount <= candidate.allocationByteCount
    }

    private fun getBytesPerPixel(config: Bitmap.Config): Int {
        return when (config) {
            Bitmap.Config.ALPHA_8 -> 1
            Bitmap.Config.RGB_565, Bitmap.Config.ARGB_4444 -> 2
            else -> 4
        }
    }

解释说明:

code 1 处创建一个可以用来复用的 Bitmap 对象。

code 2 处,将 options.inBitmap 赋值为之前创建的 reuseBitmap 对象,从而避免重新分配内存。

注意:

在上述 getBitmap 方法中,复用 inBitmap 之前,需要调用 canUseForInBitmap 方法来判断 reuseBitmap 是否可以被复用。这是因为 Bitmap 的复用有一定的限制:

在 Android 4.4 版本之前,只能重用相同大小的 Bitmap 内存区域; 4.4 之后你可以重用任何 Bitmap 的内存区域,只要这块内存比将要分配内存的 bitmap 大就可以。

在每次加载之前,除了 inBitmap 参数之外,将 Options.inMutable 置为 true,这里如果不置为 true 的话,BitmapFactory 将不会重复利用 Bitmap 内存,并输出相应 warning 日志:

Unable to reuse an immutable bitmap as an image decoder target.

BitmapRegionDecoder 图片分片显示

有时候我们想要加载显示的图片很大或者很长,比如手机滚动截图功能生成的图片。

针对这种情况,在不压缩图片的前提下,不建议一次性将整张图加载到内存,而是采用分片加载的方式来显示图片部分内容,然后根据手势操作,放大缩小或者移动图片显示区域。

BitmapRegionDecoder 基本使用

ini 复制代码
private fun showRegionImage() {
        try {
            val inputStream = assets.open("assets.jpg")
            //获得图片的宽、高
            val tmpOptions = BitmapFactory.Options()
            tmpOptions.inJustDecodeBounds = true
            BitmapFactory.decodeStream(inputStream, null, tmpOptions)
            val width = tmpOptions.outWidth
            val height = tmpOptions.outHeight
            //设置显示图片的中心区域
            val decoder = BitmapRegionDecoder.newInstance(inputStream, false)
            val options = BitmapFactory.Options()
            options.inPreferredConfig = Bitmap.Config.RGB_565
            val addLength = factor++ * 5
            val bitmap = decoder.decodeRegion(Rect(addLength, addLength, width/2 + addLength, height/2 + addLength), options)
            ivPreview.setImageBitmap(bitmap)
        } catch (e: IOException) {
        }
    }

分享总结 :

  • 在复用一个Bitmap 时候一定要调用canUseForInBitmap() 方法进行检查能否复用 ,否则会发生异常.
  • 加载一个newBitmap 去复用reuseBitmap时,如果复用成功,那么加载一个newBitmap == reuseBitmap ,源码如下:
scss 复制代码
gOptions_bitmapFieldID = GetFieldIDOrDie(env, options_class, "inBitmap",
            "Landroid/graphics/Bitmap;");

    ...            
javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID);
    ...
if (javaBitmap != nullptr) {
    bitmap::reinitBitmap(env, javaBitmap, outputBitmap.info(), isPremultiplied);
    outputBitmap.notifyPixelsChanged();
    '//可以看到如果复用成功后 ,返回的是我们一开始设置的复用Bitmap对象'
    // If a java bitmap was passed in for reuse, pass it back
    return javaBitmap;
}
    
相关推荐
一起搞IT吧3 小时前
Android功耗系列专题理论之十四:Sensor功耗问题分析方法
android·c++·智能手机·性能优化
ByNotD0g3 小时前
Doris 学习笔记
android·笔记·学习
修炼者3 小时前
【Android进阶】 RenderEffect的底层实现
android
bropro4 小时前
MySQL不使用子查询的原因
android·数据库·mysql
执笔论英雄4 小时前
【cuda】 pinpaged
android·java·数据库
新青年.5 小时前
Android(Compose)使用 LibVLC 播放 RTSP 视频流
android
一见6 小时前
WorkBuddy安装Skill的方法
android·java·javascript
毛骗导演6 小时前
万字解析 OpenClaw 源码架构-跨平台应用之Android 应用
android·前端·架构