Android Bitmap 那些事

4,585 阅读17分钟

在平时的开发中,Bitmap是我们接触最多的话题之一,因为它时不时地就来个OOM,让我们猝不及防。因此有必要来一次彻底的学习,搞清楚Bitmap的一些本质。 本文主要想讲清楚两点内容:

  1. Bitmap到底占多大内存
  2. Bitmap复用的限制

OK,开始之前先介绍下解码图片时的控制类BitmapFactory.Options

BitmapFactory.Options类解析

OptionsBitmapFactory从输入源中decode Bitmap的控制参数,其主要属性可以参见源码,此处给出一些常用属性的用法。

inMutable

若为true,则返回的Bitmap是可变的,可以作为Canvas的底层Bitmap使用。 若为false,则返回的Bitmap是不可变的,只能进行读操作。 如果要修改Bitmap,那就必须返回可变的bitmap,例如:修改某个像素的颜色值(setPixel)

inJustDecodeBounds、outWidth、outHeight

获取Bitmap的宽度和高度最好的方式: 若inJustDecodeBounds为true,则不会把bitmap加载到内存(实际是在Native层解码了图片,但是没有生成Java层的Bitmap),只是获取该bitmap的原始宽(outWidth)和高(outHeight)。例如:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
Bitmap bmp = BitmapFactory.decodeFile(path, options);
int width = options.outWidth;
int height = options.outHeight;

这里若inJustDecodeBounds为true,则outWidth表示没有经过Scale的Bitmap的原始宽高(即我们通过图片编辑软件看到的宽高),否则则为加载到内存后,真实的Bitmap宽高(经过Scale之后的宽高)。

outMimeType

表示被加载图片的格式,例如:若加载png格式的图片,则为 image/png;若加载jpg格式的图片,则为image/jpeg

从API21开始,这两个属性被废弃了。 在API19及以下,则表示以下含义: 若inPurgeable为true,则表示BitmapFactory创建的用于存储Bitmap Pixel的内存空间,可以在系统内存不足时被回收。

当APP需要再次访问Bitmap的Pixel时(例如:绘制Bitmap或是调用getPixel),系统会再次调用BitmapFactory decode方法重新生成Bitmap的Pixel数组。

inInputShareable表示是否进行深拷贝,与inPurgeable结合使用,inPurgeable为false时,该参数无意义。 若为true,share a reference to the input data (inputstream, array, etc.) ,即浅拷贝。 若为false,must make a deep copy.即深拷贝。

inPreferredConfig

表示一个像素需要多大的存储空间: 默认为ARGB_8888: 每个像素4字节. 共32位。 Alpha_8: 只保存透明度,共8位,1字节。 ARGB_4444: 共16位,2字节。 RGB_565:共16位,2字节,只存储RGB值。

inBitmap

Android在API11添加的属性,用于重用已有的Bitmap,这样可以减少内存的分配与回收,提高性能。但是使用该属性存在很多限制: 在API19及以上,存在两个限制条件:

  1. 被复用的Bitmap必须是Mutable。违反此限制,不会抛出异常,且会返回新申请内存的Bitmap。
  2. 被复用的Bitmap的内存大小(通过Bitmap.getAllocationByteCount方法获得,API19及以上才有)必须大于等于被加载的Bitmap的内存大小。违反此限制,将会导致复用失败,抛出异常IllegalArgumentException(Problem decoding into existing bitmap)

在API11 ~ API19之间,还存在额外的限制:

  1. 被复用的Bitmap的宽高必须等于被加载的Bitmap的原始宽高。(注意这里是指原始宽高,即没进行缩放之前的宽高)
  2. 被加载Bitmap的Options.inSampleSize必须明确指定为1。
  3. 被加载Bitmap的Options.inPreferredConfig字段设置无效,因为会被被复用的Bitmap的inPreferredConfig值所覆盖(不然,所占内存可能就不一样了)

关于inBitmap属性,后面会进行详细的介绍,此处不再赘述。

inScaled、inDensity、inTargetDensity、inScreenDensity

这几个属性值控制了如何对Bitmap进行缩放,以及决定了bitmap的密度densityinScaled表示是否进行缩放: 若为true,且inDensity和inTargetDensity不为0,那么缩放因子等于(inTargetDensity/inDensity),并且bitmap的density等于inTargetDensity。 若为false,则不会进行缩放,并且bitmap的density等于inDensity(不为0的前提下),否则就是系统默认密度(160)。

inScaled属性默认为true, 当从Drawable资源文件夹加载图片资源时(通过BitmapFactory.decodeResource方法加载),inDensity默认初始化为图片所在文件夹对应的密度,而inTargetDensity则初始化为当前系统密度。

当从SD卡 or 二进制流加载图片资源时,这两个属性都默认为0(即不会对图片资源进行缩放),需要我们根据实际情况进行设置,一般把inTargetDensity设置为当前系统密度,inDensity则需要根据图片实际尺寸和需求进行设置了。

inSampleSize

主要用于获取Bitmap的缩略图,例如:inSampleSize=2,那么bitmap的宽度和高度为原来尺寸的1/2。像素总数则为原来的1/4。Any value <= 1="" is="" treated="" the="" same="" as="" 1.="" 看了下代码,在native层解码生成SKBitmap的像素数据时,会根据图片原始宽高除以inSampleSize,得到缩略图的宽高。

Bitmap的内存占用

首先要明确一点,图片在内存和文件系统中的大小是两个不同的概念。这里我们主要考虑Bitmap占用内存的大小。(文件系统中的大小是单独的话题,后续会进行介绍) 决定Bitmap占用内存大小的关键因素有以下几点:

  1. 图片的原始宽高(即我们在图片编辑软件中看到的宽高)
  2. 解码图片时的Config配置(即每个像素占用几个字节)
  3. 解码图片时的缩放因子(即inTargetDensity/inDensity

所以Bitmap的内存计算公式时

originWidth * originHeight * (inTargetDensity/inDensity) * (inTargetDensity/inDensity) * 每像素占用字节数

其实,Bitmap在API12提供了getByteCount方法获取占用内存,如下所示:

public final int getByteCount() {
    return getRowBytes() * getHeight();
    }

其中getRowBytes()会调用到Native层处理,其实就是表示一行像素所占的内存大小,即width * 每像素占用字节数

在API19提供了getAllocationByteCount方法获取实际占用的内存,如下所示:

public final int getAllocationByteCount() {
    if (mBuffer == null) {
        return getByteCount();
    }
    return mBuffer.length;
    }
    

mBuffer.length实际获取的就是用来存储Bitmap像素数据的字节数组的长度。(若是通过复用其他Bitmap来解码图片,那么这个字节数组存储新Bitmap的像素数据时,可能并没有用完)

一般情况下,两者是相同的。但若是通过复用Bitmap来解码图片,那么前者表示新解码图片占用内存的大小(并非实际内存大小),后者表示被复用Bitmap真实占用的内存大小(即mBuffer的长度)。 来看个例子吧:


BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
options.inDensity = 160;
options.inTargetDensity = 160;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.a, options);
Log.i(TAG, "bitmap = " + bitmap);
Log.i(TAG, "bitmap.size = " + bitmap.getByteCount());
Log.i(TAG, "bitmap.allocSize = " + bitmap.getAllocationByteCount());
options.inBitmap = bitmap;
options.inDensity = 160;
options.inTargetDensity = 80;
options.inMutable = true;
options.inSampleSize = 1;
Bitmap bitmapAIO = BitmapFactory.decodeResource(getResources(), R.drawable.b, options);
Log.i(TAG, "bitmapAIO = " + bitmapAIO);
Log.i(TAG, "bitmap.size = " + bitmap.getByteCount());
Log.i(TAG, "bitmap.allocSize = " + bitmap.getAllocationByteCount());
Log.i(TAG, "bitmapAIO.size = " + bitmapAIO.getByteCount());
Log.i(TAG, "bitmapAIO.allocSize = " + bitmapAIO.getAllocationByteCount());
输出:
bitmap = android.graphics.Bitmap@9fb5d09
bitmap.size = 8294400
bitmap.allocSize = 8294400
bitmapAIO = android.graphics.Bitmap@9fb5d09
bitmap.size = 2073600
bitmap.allocSize = 8294400
bitmapAIO.size = 2073600
bitmapAIO.allocSize = 8294400

从上述demo,可以得出:

  1. Bitmap复用成功了,因为bitmap和bitmapAIO是相同的对象。
  2. 图片a占用内存8294400,图片b(宽和高各缩小一半)占用内存2073600,正好是图片a所占内存的1/4。
  3. getByteCount方法返回当前图片应当所占内存大小,getAllocationByteCount返回被复用Bitmap真实占用内存大小。

缩放因子和Bitmap复用限制的由来

在上述计算Bitmap占用内存的公式中,有一个缩放因子,决定了对原始图片Scale多少倍。下面我们看看Android API18和API19两个版本是如何处理对原始图片的缩放操作的(其实就是处理inTargetDensity/inDensity)。同时也从源码层面上,验证下上文提及的不同Android版本对Bitmap复用限制的差异。

因为通过BitmapFactory解码图片的方法很多,这里我们选择从Drawable文件夹解码的方法decodeResource来进行分析。

Android4.3 API18

decodeResource(Resources res, int id, Options opts) {
    Bitmap bm = null;
    InputStream is = null; 
    final TypedValue value = new TypedValue();
    is = res.openRawResource(id, value);
    bm = decodeResourceStream(res, value, is, null, opts);
    if (bm == null && opts != null && opts.inBitmap != null) {
        throw new IllegalArgumentException("Problem decoding into existing bitmap");
    }
    return bm;
    }

下面继续看decodeResourceStream方法

decodeResourceStream(Resources res, TypedValue value,
            InputStream is, Rect pad, Options opts) {
    if (opts == null) {
        opts = new Options();
    }
    if (opts.inDensity == 0 && value != null) {
        final int density = value.density;
        if (density == TypedValue.DENSITY_DEFAULT) {
            opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
        } else if (density != TypedValue.DENSITY_NONE) {
            opts.inDensity = density;
        }
    }
    if (opts.inTargetDensity == 0 && res != null) {
        opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
    }
    return decodeStream(is, pad, opts);
    }

下面继续看decodeStream方法(极度精简之后的):

boolean finish = true; 
if (opts == null || (opts.inScaled && opts.inBitmap == null)) {
    float scale = 1.0f;
    int targetDensity = 0;
    if (opts != null) {
        final int density = opts.inDensity;
        targetDensity = opts.inTargetDensity;
        if (density != 0 && targetDensity != 0) {
            scale = targetDensity / (float) density;
        }
    }
    bm = nativeDecodeStream(is, tempStorage, outPadding, opts, true, scale);
    if (bm != null && targetDensity != 0) bm.setDensity(targetDensity);
    finish = false;
    } else {
    bm = nativeDecodeStream(is, tempStorage, outPadding, opts);
    }
if (bm == null && opts != null && opts.inBitmap != null) {
    throw new IllegalArgumentException("Problem decoding into existing bitmap");
    }
    return finish ? finishDecode(bm, outPadding, opts) : bm;

decodeStream方法很关键,从中可以获取几点关键信息:

  1. 若是直接解码图片(不复用已有图片,即opts.inBitmap为null),那么通过native方法解码图片时,支持把缩放因子作为参数传递到native层,并且后续在java层直接返回了native层解码出来的Bitmap。说明在native层处理了对图片的缩放
  2. 若是通过复用已有Bitmap来解码图片,那么通过native方法解码图片时,就不支持传递缩放因子参数了,并且后续在java层,通过finishDecode方法完成了对图片的缩放。说明若复用已有Bitmap解码图片,则不支持在native层对图片进行缩放处理,需要在java层单独对图片进行缩放处理

    简单概括下:Android API18及之前的版本,不支持在native层同时使用Bitmap复用和进行缩放处理。(API18及之后是可以的,下面会介绍)

下面我们先看下,finishDecode方法是怎么在java层完成对Bitmap的缩放处理的,再看下native层的解码方法。 首先看下finishDecode方法,如下所示(精简之后):

finishDecode(Bitmap bm, Rect outPadding, Options opts) {
    final int density = opts.inDensity;
    if (density == 0) { 
        return bm;
    }
    bm.setDensity(density);
    final int targetDensity = opts.inTargetDensity;
    if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) {
            return bm;
    }
    if (opts.inScaled || isNinePatch) {
        float scale = targetDensity / (float) density;
        if (scale != 1.0f) {
            final Bitmap oldBitmap = bm;
            bm = Bitmap.createScaledBitmap(oldBitmap,
                        Math.max(1, (int) (bm.getWidth() * scale + 0.5f)),
                        Math.max(1, (int) (bm.getHeight() * scale + 0.5f)), true);
            if (bm != oldBitmap) oldBitmap.recycle();
        }
        bm.setDensity(targetDensity);
    }
    return bm;
    }

finishDecode方法很简单,就是根据缩放因子,在原来Bitmap的基础上,又新建了一个Bitmap,然后把原图回收了。这里新创建的Bitmap就是最终的Bitmap。这种方式会造成在java层重新分配内存,显然不是很好。所以在Android4.4之后,都是在native层完成对Bitmap的缩放处理的。(也就是Android4.4之后,同时支持复用已有Bitmap和在native层对原图进行缩放处理,后面进行介绍)。 下面看下native层的解码方法:

static jobject doDecode(JNIEnv* env, SkStream* stream, jobject padding,        jobject options, bool allowPurgeable, bool forcePurgeable = false,        bool applyScale = false, float scale = 1.0f) {
int sampleSize = 1;
bool isMutable = false;
bool willScale = applyScale && scale != 1.0f;
prefConfig = GraphicsJNI::getNativeBitmapConfig(env, jconfig);
isMutable = env->GetBooleanField(options, gOptions_mutableFieldID);
javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID);
if (willScale && javaBitmap != NULL) {
    return nullObjectReturn("Cannot pre-scale a reused bitmap");
    }
SkImageDecoder* decoder = SkImageDecoder::Factory(stream);
decoder->setSampleSize(sampleSize);
JavaPixelAllocator javaAllocator(env);
SkBitmap* bitmap;
bool useExistingBitmap = false;
if (javaBitmap == NULL) {
    bitmap = new SkBitmap;
    } else {
    if (sampleSize != 1) { 
        return nullObjectReturn("SkImageDecoder: Cannot reuse bitmap with sampleSize != 1");
    }
    bitmap = (SkBitmap*) env->GetIntField(javaBitmap, gBitmap_nativeBitmapFieldID);
    if (!bitmap->isImmutable()) { 
        useExistingBitmap = true;
        prefConfig = bitmap->getConfig();
    } else {
        ALOGW("Unable to reuse an immutable bitmap as an image decoder target.");
        bitmap = new SkBitmap;
    }
    }
SkBitmap* decoded;
if (willScale) { 
    decoded = new SkBitmap;
    } else {
    decoded = bitmap;
    }
if (!decoder->decode(stream, decoded, prefConfig, decodeMode, javaBitmap != NULL)) {
    return nullObjectReturn("decoder->decode returned false");
    }
int scaledWidth = decoded->width();
int scaledHeight = decoded->height();
if (willScale && mode != SkImageDecoder::kDecodeBounds_Mode) {
    scaledWidth = int(scaledWidth * scale + 0.5f);
    scaledHeight = int(scaledHeight * scale + 0.5f);
    }
if (options != NULL) {
    env->SetIntField(options, gOptions_widthFieldID, scaledWidth);
    env->SetIntField(options, gOptions_heightFieldID, scaledHeight);
    env->SetObjectField(options, gOptions_mimeFieldID,getMimeTypeString(env, decoder->getFormat()));
    }
if (mode == SkImageDecoder::kDecodeBounds_Mode) {
    return NULL;
    }
if (willScale) {
    const float sx = scaledWidth / float(decoded->width());
    const float sy = scaledHeight / float(decoded->height());
    SkBitmap::Config config = decoded->config();
    switch (config) {
        case SkBitmap::kNo_Config:
        case SkBitmap::kIndex8_Config:
        case SkBitmap::kRLE_Index8_Config:
            config = SkBitmap::kARGB_8888_Config;
            break;
        default:
            break;
        }
        bitmap->setConfig(config, scaledWidth, scaledHeight);
        bitmap->setIsOpaque(decoded->isOpaque());
        if (!bitmap->allocPixels(&javaAllocator, NULL)) {
            return nullObjectReturn("allocation failed for scaled bitmap");
        }
        bitmap->eraseColor(0);
        SkPaint paint;
        paint.setFilterBitmap(true);
        SkCanvas canvas(*bitmap);
        canvas.scale(sx, sy);
        canvas.drawBitmap(*decoded, 0.0f, 0.0f, &paint);
        }
if (useExistingBitmap) {
    return javaBitmap;
    }
return GraphicsJNI::createBitmap(env, bitmap, javaAllocator.getStorageObj(),
            isMutable, ninePatchChunk, layoutBounds, -1);
            }

OK,通过上述代码,我们了解到以下几点信息:

  1. Android 4.3在native层不支持即复用已有Bitmap,又进行缩放。
  2. 若缩放因子为1,那么在进行Bitmap复用时(假设满足复用条件),会直接在被复用Bitmap上进行解码操作(主要是修改被复用Bitmap的像素数据信息),同时返回到java层的就是被复用的Bitmap。(即如果被复用的Bitmap == 返回的被加载的Bitmap,那么说明复用成功了)。
  3. 若缩放因子大于1,且没有Bitmap复用,那么首先会解码生成一个图片原始宽高的SkBitmap,然后在再根据缩放因子,通过绘制的方式,把原始宽高的Bitmap会绘制到经过Scale之后的SkCanvas上,以得到一个缩放后的SkBitmap,最后调用java层bitmap的构造方法,创建java层的Bitmap,然后返回到放大调用处。

上面我们提出了几点在Android4.4之前复用Bitmap的限制,在doDecode方法中基本都得到了验证。唯独对宽高必须相等的限制没有见到。其实这个限制是在解码得到原始宽高的Bitmap时进行的,即上面代码中的decoder->decode方法中。这里的decoder解码器(SkImageDecoder是父类型)根据解码不同格式的图片,是不同的实现类对象。下面我们看一下SkImageDecoder_libpng类的实现(png格式的解码器)

bool SkPNGImageDecoder::onDecode(SkStream* sk_stream, SkBitmap* decodedBitmap,
                                 Mode mode) {
                                 const int sampleSize = this->getSampleSize();
SkScaledBitmapSampler sampler(origWidth, origHeight, sampleSize);
decodedBitmap->lockPixels();
void* rowptr = (void*) decodedBitmap->getPixels();
bool reuseBitmap = (rowptr != NULL);
decodedBitmap->unlockPixels();
if (reuseBitmap && (sampler.scaledWidth() != decodedBitmap->width() ||
            sampler.scaledHeight() != decodedBitmap->height())) {
    return false;
    }
if (!reuseBitmap) {
    decodedBitmap->setConfig(config, sampler.scaledWidth(),sampler.scaledHeight(), 0);
    }
if (SkImageDecoder::kDecodeBounds_Mode == mode) {
    return true;
    }
if (!reuseBitmap) {
    if (!this->allocPixelRef(decodedBitmap,SkBitmap::kIndex8_Config == config ? colorTable : NULL)) {
        return false;
    }
    }

上述代码就是解码png格式图片的关键代码,里面印证了复用Bitmap时,宽高必须相等的说法。

通过上述代码的分析,我们可以有以下(反复强调)结论:

  1. 若是直接解码图片(不复用已有图片,即opts.inBitmap为null),则是在naive层处理对原图的缩放操作。
  2. 若是通过复用已有Bitmap来解码图片,则是在java层处理对原图的缩放操作(finishDecode方法)。

Android4.4 API19

Android从API19开始,放宽了对Bitmap复用的限制。下面我们看下API19对Bitmap复用的两个限制是在哪里实现的,以及该版本是如何对原图进行缩放的。 从decodeResource -> decodeResourceStream方法的实现和API18类似,这里不再赘述,差异点出现在decodeStream方法。如下所示(精简后):

Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
Bitmap bm = decodeStreamInternal(is, outPadding, opts);
}
if (bm == null && opts != null && opts.inBitmap != null) {
    throw new IllegalArgumentException("Problem decoding into existing bitmap");
    }
setDensityFromOptions(bm, opts);
return bm;

API19版本的decodeStream和API18相比,最明显的就是删除了Java层缩放Bitmap的逻辑(finishDecode方法),因此可以确定对Bitmap的缩放处理都是在native层处理的。下面我们继续看下native方法的实现(极度精简):

static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding,        jobject options, bool allowPurgeable, bool forcePurgeable = false) {
SkBitmap::Config prefConfig = SkBitmap::kARGB_8888_Config;
bool isMutable = false;
float scale = 1.0f;
jobject javaBitmap = NULL;
sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
if (optionsJustBounds(env, options)) {
    mode = SkImageDecoder::kDecodeBounds_Mode;
    }
if (options != NULL) {
    sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
    if (optionsJustBounds(env, options)) {
        mode = SkImageDecoder::kDecodeBounds_Mode;
    }
    env->SetIntField(options, gOptions_widthFieldID, -1);
    env->SetIntField(options, gOptions_heightFieldID, -1);
    env->SetObjectField(options, gOptions_mimeFieldID, 0);
    jobject jconfig = env->GetObjectField(options, gOptions_configFieldID);
    prefConfig = GraphicsJNI::getNativeBitmapConfig(env, jconfig);
    isMutable = env->GetBooleanField(options, gOptions_mutableFieldID);
    javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID);
    if (env->GetBooleanField(options, gOptions_scaledFieldID)) { 
        const int density = env->GetIntField(options, gOptions_densityFieldID);
        const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
        const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
        if (density != 0 && targetDensity != 0 && density != screenDensity) {
                scale = (float) targetDensity / density;
        }
    }
    }
const bool willScale = scale != 1.0f;
SkImageDecoder* decoder = SkImageDecoder::Factory(stream);
decoder->setSampleSize(sampleSize);
SkBitmap* outputBitmap = NULL;
unsigned int existingBufferSize = 0;
if (javaBitmap != NULL) {
    outputBitmap = (SkBitmap*) env->GetIntField(javaBitmap, gBitmap_nativeBitmapFieldID);
    if (outputBitmap->isImmutable()) { 
        ALOGW("Unable to reuse an immutable bitmap as an image decoder target.");
        javaBitmap = NULL;
        outputBitmap = NULL;
    } else { 
        existingBufferSize = GraphicsJNI::getBitmapAllocationByteCount 
    }
    }
SkAutoTDelete adb(outputBitmap == NULL ? new SkBitmap : NULL);
if (outputBitmap == NULL) outputBitmap = adb.get();
JavaPixelAllocator javaAllocator(env);
RecyclingPixelAllocator recyclingAllocator(outputBitmap->pixelRef(), existingBufferSize);
ScaleCheckingAllocator scaleCheckingAllocator(scale, existingBufferSize);
SkBitmap::Allocator* outputAllocator = (javaBitmap != NULL) ? (SkBitmap::Allocator*)&recyclingAllocator : (SkBitmap::Allocator*)&javaAllocator;
if (decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
    if (!willScale) {
        decoder->setSkipWritingZeroes(outputAllocator == &javaAllocator);
        decoder->setAllocator(outputAllocator);
    } else if (javaBitmap != NULL) { 
        decoder->setAllocator(&scaleCheckingAllocator);
    }
    } 
SkBitmap decodingBitmap;
if (!decoder->decode(stream, &decodingBitmap, prefConfig, decodeMode)) {
    return nullObjectReturn("decoder->decode returned false");
    }
int scaledWidth = decodingBitmap.width();
int scaledHeight = decodingBitmap.height();
if (willScale && mode != SkImageDecoder::kDecodeBounds_Mode) {
    scaledWidth = int(scaledWidth * scale + 0.5f);
    scaledHeight = int(scaledHeight * scale + 0.5f);
    }
if (options != NULL) {
    env->SetIntField(options, gOptions_widthFieldID, scaledWidth);
    env->SetIntField(options, gOptions_heightFieldID, scaledHeight);
    env->SetObjectField(options, gOptions_mimeFieldID,getMimeTypeString(env, decoder->getFormat()));
    }
 if (willScale) { 
    const float sx = scaledWidth / float(decodingBitmap.width());
    const float sy = scaledHeight / float(decodingBitmap.height());
    SkBitmap::Config config = configForScaledOutput(decodingBitmap.config());
    outputBitmap->setConfig(config, scaledWidth, scaledHeight, 0,decodingBitmap.alphaType());
    if (!outputBitmap->allocPixels(outputAllocator, NULL)) {
        return nullObjectReturn("allocation failed for scaled bitmap");
    }
    SkPaint paint;
    paint.setFilterLevel(SkPaint::kLow_FilterLevel);
    SkCanvas canvas(*outputBitmap);
    canvas.scale(sx, sy);
    canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
    } else { 
    outputBitmap->swap(decodingBitmap);
    }
if (javaBitmap != NULL) {
    ...
    return javaBitmap;
    }
return GraphicsJNI::createBitmap(env, outputBitmap, javaAllocator.getStorageObj(),bitmapCreateFlags, ninePatchChunk, layoutBounds, -1);
}

从上述代码,我们可以有以下几点结论

  1. API19以上复用Bitmap时依然要求被复用的Bitmap必须可变的。
  2. native层是通过把原始bitmap重新draw到一个放大(sx,xy)倍的Bitmap上,来实现缩放处理的。
  3. 相比API18,API19引入了两个不同策略的内存分配器:RecyclingPixelAllocator和ScaleCheckingAllocator,并在创建分配器实例时,将被复用位图的像素缓冲区大小传给了分配器实例,而Bitmap复用中的内存大小限制就在这两个Allocator里。同时根据上述代码逻辑可知:
    1. 若是复用Bitmap且缩放因为为1,那么使用RecyclingPixelAllocator进行内存分配。
    2. 若是复用Bitmap且缩放因子不为1,那么使用ScaleCheckingAllocator进行内存分配。

下面我们看下这两个Allocator怎么进行bitmap复用限制的。

首先看下在RecyclingPixelAllocator的实现。

class RecyclingPixelAllocator : public SkBitmap::Allocator {
public:
    RecyclingPixelAllocator(SkPixelRef* pixelRef, unsigned int size)
            : mPixelRef(pixelRef), mSize(size) {
        SkSafeRef(mPixelRef);
    }
    ~RecyclingPixelAllocator() {
        SkSafeUnref(mPixelRef);
    }
    virtual bool allocPixelRef(SkBitmap* bitmap, SkColorTable* ctable) {
        if (!bitmap->getSize64().is32() || bitmap->getSize() > mSize) {
            ALOGW("bitmap marked for reuse (%d bytes) can't fit new bitmap (%d bytes)",
                    mSize, bitmap->getSize());
            return false;
        }
        SkImageInfo bitmapInfo;
        if (!bitmap->asImageInfo(&bitmapInfo)) {
            ALOGW("unable to reuse a bitmap as the target has an unknown bitmap configuration");
            return false;
        }
        SkPixelRef* pr = new AndroidPixelRef(*static_cast(mPixelRef),
                bitmapInfo, bitmap->rowBytes(), ctable);
        bitmap->setPixelRef(pr)->unref();
        bitmap->lockPixels();
        return true;
    }
    private:
    SkPixelRef* const mPixelRef;
    const unsigned int mSize;
    };

从上述代码可知,API19是在为bitmap分配像素缓冲区时,判断是否可以复用已有bitmap的。若被复用bitmap的像素缓冲区大小大于等于需要为新bitmap分配的像素缓冲区大小,那么就可以复用,否则就不可以复用。

同时还有很关键的一点结论:RecyclingPixelAllocator.allocPixelRef方法才是真正复用被复用Bitmap像素缓冲区的地方

然后,看下ScaleCheckingAllocator的实现.

class ScaleCheckingAllocator : public SkBitmap::HeapAllocator {
public:
    ScaleCheckingAllocator(float scale, int size)
            : mScale(scale), mSize(size) {
    }
    virtual bool allocPixelRef(SkBitmap* bitmap, SkColorTable* ctable) {
        const int bytesPerPixel = SkBitmap::ComputeBytesPerPixel(
                configForScaledOutput(bitmap->config()));
        const int requestedSize = bytesPerPixel * (int)(bitmap->width() * mScale + 0.5f) * (int)(bitmap->height() * mScale + 0.5f);
        if (requestedSize > mSize) {
            ALOGW("bitmap for alloc reuse (%d bytes) can't fit scaled bitmap (%d bytes)",
                    mSize, requestedSize);
            return false;
        }
        return SkBitmap::HeapAllocator::allocPixelRef(bitmap, ctable);
    }
    private:
    const float mScale;
    const int mSize;
    };

ScaleCheckingAllocator.allocPixelRef方法根据bitmap的config,原始宽高,缩放因子等因素,计算出缩放后需要多大的像素缓冲区空间,以此来判断是否可以复用。若可以复用,那么这里会解码出一个原始宽高的bitmap(真正分配了像素缓冲区),然后在BitmapFactory.doDecode方法中,处理对原图的缩放,此时RecyclingPixelAllocator类负责为缩放后的bitmap分配像素缓冲区,也就达到了bitmap复用的目的(也就是我们上面说的RecyclingPixelAllocator.allocPixelRef方法才是真正复用被复用Bitmap像素缓冲区的地方)。

综合来看,所谓Bitmap复用,主要就是复用被复用Bitmap的像素缓冲区数据,避免了内存的重复申请和释放。(同时修改bitmap宽度,高度等信息)

最后我们再看下API19版本,png解码器的工作(精简后):

bool SkPNGImageDecoder::onDecode(SkStream* sk_stream, SkBitmap* decodedBitmap,Mode mode) {
png_uint_32 origWidth, origHeight;
int bitDepth, colorType, interlaceType;
png_get_IHDR(png_ptr, info_ptr, &origWidth, &origHeight, &bitDepth,&colorType, &interlaceType, int_p_NULL, int_p_NULL);
if (!this->getBitmapConfig(png_ptr, info_ptr, &config, &hasAlpha, &theTranspColor)) {
    return false;
    }
const int sampleSize = this->getSampleSize();
SkScaledBitmapSampler sampler(origWidth, origHeight, sampleSize);
decodedBitmap->setConfig(config, sampler.scaledWidth(), sampler.scaledHeight());
}
if (SkImageDecoder::kDecotonguodeBounds_Mode == mode) {
    return true;
    }
if (!this->allocPixelRef(decodedBitmap,SkBitmap::kIndex8_Config == config ? colorTable : NULL)) {
    return false;
    }
}
SkBitmap::Allocator* SkImageDecoder::setAllocator(SkBitmap::Allocator* alloc) {
    if (alloc) alloc->ref();
    if (fAllocator) fAllocator->unref();
    fAllocator = alloc;
    return alloc;
    }
bool SkImageDecoder::allocPixelRef(SkBitmap* bitmap,SkColorTable* ctable) const {
    return bitmap->allocPixels(fAllocator, ctable);
    }
bool SkBitmap::allocPixels(Allocator* allocator, SkColorTable* ctable) {
    HeapAllocator stdalloc;
    if (NULL == allocator) {
        allocator = &stdalloc;
    }
    return allocator->allocPixelRef(this, ctable);
    }

从上述代码可知:相比于API18,API19上SkImageDecoder_libpng.onDecode的实现稍微有点不同:主要是不再进行Bitmap复用逻辑的处理,因为在BitmapFactory.doDecode方法中每次解码(SkImageDecoder_libpng.onDecode)传进来的参数都是新创建的decodingBitmap。现在是在BitmapFactory.doDecode方法中处理Bitmap的复用(通过RecyclingPixelAllocator实现)。

这样整个逻辑就串联起来了,这里简单总结下API19解码图片的几个点:

  1. 在java层不再处理图片缩放,而是统一在native层BitmapFactory.doDecode方法中处理对Bitmap的缩放。
  2. 验证了上文说的API19对Bitmap复用的两个限制。
  3. 所谓Bitmap复用,主要就是复用被复用Bitmap的像素缓冲区,避免了内存的重复申请和释放。
  4. RecyclingPixelAllocator.allocPixelRef方法才是真正复用被复用Bitmap像素缓冲区的地方。

使用Bitmap的建议

网上有很多这方面的资料,这里不打算赘述。 推荐仔细阅读下官方教程:Displaying Bitmaps Efficiently

同时如果想在API19以下,突破Bitmap复用的限制(达到和API19以上一样的效果),那么可以参考Github上的一个方案:BitmapFactoryCompat

OK,任务完成,希望对有需要的人有所帮助。