Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the Parcelable-Relevant Size of a Bitmap?

Tags:

android

On API Level 19+ devices, we have getByteCount() and getAllocationByteCount(), each of which return a size of the Bitmap in bytes. The latter takes into account the fact that a Bitmap can actually represent a smaller image than its byte count (e.g., the Bitmap originally held a larger image, but then was used with BitmapFactory.Options and inBitmap to hold a smaller image).

In most Android IPC scenarios, particularly those involving Parcelable, we have a 1MB "binder transaction limit".

For the purposes of determining whether a given Bitmap is small enough for IPC, do we use getByteCount() or getAllocationByteCount()?

My gut instinct says that we use getByteCount(), as that should be the number of bytes the current image in the Bitmap is taking up, but I was hoping somebody had a more authoritative answer.

like image 229
CommonsWare Avatar asked Jul 14 '15 20:07

CommonsWare


1 Answers

The size of the image data written to the parcel is getByteCount() plus the size of the Bitmap's color table, if it has one. There are also approximately 48 bytes of Bitmap attributes written to the parcel. The following code analysis and tests provide the basis for these statements.

The native function to write a Bitmap to a Parcel begins at line 620 of this file. The function is included here with explanation added:

static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject,
                                     jlong bitmapHandle,
                                     jboolean isMutable, jint density,
                                     jobject parcel) {
    const SkBitmap* bitmap = reinterpret_cast<SkBitmap*>(bitmapHandle);
    if (parcel == NULL) {
        SkDebugf("------- writeToParcel null parcel\n");
        return JNI_FALSE;
    }

    android::Parcel* p = android::parcelForJavaObject(env, parcel);

The following seven ints are the first data written to the parcel. In Test #2, described below, these values are read from a sample parcel to confirm the size of data written for the Bitmap.

p->writeInt32(isMutable);
p->writeInt32(bitmap->colorType());
p->writeInt32(bitmap->alphaType());
p->writeInt32(bitmap->width());
p->writeInt32(bitmap->height());
p->writeInt32(bitmap->rowBytes());
p->writeInt32(density);

If the bitmap has a color map, it is written to the parcel. A precise determination the parcel size of a bitmap must address this also.

if (bitmap->colorType() == kIndex_8_SkColorType) {
    SkColorTable* ctable = bitmap->getColorTable();
    if (ctable != NULL) {
        int count = ctable->count();
        p->writeInt32(count);
        memcpy(p->writeInplace(count * sizeof(SkPMColor)),
               ctable->lockColors(), count * sizeof(SkPMColor));
        ctable->unlockColors();
    } else {
        p->writeInt32(0);   // indicate no ctable
    }
}

Now we get to the core of the question: How much data is written for the bitmap image? The amount is determined by this call to bitmap->getSize(). That function is analyzed below. Note here that the value is stored in size, which is used in the code following to write the blob for the image data, and copy the data into the memory pointed to by the blob.

size_t size = bitmap->getSize();

A variable size block of data is written to a parcel using a blob. If the block is less than 40K, it is written "in place" in the parcel. Blocks larger than 40K are written to shared memory using ashmem, and the attributes of the ashmem region are written in the parcel. The blob itself is just a small descriptor containing a few members that include a pointer to the block, its length, and a flag indicating if the block is in-place or in shared memory. The class definition for WriteableBlob is at line 262 of this file. The definition of writeBlob() is at line 747 of this file. writeBlob() determines if the data block is small enough to be written in-place. If so, it expands the parcel buffer to make room. If not, an ashmem region is created and configured. In both cases, the members of the blob (pointer, size, flag) are defined for later use when the block is copied. Note that for both cases, size defines the size of the data that will be copied, either in-place or to shared memory. When writeBlob() completes, the target data buffer is defined in blob and values written to the parcel describing how the image data block is stored (in-place or shared memory) and, for shared memory, the attributes of the ashmem region.

android::Parcel::WritableBlob blob;
android::status_t status = p->writeBlob(size, &blob);
if (status) {
    doThrowRE(env, "Could not write bitmap to parcel blob.");
    return JNI_FALSE;
}

With the target buffer for the block now setup, the data can be copied using the pointer in the blob. Note that size defines the amount of data copied. Also note that there is only one size. The same value is used for in-place and shared memory targets.

bitmap->lockPixels();
const void* pSrc =  bitmap->getPixels();
if (pSrc == NULL) {
    memset(blob.data(), 0, size);
} else {
    memcpy(blob.data(), pSrc, size);
}
bitmap->unlockPixels();

blob.release();
return JNI_TRUE;
}

That completes the analysis of Bitmap_writeToParcel. It is now clear that while small (<40K) images are written in-place and larger images are written to shared memory, the size of the data written is the same for both cases. The easiest and most direct way to see what that size is, is to create a test case using an image <40K, so that it will be written in-place. The size of the resulting parcel then reveals the size of the image data.

A second method for determining what size is requires an understanding of SkBitmap::getSize(). This is the function used in the code analyzed above to get the size of the image block.

SkBitmap::getSize() is define at line 130 of this file. It is:

size_t getSize() const { return fHeight * fRowBytes; }

Two other functions in the same file relevant to this explanation are height(), defined at line 98:

int height() const { return fHeight; }

and rowBytes(), defined at line 101:

int rowBytes() const { return fRowBytes; }

We saw these functions used in Bitmap_writeToParcel when the attributes of a bitmap are written to a parcel:

p->writeInt32(bitmap->height());
p->writeInt32(bitmap->rowBytes());

With this understanding of these functions, we now see that by dumping the first few ints in a parcel, we can see the values of fHeight and fRowBytes, from which we can infer the value returned by getSize().

The second code snippet below does this and provides further confirmation that the size of the data written to the Parcel corresponds to the value returned by getByteCount().

Test #1

This test creates a bitmap smaller than 40K to produce in-place storage of the image data. The parcel data size is then examined to show that getByteCount() determines the size of the image data stored in the parcel.

The first few statements in the code below are to produce a Bitmap smaller than 40K.

The logcat output confirms that the size of the data written to the Parcel corresponds to the value returned by getByteCount().

byteCount=38400 allocatedByteCount=38400 parcelDataSize=38428
byteCount=7680 allocatedByteCount=38400 parcelDataSize=7708

Code that produced the output shown:

    // Setup to get a mutable bitmap less than 40 Kbytes
    String path = "someSmallImage.jpg";
    Bitmap bm0 = BitmapFactory.decodeFile(path);
    // Need it mutable to change height
    Bitmap bm1 = bm0.copy(bm0.getConfig(), true);
    // Chop it to get a size less than 40K
    bm1.setHeight(bm1.getHeight() / 32);

    // Now we have a BitMap with size < 40K for the test
    Bitmap bm2 = bm1.copy(bm0.getConfig(), true);

    // What's the parcel size?
    Parcel p1 = Parcel.obtain();
    bm2.writeToParcel(p1, 0);

    // Expect byteCount and allocatedByteCount to be the same
    Log.i("Demo", String.format("byteCount=%d allocatedByteCount=%d parcelDataSize=%d",
            bm2.getByteCount(), bm2.getAllocationByteCount(), p1.dataSize()));

    // Resize to make byteCount and allocatedByteCount different
    bm2.setHeight(bm2.getHeight() / 4);

    // What's the parcel size?
    Parcel p2 = Parcel.obtain();
    bm2.writeToParcel(p2, 0);

    // Show that byteCount determines size of data written to parcel
    Log.i("Demo", String.format("byteCount=%d allocatedByteCount=%d parcelDataSize=%d",
            bm2.getByteCount(), bm2.getAllocationByteCount(), p2.dataSize()));

    p1.recycle();
    p2.recycle();

Test #2

This test stores a bitmap to a parcel, then dumps the first few ints to get the values from which image data size can be inferred.

The logcat output with comments added:

// Bitmap attributes

bc=12000000 abc=12000000 hgt=1500 wid=2000 rbyt=8000 dens=213

// Attributes after height change. byteCount changed, allocatedByteCount not.

bc=744000 abc=12000000 hgt=93 wid=2000 rbyt=8000 dens=213

// Dump of parcel data. Parcel data size is 48. Image too large for in-place.

pds=48 mut=1 ctyp=4 atyp=1 hgt=93 wid=2000 rbyt=8000 dens=213

// Show value of getSize() derived from parcel data. It equals getByteCount().

bitmap->getSize()= 744000 getByteCount()=744000

Code that produced this output:

String path = "someImage.jpg";
Bitmap bm0 = BitmapFactory.decodeFile(path);
// Need it mutable to change height
Bitmap bm = bm0.copy(bm0.getConfig(), true);

// For reference, and to provide confidence that the parcel data dump is
// correct, log the bitmap attributes.
Log.i("Demo", String.format("bc=%d abc=%d hgt=%d wid=%d rbyt=%d dens=%d",
        bm.getByteCount(), bm.getAllocationByteCount(),
        bm.getHeight(), bm.getWidth(), bm.getRowBytes(), bm.getDensity()));
// Change size
bm.setHeight(bm.getHeight() / 16);
Log.i("Demo", String.format("bc=%d abc=%d hgt=%d wid=%d rbyt=%d dens=%d",
        bm.getByteCount(), bm.getAllocationByteCount(),
        bm.getHeight(), bm.getWidth(), bm.getRowBytes(), bm.getDensity()));

// Get a parcel and write the bitmap to it.
Parcel p = Parcel.obtain();
bm.writeToParcel(p, 0);

// When the image is too large to be written in-place,
// the parcel data size will be ~48 bytes (when there is no color map).
int parcelSize = p.dataSize();

// What are the first few ints in the parcel?
p.setDataPosition(0);
int mutable   = p.readInt(); //1
int colorType = p.readInt(); //2
int alphaType = p.readInt(); //3
int width     = p.readInt(); //4
int height    = p.readInt(); //5 bitmap->height()
int rowBytes  = p.readInt(); //6 bitmap->rowBytes()
int density   = p.readInt(); //7

Log.i("Demo", String.format("pds=%d mut=%d ctyp=%d atyp=%d hgt=%d wid=%d rbyt=%d dens=%d",
        parcelSize,  mutable, colorType, alphaType, height, width, rowBytes, density));

// From code analysis, we know that the value returned
// by SkBitmap::getSize() is the size of the image data written.
// We also know that the value of getSize() is height()*rowBytes().
// These are the values in ints 5 and 6.
int imageSize = height * rowBytes;

// Show that the size of image data stored is Bitmap.getByteCount()
Log.i("Demo", String.format("bitmap->getSize()= %d getByteCount()=%d", imageSize, bm.getByteCount()));

p.recycle();
like image 147
Bob Snyder Avatar answered Nov 24 '22 22:11

Bob Snyder