Android Studio3.5 JAVA调用C++源码方法总结

前言

本文主要讲述如何在Android Studio中通过JAVA调用C++源码,最终将项目打包成apk文件发布。整体流程如下图所示:
在这里插入图片描述
主要涉及如下几个方面:

1、Android Studio中整个程序的运行流程;
2、C++源码如何通过NDK或者Cmake工具打包成so包;
3、JAVA如何通过JNI调用so包,JNI的使用方法;
4、将整个项目打包成apk包发布。

整篇文章都是自己在实战出摸索总结出来的经验,希望能给大家带来帮助。若有不当之处,还请大家斧正。

基础概念解释

Gradle

网上关于Gradle的解释比较多,也比较官方,这个东西不用深究太多,大致知道Gradle是Android Studio用来进行构建和打包操作的就行了。同时要知道,对Gradle进行的配置文件主要是两个,一个是app中的build.gradle文件,一个是整个项目的build.gradle文件。

Gradle可以通过两个外部构建工具进行扩展:ndk-build,Cmake。这两个构建工具功能相同,二者选其一即可。

NDK

Native Development Kit,原生开发工具包,一组可以在Android应用中利用C和C++代码的工具。它包含一个ndk-build组件,可以用来生成so文件。

ndk-build主要通过配置Android.mk文件实现构建。Android.mk文件是在设置了app的build.gradle文件后自动生成的,不需要用户自己新建。

Cmake工具

Cmake主要通过配置 CmakeList.txt文件实现构建。其中CmakeList.txt文件则是自己新建的,新建好后要在build.gradle文件中添加一个cmake 节点,告诉Gradle根据这个文件进行构建。

so文件

so文件是linux系统下二进制共享文件。由于Android系统和linux系统内核相同,因此Android系统也支持so文件。

JNI

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++源码相关介绍

我的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。

工程目录结构

最终的工程目录结构先给大家展示下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Cmake工具生成so文件

1、在cpp文件夹下的CmakeList.txt文件进行如下修改:
在这里插入图片描述
2、在app下的build.gradle文件下进行修改:
android节点下添加如下节点,表示按照src/main/cpp/下的CmakeList.txt文件进行构建.
在这里插入图片描述

Build—>Make prject,so文件存放地址如下:
在这里插入图片描述

Android Studio中程序运行的流程

所有东西都弄好后,点击在这里插入图片描述按钮,会发现程序是按照如下流程运行的,可以在相应位置设置断点查看程序走势。

在这里插入图片描述

C++端进行JNI编程

以下面这个简单例子为例。

这里是名为 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参考资料

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中没有对应关系的类型,就会转换成这个类型,例如自定义类的对象数组。

项目打包成apk并在手机安装

Android Studio生成Debug版apk方式及位置:亲测有效,生成的apk文件可直接拷贝到手机中,系统识别出是apk文件会自动安装。

Android Studio打包Release版APK文件的具体方法介绍

JNI异常处理

两种异常:
1、在C++文件中通过env调用java的方法时,会发生异常
2、在C++代码中发生异常,需要将这个异常返回给JAVA层处理;
参考:Android-JNI开发系列《三》-异常处理

相关基础知识

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详解—从不懂到理解:这篇文章干货很多

相关资料:
Android:JNI 与 NDK到底是什么?(含实例教学)

  • 0
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值