Android C++系列:JNI操作Bitmap

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 在 Android 通过 JNI 去调用 Bitmap,通过 CMake 去编 so 动态链接库的话,需要添加 jnigraphics 图像库。

image.png


Java操作Bitmap


Bitmap代表一个位图,BitmapDrawable*里封装的图片就是一个 Bitmap对象。开发者为了把一个 Bitmap对象包装成 BitmapDrawable 对象,可以调用 BitmapDrawable的构造器:


BitmapDrawable bitmapDrawable = new BitmapDrawable(bitmap);


如果需要获取 BitmapDrawable 所包装的 Bitmap 对象,则可以用 BitmapDrawablegetBitmap() 的方法:


Bitmap bitmap = bitmapDrawable.getBitmap();


Bitmap的创建


在 Bitmap 类中, Bitmap 构造方法默认权限,而且这个类也是 final的,所以我们无法 new 一个 Bitmap对象。可以根据静态方法 createBitmap 来创建 Bitmap类。这些重载的方法有13个,这些方法大致可以分为三类:


1. 根据已有的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) 


2. 通过像素点数组创建空的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) 


3. 创建缩放的Bitmap


/**
* 对Bitmap进行缩放,缩放成宽 dstWidth、高 dstHeight 的新Bitmap
*/
public static Bitmap createScaledBitmap(@NonNull Bitmap src, int dstWidth, int dstHeight,boolean filter)


此外还可以通过BitmapFactory创建:


  • decodeByteArray(byte[] data, int offset, int length, Options opts):从指定字节数组的 offset 位置开始,将长度 length 的字节数据解析成 Bitmap 对象。


  • decodeFile(String pathName, Options opts):从 pathName 指定的文件中解析、创建 Bitmap 对象。


  • decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts):用于从 FileDescriptor 对应的文件中解析、创建 Bitmap 对象。


  • decodeResource(Resources res, int id, Options opts):用于给定的资源 ID 从指定资源中解析、创建 Bitmap 对象。


  • decodeStream(InputStream is, Rect outPadding, Options opts):用于从指定输入流中解析、创建 Bitmap 对象。


JNI操作Bitmap


在 Android 通过 JNI 去调用 Bitmap,通过 CMake 去编 so 动态链接库的话,需要添加 jnigraphics 图像库。


target_link_libraries( # Specifies the target library.
                        native-operation
                        jnigraphics
                        ${log-lib} )


在 Android 中关于 JNI Bitmap 的操作,都定义在 bitmap.h 的头文件里面了,主要就三个函数,明白它们的含义之后就可以去实践体会了。


检索 Bitmap 对象信息


AndroidBitmap_getInfo 函数允许原生代码检索 Bitmap 对象信息,如它的大小、像素格式等,函数签名如下:


/**
  * Given a java bitmap object, fill out the AndroidBitmapInfo struct for it.
  * If the call fails, the info parameter will be ignored.
  */
 int AndroidBitmap_getInfo(JNIEnv* env, jobject jbitmap,
                           AndroidBitmapInfo* info);


其中,第一个参数就是 JNI 接口指针,第二个参数就是 Bitmap 对象的引用,第三个参数是指向 AndroidBitmapInfo 结构体的指针。


AndroidBitmapInfo 结构体如下:


/** Bitmap info, see AndroidBitmap_getInfo(). */
 typedef struct {
     /** The bitmap width in pixels. */
     uint32_t    width;
     /** The bitmap height in pixels. */
     uint32_t    height;
     /** The number of byte per row. */
     uint32_t    stride;
     /** The bitmap pixel format. See {@link AndroidBitmapFormat} */
     int32_t     format;
     /** Unused. */
     uint32_t    flags;      // 0 for now
 } AndroidBitmapInfo;


其中,width 就是 Bitmap 的宽,height 就是高,format 就是图像的格式,而 stride 就是每一行的字节数。


图像的格式有如下支持:


/** Bitmap pixel format. */
 enum AndroidBitmapFormat {
     /** No format. */
     ANDROID_BITMAP_FORMAT_NONE      = 0,
     /** Red: 8 bits, Green: 8 bits, Blue: 8 bits, Alpha: 8 bits. **/
     ANDROID_BITMAP_FORMAT_RGBA_8888 = 1,
     /** Red: 5 bits, Green: 6 bits, Blue: 5 bits. **/
     ANDROID_BITMAP_FORMAT_RGB_565   = 4,
     /** Deprecated in API level 13. Because of the poor quality of this configuration, it is advised to use ARGB_8888 instead. **/
     ANDROID_BITMAP_FORMAT_RGBA_4444 = 7,
     /** Alpha: 8 bits. */
     ANDROID_BITMAP_FORMAT_A_8       = 8,
 };


如果 AndroidBitmap_getInfo 执行成功的话,会返回 0 ,否则返回一个负数,代表执行的错误码列表如下:


/** AndroidBitmap functions result code. */
 enum {
     /** Operation was successful. */
     ANDROID_BITMAP_RESULT_SUCCESS           = 0,
     /** Bad parameter. */
     ANDROID_BITMAP_RESULT_BAD_PARAMETER     = -1,
     /** JNI exception occured. */
     ANDROID_BITMAP_RESULT_JNI_EXCEPTION     = -2,
     /** Allocation failed. */
     ANDROID_BITMAP_RESULT_ALLOCATION_FAILED = -3,
 };


访问原生像素缓存


AndroidBitmap_lockPixels 函数锁定了像素缓存以确保像素的内存不会被移动。


如果 Native 层想要访问像素数据并操作它,该方法返回了像素缓存的一个原生指针,


/**
  * Given a java bitmap object, attempt to lock the pixel address.
  * Locking will ensure that the memory for the pixels will not move
  * until the unlockPixels call, and ensure that, if the pixels had been
  * previously purged, they will have been restored.
  *
  * If this call succeeds, it must be balanced by a call to
  * AndroidBitmap_unlockPixels, after which time the address of the pixels should
  * no longer be used.
  *
  * If this succeeds, *addrPtr will be set to the pixel address. If the call
  * fails, addrPtr will be ignored.
  */
 int AndroidBitmap_lockPixels(JNIEnv* env, jobject jbitmap, void** addrPtr);


其中,第一个参数就是 JNI 接口指针,第二个参数就是 Bitmap 对象的引用,第三个参数是指向像素缓存地址的指针。


AndroidBitmap_lockPixels 执行成功的话返回 0 ,否则返回一个负数,错误码列表就是上面提到的。


释放原生像素缓存


对 Bitmap 调用完 AndroidBitmap_lockPixels 之后都应该对应调用一次 AndroidBitmap_unlockPixels 用来释放原生像素缓存。


当完成对原生像素缓存的读写之后,就应该释放它,一旦释放后,Bitmap Java 对象又可以在 Java 层使用了,函数签名如下:


/**
  * Call this to balance a successful call to AndroidBitmap_lockPixels.
  */
 int AndroidBitmap_unlockPixels(JNIEnv* env, jobject jbitmap);


其中,第一个参数就是 JNI 接口指针,第二个参数就是 Bitmap 对象的引用,如果执行成功返回 0,否则返回 1。


对 Bitmap 的操作,最重要的就是 AndroidBitmap_lockPixels 函数拿到所有像素的缓存地址,然后对每个像素值进行操作,从而更改 Bitmap 。


实战


通过对 Bitmap 进行旋转,上下翻转,左右镜像来体验 JNI 的开发。


通过 JNI 将 Bitmap 旋转


首先定义一个这样的 native 函数:


// 顺时针旋转 90° 的操作
     public native Bitmap rotateBitmap(Bitmap bitmap);


传入一个 Bitmap 对象,然后返回一个 Bitmap 对象。


然后在 C++ 代码中,首先检索 Bitmap 的信息,看看是否成功。


AndroidBitmapInfo bitmapInfo;
    int ret;
    if ((ret = AndroidBitmap_getInfo(env, bitmap, &bitmapInfo)) < 0) {
        LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
        return NULL;
    }


接下来就是获得 Bitmap 的像素缓存指针:


// 读取 bitmap 的像素内容到 native 内存
    void *bitmapPixels;
    if ((ret = AndroidBitmap_lockPixels(env, bitmap, &bitmapPixels)) < 0) {
        LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
        return NULL;
    }


这个指针指向的就是 Bitmap 像素内容,它是一个以一维数组的形式保存所有的像素点的值,但是我们在定义 Bitmap 图像时,都会定义宽和高,这就相对于是一个二维的了,那么就存在 Bitmap 的像素内容如何转成指针指向的一维内容,是按照行排列还是按照列排列呢?


在这里是按照行进行排列的,而且行的排列是从左往右,列的排列是从上往下,起始点就和屏幕坐标原点一样,位于左上角。


通过 AndroidBitmap_lockPixels 方法,bitmapPixels 指针就指向了 Bitmap 的像素内容,它的长度就是 Bitmap 的宽和高的乘积。


要将 Bitmap 进行旋转,可以通过直接更改 bitmapPixels 指针指向的像素点的值,也可以通过创建一个新的 Bitmap 对象,然后将像素值填充到 Bitmap 对象中,这里选择后者的实现方式。


首先创建一个新的 Bitmap 对象,参考之前文章中提到的方式:Android 通过 JNI 访问 Java 字段和方法调用。


在 Java 代码中,通过 createBitmap 方法可以创建一个 Bitmap,如下所示:


Bitmap.createBitmap(int width, int height, @NonNull Config config)`


所以在 JNI 中就需要调用 Bitmap 的静态方法来创建一个 Bitmap 对象。


jobject generateBitmap(JNIEnv *env, uint32_t width, uint32_t height) {
    jclass bitmapCls = env->FindClass("android/graphics/Bitmap");
    jmethodID createBitmapFunction = env->GetStaticMethodID(bitmapCls,
                                                            "createBitmap",
                                                            "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
    jstring configName = env->NewStringUTF("ARGB_8888");
    jclass bitmapConfigClass = env->FindClass("android/graphics/Bitmap$Config");
    jmethodID valueOfBitmapConfigFunction = env->GetStaticMethodID(
            bitmapConfigClass, "valueOf",
            "(Ljava/lang/String;)Landroid/graphics/Bitmap$Config;");
    jobject bitmapConfig = env->CallStaticObjectMethod(bitmapConfigClass,
                                                       valueOfBitmapConfigFunction, configName);
    jobject newBitmap = env->CallStaticObjectMethod(bitmapCls,
                                                    createBitmapFunction,
                                                    width,
                                                    height, bitmapConfig);
    return newBitmap;
}


首先通过 FindClass 方法找到 Config 类,得到一个 ARGB_8888 的配置,然后得到 Bitmap 类,调用它的静态方法 createBitmap 创建一个新的 Bitmap 对象,具体可以参考之前的文章。


在这里要传入新 Bitmap 的宽高,这个宽高也是通过 AndroidBitmap_getInfo 方法得到原来的宽高之后,根据不同的操作计算后得到的。


// 旋转操作,新 Bitmap 的宽等于原来的高,新 Bitmap 的高等于原来的宽
  uint32_t newWidth = bitmapInfo.height;
   uint32_t newHeight = bitmapInfo.width;


有了新的 Bitmap 对象,又有了原有的 Bitmap 像素指针,接下来就是创建新的像素指针,并填充像素内容,然后把这个像素内容再填充到 Bitmap 上。


// 创建一个新的数组指针,把这个新的数组指针填充像素值
  uint32_t *newBitmapPixels = new uint32_t[newWidth * newHeight];
    int whereToGet = 0;
    for (int y = 0; y < newHeight; ++y) {
        for (int x = newWidth - 1; x >= 0; x--) {
            uint32_t pixel = ((uint32_t *) bitmapPixels)[whereToGet++];
            newBitmapPixels[newWidth * y + x] = pixel;
        }
    }


在这两个 for循环里面就是从原来的像素指针中取出像素值,然后把它按照特定的排列顺序填充到新的像素指针中对应位置的值,这里也就是前面强调的像素指针是按照行进行排列的,起点是 Bitmap 的左上角。


void *resultBitmapPixels;
    if ((ret = AndroidBitmap_lockPixels(env, newBitmap, &resultBitmapPixels)) < 0) {
        LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
        return NULL;
    }
    int pixelsCount = newWidth * newHeight;
    memcpy((uint32_t *) resultBitmapPixels, newBitmapPixels, sizeof(uint32_t) * pixelsCount);
    AndroidBitmap_unlockPixels(env, newBitmap);


再次创建一个 resultBitmapPixels 指针,并调用 AndroidBitmap_lockPixels 方法获取新的 Bitmap 的像素指针缓存,然后调用 memcpy 方法,将待填充的像素指针填充到 resultBitmapPixels 上,这样就完成了像素的赋值,最后调用 AndroidBitmap_unlockPixels 方法释放像素指针缓存,完成整个赋值过程。


就这样通过读取原有 Bitmap 的像素内容然后进行操作后再赋值给新的 Bitmap 对象就完成了 JNI 操作 Bitmap 。


通过 JNI 将 Bitmap 上下翻转和左右镜像 将 Bitmap 进行上下翻转以及左右镜像和旋转操作类似了,只是针对像素指针的操作方式不同。


上下翻转的操作:


int whereToGet = 0;
   for (int y = 0; y < newHeight; ++y) {
       for (int x = 0; x < newWidth; x++) {
           uint32_t pixel = ((uint32_t *) bitmapPixels)[whereToGet++];
           newBitmapPixels[newWidth * (newHeight - 1 - y) + x] = pixel;
       }
   }


左右镜像的操作:


int whereToGet = 0;
    for (int y = 0; y < newHeight; ++y) {
        for (int x = newWidth - 1; x >= 0; x--) {
            uint32_t pixel = ((uint32_t *) bitmapPixels)[whereToGet++];
            newBitmapPixels[newWidth * y + x] = pixel;
        }
    }


其他的操作都相同了


总结


本文介绍了Android Bitmap相关知识,以及在JNI层操纵Bitmap:检索Bitmap对象信息、访问原生像素缓存等,并介绍了JNI层旋转、镜像Bitmap的操作实战。

目录
相关文章
|
3月前
|
Java Android开发 C++
Android Studio JNI 使用模板:c/cpp源文件的集成编译,快速上手
本文提供了一个Android Studio中JNI使用的模板,包括创建C/C++源文件、编辑CMakeLists.txt、编写JNI接口代码、配置build.gradle以及编译生成.so库的详细步骤,以帮助开发者快速上手Android平台的JNI开发和编译过程。
234 1
|
4月前
|
C++ 容器
C++中向量的操作vector
C++中向量的操作vector
|
2月前
|
存储 缓存 编解码
Android经典面试题之图片Bitmap怎么做优化
本文介绍了图片相关的内存优化方法,包括分辨率适配、图片压缩与缓存。文中详细讲解了如何根据不同分辨率放置图片资源,避免图片拉伸变形;并通过示例代码展示了使用`BitmapFactory.Options`进行图片压缩的具体步骤。此外,还介绍了Glide等第三方库如何利用LRU算法实现高效图片缓存。
64 20
Android经典面试题之图片Bitmap怎么做优化
|
2月前
|
Java Android开发 C++
🚀Android NDK开发实战!Java与C++混合编程,打造极致性能体验!📊
在Android应用开发中,追求卓越性能是不变的主题。本文介绍如何利用Android NDK(Native Development Kit)结合Java与C++进行混合编程,提升应用性能。从环境搭建到JNI接口设计,再到实战示例,全面展示NDK的优势与应用技巧,助你打造高性能应用。通过具体案例,如计算斐波那契数列,详细讲解Java与C++的协作流程,帮助开发者掌握NDK开发精髓,实现高效计算与硬件交互。
129 1
|
4月前
|
Java Android开发 C++
🚀Android NDK开发实战!Java与C++混合编程,打造极致性能体验!📊
【7月更文挑战第28天】在 Android 开发中, NDK 让 Java 与 C++ 混合编程成为可能, 从而提升应用性能。**为何选 NDK?** C++ 在执行效率与内存管理上优于 Java, 特别适合高性能需求场景。**环境搭建** 需 Android Studio 和 NDK, 工具如 CMake。**JNI** 构建 Java-C++ 交互, 通过声明 `native` 方法并在 C++ 中实现。**实战** 示例: 使用 C++ 计算斐波那契数列以提高效率。**总结** 混合编程增强性能, 但增加复杂性, 使用前需谨慎评估。
140 4
|
3月前
|
JSON Android开发 C++
Android c++ core guideline checker 应用
Android c++ core guideline checker 应用
|
3月前
|
JSON Android开发 数据格式
Android c++ core guideline checker 应用问题之JSON compilation database的定义如何解决
Android c++ core guideline checker 应用问题之JSON compilation database的定义如何解决
|
3月前
|
IDE 开发工具 Android开发
Android c++ core guideline checker 应用问题之clang-tidy 检查后发现的问题如何解决
Android c++ core guideline checker 应用问题之clang-tidy 检查后发现的问题如何解决
|
3月前
|
算法 Java Linux
Intellij Java JNI 调用 C++
Intellij Java JNI 调用 C++
38 0
|
5月前
|
算法 前端开发 Linux
【常用技巧】C++ STL容器操作:6种常用场景算法
STL在Linux C++中使用的非常普遍,掌握并合适的使用各种容器至关重要!
89 10