Android如何优化Bitmap内存占用

发布于 2021-3-25 15:46
浏览
0收藏

前置概念-屏幕密度
搞清楚 DisplayMetrics 的两个变量,
density 是显示的逻辑密度,是密度与独立像素单元的比例因子,
densityDpi 是屏幕每英寸对应多少个点

 

图片占内存多少的计算原理
找到每个像素占用的字节数*总像素数即可

 

如何优化
知道了原因,那么据此即可优化内存使用。

1、合理选择 jpg 和 png
在文件系统中,jpg 是一种有损压缩的图片存储格式, png 则是无损压缩的图片存储格式,对于同一张图片,jpg 会比png 小一些。

需要注意的是 jpg 的图片没有 alpha 通道

从出图的角度来看的话,jpg 格式的图片大小也不一定比 png 的小,这要取决于图像信息的内容。
JPG 不适用于所含颜色很少、具有大块颜色相近的区域或亮度差异十分明显的较简单的图片。对于需要高保真的较复杂的图像,PNG 虽然能无损压缩,但图片文件较大。

如果仅仅是为了 Bitmap 读到内存中的大小而考虑的话,二者的差别主要体现在

  • alpha 如果需要 alpha 通道,那么没有别的选择,用 png。
  • 你的图色值丰富还是单调?就像刚才提到的,如果色值丰富,那么用jpg,如果作为按钮的背景,用 png。
  • 对安装包大小的要求是否非常严格?如果你的 app 资源很少,安装包大小问题不是很凸显,看情况选择 jpg 或者 png(不过,应该没有不严格的吧)
  • 目标用户的 cpu jpg 的图像压缩算法比 png 耗时

2、使用inSampleSize
主要用在 图片资源本身较大,或者适当地采样并不会影响视觉效果的条件下,这时候我们输出地目标可能相对较小,对图片分辨率、大小要求不是非常的严格。

举个栗子:

我们现在有个需求,要求将一张图片进行模糊,然后作为 ImageView 的 src 呈现给用户,而我们的原始图片大小为 1080*1920,如果直接拿来模糊的话,一方面模糊的过程费时费力,另一方面生成的图片又占用内存,实际上在模糊运算过程中可能会存在输入和输出并存的情况,此时内存将会有一个短暂的峰值。可能会导致『OOM』。

既然图片最终是要被模糊的,也看不太清楚,还不如直接用一张采样后的图片,如果采样率为 2,那么读出来的图片只有原始图片的 1/4 大小

BitmapFactory.Options options = new Options();
options.inSampleSize = 2;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId, options);


3、使用矩阵
用到 Bitmap 的地方,总会见到 Matrix。大图小用用采样,小图大用用矩阵。

用前面模糊图片的例子,采样了,内存是小了,可是图的尺寸也小了,我要用 Canvas 绘制这张图就应该用到矩阵了,

Matrix matrix = new Matrix();
matrix.preScale(2, 2, 0f, 0f);
//如果使用直接替换矩阵的话,在Nexus6 5.1.1上必须关闭硬件加速
canvas.concat(matrix);
canvas.drawBitmap(bitmap, 0,0, paint);


需要注意的是,在使用搭载 5.1.1 原生系统的 Nexus6 进行测试时发现,如果使用 Canvas 的 setMatrix 方法,可能会导致与矩阵相关的元素的绘制存在问题,本例当中如果使用 setMatrix 方法,bitmap 将不会出现在屏幕上。因此请尽量使用 canvas 的 scale、rotate 这样的方法,或者使用 concat 方法。

Matrix matrix = new Matrix();
matrix.preScale(2, 2, 0, 0);
canvas.drawBitmap(bitmap, matrix, paint);


绘制出来的图就是放大以后的效果了,不过占用的内存却仍然是采样出来的大小。
把图片放到 ImageView 中

Matrix matrix = new Matrix();
matrix.postScale(2, 2, 0, 0);
imageView.setImageMatrix(matrix);
imageView.setScaleType(ScaleType.MATRIX);
imageView.setImageBitmap(bitmap);


4、合理选择Bitmap的像素格式
ARGB8888格式的图片,每像素占用 4 Byte,而 RGB565则是 2 Byte

Android如何优化Bitmap内存占用-开源基础软件社区

5、索引位图(Indexed Bitmap)
索引位图,每个像素只占 1 Byte,不仅支持 RGB,还支持 alpha,而且看上去效果还不错!可惜Android 官方并不支持这个。

 public enum Config {
    // these native values must match up with the enum in SkBitmap.h

    ALPHA_8     (2),
    RGB_565     (4),
    ARGB_4444   (5),
    ARGB_8888   (6);

    final int nativeInt;
}


Skia 引擎是支持的

enum Config {
    kNo_Config,   //!< bitmap has not been configured
    kA8_Config,   //!< 8-bits per pixel, with only alpha specified (0 is transparent, 0xFF is opaque)

   //看这里看这里!!↓↓↓↓↓
    kIndex8_Config, //!< 8-bits per pixel, using SkColorTable to specify the colors  
    kRGB_565_Config, //!< 16-bits per pixel, (see SkColorPriv.h for packing)
    kARGB_4444_Config, //!< 16-bits per pixel, (see SkColorPriv.h for packing)
    kARGB_8888_Config, //!< 32-bits per pixel, (see SkColorPriv.h for packing)
    kRLE_Index8_Config,

    kConfigCount
};


Java 层的枚举变量的 nativeInt 对应的就是 Skia 库当中枚举的索引值,所以,如果我们能够拿到这个索引是不是就可以了?对不起,拿不到。
不过在 png 的解码库里面有这么一段代码

bool SkPNGImageDecoder::getBitmapColorType(png_structp png_ptr, png_infop info_ptr,
                                       SkColorType* colorTypep,
                                       bool* hasAlphap,
                                       SkPMColor* SK_RESTRICT theTranspColorp) {
png_uint_32 origWidth, origHeight;
int bitDepth, colorType;
png_get_IHDR(png_ptr, info_ptr, &origWidth, &origHeight, &bitDepth,
             &colorType, int_p_NULL, int_p_NULL, int_p_NULL);

#ifdef PNG_sBIT_SUPPORTED
  // check for sBIT chunk data, in case we should disable dithering because
  // our data is not truely 8bits per component
  png_color_8p sig_bit;
  if (this->getDitherImage() && png_get_sBIT(png_ptr, info_ptr, &sig_bit)) {
#if 0
    SkDebugf("----- sBIT %d %d %d %d\n", sig_bit->red, sig_bit->green,
             sig_bit->blue, sig_bit->alpha);
#endif
    // 0 seems to indicate no information available
    if (pos_le(sig_bit->red, SK_R16_BITS) &&
        pos_le(sig_bit->green, SK_G16_BITS) &&
        pos_le(sig_bit->blue, SK_B16_BITS)) {
        this->setDitherImage(false);
    }
}
#endif


if (colorType == PNG_COLOR_TYPE_PALETTE) {
    bool paletteHasAlpha = hasTransparencyInPalette(png_ptr, info_ptr);
    *colorTypep = this->getPrefColorType(kIndex_SrcDepth, paletteHasAlpha);
    // now see if we can upscale to their requested colortype
    //这段代码,如果返回false,那么colorType就被置为索引了,那么我们看看如何返回false
    if (!canUpscalePaletteToConfig(*colorTypep, paletteHasAlpha)) {
        *colorTypep = kIndex_8_SkColorType;
    }
} else {
...... 
}
return true;
}


canUpscalePaletteToConfig函数如果返回false,那么colorType就被置为kIndex_8_SkColorType了。

static bool canUpscalePaletteToConfig(SkColorType dstColorType, bool srcHasAlpha) {
  switch (dstColorType) {
    case kN32_SkColorType:
    case kARGB_4444_SkColorType:
        return true;
    case kRGB_565_SkColorType:
        // only return true if the src is opaque (since 565 is opaque)
        return !srcHasAlpha;
    default:
        return false;
 }
}


如果传入的 dstColorType 是 kRGB_565_SkColorType,同时图片还有 alpha 通道,那么返回 false

这个dstColorType 是哪儿来的?在 decode 时,传入的 Options 的 inPreferredConfig。

 

测试:
在 assets 目录当中放了一个叫 index.png 的文件,大小192*192,这个文件是通过 PhotoShop 编辑之后生成的索引格式的图片。

try {
   Options options = new Options();
   options.inPreferredConfig = Config.RGB_565;
Bitmap bitmap = BitmapFactory.decodeStream(getResources().getAssets().open("index.png"), null, options);
   Log.d(TAG, "bitmap.getConfig() = " + bitmap.getConfig());
   Log.d(TAG, "scaled bitmap.getByteCount() = " + bitmap.getByteCount());
   imageView.setImageBitmap(bitmap);
} catch (IOException e) {
    e.printStackTrace();
}


程序运行在 Nexus6上,由于从 assets 中读取不涉及前面讨论到的 scale 的问题,所以这张图片读到内存以后的大小理论值(ARGB8888):
192 * 192 *4=147456
好,运行我们的代码,看输出的 Config 和 ByteCount:

D/MainActivity: bitmap.getConfig() = null
D/MainActivity: scaled bitmap.getByteCount() = 36864


如果前面的讨论是没有问题的话,那么这次解码出来的 Bitmap 应该是索引格式,那么占用的内存只有 ARGB 8888 的1/4

Config 为什么为 null

public final Bitmap.Config getConfig ()
Added in API level 1
If the bitmap’s internal config is in one of the public formats, return that config, otherwise return null.

由于官方并未做出支持,因此这个方法有诸多限制,比如不能在 xml 中直接配置,,生成的 Bitmap 不能用于构建 Canvas 等等。

6、可以用代码实现的尽量不使用资源
有时候有些场景其实不需要图片也能完成的。比如在开发中我们会经常遇到 Loading,这些 Loading 通常就是几帧图片,图片也比较简单,只需要黑白灰加 alpha 就齐了。自定义一个 View,覆写 onDraw 自己画一下好了
————————————————
版权声明:本文为博主「阿拉阿伯」的原创文章

分类
收藏
回复
举报
回复
添加资源
添加资源将有机会获得更多曝光,你也可以直接关联已上传资源 去关联
    相关推荐