概述
现在的手机应用基本上离不开图片,然后,图片在手机中的存在方式大概为两种形式,一种是 webapp 中嵌套在 html 页面中的图片,一种是作为本地资源,解析之后,显示在 ImageView 等组件上,我们今天要优化的当然是后者那种用法。说到优化,自然联系到 Bitmap 对象了。
Bitmap 类
Bitmap 根据 api 的介绍
首先了解一下 Bitmap 类里面的两个嵌套枚举类
Bitmap.Config 类
对这这个枚举类的简单的解释如下
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 的解码
这个是 ARGB_8888 的解码
可见大小接近压缩了一半。
Bitmap.CompressFormate 枚举类
概述:定义 Bitmap 以文件存储形式的压缩格式
这个类主的用途是在压缩图片的时候,设置压缩格式
- 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 )
然后下面给出压缩的内存结果
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 的知识我会在后续的章节补上。