前言
Android中使用到图片的场景还是很多的,我们通常都是使用Bitmap来展示图片。但Bitmap用不好就会造成oom现象,这里简单介绍一下Bitmap类的源码,以及使用Bitmap的技巧。
Bitmap类分析
Bitmap.Config
Config是Bitmap中的一个枚举类,主要负责配置Bitmap的内存存储方式,不同的存储方式会有不同的图像质量。当然,越高的质量需要的内存越多,
public enum Config {
ALPHA_8(1),
RGB_565(3),
ARGB_4444(4),
ARGB_8888(5);
...
}
ALPHA_8: 每个像素占用一个字节 (8bit) , 只有透明度,没有颜色。RGB_565: 每个像素占用两个字节(16bit, Red=5, Green=6, Blue=5), 没有透明度。通常需要压缩图片使用内存时会用到这个配置。ARGB_4444: 每个像素占用两个字节(16bit, Alpha=4, Red=4, Green=4, Blue=4),相对RGB_565而言有透明度,但是图片质量不高,现在已废弃,在android 4.4上面,设置的ARGB_4444会被系统使用ARGB_8888替换。ARGB_8888: Bitmap默认的配置,有透明度,能够提供较高的图片质量,应尽可能的使用它(源码中的注释)。
Bitmap占用内存计算
官方的公式为,
比如,要加载一张1024 * 1024的使用ARGB_8888配置的图片,Bitmap占用的字节数是,
可见ARGB_8888占用的内存是很大的,如果使用RGB_565,则占用内存可以缩小一倍。
以下代码是一种通用的计算Bitmap占用内存的实现,兼容了Android旧版本的操作系统,
public static int getSizeInBytes(@Nullable Bitmap bitmap) {
if (bitmap == null) {
return 0;
}
//在Android KitKat版本使用getAllocationByteCount可能会抛出NPE异常
//这是系统bug需要做一下处理, 可以看看底下截图的issue
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
try {
return bitmap.getAllocationByteCount();
} catch (NullPointerException npe) {
// Swallow exception and try fallbacks.
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
return bitmap.getByteCount();
}
// Estimate for earlier platforms.
return bitmap.getWidth() * bitmap.getRowBytes();
}

Bitmap.CompressFormat
CompressFormat是可以指定Bitmap被压缩成某种格式的流,
public enum CompressFormat {
//有损格式
JPEG (0),
//无损格式
PNG (1),
//谷歌推出的一种新的格式,质量和JPG差不多但是压缩比更高
WEBP (2);
...
}
//通常调用compress使用
//quality为压缩的质量0-100, 0表示压缩成小尺寸,100表示压缩以获得最高质量。
bitmap.compress(CompressFormat.JPEG, quality, OutputStream);
Bitmap采样
其实通常情况下,我们要加载的图片资源都是大于我们的View控件的。如果直接去加载必然会造成内存资源的浪费,所以我们可以对Bitmap进行采样,减小内存占用。以下是一种高效加载位图的实现,
/**
* @param reqWidth 希望图片压缩后的图片宽度
* @param reqHeight 希望图片压缩后的图片高度
* @return
*/
private static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// 原始资源图片宽高
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
//计算最大的采样值(必须是2的倍数)
//并且采样后的宽高必须都小于原始资源图片宽高
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
final BitmapFactory.Options options = new BitmapFactory.Options();
//inJustDecodeBounds=true,
//表示可以在解析图片获取宽和高时,不用申请内存
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 重置回去, 然后给bitmap分配内存
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
源码分析
我们使用BitmapFactory创建Bitmap对象,最终都会走到JNI接口中去,以下是其中一个native方法接口,
private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
Rect padding, Options opts);
查看源码的实现,
//省略了部分代码
static jobject nativeDecodeStream(JNIEnv*env, jobject clazz, jobject is, jbyteArray storage,
jobject padding, jobject options) {
...
//可见Bitmap最终是由skia图形引擎(一个二维图像引擎,Android底层的图形渲染都是基于它,具体可以谷歌)实现的
//创建了一个SK流
std::unique_ptr<SkStream> stream(CreateJavaInputStreamAdaptor(env, is, storage));
...
//调用native的BitmapFactory去解码生成的buffer
bitmap = doDecode(env, std::move (bufferedStream), padding, options);
return bitmap;
}
static jobject doDecode(JNIEnv*env, std::unique_ptr<SkStreamRewindable>stream,
jobject padding, jobject options) {
...
//省略代码,包括初始化options等等
//可见android::Bitmap对象是重复使用的,避免频繁创建对象
android::Bitmap * reuseBitmap = nullptr;
unsigned int existingBufferSize = 0;
if (javaBitmap != NULL) {
reuseBitmap = &bitmap::toBitmap (env, javaBitmap);
...
}
//从这里可以看出Android的Bitmap的实现是由SkBitmap来完成的
SkBitmap::HeapAllocator heapAllocator;
SkBitmap::Allocator * decodeAllocator;
...
//对应Java层的isMutable
if (isMutable) bitmapCreateFlags |= android::bitmap::kBitmapCreateFlag_Mutable;
//是否支持硬件层面生成bitmap
if (isHardware) {
sk_sp<Bitmap> hardwareBitmap = Bitmap::allocateHardwareBitmap (outputBitmap);
if (!hardwareBitmap.get()) {
return nullObjectReturn("Failed to allocate a hardware bitmap");
}
return bitmap::createBitmap (env, hardwareBitmap.release(), bitmapCreateFlags,
ninePatchChunk, ninePatchInsets, -1);
}
//最终返回一个创建好的bitmap对象
return bitmap::createBitmap (env, defaultAllocator.getStorageObjAndReset(),
bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}
通过阅读源码可以知道,Android的Bitmap其实分为两部分,一部分是Java层面的,一部分是native层面的。Java层面的Bitmap对象可以在不用的时候被系统回收掉,但是native层分配的内存区域就不行了,虚拟机是不能直接回收的。所以我们看一下Bitmap.recycle()方法,它的确调用了JNI nativeRecycle,并将对应native层对象的指针作为实参传入,不言而喻是为了让native层释放掉对应指针上的native Bitmap对象,
static jboolean Bitmap_recycle(JNIEnv* env, jobject, jlong bitmapHandle) {
LocalScopedBitmap bitmap(bitmapHandle);
//释放对应bitmap内存
bitmap->freePixels();
return JNI_TRUE;
}
...
void freePixels() {
mInfo = mBitmap->info();
mHasHardwareMipMap = mBitmap->hasHardwareMipMap();
mAllocationSize = mBitmap->getAllocationByteCount();
mRowBytes = mBitmap->rowBytes();
mGenerationId = mBitmap->getGenerationID();
mIsHardware = mBitmap->isHardware();
//最终调用SkBitmap reset方法释放内存
mBitmap.reset();
}
...
void SkBitmap::reset() {
fPixelRef = nullptr; // Free pixels.
fPixmap.reset();
fFlags = 0;
}
使用注意事项
- 及时回收分配的Bitmap内存,尤其是在加载很多图片的时候,不然很容易出现oom。如果在Activity中加载Bitmap,在其onStop或者onDestroy时调用
Bitmap.recycle()方法释放native层分配的内存,避免内存泄露。 - 在创建Bitmap时很容易抛出异常,所以创建Bitmap时尽量加上try-catch。
- 缓存、复用Bitmap对象。
- 加载Bitmap时进行采样 (压缩图片)。