概述

现在的手机应用基本上离不开图片,然后,图片在手机中的存在方式大概为两种形式,一种是 webapp 中嵌套在 html 页面中的图片,一种是作为本地资源,解析之后,显示在 ImageView 等组件上,我们今天要优化的当然是后者那种用法。说到优化,自然联系到 Bitmap 对象了。

Bitmap 类

Bitmap 根据 api 的介绍

首先了解一下 Bitmap 类里面的两个嵌套枚举类

Bitmap.Config 类

android的Bitmap是什么 android bitmap内存_优化

对这这个枚举类的简单的解释如下

ALPHA_8 会将每一个像素存储为单个的透明原色,只用八位存储了透明度
ARGB_4444 过时的,因为严重影响了图片质量,所以被弃用,用ARGB_888代替
ARGB_8888 每个像素点被存储为4个字节
RGB_565 每个像素点被存储为2个字节,只存储 RGB 三原色,5位存储红原色(32种编码),5位存储蓝原色(三十二种编码),6位存储绿原色(64种编码)

那么 ARGB_8888 和 RGB_565 有什么不同呢?
其实我们直接从名称上可以看到 ARGB_8888 代表的意思是,A是透明度,R是红原色,G绿原色,B是蓝原色,每一位都8位(一个字节)编码,所以有256种色值。所以 RGB_565 的话,是没有透明度的,而且,RGB 三原色的编码位数分别为5,6,5也即是各有 32种,64种,32种色值。

那个总结一下就是:
对图片显示质量要求不高,尽量用 RGB_565,经过试验大概能压缩个一般的

那么我们怎么使用这个类呢?

public static Bitmap streamBitmap(Context context,int resourseId){
    BitmapFactory.Options  options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.RGB_565;
    // 5.0(api 20)以下版本,2.3.3 (api 10)以上 版本 才有用,设置为 true 的时候,在系统内存低的时候会将 bitmap 存储在内存的像素数组回收
    // 在你需要重新访问像素数组的时候,BitmapFactory 的 decoder 会重新去 decode出来
    // 即使这个字段能防止 daivik 虚拟机内存溢出,但是严重影响了 UI绘制的性能,所以不建议使用
    options.inInputShareable = true;
    options.inPurgeable = true;
    // 使用

    Bitmap bitmap = null;
    // 使用这个方式获得一个 Bitmap 效率要高一点
    InputStream is = context.getResources().openRawResource(resourseId);
    bitmap = BitmapFactory.decodeStream(is,null,options);
    return bitmap;
}

分别使用 RGB_565 和 ARGB_8888 解码出来的 Bitmap 的大小

这个是 RGB_565 的解码

android的Bitmap是什么 android bitmap内存_android图片_02

这个是 ARGB_8888 的解码

android的Bitmap是什么 android bitmap内存_bitmap-内存_03

可见大小接近压缩了一半。

Bitmap.CompressFormate 枚举类

概述:定义 Bitmap 以文件存储形式的压缩格式

android的Bitmap是什么 android bitmap内存_bitmap-内存_04

这个类主的用途是在压缩图片的时候,设置压缩格式

  • PNG 无损压缩格式,有透明度
  • JPEG 有损压缩格式,无透明度
  • WEBP 有损压缩格式,goolge 提出来替换 JEPG 的一种图片格式,压缩率为 JPEG 的三分之二,主要用于网络(节省流量)

压缩图片的发生情景主要在这种情景,图片太大,加载进内存特别占空间,同时对图片显示质量要求不算很高的情况下,可以考虑压缩图片。

public static Bitmap streamBitmap(Context context,int resourseId){
    BitmapFactory.Options  options = new BitmapFactory.Options();
//        options.inPreferredConfig = Bitmap.Config.RGB_565;
    options.inPreferredConfig = Bitmap.Config.ARGB_8888;
    // 5.0(api 20)以下版本,2.3.3 (api 10)以上 版本 才有用,设置为 true 的时候,在系统内存低的时候会将 bitmap 存储在内存的像素数组回收
    // 在你需要重新访问像素数组的时候,BitmapFactory 的 decoder 会重新去 decode出来
    // 即使这个字段能防止 daivik 虚拟机内存溢出,但是严重影响了 UI绘制的性能,所以不建议使用
    options.inInputShareable = true;
    options.inPurgeable = true;
    // 使用

    Bitmap bitmap = null;
    Bitmap mBitmap = null;
    // 使用这个方式获得一个 Bitmap 效率要高一点
    InputStream is = context.getResources().openRawResource(resourseId);
    bitmap = BitmapFactory.decodeStream(is, null, options);

    mBitmap = BitmapFactory.decodeResource(context.getResources(),resourseId,options);
    return mBitmap;
}

//    public static Bitmap

public static Bitmap  copmressBitmap(Context context){
    BitmapFactory.Options options = new BitmapFactory.Options();
    Bitmap bitmap;
    // 设置为 true 的话, 图片不会被加载进内存,调用 BitmapFactory 的 decode() 方法之后,返回一个null 的bitmap对象
    // 但是会返回 outXXX 字段( outWidth outHeigth 字段) 你可以在这里获取 bitmap 的大小
     // 图片的原始分辨率为 1024 * 768
    options.inJustDecodeBounds = true;
    Bitmap mBitmap =BitmapFactory.decodeResource(context.getResources(), R.drawable.placeholder);
    int width = mBitmap.getWidth();
    int heigth  = mBitmap.getHeight();

    // 因为返回2 所以高和宽会被压缩为原来的 1/2 像素数量变为原来 1/16
    // 压缩是为了考虑低端机同时可以避免使用多套图片,减缩 apk 的大小
    options.inSampleSize = calcCompressSize(width,heigth,512,384);

    options.inJustDecodeBounds = false;
    options.inPreferredConfig = Bitmap.Config.ARGB_8888;
    bitmap = BitmapFactory.decodeResource(context.getResources(),R.drawable.placeholder,options);

    return bitmap;
}

private static int calcCompressSize(int outWidth,int outHeight, int reqwidth, int reqheight) {
    if(outWidth>reqwidth || outHeight> reqheight){
        // 压缩比例
        int widthRatio = Math.round((float)outWidth/(float)reqwidth);
        int heigthRatio = Math.round((float)outHeight/(float)reqheight);
        return widthRatio<heigthRatio ? heigthRatio:widthRatio; // 这样就会 返回 2
    }
    return 1;
}

下面是压缩之后的效果图(上面一张是没有压缩的,宽和高都是 wrap_content )

android的Bitmap是什么 android bitmap内存_android图片_05

然后下面给出压缩的内存结果

android的Bitmap是什么 android bitmap内存_数组_06

count 值是没有压缩之后的 compressCount 是压缩比为 2 压缩之后的,明显内存为原来的 1/4了

然后在有右图的 watches 里面可以看到具体的 bitmap 大小。

然而,这个 Bitmap.CompressFormat 的用法主要是这样

bitmap.compress(Bitmap.CompressFormat.JPEG,new ByteArrayOutputStream());

在将文件保存为本地的时候,设置存储格式

其实这里细心的同学可能发现,这个 BItmap 的size究竟和什么有关呢?明明图片大小是 1024 * 768 ,不压缩解析出来大小却是 2048 * 1536 (放大一倍),同时我们嗨呀考虑到显示图片的 ImageView 本身是有大小的,所以最佳情况是 ImageView 的大小和 Bitmap 的大小是一样的,这个话题我们后面会做研究,这里先提出来。

Bitmap 类

我们简单的学习一下 Bitmap 的源码

由于 Bitmap 的构造方法是不带修饰符的,所以直接被 new 出来,根据 api 的解释:、

/**私有的构造方法,接受一个已经存在的 native bitmap 指针
 * Private constructor that must received an already allocated native bitmap
 * int (pointer).
 */
@SuppressWarnings({"UnusedDeclaration"}) // called from JNI

Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density,
        boolean isMutable, boolean requestPremultiplied,
        byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
    if (nativeBitmap == 0) {
        throw new RuntimeException("internal error: native bitmap is 0");
    }

    mWidth = width;
    mHeight = height;
    mIsMutable = isMutable;
    mRequestPremultiplied = requestPremultiplied;
    mBuffer = buffer;

    // we delete this in our finalizer
    mNativeBitmap = nativeBitmap;

    mNinePatchChunk = ninePatchChunk;
    mNinePatchInsets = ninePatchInsets;
    if (density >= 0) {
        mDensity = density;
    }

    int nativeAllocationByteCount = buffer == null ? getByteCount() : 0;
    mFinalizer = new BitmapFinalizer(nativeBitmap, nativeAllocationByteCount);
}

构造方法中只是进行了一系列的赋值,从数据域的 api 解释,来了解一下这个构造方法和 Bitmap 对象

/**标志着 Bitmap 被创建的时候,像素是否已知的
 * Indicates that the bitmap was created for an unknown pixel density.
 *
 * @see Bitmap#getDensity()
 * @see Bitmap#setDensity(int)
 */
public static final int DENSITY_NONE = 0;

/**
 * Note:  mNativeBitmap is used by FaceDetector_jni.cpp
 * Don't change/rename without updating FaceDetector_jni.cpp
 * 
 * @hide
 */
public final long mNativeBitmap;

/** 读写的缓冲数组
 * Backing buffer for the Bitmap.
 * Made public for quick access from drawing methods -- do NOT modify
 * from outside this class
 *
 * @hide
 */
@SuppressWarnings("UnusedDeclaration") // native code only
public byte[] mBuffer;

@SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"}) // Keep to finalize native resources
private final BitmapFinalizer mFinalizer; // bitmap 对象回收器
 // 决定 bitmap 的像素点是否可以改变的
private final boolean mIsMutable;

/**标志 bitmap 的内容是否被预先计算的
 * Represents whether the Bitmap's content is requested to be pre-multiplied.
 * Note that isPremultiplied() does not directly return this value, because
 * isPremultiplied() may never return true for a 565 Bitmap or a bitmap
 * without alpha.
 *
 * setPremultiplied() does directly set the value so that setConfig() and
 * setPremultiplied() aren't order dependent, despite being setters.
 *
 * The native bitmap's premultiplication state is kept up to date by
 * pushing down this preference for every config change.
 */
private boolean mRequestPremultiplied;
 // 和 .9 有关的 byte 数组
private byte[] mNinePatchChunk; // may be null
private NinePatch.InsetStruct mNinePatchInsets; // may be null
private int mWidth; // bitmap 的宽
private int mHeight; // bitmap 的高
private boolean mRecycled; // bitmap 对象是否被回收了

// Package-scoped for fast access.
int mDensity = getDefaultDensity();  // desity 值,默认等于屏幕的 desity

private static volatile Matrix sScaleMatrix;

private static volatile int sDefaultDensity = -1;

根据 api 的说明,这个构造方法只能被 jni 的代码调用,我们平时都是通过 BitmapFactory 的一系列的 decodeXXXX() 方法获得一个 Bitmap 对象

我们可以看到有四个 nativeDecodeXXXXX() 的方法,其实全部我们可以访问的方法都是调用对应的 native 方法的,( decodeFile() decodeStream() decodeResourse() 方法都是调用 decodeStram()方法的,所以它们没有对应的 native()方法,之前提到过 decodeResourse() 方法要比 decodeStream() 方法要慢,就是因为这个原因 )

我们来看一下 decodeStream()方法

public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
    // we don't throw in this case, thus allowing the caller to only check
    // the cache, and not force the image to be decoded. 输入流对象为空
    if (is == null) {
        return null;
    }

    Bitmap bm = null;
    // 
    // 缓存跟踪工具
    Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap");
    try {
   // 判断是否 asset 资源
        if (is instanceof AssetManager.AssetInputStream) {
            final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
            bm = nativeDecodeAsset(asset, outPadding, opts);
        } else {
            bm = decodeStreamInternal(is, outPadding, opts);
        }

        if (bm == null && opts != null && opts.inBitmap != null) {
            throw new IllegalArgumentException("Problem decoding into existing bitmap");
        }

        setDensityFromOptions(bm, opts);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
    }

    return bm;
}

( 最终我还是决心打开 fuck source code )
这边涉及到 JNI 和 NDK 的知识我会在后续的章节补上。