当前位置: 首页 > news >正文

Android JNI 之 Bitmap 操作

公众号回复:OpenGL,领取学习资源大礼包

在 Android 中通过 JNI 去操作 Bitmap。

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

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

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

检索 Bitmap 对象信息

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

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

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

AndroidBitmapInfo 结构体如下:

 1/** Bitmap info, see AndroidBitmap_getInfo(). */
 2typedef struct {
 3    /** The bitmap width in pixels. */
 4    uint32_t    width;
 5    /** The bitmap height in pixels. */
 6    uint32_t    height;
 7    /** The number of byte per row. */
 8    uint32_t    stride;
 9    /** The bitmap pixel format. See {@link AndroidBitmapFormat} */
10    int32_t     format;
11    /** Unused. */
12    uint32_t    flags;      // 0 for now
13} AndroidBitmapInfo;

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

图像的格式有如下支持:

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

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

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

访问原生像素缓存

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

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

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

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

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

释放原生像素缓存

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

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

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

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

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

实践

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

效果如下:

具体代码可以参考我的 Github 项目,欢迎 Star。

https://github.com/glumes/AndroidDevWithCpp

通过 JNI 将 Bitmap 旋转

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 1jobject generateBitmap(JNIEnv *env, uint32_t width, uint32_t height) {
 2
 3    jclass bitmapCls = env->FindClass("android/graphics/Bitmap");
 4    jmethodID createBitmapFunction = env->GetStaticMethodID(bitmapCls,
 5                                                            "createBitmap",
 6                                                            "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
 7    jstring configName = env->NewStringUTF("ARGB_8888");
 8    jclass bitmapConfigClass = env->FindClass("android/graphics/Bitmap$Config");
 9    jmethodID valueOfBitmapConfigFunction = env->GetStaticMethodID(
10            bitmapConfigClass, "valueOf",
11            "(Ljava/lang/String;)Landroid/graphics/Bitmap$Config;");
12
13    jobject bitmapConfig = env->CallStaticObjectMethod(bitmapConfigClass,
14                                                       valueOfBitmapConfigFunction, configName);
15
16    jobject newBitmap = env->CallStaticObjectMethod(bitmapCls,
17                                                    createBitmapFunction,
18                                                    width,
19                                                    height, bitmapConfig);
20    return newBitmap;
21}

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

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

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

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

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

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

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

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

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

通过 JNI 将 Bitmap 上下翻转和左右镜像

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

上下翻转的操作:

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

左右镜像的操作:

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

其他的操作都相同了,具体还是看项目代码吧。


技术交流,欢迎加我微信:ezglumes ,拉你入技术交流群。

扫码关注公众号【音视频开发进阶】,一起学习多媒体音视频开发~~~

喜欢就点个「在看」吧 ▽

相关文章:

  • 游戏中的角色是如何“动”起来的?
  • Android JNI 中的线程操作
  • 【音视频连载-004】基础学习篇-SDL 加载图片并显示
  • OpenGL 利用 Alpha 透明度进行测试
  • 【音视频连载-005】基础学习篇-SDL 加载 YUV 文件并显示
  • 【音视频连载-006】基础学习篇-SDL 播放 YUV 视频文件
  • Android 使用 OpenGL ES 绘制球面
  • 【音视频连载-007】基础学习篇-SDL 播放 PCM 音频文件(上)
  • PBO是OpenGL最高效的像素拷贝方式吗?
  • 游戏开发入门(一)游戏发展史
  • 【音视频连载-008】基础学习篇-SDL 播放 PCM 音频文件(下)
  • memcpy速度太慢?掌握这个技术让内存拷贝效率成倍提升
  • DXOMark是如何评价音频质量的
  • 【每周一记-001】~~~
  • 【音视频连载-009】第二季 FFmpeg 打造简易播放器
  • 【node学习】协程
  • iOS 颜色设置看我就够了
  • Java 11 发布计划来了,已确定 3个 新特性!!
  • JavaScript学习总结——原型
  • java正则表式的使用
  • k8s 面向应用开发者的基础命令
  • Vue2 SSR 的优化之旅
  • 关键词挖掘技术哪家强(一)基于node.js技术开发一个关键字查询工具
  • 计算机在识别图像时“看到”了什么?
  • 爬虫模拟登陆 SegmentFault
  • 容器化应用: 在阿里云搭建多节点 Openshift 集群
  • 融云开发漫谈:你是否了解Go语言并发编程的第一要义?
  • 学习JavaScript数据结构与算法 — 树
  • linux 淘宝开源监控工具tsar
  • PostgreSQL 快速给指定表每个字段创建索引 - 1
  • 阿里云ACE认证之理解CDN技术
  • 阿里云服务器购买完整流程
  • ​Linux·i2c驱动架构​
  • #QT(智能家居界面-界面切换)
  • #控制台大学课堂点名问题_课堂随机点名
  • (145)光线追踪距离场柔和阴影
  • (bean配置类的注解开发)学习Spring的第十三天
  • (C语言)fread与fwrite详解
  • (html5)在移动端input输入搜索项后 输入法下面为什么不想百度那样出现前往? 而我的出现的是换行...
  • (libusb) usb口自动刷新
  • (待修改)PyG安装步骤
  • (附源码)spring boot校园健康监测管理系统 毕业设计 151047
  • (篇九)MySQL常用内置函数
  • (七)微服务分布式云架构spring cloud - common-service 项目构建过程
  • (算法)Travel Information Center
  • (未解决)macOS matplotlib 中文是方框
  • (心得)获取一个数二进制序列中所有的偶数位和奇数位, 分别输出二进制序列。
  • (原創) 如何將struct塞進vector? (C/C++) (STL)
  • (转)大型网站的系统架构
  • (转)母版页和相对路径
  • .net 4.0 A potentially dangerous Request.Form value was detected from the client 的解决方案
  • .Net Framework 4.x 程序到底运行在哪个 CLR 版本之上
  • .NET 使用 ILMerge 合并多个程序集,避免引入额外的依赖
  • /etc/motd and /etc/issue
  • :O)修改linux硬件时间