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 目录来做一个对比,根据这个对比值进行缩放操作。具体公式为如下所示:
- 缩放比例 scale = 当前设备屏幕密度 / 图片所在 drawable 目录对应屏幕密度
- 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;
}