前言
我们都知道Google当初为了让Java开发者能快速介入到Android开发,把linux操作系统中c/c++实现的各种系统能力利用Java封装起来,此举确实吸引了大量的Java开发者转战移动平台,最终极大的丰富了Android的生态,所以直到今天Java(kotlin)仍然是Android的主要开发语言。Java确实让开发者更容易的开发移动应用程序,但是也把开发者隔离在Android系底层能力之外,但是语言层面并不是没有从Java到c++的渠道,JAVA Native Interface(JNI)就是最重要的一种方法。
什么是JNI
JNI全称Java Native Interface。Java本地接口。这是Java设计的一套Java与c/c++进行沟通的接口规范。
我们先设想一下,一个Java的调用方法想要进入C++中需要什么?Java需要什么?c++需要什么?
比如两段代码
java方法
csharp
package com.example.applicationnative
public class NativeFun {
// native关键字表明这是一个本地方法
native string getContent(int age,string name);
}
cpp方法
scss
// native-lib.cpp
// cpp ==>complie ==> xxx.so
JNIEXPORT jstring JNICALL c_get_content(JNIEnv *env,jobject jb,jint a,jstring n ){
...
...
return env->NewStringUTF(content.c_str());
}
我分别写了Java方法和cpp方法,我通过在Java中把方法声明为native,向VM表示我希望这个方法最终调用到cpp中对应的某个方法。但是VM也是两眼一抹黑,只知道Java native方法,但是不知道它想找哪个cpp函数。 因此,想要正常的让Java方法和cpp函数建立起映射过关系,大概需要做好三件事:
- 函数建立映射关系
- 提供参数的数据类型的转换
- 提供对数据的操作函数(以及一些其他函数)
而这三点,恰恰是JNI规范的主要内容。
函数映射
静态注册
静态注册本质上是通过函数名之间建立起对应关系,由于Java方法所处的全路径+类名+函数名一般就能定位某个方法了,因此,假如我们把cpp中的函数名写成如下这种:
scss
// Java_包名_类名_方法名
JNIEXPORT jstring JNICALL Java_com_example_applicationnative_NativeFun_getContent(JNIEnv *env,jobject jb,jint a,jdouble s){
...
...
return env->NewStringUTF(content.c_str());
}
根据上面函数名的命名规则,cpp就可以找到对应的方法在哪个位置,这种规范类似于一种约定。当然,这是一个简化的版本,如果是重载的方法,就会同名的情况,因此完整版的cpp函数命名方式应该是这样的:
scss
// Java_包名_类名_方法名
JNIEXPORT jstring JNICALL Java_com_example_applicationnative_NativeFun_getContent__ILjava_lang_String_2(JNIEnv *env,jobject jb,jint a,jdouble s){
...
...
return env->NewStringUTF(content.c_str());
}
I表示参数int,java_lang_string_2表示string类型,这样,就可以真正唯一确定某个方法的位置了。不过一般我们定义native重载方法不是很多,所以一般用简化版即可。
动态注册
有静态注册自然有动态注册,静态注册需要cpp函数名有一定的规则,因此写起来还比较繁琐,但是动态注册对cpp函数名就没有限制了。
ini
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *unused) {
...
...
// getContent就是Java的native方法,c_get_content则是对应的cpp函数的指针
JNINativeMethod cMthods[] = {{"getContent","()Ljava/lang/String;",(jstring *)c_get_content}};
const char *className = "com/example/applicationnative/NativeDemo";
// 获得env指针 env是指向指针的指针
if ((vm->GetEnv((void **) &env,JNI_VERSION_1_6))!=JNI_OK){
return result;
}
if (env == nullptr){
return result;
}
//获取到对应的类
jclass currentClass = env->FindClass(className);
if (currentClass == nullptr){
return result;
}
// env->RegisterNatives 手动注册,建立起c_get_content与getContent的映射关系
jint registerResult = env->RegisterNatives(currentClass,cMthods, getArrayLength(cMthods));
if (registerResult<0){
return result;
}
}
上面的代码能看懂注释即可,其实就是调用了JNI规范中定义的一个接口RegisterNatives函数,这个函数需要实现动态注册的功能。
数据类型转换
有了注册的能力,JNI就可以初步建立起Java与cpp的对应关系。不过沟通需要数据的传递,Java的数据类型和cpp的数据类型是不同的,Java中有的基本类型cpp或许可以直接对应,但是很多是不行的。因此由JNI统一为Java的数据类型在cpp层面定义对应的一套数据类型的映射关系。
这种数据类型可以分为两种:原始类型和引用类型。
原始类型的映射
Java中的char和cpp中的char就无法相容,因编码不同导致所占的字节数不同,无法转换。就需要定义jchar
引用类型的映射
类型转换的问题
为了实现Java与cpp的数据传递,JNI在cpp层定义了一组数据类型来与Java层对应,但是这出现了JNI数据类型与cpp原生数据类型之前的类型转换的问题,如果JNI函数需要与第三方cpp库打交道,那么这种数据类型的转换在所难免。
不过只要大家能够根据数据类型的占位和编码的区别来进行类型转换,就不会出大问题,作为JNI的初步引导级别的文章在此就不继续延伸了。
操作函数表
有了方法的映射和数据类型的映射,但是还缺少一样东西,那便是对数据的操作接口。JNI中定义了大量的函数接口,不仅有对cpp层的映射数据类型的操作接口,还有对Java层的类和方法的操作接口。具体的接口可以看官方文档
我也大致对相关的接口进行了分类总结:
这些参数都定义在一个JNINativeInterface的结构体中,然后给它取了一个别名JNIEnv指针
arduino
// JNIEnv只是类型别名,函数指针数组定义再结构体JNINativeInterface中
typedef const struct JNINativeInterface *JNIEnv;
于是JNIEnv类型的指针指向了这个结构体,从而可以访问结构体中定义的所有函数
而在我们定义的JNI函数中,JNIEnv这个指针类型的参数是每个被映射的cpp函数中传入的第一个参数,也就方便我们可以使用JNI定义的操作函数。
有了方法映射,数据类型映射,以及函数操作表这三部分规范之后,我们才可以说JNI完整实现了Java和cpp的的相互沟通机制。
JNI的实际操作
上半部分从概念上讲了JNI到底是一个怎样的接口规范,它主要定义了什么内容,当然JNI除了上文讲的三个部分之外,还定义了诸如引用的创建和释放,异常的处理,jvm的操作等,这些部分是对JNI的完善,并不影响我们理解JNI规范的框架。纸上得来终觉浅,如果不使用来个实际的demo来运行一下,就难以加深理解。
环境初始化
在编写cpp代码模块代码之前,我们往往需要在Android studio中做好相关的配置,在此就不多赘述了,建议可以直接参考Android官网的步骤
Java代码
假设在此之前,你已经通过官网自动创建了一个c++项目,里面会包含一个默认的JNI函数,我们可以不去管它 我们在Android工程中定义一个类
arduino
package com.example.applicationnative;
public class NativeFun {
public native String callFromJava(String content,int v);
public native String callFromJavaAdd(int x,int y);
public void callFromCpp(String content){
Log.i("JNI_TEST",content);
}
}
然后再我们的MainActivity中创建这个类的对象,并且调用前两个方法。
cpp 代码
如果我们按照官网的步骤进行初始化,则会产生一个默认的cpp文件,里面有一个默认的映射的方法,可以不用管。cpp的目录结构大概如下图
CMakeLists.txt是cmake读取用来构建makefile构建文档,构建部分我们放在后面来讲,在此按下不表。native-lib.cpp是Android给我们默认创建的cpp文件,我们可以不改,里面默认的代码我们也可以不管,直接在后面写我们自己的demo代码。
根据前文描述我们知道,实现java与cpp的沟通我们需要谨记三个关键点,函数的映射,数据类型的映射,JNI函数表。JNI的连接需要函数映射和数据包类型的映射,而本地功能的具体实现则需要依赖函数表。
首先我们需要成功建立起Java与cpp的链接。
arduino
// 第一部分
#include <jni.h>
#include <android/log.h>
// 第二部分
#define LOG_TAG "native_demo"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
// 第三部分
//映射 callFromJavaAdd
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_applicationnative_NativeFun_callFromJavaAdd(JNIEnv *env, jobject thiz, jint x,
jint y) {
return env->NewStringUTF("hello callFromJavaAdd from native");
}
// 第四部分
// 映射callFromJavaStr方法
JNIEXPORT jstring JNICALL c_callFromJavaStr(JNIEnv *env, jobject thiz, jstring content, jint v) {
return env->NewStringUTF("hello callFromJavaStr from native");
}
JNINativeMethod cNativeMthods[] = {{"callFromJavaStr", "(Ljava/lang/String;I)Ljava/lang/String;", (jstring *) c_callFromJavaStr}};
const char *className = "com/example/applicationnative/NativeFun";
// 定义一个获取数组长度的方法
template<class T>
JNIEXPORT jint JNICALL getArrayLength(T &arr) {
return sizeof(arr) / sizeof(arr[0]);
}
代码分析
上面的代码是需要和上层Java native方法形成映射的cpp函数,我们希望同时使用动态注册和静态注册,我们可以简单的解释一下这个cpp文件中代码的含义:
预编译指令
第一部分 #include是预编译指令,包含对应的头文件,为了实现对外的接口暴露,cpp使用头文件的方式,把类,函数,变量的声明放在头文件中,而对应的定义则放在对应的源文件中,其他cpp代码想要引用该代码的能力时,就把对应的头文件包含进来即可。目前我们先包含了jni.h和log.h。
宏定义指令
第二部分 #define同样是预编译指令,是cpp的宏定义,宏定义的原理就是粗暴的文本替换,比如 #define LOG_TAG "native_demo"命令,在接下来使用LOG_TAG 的地方就会在预编译阶段直接替换成 "native_demo"。__android_log_print是log.h中声明的一个log函数,能够帮助我们把log打到Androidstudio的log窗口。
静态注册的函数
第三部分 定义了一个cpp函数,它是NativeFun类的callFromJava函数在cpp层的映射函数,虚拟机通过它有规律的命名方式就能建立其Java和cpp的函数映射关系。但是对于函数体可以简单解释一下:
arduino
extern : extern关键字直接放在变量或者函数之前,意思就是向编译器表示:我保证这个变量(函数)已经定义过了;
extern "C" 后面跟"C" 然后再跟着函数或变量之前,意思是向编译器表示 我希望这个函数(变量)能够按照C的方式进行编译,而不是cpp的方式编译。这两者的差别主要在于cpp为了支持多态区分同名的函数,会把同名的函数的函数名解析成函数名+参数类型相关的函数名,比如int f(int a); =>f_int类似这种形式,而C不会对函数名做修改,所以假如在C代码中调用c++的函数,在链接阶段会出现函数名找不到的情况(因为两者解析函数名的方式不一样)。假如你的代码里函数名不重要,这个修饰符不加也行。但是我们现在就是要依赖固定规则的函数名来实现映射关系,所以这个extern "C"不可或缺。
JNIEXPORT :从名称上也能大概理解,我们是否想要把这个JNI接口调用暴露出去,要的话就要加上这个。
JNICALL :JNI的调用规范。
如果点击代码跳转,我们会发现这两个都是宏定义,
#define JNIEXPORT __attribute__ ((visibility ("default"))) // defalut就是暴露接口对外可见,如果希望设置不可见用hidden
#define JNICALL // JNICALL定义为空(linux平台),如果不加这个其实也行
动态注册逻辑
第四部分是cpp函数以及一些变量的定义,c_callFromJavaStr是我们希望与Java层的callFromJavaStr形成映射的函数,但是我们能看到这没有按照JNI静态注册时要求的函数命名规则来定义函数,因此我们就只能使用动态注册,即手动建立起Java方法与cpp函数的映射关系。
一般而言更加推荐使用动态注册的方式来建立Java与cpp的映射关系。
想要通过动态注册的方式来实现函数映射,依赖函数表中的注册函数:
arduino
// clazz 是Java方法对应的class对象
// JNINativeMethod 是一个结构体,也可以理解为一个对象,包含Java方法与JNI函数的一些信息,
// 注意JNINativeMethod * 指着类型的是表示JNINativeMethod 数组
// nMethods 表示当前JNINativeMethod 数组有几个元素
// 从上面的参数定义可以看出,RegisterNatives可以一次性定义多个方法
// 该函数的返回值是jint类型,对应了Java的int类型,返回0是成功,小于0是失败
jint RegisterNatives(jclass clazz,const JNINativeMethod *methods, jint nMethods); //动态注册接口
// JNINAtiveMethod的结构定义
typedef struct {
char *name; // Java函数名 字符串类型
char *signature; // 函数签名的指针 字符串类型
void *fnPtr; // cpp对应的函数指针,本来void *可以表示任意类型的指针,但是JNI文档中指出我们应该填入一个函数指针
} JNINativeMethod; // 根据这个结构体定义了一个JNINativeMethod类型
动态注册实现
根据上面的文档,我们大概了解了怎么调用注册函数,接下来的问题是,什么时候调用这个函数?显然我们需要依赖JNI规范中定义的某些生命周期类型的回调函数。恰好就有两个JNI_Onload,JNI_OnUnload,顾名思义,前者是在对应的Android native library加载时,调用这个函数;后者则是本地库被卸载清理时调用。显然我们需要前者这个回调函数。
于是我们继续在cpp文件的第四部分代码后面添加如下代码
ini
extern "C"
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *unused) {
jint result = -1;
//中间是动态注册的逻辑
result = JNI_VERSION_1_6;
return result;
}
我们能看到JNI_OnLoad的参数并不是我们上面看到的JNIEnv *env开头,而是JavaVM *vm,但是不用慌,通过JavaVM指针我们就能拿到JNIEnv的指针。
arduino
jint GetEnv(void **env, jint version);
Java Signature
构造参数中,还有个signature不算太长久,指的是函数签名
Java签名表示Java中对字段/方法的描述方式。即向虚拟机描述某个字段(方法)长什么样子。Java签名常常被用于反射,字节码等技术中。 在Java字段中的基本类型,引用类型都有特定的表述方式来描述它们
而Java方法的签名描述方式是固定:
scss
// param_list 多个方法参数之前不需要区隔,直接连在一起
(param_list)return_type
我们来举个例子:
typescript
class A{
string getRealContent(String origin,int index){
return "";
}
}
//那么getRealContent的方法签名就是如下
(Ljava/lang/String;I)Ljava/lang/String;
构造注册参数
经过我们对Java签名的了解之后,我们再回看RegisterNatives的参数要求也不奇怪,想要唯一确定一个方法的位置,首先需要直到它在哪个类里,其次需要直到它的方法名,然后需要直到这个方法长什么样子(避免同名的方法无法分辨的情况),也就是签名。于是需要jclass,JNINativeMethod.
scss
// cNativeMthods中我们定义了一个元素,包含了callFromJavaStr的描述和对应映射的cpp函数
JNINativeMethod cNativeMthods[] = {{"callFromJavaStr", "(Ljava/lang/String;I)Ljava/lang/String;", (jstring *) c_callFromJavaStr}};
// 定义了Java方法所在的全类名,帮助我们找到对应的class
const char *className = "com/example/applicationnative/NativeFun";
// 定义了一个获取数组的大小的方法模板,
template<class T>
JNIEXPORT jint JNICALL getArrayLength(T &arr) {
return sizeof(arr) / sizeof(arr[0]);
}
extern "C"
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *unused) {
JNIEnv *env;
jint result = -1;
LOGI("JNI_OnLoad 函数开始 ============》 ");
// 获得env指针 env是指向指针的指针
if ((vm->GetEnv((void **) &env, JNI_VERSION_1_6)) != JNI_OK) {
LOGI("获取环境指针 失败了");
return result;
}
if (env == nullptr) {
LOGI("env=null ============》 ");
return result;
}
//获取到对应的类
jclass currentClass = env->FindClass(className);
if (currentClass == nullptr) {
LOGI("查找对应的类失败了============》 ");
return result;
}
// 手动注册方法映射
jint registerResult = env->RegisterNatives(currentClass, cNativeMthods,
getArrayLength(cNativeMthods));
//检查注册结果
if (registerResult < 0) {
LOGI("注册失败了============》 ");
return result;
}
LOGI("注册成功!!!============》 ");
result = JNI_VERSION_1_6;
return result;
}
通过手动注册函数的方式,我们也建立起了方法的映射。理论上现在打包运行之后就可以实现Java与c++的沟通了。
cpp反向调用
但是无论是静态注册还是动态注册,本质上都是Java层主动的调用cpp层的方法,沟通必须是相互的,那么cpp肯定也需要有能主动调用Java的方法。而调用Java方法主要依赖CallMethod方法。而调用这个对象的方法显然需要对象实例,以及方法的名称、签名等一些信息来定位。
scss
JNIEXPORT void JNICALL call_java_method(JNIEnv *env,jobject obj) {
// 仍然是"com/example/applicationnative/NativeFun"
jclass clazz = env->FindClass(className);
if (clazz == nullptr){
return;
}
// 通过class,以及方法的标识信息来找到对应的方法id
jmethodID method_id = env->GetMethodID(clazz,"callFromCpp","(Ljava/lang/String;)V");
// 想要把信息传递到Java层,必须使用映射的数据类型。
jstring content = env->NewStringUTF("来自cpp的问候");
// 调用Java方法
env->CallVoidMethod(obj,method_id,content);
// 用完删除局部引用是个好习惯,避免内存泄漏
env->DeleteLocalRef(clazz);
env->DeleteLocalRef(content);
}
然后我们在合适的时机调用call_java_method即可,由于需要获取jobject,我们可以选择在Java方法调用过来时再调用该函数
scss
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_applicationnative_NativeFun_callFromJavaAdd(JNIEnv *env, jobject thiz, jint x,
jint y) {
LOGI("x=%d y=%d",x,y);
// thiz就是当前调用了callFromJavaAdd本地方法的对象实例
call_java_method(env,thiz); //调用Java方法
return env->NewStringUTF("hello callFromJavaAdd from native");
}
完整代码
我们简单整理一下代码,cpp代码在调用函数时,函数必须已经声明过或者定义过了,也就是说cpp里某段代码想要调用某个函数,那这个函数必须在这段代码之前已经声明或者定义过,在这段代码之后都无法识别,因此我们一般会把一个文件里的函数声明都统一放在一个头文件里,然后再cpp文件的顶部进行引用即可。本例中我就定义了一个native.h头文件。
以下是JNI的完整代码:
arduino
// native.h 头文件
// 预编译 条件判断指令,防止头文件被重复引用
#ifndef APPLICATIONNATIVE_NATIVE_H
#define APPLICATIONNATIVE_NATIVE_H
// 模板函数
template<class T>
JNIEXPORT jint JNICALL getArrayLength(T &arr) {
return sizeof(arr) / sizeof(arr[0]);
}
//提前声明这个函数
void call_java_method(JNIEnv *env,jobject obj);
#endif //APPLICATIONNATIVE_NATIVE_H
scss
// native-lib.cpp 文件
#include <jni.h>
#include <string>
#include <android/log.h>
#include "native.h"
#define LOG_TAG "wuzhao"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
const char *className = "com/example/applicationnative/NativeFun";
JNIEXPORT jstring JNICALL c_other_str(JNIEnv *env, jobject jb) {
Test::Test data_decrpty;
std::string content = "other str";
LOGI("原始数据内容: %s", content.c_str());
data_decrpty.encrpty(content);
LOGI("加密后的数据内容: %s", content.c_str());
data_decrpty.decrpty(content);
LOGI("解密后的数据内容: %s", content.c_str());
return env->NewStringUTF(content.c_str());
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_applicationnative_NativeFun_callFromJavaAdd(JNIEnv *env, jobject thiz, jint x,
jint y) {
LOGI("x=%d y=%d",x,y);
call_java_method(env,thiz);
return env->NewStringUTF("hello callFromJavaAdd from native");
}
extern "C"
JNIEXPORT jstring JNICALL c_callFromJavaStr(JNIEnv *env, jobject thiz, jstring content, jint v) {
const char *c = env->GetStringUTFChars(content,NULL);
LOGI("receive content %s %d",c,v);
return env->NewStringUTF("hello callFromJavaStr from native");
}
JNINativeMethod cNativeMthods[] = {{"callFromJavaStr", "(Ljava/lang/String;I)Ljava/lang/String;", (void *) c_callFromJavaStr}};
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *unused) {
JNIEnv *env;
jint result = -1;
LOGI("JNI_OnLoad 函数开始 ============》 ");
// 获得env指针 env是指向指针的指针
if ((vm->GetEnv((void **) &env, JNI_VERSION_1_6)) != JNI_OK) {
LOGI("获取环境指针 失败了");
return result;
}
if (env == nullptr) {
LOGI("env=null ============》 ");
return result;
}
//获取到对应的类
jclass currentClass = env->FindClass(className);
if (currentClass == nullptr) {
LOGI("查找对应的类失败了============》 ");
return result;
}
// 注册本地防范
jint registerResult = env->RegisterNatives(currentClass, cNativeMthods,
getArrayLength(cNativeMthods));
//检查注册结果
if (registerResult < 0) {
LOGI("注册失败了============》 ");
return result;
}
LOGI("注册成功!!!============》 ");
result = JNI_VERSION_1_6;
return result;
}
JNIEXPORT void JNICALL call_java_method(JNIEnv *env,jobject obj) {
jclass clazz = env->FindClass(className);
if (clazz == nullptr){
return;
}
jmethodID method_id = env->GetMethodID(clazz,"callFromCpp","(Ljava/lang/String;)V");
jstring content = env->NewStringUTF("来自cpp的问候");
env->CallVoidMethod(obj,method_id,content);
env->DeleteLocalRef(clazz);
env->DeleteLocalRef(content);
}
arduino
// NativeFun.java
package com.example.applicationnative;
import android.util.Log;
public class NativeFun {
native String callFromJavaAdd(int x,int y);
native String callFromJavaStr(String content,int v);
public void callFromCpp(String content){
Log.i("wuzhao","java-callFromCpp "+content);
}
}
以上三个文件是实现JNI机制相关的代码了,这个时候我们就可以运行项目来测试代码运行是否如预期一样。
当然还有一个静态代码块中加载so的操作,这个在代码生成时就有,理论上我们可以把它加在我们认为合适的地方
arduino
static{
System.loadLibrary("applicationnative") // 是我们在CMakeLists.txt中定义的名字,见下文
}
一些工程编译的问题
cpp的编译打包流程
不过在简单介绍cmake之前,或许还得先从cpp的编译流程说起: cpp的代码的编译打包过程大概可以分为三个步骤:
- 预编译处理
- 在编译之前,编译器会对代码中以#开头的指令进行预处理。处理过后,这些指令就没有了。
- 代码编译
- 编译器是一个文件接着一个文件的进行编译的,不同的代码文件相互不可见,编译后生成中间代码保存在.o或.obj文件中。
- 链接
- 把所有的中间代码文件链接成一个可执行文件。
CMake
所有的编译型的编程语言的大型工程都需要考虑一个问题,就是如何编译只有局部代码改动的工程,我们都知道编译器应该只编译修改过的代码文件,而不是整个工程。因此再c/cpp工程中帮助开发者解决这个问题的就是makefile命令文件以及make命令。makefile文件中,能够定义工程项目中各个模块的依赖关系,从而帮助编译器实现最小范围的重新编译。然而makefile在大型工程中直接编写也非常复杂,因此就出现了cmake这个用简单指令生成makefile的工具。而cmake也需要一个配置文件来生成makefile文件,这个配置文件就是CMakeLists.txt.
简单来说,cmake就是生成makefile文件的工具,用来帮助开发者管理大型工程。从某种程度上说,cmake有点像Android开发中的gradle脚本。 [图片上传失败...(image-986863-1693065339978)]
对于开发者而言,可以把cmake和MakeLists.txt视作工程编译构建的核心,因为它对工程构建进行了更高层次的抽象,使得配置变得更加简单。
CMakeList.txt
CMakelists.txt是cmake执行需要读取的配置文件,里面主要定义了项目名,编译的源代码和构建类型,以及链接的库
bash
# Sets the minimum CMake version required for this project.
cmake_minimum_required(VERSION 3.22.1)
# 项目名
project("applicationnative")
# SHARED 表示生成动态链接库so文件 随后跟着源代码文件,如果多个文件可以直接跟在后面用空格分隔
# (${CMAKE_PROJECT_NAME} 是最终生成的so的名字,这里我们直接使用项目名,也可以自行定义
add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
native-lib.cpp )
# cpp代码中如果用到了其他的动态链接库里的功能,就需要在这里链接进来
target_link_libraries(${CMAKE_PROJECT_NAME}
# List libraries link to the target library
android
log)
# demo中,我们使用了Android提供的log库
CMakeLists.txt需要配置在gradle脚本中,主要是为了方便我们利用gradle直接编译cpp文件,而不需要我们自己手动调用cmake make命令了。
cmake还有很多命令,读者可以自行去官网学习