赞
踩
本文主要讲述如何在Android Studio中通过JAVA调用C++源码,最终将项目打包成apk文件发布。整体流程如下图所示:
主要涉及如下几个方面:
1、Android Studio中整个程序的运行流程;
2、C++源码如何通过NDK或者Cmake工具打包成so包;
3、JAVA如何通过JNI调用so包,JNI的使用方法;
4、将整个项目打包成apk包发布。
整篇文章都是自己在实战出摸索总结出来的经验,希望能给大家带来帮助。若有不当之处,还请大家斧正。
网上关于Gradle的解释比较多,也比较官方,这个东西不用深究太多,大致知道Gradle是Android Studio用来进行构建和打包操作的就行了。同时要知道,对Gradle进行的配置文件主要是两个,一个是app中的build.gradle文件,一个是整个项目的build.gradle文件。
Gradle可以通过两个外部构建工具进行扩展:ndk-build,Cmake。这两个构建工具功能相同,二者选其一即可。
Native Development Kit,原生开发工具包,一组可以在Android应用中利用C和C++代码的工具。它包含一个ndk-build组件,可以用来生成so文件。
ndk-build主要通过配置Android.mk文件实现构建。Android.mk文件是在设置了app的build.gradle文件后自动生成的,不需要用户自己新建。
Cmake主要通过配置 CmakeList.txt文件实现构建。其中CmakeList.txt文件则是自己新建的,新建好后要在build.gradle文件中添加一个cmake 节点,告诉Gradle根据这个文件进行构建。
so文件是linux系统下二进制共享文件。由于Android系统和linux系统内核相同,因此Android系统也支持so文件。
Java Native Interface,Java本地接口。Java不是完善的,Java的不足除了体现在运行速度上要比传统的C++慢许多之外,Java无法直接访问到操作体系底层(如系统硬件等),为此 Java使用native法子来扩大Java程序的功效。可以将native法子比作Java程序同C程序的接口。
JNI提供了JAVA和C++的数据类型对应关系。对于没有对应关系的数据结构,JNI中有一个env结构体,代表了 Java 在本线程的执行环境,可以通过这个结构体调用JNI的一些函数,实现类型的相互转换。
JNI数据类型对应关系表
有关JNI函数签名信息、JNIEnv介绍、add_library 指令、target_link_libraries 指令、Abi架构的更多基础知识,可以查看这篇文章:JNI技术简介
我之前写过一篇比较基础的帖子,里面介绍了一个基础JNI Demo的实现过程,包括环境搭建以及Demo的详细实现过程,零基础的话可以去看看,链接。
我的C++源码的接口函数的形式如下所示:
map<string, vec_array> detect_cards(vector<Coordinate> corners,int img_row,int img_col) ;
输入3个参数,返回一个map类型的值。
第一个参数是一个复合类型,vector的每个元素是Coordinate结构体,结构体定义如下:
struct Coordinate
{
double x;
double y;
int label;
};
返回值是一个map,map的值也是一个复合类型,vec_array的定义如下:
struct s_array
{
double Number[8];
};
typedef std::vector<s_array> vec_array;
1、先用JAVA中设计一个函数的头部,与C++的函数相对应,函数体不用实现;
2、新建一个JAVA类,包含第一步声明的函数头部作为成员方法,加上native关键字,表示此方法不是在JAVA实现的,是在C++中实现的;
3、通过javac命令生成第二步新建的java类的C++版的.h头文件,放在cpp文件夹中。cpp文件夹存放C++相关的源文件。该.h头文件会以JNI的语法生成一个C++函数声明,与第一步中的java函数相对应。
4、在cpp文件夹中新建一个C++源文件,对第三步中的.h头文件中的函数进行实现。
5、对工程执行Make Project命令,生成so文件。
6、在java的MainActivity类中加载so文件库,并新建一个第二步类的对象,利用这个对象调用第一步的函数。
综上,整个过程就像一个“嫁接”的过程,在java中声明一个函数后不实现,再利用javac命令生成这个函数的C++形式,然后在C++中对这个函数进行实现,再将C++的函数打包成so文件,最后在java中加载so库后调用。
可能我上面表述的有点混乱,没关系,可以看看下面的流程图,应该会明白的更多一点。。。
整体流程的详细操作步骤,都可以在我的这篇博客中找到参考:Android Studio3.5实现调用C++模块(附JNIDemo)。不过在本工程中,我新建项目的类型和构建方法与上篇略有不同。
上篇文章项目类型是空项目,但是本工程中项目类型是Native C++。
上篇文章用的构建方法是ndk-build,但是本篇文章用的是Cmake工具。当你构建项目时选的项目类型是Native C++时,系统会自动在CPP文件夹下建一个CmakeList.txt文件和一个cpp文件示例。
注:通过Cmake工具进行构建时,不需要像上篇文章那样执行“Link C++ project with Gradle”命令,Android studio会自动将java类中的native函数和其C++实现函数进行关联,生成如下所示标记:
同时,上篇文章只是一个基础的JNI Demo,C++源码不是很复杂,但是本工程涉及的C++源码接口函数比较复杂。由于native函数的参数和返回值都是复杂数据类型,JNI中没有复杂数据类型的对应关系。因此我的解决方法是:在JAVA中编写一个类,包含Coordinate和vec_array结构体的属性,同时定义各个属性的get/set方法。这样在native函数中解析参数时,就可以通过JNI的env结构体来调用对象的get方法获取数据;在返回时,可以通过env结构调用对象的set方法来给对象赋值,封装成JNI对象返回给JAVA。注意:复合数据结构要从最底层结构一层层赋值,不能直接传。
因此下面着重介绍两点:1、详细介绍下Cmake工具生成so文件的方法;2、JNI的用法:如何在C++端利用JNI语法对JNI参数进行解析,以及如何将C++类型的数据封装成JNI形式的数据,返回给JAVA。
最终的工程目录结构先给大家展示下:
1、在cpp文件夹下的CmakeList.txt文件进行如下修改:
2、在app下的build.gradle文件下进行修改:
android节点下添加如下节点,表示按照src/main/cpp/下的CmakeList.txt文件进行构建.
Build—>Make prject,so文件存放地址如下:
所有东西都弄好后,点击按钮,会发现程序是按照如下流程运行的,可以在相应位置设置断点查看程序走势。
以下面这个简单例子为例。
这里是名为 Sample1.java 的 Java 源代码文件的示例:
package com.ibm.course.jni; public class Sample1 { public native int intMethod(int n); //native表示这是一个本地函数,具体实现在C++中 public native boolean booleanMethod(boolean bool); public native String stringMethod(String text); public native int intArrayMethod(int[] intArray); //主函数 public static void main(String[] args) { System.loadLibrary("Sample1"); //加载本地函数库 Sample1 sample = new Sample1(); //新建一个包含native函数的类的对象 int square = sample.intMethod(5); //通过这个对象调用native函数 boolean bool = sample.booleanMethod(true); String text = sample.stringMethod("JAVA"); int sum = sample.intArrayMethod(new int[] { 1, 1, 2, 3, 5, 8, 13 }); //调用结果展示 System.out.println("intMethod: " + square); System.out.println("booleanMethod: " + bool); System.out.println("stringMethod: " + text); System.out.println("intArrayMethod: " + sum); } }
JNI函数的意义
下面展示了在C++端如何实现 public native String stringMethod(String text)函数,在java中执行String text = sample.stringMethod(“JAVA”);这句时,就会调用如下C++函数。
#include <jni.h> JNIEXPORT jstring JNICALL Java_Sample1_stringMethod (JNIEnv *env, jobject obj, jstring string) { //说明:这是一个C++本地函数,函数头部将JAVA和C++连接起来,此函数内部通过JNIENV提供的函数对JNI类型数据进行转换,变成C++/C可以使用的类型。 //env指针指向一个函数指针表,在VC中可以直接用"->"操作符访问其中的函数。 const char *str = env->GetStringUTFChars(string, 0); //调用env中的GetStringUTFChars函数,将jstring类型的string变量转变成char *类型的str变量,下面就可以向C++那样用str这个变量了 char cap[128]; strcpy(cap, str); env->ReleaseStringUTFChars(string, str); //用完之后要释放 return env->NewStringUTF(strupr(cap)); //将C++类型的cap变量通过env的NewStringUTF转变成JNI函数返回值类型jstring }
综上,在C++函数中不能直接使用JNI的一些数据类型,要通过env提供的函数进行转换后才能使用。返回时也是同样道理,不能直接返回C++类型的数据,要将C++类型数据经过env的函数封装成JNI的数据类型后再返回。
下面再提供一些JNI示例,可以通过这些示例看看各种JNI类型的参数数据是如何解析成C++类型的数据,以及C++类型的数据如何封装成JNI类型的数据返回。
//参数是jintArray使用示例 JNIEXPORT jint JNICALL Java_Sample1_intArrayMethod (JNIEnv *env, jobject obj, jintArray array) { int i, sum = 0; jsize len = env->GetArrayLength(array); jint *body = env->GetIntArrayElements(array, 0); for (i=0; i<len; i++) { sum += body[i]; } env->ReleaseIntArrayElements(array, body, 0); return sum; }
//参数是jstring,返回值是字符串对象数组 #define ARRAY_LENGTH 5 JNIEXPORT jobjectArray JNICALL Java_Sample3_stringMethod (JNIEnv *env, jobject obj, jstring string) { //新建jni字符串数组 7 jclass objClass = (*env)->FindClass(env, "java/lang/String"); 8 jobjectArray texts= (*env)->NewObjectArray(env,(jsize)ARRAY_LENGTH, objClass, 0); //C数组 char* sa[] = { "Hello,", "world!", "JNI", "很", "好玩" }; //C数组存入JNI数组 jstring jstr; int i=0; for(;i<ARRAY_LENGTH;i++) { 16 jstr = (*env)->NewStringUTF( env, sa[i] ); //NewStringUTF()函数:将C语言的字符串转换为jstring类型。 17 (*env)->SetObjectArrayElement(env, texts, i, jstr);//必须放入jstring } return texts; }
第7、8行是我们需要特别关注的地方:JNI框架并 没有定义专门的字符串数组,而是使用jobjectArray——对象数组,对象数组的基类是jclass,jclass是JNI框架内特有的类型,相当 于Java语言中的Class类型。在本例程中,通过FindClass()函数在JNI上下文中获取到java.lang.String的类型 (Class),并将其赋予jclass变量。
在例程中我们定义了一个长度为5的对象数组texts,并在程序的第16行向其中循环放入预先定义好的sa数组中的字符串,当然前置条件是使用NewStringUTF()函数将C语言的字符串转换为jstring类型。
//返回一个结构,这里返回一个硬盘信息的简单结构类型 JNIEXPORT jobject JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_getStruct (JNIEnv *env, jobject obj) { /**//* 下面为获取到Java中对应的实例类中的变量*/ //获取Java中的实例类 jclass objectClass = (env)->FindClass("com/sundy/jnidemo/DiskInfo"); //获取类中每一个变量的定义 //名字 jfieldID str = (env)->GetFieldID(objectClass,"name","Ljava/lang/String;"); //序列号 jfieldID ival = (env)->GetFieldID(objectClass,"serial","I"); //给每一个实例的变量付值 (env)->SetObjectField(obj,str,(env)->NewStringUTF("my name is D:")); (env)->SetShortField(obj,ival,10); return obj; }
//返回一个结构数组,返回一个硬盘信息的结构数组 JNIEXPORT jobjectArray JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_getStructArray (JNIEnv *env, jobject _obj) { //申明一个object数组 jobjectArray args = 0; //数组大小 jsize len = 5; //获取object所属类,一般为ava/lang/Object就可以了 jclass objClass = (env)->FindClass("java/lang/Object"); //新建object数组 args = (env)->NewObjectArray(len, objClass, 0); /**//* 下面为获取到Java中对应的实例类中的变量*/ //获取Java中的实例类 jclass objectClass = (env)->FindClass("com/sundy/jnidemo/DiskInfo"); //获取类中每一个变量的定义 //名字 jfieldID str = (env)->GetFieldID(objectClass,"name","Ljava/lang/String;"); //序列号 jfieldID ival = (env)->GetFieldID(objectClass,"serial","I"); //给每一个实例的变量付值,并且将实例作为一个object,添加到objcet数组中 for(int i=0; i < len; i++ ) { //给每一个实例的变量付值 jstring jstr = WindowsTojstring(env,"我的磁盘名字是 D:"); //(env)->SetObjectField(_obj,str,(env)->NewStringUTF("my name is D:")); (env)->SetObjectField(_obj,str,jstr); (env)->SetShortField(_obj,ival,10); //添加到objcet数组中 (env)->SetObjectArrayElement(args, i, _obj); } //返回object数组 return args; }
//返回值是整型数组 JNIEXPORT jintArray JNICALL Java_Sample2_intMethod(JNIEnv *env, jobject obj) { int i = 1; jintArray array;//定义数组对象 array = (*env)-> NewIntArray(env, 10); for(; i<= 10; i++) (*env)->SetIntArrayRegion(env, array, i-1, 1, &i); /* 获取数组对象的元素个数 */ int len = (*env)->GetArrayLength(env, array); /* 获取数组中的所有元素 */ jint* elems = (*env)-> GetIntArrayElements(env, array, 0); for(i=0; i<len; i++) printf("ELEMENT %d IS %d\n", i, elems[i]); return array; }
//JNI创建Java对象、调用对象方法 extern "C" JNIEXPORT jobject JNICALL Java_....getList(JNIEnv *env, jobject instance) { .... // 获取 ArrayList 类 jclass list_jclass = env->FindClass("java/util/ArrayList"); // 获取 ArrayList 构造函数id jmethodID list_init = env->GetMethodID(list_jcs, "<init>", "()V"); // 创建一个 ArrayList 对象 jobject list_obj = env->NewObject(list_jcs, list_init, ""); // 获取 ArrayList 对象的 add() 的 methodID jmethodID list_add = env->GetMethodID(list_jcs, "add", "(Ljava/lang/Object;)Z"); //调用 add()方法 env->CallVoidMethod(list_obj,list_add,add参数列表); //CallVoidMethod中的Void,根据所调用方法的返回值决定,若是调用的方法返回值为空,则是CallVoidMethod,若是返回值为int类型,则是CallIntMethod,依次类推。 .... }
FindClass:参数是类所在的包名路径字符串
GetMethodID:第一个参数是类名,第二个是要获取id号的函数名,第三个是函数的签名。返回某函数在该类中的id号。函数签名的含义参考:JNI技术简介
CallVoidMethod参数说明:第一个参数是对象,第二个是要调用的对象的方法,后面依次加上方法的参数。
由上例可以看出,在JNI中创建java对象,要先用env中FindClass函数获取对象类型,然后再通过GetMethodID获取对象的构造函数id,最后通过NewObject函数创建对象。
在JNI中调用对象的方法时,步骤都是先获取方法在类中的id后,再通过方法ID进行调用。
JNI处理复合结构示例
关于复合结构在JNI的用法可以参考如下几篇文章:
JNI返回复杂对象:map值为vector,vector里为结构体
jni 返回map示例
java中HashMap和ArrayList嵌套结构的访问:java中是如何给复合数据赋值的,就要用JNI语法按照JAVA这个赋值步骤一样赋值。
JNI函数详解、GetIntArrayElements等方法参数和使用详解
GetObjectClass、FindClass和GetMethodID用法,JNI使用JAVA对象的方法。
jni中的NewStringUTF这个函数调用后需要释放内存吗?
java创建自定义类的对象数组,对象数组中的对象必须new以后才能赋值。
string中c_str()的用法:
string.c_str是Borland封装的String类中的一个函数,它返回当前字符串的首字符地址。c_str() 以 char* 形式传回 string 内含字符串,如果一个函数要求char参数,可以使用c_str()方法。但是不能直接赋值给char。
如何将java传入的String参数转换为c的char*,然后使用?
java传入的String参数,在c文件中被jni转换为jstring的数据类型,在c文件中声明char* test,然后test = (char*)(*env)->GetStringUTFChars(env, jstring, NULL);注意:test使用完后,通知虚拟机平台相关代码无需再访问:(*env)->ReleaseStringUTFChars(env, jstring, test);
jobjectArray
对象数组。JNI中没有对应关系的类型,就会转换成这个类型,例如自定义类的对象数组。
Android Studio生成Debug版apk方式及位置:亲测有效,生成的apk文件可直接拷贝到手机中,系统识别出是apk文件会自动安装。
Android Studio打包Release版APK文件的具体方法介绍
两种异常:
1、在C++文件中通过env调用java的方法时,会发生异常
2、在C++代码中发生异常,需要将这个异常返回给JAVA层处理;
参考:Android-JNI开发系列《三》-异常处理
Android Studio JNI输出Log
Android studio调试详解
本地函数与JAVA端传数据的方式:
1、传json串
2、传对象
传json:
java端:将corners打包成json串,然后传递
C++端:接收json串后进行解析,处理,然后将要返回的东西打包成json串
注意点:接收到字符串后要逐个自己解析。可能也要用到JNI函数
传对象:
java端:新建一个数据结构类,把类实例化的对象传给本地函数
C++端:通过JNI函数对类对象实现解析,获取数据值。处理。然后将要返回的东西打包成类返回。
参考资料:
so文件生成方法
JNI详解—从不懂到理解:这篇文章干货很多
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。