【安卓随笔】轻度自虐之使用CMake开发NDK(案例:YUV转RGB)

2023-11-05 05:50

本文主要是介绍【安卓随笔】轻度自虐之使用CMake开发NDK(案例:YUV转RGB),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

       消失了几个月我又回来了,距离上一次承诺更新NDK的知识依旧过了好久,我想说我真的没有太监。。。最近换了工作,来到了魔都混日子,因为找工作耽误很多写博文的时间。不得不说现在安卓开发的工作真难找啊,找了一个多月才找到一个6,7k的- -希望大家不要裸辞,慎重跳槽。。。不过这家公司的需求都比较复杂,属于之前接触较少的,而且对NDK开发有很大的要求,也可以趁机锻炼一下自己!

        本文的标题中包含自虐的字眼,没错,NDK开发就是个自虐的玩意儿,有时候真的让人抓狂,可能搞一整天也都是徒劳。。。不过今天介绍一种可以把重度自虐变成轻度自虐的简便方法,那么我们现在进入正题!

         Android Studio 2.2后,加入了对CMake的支持,从此AS开发NDK的能力和简便性得到了巨大的飞跃,本文就来介绍一下操作流程。

        首先需要下载NDK的开发环境,打开Default Setting->Android SDK->SDK Tools,选中CMake,LLDB,NDK下载,如图所示:



NDK即Native Development Kit,我们使用C/C++来进行开发的必要环境,LLDB是NDK的调试工具,而CMake则是我们今天的主角。

安装完成后,我们新建一个工程,本文将用JNI实现YUV2RGB这个经典算法,所以将工程命名为YUV2RGB,特别注意的是在此处勾选上include c++ support,这样studio就会给我们生成一个已经搭建好基础的NDK工程。


在一路Next时,不要手快,在这一步一点要勾上Exception Support和Runtime Type Information Support,来支持rtti和exception,否则某些机型也许无法跑你的程序。


工程创建完毕后,我们来看看目录和之前的普通工程有什么不一样。在App目录下多了一个CMakeLists.txt,这个可不是单纯的文本文档,这里就是配置CMake的地方,在main目录下多了一个Cpp文件夹,这里面用来存放用C/C++写的代码。


默认的情况下CMakeLists.txt是存放在app/目录下,当然我们可以改变它的位置,这里我们就看一看这个工程的gradle文件又有什么特别之处:


在defaultConfig下面多了一个externalNativeBuild {cmake {cppFlags "-frtti -fexceptions"}},这里就是我们一开始勾选的rtti支持,如果一开始忘记勾选,在此加入也可以。

在android下面也多了一个externalNativeBuild {cmake {path "CMakeLists.txt"}},这里就是指定的CMakeLists文件路径,如果你要把该文件放到其他位置,只需要在此修改即可。

那么我们先来分析CMakeLists这个文件。一打开你也许会略微蛋疼,充斥着没有颜色提示的文本,各种奇怪的符号,比较AS对CMake支持的时间不长,也许以后谷歌会对这一块更加完善。在自动生成的CMakeLists里,充满了各种注释,如果我们整理一下,一切就很清晰了,比如这样:

cmake_minimum_required(VERSION 3.4.1)add_library(native-lib SHARED src/main/cpp/native-lib.cpp )find_library(log-lib log)target_link_libraries(native-lib  ${log-lib} )
      这下就清楚多了吧,其实对于一个比较基本的NDK程序,这几行就足够了,先声明出CMake的版本,然后添加你自己编写的Cpp文件和文件的位置,将log support library的位置储存到log-lib中,最后连接所有存在的动态库文件就大功告成。

为了保证本文讲的更加清楚,我们不使用已经自动创建好的部分,重新在Cpp文件夹里创建一个cpp文件yuv2rgb-lib.cpp,然后在cMakeLists中这样配置:

cmake_minimum_required(VERSION 3.4.1)add_library(native-lib SHARED src/main/cpp/native-lib.cpp )add_library(yuv2rgb-lib SHARED src/main/cpp/yuv2rgb-lib.cpp )find_library(log-lib log)target_link_libraries(native-lib yuv2rgb-lib ${log-lib} )

这样就可以轻而易举的把新创建的文件放入CMake的编译队列中。那么我们自己创建的这个Cpp文件里应该怎么写呢?里面是一个YUV->RGB的算法,当然这个算法用java也可以实现,但是执行效率就不言而喻,使用Android原生的YuvImage也可以对其进行转换,这里底层也是使用了native方法,但是这个方法据说在某些机型里是无法使用的。那么最稳的方式就是我们自己用C/C++来写一个。

#include <jni.h>
extern "C"{
jintArray Java_com_lbw_camerapreviewcallback_NdkLoader_yuv2Rgb(JNIEnv *env,jobject thiz, jbyteArray buf,jint width, jint height) {jbyte *yuv420sp = (env)->GetByteArrayElements(buf, 0);int frameSize = width * height;jint rgb[frameSize];int i = 0, j = 0, yp = 0;int uvp = 0, u = 0, v = 0;for (j = 0, yp = 0; j < height; j++) {uvp = frameSize + (j >> 1) * width;u = 0;v = 0;for (i = 0; i < width; i++, yp++) {int y = (0xff & ((int) yuv420sp[yp])) - 16;if (y < 0)y = 0;if ((i & 1) == 0) {v = (0xff & yuv420sp[uvp++]) - 128;u = (0xff & yuv420sp[uvp++]) - 128;}int y1192 = 1192 * y;int r = (y1192 + 1634 * v);int g = (y1192 - 833 * v - 400 * u);int b = (y1192 + 2066 * u);if (r < 0) r = 0; else if (r > 262143) r = 262143;if (g < 0) g = 0; else if (g > 262143) g = 262143;if (b < 0) b = 0; else if (b > 262143) b = 262143;rgb[yp] = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) | ((b >> 10) & 0xff);}}jintArray result = (env)->NewIntArray(frameSize);(env)->SetIntArrayRegion(result, 0, frameSize, rgb);(env)->ReleaseByteArrayElements(buf, yuv420sp, 0);return result;
}
}
YUV和RGB的转换公式如下:


其中的数学原理我们这里不再探究,毕竟这里只是拿他当一个小例子,有兴趣的可以去这篇博客里学习。

我们来看这个函数里的几个参数JNIEnv *env, jobject thiz, jbyteArray buf, jint width, jint height。

也许最让人疑惑的就是JNIEnv *env和jobject thiz,JNIEnv*是指向JNI函数表的接口指针,可以通过它对Java端的代码进行操作。jobject thiz,如果native方法没用用static修饰的话,它就是native方法的类实例。如果是static修饰的话,就代表native方法的类的class对象实例。

以上这两个参数都是必须带有的,而后面的则是我们可以自定义的。我们先来看下这个native方法的声明:

 public static native int[] yuv2Rgb(byte[] buf, int width, int height);

没错,我们在java代码中只需要传入这3个自定义的参数,一个包含原始YUV数据的byte数组,和int型的图像宽高,他们在JNI中对应的类型为jbyteArray和jint。在NDK中有些类型是无法直接使用的,我们需要调用NDK的方法来将他们转换为C++中可用的类型。具体的这里我们不细说,因为很多前辈们已经讲了很多遍了,想学习的可以点击这里。

要注意的是C/C++里可没有像Java GC这样好用的垃圾回收机制,在代码的最后记得回收相关的变量。

最重要的一点是,在代码的开头有一段类似Java包名的信息:Java_com_lbw_camerapreviewcallback_NdkLoader_yuv2Rgb,这里就是程序寻找Java部分声明的指引,这里程序就会去com.lbw.camerapreviewcallback.NdkLoader这个类里寻找native方法的声明。这里的包名并没有写成本程序的包名,因为我们接下来要在其他程序里去使用它,这里我不再去封装jar或者aar包,所以直接写成这个包名。

那么如何把cpp编译为SO文件呢?我们只需要Build->Rebuild project,重新构建一下工程,然后在app/build/intermediates/cmake目录下就可以找到生成好的SO文件。



然后我们新建一个工程,用来试验这个SO库到底能不能用。而包名就用我们刚才定义的那个,让JNI程序可以直接适应这个新的程序,那么我们就创建一个对应的NdkLoader类,内容如下:

public class NdkLoader {static {System.loadLibrary("yuv2rgb-lib");}public static native int[] yuv2Rgb(byte[] buf, int width, int height);
}

至于YUV数据的来源,当然还是从相机的预览帧里取了,所以我这里简单的写了一个相机预览的程序,加入了预览回调,当点击按钮时,截取当前帧,转换为RGB数据,并存到SD卡里。只贴出关键代码:

 @Overridepublic void onPreviewFrame(byte[] data, Camera camera) {camera.addCallbackBuffer(data);if (isCatch) {isCatch = false;int[] result = NdkLoader.yuv2Rgb(data,camera.getParameters().getPreviewSize().width,camera.getParameters().getPreviewSize().height);Bitmap bitmap = Bitmap.createBitmap(result,camera.getParameters().getPreviewSize().width,camera.getParameters().getPreviewSize().height, Bitmap.Config.RGB_565);try {FileOutputStream fileOutputStream = new FileOutputStream(new File("sdcard/result.jpg"));BufferedOutputStream bos = new BufferedOutputStream(fileOutputStream);bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos);bos.flush();fileOutputStream.close();bos.close();bitmap.recycle();bitmap = null;} catch (IOException e) {e.printStackTrace();}}}


这里就不在讲述怎么导入SO文件,当我们点击按钮后,就通过NdkLoader这个类调用yuv2Rgb这个native方法,将byte[] data转换成一个RGB的byte[] result,再将它存入SD卡。


点击按钮后,去SD卡里寻找这张图片。果然,这里已经转换成功了,到此我们的这个SO库也就做好了。


当然为了确保每个程序都可以使用,肯定不能靠这样一直修改包名,所以这里建议将NdkLoader做成jar包,这样不管在任何程序里,只要导入这个jar包就可以使用了。不过我更推荐使用aar,这样就把SO文件也引入了进去,aar的制作方法可以参见我之前的博客。

本篇博文只是讲述了NDK开发的入门之入门的知识,希望可以帮到一些刚刚接触这一块的朋友。而真正的NDK开发绝对是非常博大精深的,绝对要比我们平时用Android SDK写的东西难之又难,而资料也更加稀少与晦涩。所以我以后还会继续分享一些NDK开发的心得,由于我本人并不是专业开发C/C++的,博文中难免会有许多错误,希望大家及时指正!下一篇博客的内容其实已经构思好了,就是导入OpenCV的native库进行NDK二次开发!希望大家都可以在这条自虐的道路上越走越远...敬请期待!

NDK的项目和相机预览的项目我都进行了上传,本着“技术来源于分享“的原则,依旧是0分,需要的同学可以自行下载。

点此下载

这篇关于【安卓随笔】轻度自虐之使用CMake开发NDK(案例:YUV转RGB)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/347842

相关文章

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

Android 悬浮窗开发示例((动态权限请求 | 前台服务和通知 | 悬浮窗创建 )

《Android悬浮窗开发示例((动态权限请求|前台服务和通知|悬浮窗创建)》本文介绍了Android悬浮窗的实现效果,包括动态权限请求、前台服务和通知的使用,悬浮窗权限需要动态申请并引导... 目录一、悬浮窗 动态权限请求1、动态请求权限2、悬浮窗权限说明3、检查动态权限4、申请动态权限5、权限设置完毕后

C++ Primer 多维数组的使用

《C++Primer多维数组的使用》本文主要介绍了多维数组在C++语言中的定义、初始化、下标引用以及使用范围for语句处理多维数组的方法,具有一定的参考价值,感兴趣的可以了解一下... 目录多维数组多维数组的初始化多维数组的下标引用使用范围for语句处理多维数组指针和多维数组多维数组严格来说,C++语言没

在 Spring Boot 中使用 @Autowired和 @Bean注解的示例详解

《在SpringBoot中使用@Autowired和@Bean注解的示例详解》本文通过一个示例演示了如何在SpringBoot中使用@Autowired和@Bean注解进行依赖注入和Bean... 目录在 Spring Boot 中使用 @Autowired 和 @Bean 注解示例背景1. 定义 Stud

使用 sql-research-assistant进行 SQL 数据库研究的实战指南(代码实现演示)

《使用sql-research-assistant进行SQL数据库研究的实战指南(代码实现演示)》本文介绍了sql-research-assistant工具,该工具基于LangChain框架,集... 目录技术背景介绍核心原理解析代码实现演示安装和配置项目集成LangSmith 配置(可选)启动服务应用场景

使用Python快速实现链接转word文档

《使用Python快速实现链接转word文档》这篇文章主要为大家详细介绍了如何使用Python快速实现链接转word文档功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 演示代码展示from newspaper import Articlefrom docx import

oracle DBMS_SQL.PARSE的使用方法和示例

《oracleDBMS_SQL.PARSE的使用方法和示例》DBMS_SQL是Oracle数据库中的一个强大包,用于动态构建和执行SQL语句,DBMS_SQL.PARSE过程解析SQL语句或PL/S... 目录语法示例注意事项DBMS_SQL 是 oracle 数据库中的一个强大包,它允许动态地构建和执行

SpringBoot中使用 ThreadLocal 进行多线程上下文管理及注意事项小结

《SpringBoot中使用ThreadLocal进行多线程上下文管理及注意事项小结》本文详细介绍了ThreadLocal的原理、使用场景和示例代码,并在SpringBoot中使用ThreadLo... 目录前言技术积累1.什么是 ThreadLocal2. ThreadLocal 的原理2.1 线程隔离2