Bitmap创建过程

前言

在安卓中我们图片相关的操作一定离不开Bitmap位图,可见Bitmap在图像显示中的位置是多么的重要,而且Bitmap操作不当即会引发OOM,因此我详细复习了一下Bitmap相关的知识点在此记录一下。

Bitmap介绍

位图(Bitmap),又称栅格图(英语:Raster graphics)或点阵图,是使用像素阵列(Pixel-array/Dot-matrix点阵)来表示的图像。自2005年Skia被Google收购后,一直相当神秘低调,直到2007年初,Skia GL相关的源代码才被揭露,作为Google Android平台的图形引擎,稍后的Google Chrome浏览器也采用Skia引擎。随着Android与Chrome (开放版本称为"Chromium")两大专案公布源代码后,Skia也一并公开原始源代码,以Apache License v2发布(注意,这意味着与GPLv2授权不相容) ,而Android与Chrome的源代码库中都有一份[Skia]的复制,因需求不同,做了部份的修改,比方说Chrome专案底下的 [chrome/trunk/src/skia],需要注意的是,Skia本身是不涉及底层环境,如Linux Framebuffer或Gtk+衔接的处理,这也是何以Android (透过Linux Framebuffer)与Chrome (开发中的Linux版本使用Gtk+)需要提供一份修改,以便系统接轨。 [1]

Bitmap创建过程 (Android N)

Bitmap创建

创建bitmap有很多的api,将这些api归类下,大致分文三种创建形式。

  • 根据现有的Bitmap创建新的Bitmap
/**
* 通过矩阵的方式,返回原始 Bitmap 中的一个不可变子集。新 Bitmap 可能返回的就是原始的 Bitmap,也可能还是复制出来的。
* 新 Bitmap 与原始 Bitmap 具有相同的密度(density)和颜色空间;
*
* @param source   原始 Bitmap
* @param x        在原始 Bitmap 中 x方向的其起始坐标(你可能只需要原始 Bitmap x方向上的一部分)
* @param y        在原始 Bitmap 中 y方向的其起始坐标(你可能只需要原始 Bitmap y方向上的一部分)
* @param width    需要返回 Bitmap 的宽度(px)(如果超过原始Bitmap宽度会报错)
* @param height   需要返回 Bitmap 的高度(px)(如果超过原始Bitmap高度会报错)
* @param m        Matrix类型,表示需要做的变换操作
* @param filter   是否需要过滤,只有 matrix 变换不只有平移操作才有效
*/
public static Bitmap createBitmap(@NonNull Bitmap source, int x, int y, int width, int height,
            @Nullable Matrix m, boolean filter) 
  • 根据颜色像素数组创建空的Bitmap
/**
 *
 * 返回具有指定宽度和高度的不可变位图,每个像素值设置为colors数组中的对应值。
 * 其初始密度由给定的确定DisplayMetrics。新创建的位图位于sRGB 颜色空间中。
 * @param display  显示将显示此位图的显示的度量标准
 * @param colors   用于初始化像素的sRGB数组
 * @param offset   颜色数组中第一个颜色之前要跳过的值的数量
 * @param stride   行之间数组中的颜色数(必须> = width或<= -width)
 * @param width    位图的宽度
 * @param height   位图的高度
 * @param config   要创建的位图配置。如果配置不支持每像素alpha(例如RGB_565),
 * 那么colors []中的alpha字节将被忽略(假设为FF)
 */
public static Bitmap createBitmap(@NonNull DisplayMetrics display,
        @NonNull @ColorInt int[] colors, int offset, int stride,
        int width, int height, @NonNull Config config)
  • 对现有Bitmap进行缩放处理
/**
* 对Bitmap进行缩放,缩放成宽 dstWidth、高 dstHeight 的新Bitmap
*/
public static Bitmap createScaledBitmap(@NonNull Bitmap src, int dstWidth, int dstHeight,boolean filter)

至此我们将这15种创建函数进行了行为分类成这三种模式。经过一轮追溯,我们看下这三个创建方式最终会调用到Bitmap#createBitmap()的方法之中。

public static Bitmap createBitmap(@Nullable DisplayMetrics display, int width, int height,
            @NonNull Config config, boolean hasAlpha, @NonNull ColorSpace colorSpace) {
        // 省略逻辑异常语句...
        // 最终会调用到这个JNI方法中
        Bitmap bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true,
                colorSpace == null ? 0 : colorSpace.getNativeInstance());

        if (display != null) {
            bm.mDensity = display.densityDpi;
        }
        bm.setHasAlpha(hasAlpha);
        if ((config == Config.ARGB_8888 || config == Config.RGBA_F16) && !hasAlpha) {
            nativeErase(bm.mNativePtr, 0xff000000);
        }
        return bm;
    }

到这里我们发现最终的Bitmap的创建是交由JNI调用Native方法进行时机的处理。ok 我们分析下Native的部分。我们找下JNI引用桥代码

static const JNINativeMethod gBitmapMethods[] = {
    {   "nativeCreate",             "([IIIIIIZJ)Landroid/graphics/Bitmap;",
        (void*)Bitmap_creator },
    {   "nativeCopy",               "(JIZ)Landroid/graphics/Bitmap;",
        (void*)Bitmap_copy },
    {   "nativeCopyAshmem",         "(J)Landroid/graphics/Bitmap;",
        (void*)Bitmap_copyAshmem },
    {   "nativeCopyAshmemConfig",   "(JI)Landroid/graphics/Bitmap;",
        (void*)Bitmap_copyAshmemConfig },
    {   "nativeGetNativeFinalizer", "()J", (void*)Bitmap_getNativeFinalizer },
    {   "nativeRecycle",            "(J)V", (void*)Bitmap_recycle },
    {   "nativeReconfigure",        "(JIIIZ)V", (void*)Bitmap_reconfigure },
    {   "nativeCompress",           "(JIILjava/io/OutputStream;[B)Z",
        (void*)Bitmap_compress },
    {   "nativeErase",              "(JI)V", (void*)Bitmap_erase },
    {   "nativeErase",              "(JJJ)V", (void*)Bitmap_eraseLong },
    {   "nativeRowBytes",           "(J)I", (void*)Bitmap_rowBytes },
    {   "nativeConfig",             "(J)I", (void*)Bitmap_config },
    {   "nativeHasAlpha",           "(J)Z", (void*)Bitmap_hasAlpha },
    {   "nativeIsPremultiplied",    "(J)Z", (void*)Bitmap_isPremultiplied},
    {   "nativeSetHasAlpha",        "(JZZ)V", (void*)Bitmap_setHasAlpha},
    {   "nativeSetPremultiplied",   "(JZ)V", (void*)Bitmap_setPremultiplied},
    {   "nativeHasMipMap",          "(J)Z", (void*)Bitmap_hasMipMap },
    {   "nativeSetHasMipMap",       "(JZ)V", (void*)Bitmap_setHasMipMap },
    {   "nativeCreateFromParcel",
        "(Landroid/os/Parcel;)Landroid/graphics/Bitmap;",
        (void*)Bitmap_createFromParcel },
    {   "nativeWriteToParcel",      "(JILandroid/os/Parcel;)Z",
        (void*)Bitmap_writeToParcel },
    {   "nativeExtractAlpha",       "(JJ[I)Landroid/graphics/Bitmap;",
        (void*)Bitmap_extractAlpha },
    {   "nativeGenerationId",       "(J)I", (void*)Bitmap_getGenerationId },
    {   "nativeGetPixel",           "(JII)I", (void*)Bitmap_getPixel },
    {   "nativeGetColor",           "(JII)J", (void*)Bitmap_getColor },
    {   "nativeGetPixels",          "(J[IIIIIII)V", (void*)Bitmap_getPixels },
    {   "nativeSetPixel",           "(JIII)V", (void*)Bitmap_setPixel },
    {   "nativeSetPixels",          "(J[IIIIIII)V", (void*)Bitmap_setPixels },
    {   "nativeCopyPixelsToBuffer", "(JLjava/nio/Buffer;)V",
                                            (void*)Bitmap_copyPixelsToBuffer },
    {   "nativeCopyPixelsFromBuffer", "(JLjava/nio/Buffer;)V",
                                            (void*)Bitmap_copyPixelsFromBuffer },
    {   "nativeSameAs",             "(JJ)Z", (void*)Bitmap_sameAs },
    {   "nativePrepareToDraw",      "(J)V", (void*)Bitmap_prepareToDraw },
    {   "nativeGetAllocationByteCount", "(J)I", (void*)Bitmap_getAllocationByteCount },
    {   "nativeCopyPreserveInternalConfig", "(J)Landroid/graphics/Bitmap;",
        (void*)Bitmap_copyPreserveInternalConfig },
    {   "nativeWrapHardwareBufferBitmap", "(Landroid/hardware/HardwareBuffer;J)Landroid/graphics/Bitmap;",
        (void*) Bitmap_wrapHardwareBufferBitmap },
    {   "nativeGetHardwareBuffer", "(J)Landroid/hardware/HardwareBuffer;",
        (void*) Bitmap_getHardwareBuffer },
    {   "nativeComputeColorSpace",  "(J)Landroid/graphics/ColorSpace;", (void*)Bitmap_computeColorSpace },
    {   "nativeSetColorSpace",      "(JJ)V", (void*)Bitmap_setColorSpace },
    {   "nativeIsSRGB",             "(J)Z", (void*)Bitmap_isSRGB },
    {   "nativeIsSRGBLinear",       "(J)Z", (void*)Bitmap_isSRGBLinear},
    {   "nativeSetImmutable",       "(J)V", (void*)Bitmap_setImmutable},

    // ------------ @CriticalNative ----------------
    {   "nativeIsImmutable",        "(J)Z", (void*)Bitmap_isImmutable}
};
Bitmap.cpp # Bitmap_creator()
static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,
                              jint offset, jint stride, jint width, jint height,
                              jint configHandle, jboolean isMutable,
                              jlong colorSpacePtr) {
    // 1
    SkColorType colorType = GraphicsJNI::legacyBitmapConfigToColorType(configHandle);
    if (NULL != jColors) {
        size_t n = env->GetArrayLength(jColors);
        if (n < SkAbs32(stride) * (size_t)height) {
            doThrowAIOOBE(env);
            return NULL;
        }
    }
    // ARGB_4444 is a deprecated format, convert automatically to 8888
    if (colorType == kARGB_4444_SkColorType) {
        colorType = kN32_SkColorType;
    }
    sk_sp<SkColorSpace> colorSpace;
    if (colorType == kAlpha_8_SkColorType) {
        colorSpace = nullptr;
    } else {
        colorSpace = GraphicsJNI::getNativeColorSpace(colorSpacePtr);
    }
    SkBitmap bitmap;
    // 2
    bitmap.setInfo(SkImageInfo::Make(width, height, colorType, kPremul_SkAlphaType,
                colorSpace));
    // 3 
    Bitmap* nativeBitmap = GraphicsJNI::allocateJavaPixelRef(env, &bitmap, NULL);
    if (!nativeBitmap) {
        ALOGE("OOM allocating Bitmap with dimensions %i x %i", width, height);
        doThrowOOME(env);
        return NULL;
    }
    if (jColors != NULL) {
        GraphicsJNI::SetPixels(env, jColors, offset, stride, 0, 0, width, height, &bitmap);
    }
    // 4
    return GraphicsJNI::createBitmap(env, nativeBitmap,getPremulBitmapCreateFlags(isMutable));

代码块中的1处 将位图格式转换 RGB_565 ARGB_8888 等转换为skia域的颜色类型kBGRA_8888_SkColorType,而ARGB_4444由于显示质量原因被标记过时,在进行Skia颜色转换的时候被强制转换为kBGRA_8888_SkColorType。

enum SkColorType {
    kUnknown_SkColorType,      //!< uninitialized
    kAlpha_8_SkColorType,      //!< pixel with alpha in 8-bit byte
    kRGB_565_SkColorType,      //!< pixel with 5 bits red, 6 bits green, 5 bits blue, in 16-bit word
    kARGB_4444_SkColorType,    //!< pixel with 4 bits for alpha, red, green, blue; in 16-bit word
    kRGBA_8888_SkColorType,    //!< pixel with 8 bits for red, green, blue, alpha; in 32-bit word
    kRGB_888x_SkColorType,     //!< pixel with 8 bits each for red, green, blue; in 32-bit word
    kBGRA_8888_SkColorType,    //!< pixel with 8 bits for blue, green, red, alpha; in 32-bit word
    kRGBA_1010102_SkColorType, //!< 10 bits for red, green, blue; 2 bits for alpha; in 32-bit word
    kRGB_101010x_SkColorType,  //!< pixel with 10 bits each for red, green, blue; in 32-bit word
    kGray_8_SkColorType,       //!< pixel with grayscale level in 8-bit byte
    kRGBA_F16_SkColorType,   //!< pixel with half floats for red, green, blue, alpha; in 64-bit word
    kRGBA_F32_SkColorType,     //!< pixel using C float for red, green, blue, alpha; in 128-bit word
    kLastEnum_SkColorType     = kRGBA_F32_SkColorType,//!< last valid value
#if SK_PMCOLOR_BYTE_ORDER(B,G,R,A)
    kN32_SkColorType          = kBGRA_8888_SkColorType,//!< native ARGB 32-bit encoding
#elif SK_PMCOLOR_BYTE_ORDER(R,G,B,A)
    kN32_SkColorType          = kRGBA_8888_SkColorType,//!< native ARGB 32-bit encoding
#else
    #error "SK_*32_SHIFT values must correspond to BGRA or RGBA byte order"
#endif
};

根据以上的枚举类,我们知道所有的Skia图片显示格式的种类以及对应的字节大小。这里将在后续计算Bitmap所占内存大小的计算起到重中之重的角色。之后回到代码2处,SkImageInfo::Make()入参中的width、height、colorType为后续计算bitmap大小起到提供数据的作用,make()函数创建出来了SkImageInfo,这个对象存入了SkBitmap中。fWidth的赋值是一个关键点,后面Java层通过getAllocationByteCount获取Bitmap内存占用中会用到它计算一行像素占用空间,再用一行的所占用的控件乘以高度(行数)就是对应的总量。代码3处为SkBitmap bitmap;变量进行分配指定的地址空间,代码4 调用JNI方法,并创建处Bitmap对象。我们代码走一编3、4步骤。


Graphics.cpp
int register_android_graphics_Graphics(JNIEnv* env)
{
    jmethodID m;
    jclass c;
    gVMRuntime_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "dalvik/system/VMRuntime"));
    m = env->GetStaticMethodID(gVMRuntime_class, "getRuntime", "()Ldalvik/system/VMRuntime;");
    gVMRuntime = env->NewGlobalRef(env->CallStaticObjectMethod(gVMRuntime_class, m));
    gVMRuntime_newNonMovableArray = GetMethodIDOrDie(env, gVMRuntime_class, "newNonMovableArray","(Ljava/lang/Class;I)Ljava/lang/Object;");
    gVMRuntime_addressOf = GetMethodIDOrDie(env, gVMRuntime_class, "addressOf", "(Ljava/lang/Object;)J");
    return 0;
}

第一个红框调用了VMRuntime的newNonMovableArray方法,拿到虚拟机分配Heap对象,再调用VMRuntime的addressOf方法拿到其对应的内存地址,这回再调用Native方法的Bitmap.cpp构造方法,创建出Bitmap对象。而后又调用了getSkitmap(SkBitmap* outBitmap)方法。

Bitmap.cpp

空间分配以后,进行调用Native层的Bitmap构造方法,new android::Bitmap(env, arrayObj, (void*) addr, info, rowBytes, ctable),这里能够看到mPixelStorage保存之前分配后的Heap对象的弱引用,jstrongRef在构造方法中先被初始化为null。

Bitmap.cpp

之后调用getSkBitmap,setPixelRef()方法中对强指针进行了赋值


Bitmap.cpp
Bitmap.cpp

强指针被指向这个Heap对象。总结一下,native层的Bitmap构造函数,mPixelStorage保存前面创建Heap对象的弱引用,mPixelRef指向WrappedPixelRef。outBitmap拿到mPixelRef强引用对象,这里理解为拿到SkBitmap对象。Bitmap* nativeBitmap = GraphicsJNI::allocateJavaPixelRef完成Bitmap Heap分配,创建native层Bitmap,SkBitmap对象,最后自然是创建Java层Bitmap对象,把该包的包上。native层是通过JNI方法,在Java层创建一个数组对象的,这个数组是对应在Java层的Bitmap对象的buffer数组,所以pixels还是保存在Java堆。而在native层这里它是通过weak指针来引用的,在需要的时候会转换为strong指针,用完之后又去掉strong指针,这样这个数组对象还是能够被Java堆自动回收。里面jstrongRef一开始是赋值为null的,但是在bitmap的getSkBitmap方法会使用weakRef给他赋值。
因此证明了,Android N 版本Bitmap对象是分配在Dalvik堆上。

jobject GraphicsJNI::createBitmap(JNIEnv* env, android::Bitmap* bitmap,
        int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets,
        int density) {
    bool isMutable = bitmapCreateFlags & kBitmapCreateFlag_Mutable;
    bool isPremultiplied = bitmapCreateFlags & kBitmapCreateFlag_Premultiplied;
    // The caller needs to have already set the alpha type properly, so the
    // native SkBitmap stays in sync with the Java Bitmap.
    assert_premultiplied(bitmap->info(), isPremultiplied);

    jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
            reinterpret_cast<jlong>(bitmap), bitmap->javaByteArray(),
            bitmap->width(), bitmap->height(), density, isMutable, isPremultiplied,
            ninePatchChunk, ninePatchInsets);
    hasException(env); // For the side effect of logging.
    return obj;
}

之后将创建好的Bitmap对象返回。

Bitmap创建过程 (Android O)

流程和N差不多

Bitmap.cpp

sk_sp<Bitmap> Bitmap::allocateHeapBitmap(SkBitmap* bitmap) {
   return allocateBitmap(bitmap, &android::allocateHeapBitmap);
}

static sk_sp<Bitmap> allocateBitmap(SkBitmap* bitmap, AllocPixelRef alloc) {
            const SkImageInfo& info = bitmap->info();
            if (info.colorType() == kUnknown_SkColorType) {
                LOG_ALWAYS_FATAL("unknown bitmap configuration");
                return nullptr;
            }
        
            size_t size;
        
            // we must respect the rowBytes value already set on the bitmap instead of
            // attempting to compute our own.
            const size_t rowBytes = bitmap->rowBytes();
            if (!computeAllocationSize(rowBytes, bitmap->height(), &size)) {
                return nullptr;
            }
        
            // 进行分配内存
            auto wrapper = alloc(size, info, rowBytes);
            if (wrapper) {
                wrapper->getSkBitmap(bitmap);
            }
            return wrapper;
        }

这里调用alloc(size, info, rowBytes)来进行内存分配。alloc是通过参数传递进来的,其实它是一个函数指针,我们来看它的定义。

typedef sk_sp<Bitmap> (*AllocPixelRef)(size_t allocSize, const SkImageInfo& info, size_t rowBytes);

static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes) {
    // 真正的在Native层进行内存分配
    void* addr = calloc(size, 1);
    if (!addr) {
        return nullptr;
    }
    return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes));
}

之后再回溯到主流程,之后正常调用createBitmap方法,那么就与Android N 版本后半部分流程一致了。


Bitmap.cpp

到这里我们就可以区分出来了Bitmap在不同Android版本下,对内存分配的差异做了比较。这里我们再提一下,在C语言中的内存分配函数比较。

C中分配内存的函数主要有两个,malloc()和calloc()。
  • 参数区别
void *__cdecl calloc(size_t _NumOfElements,size_t _SizeOfElements);
void *__cdecl malloc(size_t _Size);

malloc函数:malloc(size_t size)函数有一个参数,即要度分配的内存空间的大小。
calloc函数:calloc(size_t numElements,size_t sizeOfElement)有两个参数,分别为元素的数目和每个元素的大小,这两个参数的乘积就是要分配的内存空间的大小。

  • 初始化内存空间上的区问别
    malloc函数:不能初始化所分配的内存空间,在动态分配完内存后,里边答数据是随机的垃圾数据。
    calloc函数:能初始化所分配的内存空间,在动态分配完内存后,自动初始化该内存空间为零。

  • 函数返回值上的区别
    malloc函数:函数返回值是一个对象。
    calloc函数:函数返回值是一个数组。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容