本文主要讲述如何在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 detect_cards(vector 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 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
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; iReleaseIntArrayElements(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(;iNewStringUTF( 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
//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, "", "()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函数解释
JNI对象和域的访问设置方法
NewObject用法
C++ String返回时转换成JNI Jstring
jni中的NewStringUTF这个函数调用后需要释放内存吗?
java创建自定义类的对象数组,对象数组中的对象必须new以后才能赋值。
jdouble数组新建和赋值
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文件的具体方法介绍
Android Studio JNI输出Log
Android studio调试详解
关于不同cpu架构APP的兼容问题
本地函数与JAVA端传数据的方式:
1、传json串
2、传对象
传json:
java端:将corners打包成json串,然后传递
C++端:接收json串后进行解析,处理,然后将要返回的东西打包成json串
注意点:接收到字符串后要逐个自己解析。可能也要用到JNI函数
传对象:
java端:新建一个数据结构类,把类实例化的对象传给本地函数
C++端:通过JNI函数对类对象实现解析,获取数据值。处理。然后将要返回的东西打包成类返回。
参考资料:
so文件生成方法
JNI详解—从不懂到理解:这篇文章干货很多