Android Bitmap解析

1,355 阅读5分钟

前言

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占用内存计算

官方的公式为,

图片占用的内存 = {图片总像素数} \times {每像素字节数}

比如,要加载一张1024 * 1024的使用ARGB_8888配置的图片,Bitmap占用的字节数是,

{1024} \times {1024} \times {4} = 4MB

可见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;
}

使用注意事项

  1. 及时回收分配的Bitmap内存,尤其是在加载很多图片的时候,不然很容易出现oom。如果在Activity中加载Bitmap,在其onStop或者onDestroy时调用Bitmap.recycle()方法释放native层分配的内存,避免内存泄露。
  2. 在创建Bitmap时很容易抛出异常,所以创建Bitmap时尽量加上try-catch。
  3. 缓存、复用Bitmap对象。
  4. 加载Bitmap时进行采样 (压缩图片)。