Gradle 基本结构以及主要配置
简单来说 就是指定 一下cmake文件的位置,以及 你要编译的cpp版本,和abi版本
大多数情况 gradle配置就关注这2个即可
Cmake重要配置
介绍一些重要的,其余多数百度一下 即可
scss
# 这里是打印出来的一些 cmake 路径信息,方便后面引入外部库使用 比如可以 /.. 这样指定外部路径
message(${CMAKE_CURRENT_LIST_FILE})
message(${CMAKE_CURRENT_LIST_DIR})
下面这个配置很有用,避免你写 "" 引入了,可以直接 尖括号引入 很方便
bash
include_directories(${CMAKE_SOURCE_DIR}/base/)
1个module 可以配置多个so库的
根据你gradle版本的不同, 可以在 对应的build目录下找到对应的名字
动态库固定都是lib开头 后面的就是你配置的名称了
target_link_libraries 配置可以有多个
bash
target_link_libraries(${CMAKE_PROJECT_NAME}
people-lib
# List libraries link to the target library
android
log)
target_link_libraries(dynamic-lib
# List libraries link to the target library
android
log)
这里要注意的是很多新手会搞错,以为一个target_link_libraries 可以写多个 addLibrary中的库,这样会导致 错误 ,比如上面的dynamic-lib 你要是放到 people-lib下 就会导致dynamic-lib 拿不到 log 库的。这点一定要注意
静态注册JNI方法
可以看下kotlin的写法
对应的cpp写法
这种写法其实现在不用记了,因为 as 已经足够智能,让他自动生成就好
动态注册方法
相比于静态注册jni方法,动态注册jni方法 更加方便,下面完整的介绍一下 如何实现 动态注册jni方法
为了调试方便,我们可以首先 新建一个base.h 文件,封装一下 c++的log 方法
c
#include <jni.h>
#include <android/log.h>
#ifndef NDKTEST_BASE_H
#define NDKTEST_BASE_H
#define LOG_TAG "kkk"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
#endif //NDKTEST_BASE_H
然后新建一个cpp文件 首先实现一个方法
动态库在加载的时候 会回调到这个方法里 所以我们只要 在这个方法里 实现我们动态注册jni方法 就可以了
JNI_OnLoad 写法是固定的
c
// 动态库被系统加载时 会自动回调这个方法
JNIEXPORT int JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
LOGE("jni onload call");
return JNI_VERSION_1_6;
}
java端上 ,我们实现一个类
c
public class JNIDynamicLoad {
static {
System.loadLibrary("dynamic-lib");
}
public native int sum(int x,int y);
public native String getNativeString();
}
在ide中会提示你红字,但其实不用管,我们既然是动态注册,就完全不用理会ide给你的提示
我们可以首先实现一下 这2个方法
注意这2个方法的返回值不要搞错了,必须得是j开头的,千万不要想当然惯性写成string和int了
c
// jni方法的前2个参数 永远固定 就是这2个类型
jstring getMessage(JNIEnv *env, jobject job) {
return env->NewStringUTF("this is msg");
}
jint plus(JNIEnv *env, jobject job, int x,int y) {
return x + y;
}
另外我们还要定义一下 java中的2个jni方法
这里其实能看出来 就是在这个数组中 我们建立了 java方法和c++方法的关联关系
c
static JNINativeMethod gMethods[] = {
{"getNativeString", "()Ljava/lang/String;", (void *) getMessage},
{"sum", "(II)I", (void *) plus},
};
这其中第二个参数可能会有人觉得奇怪,但是有过字节码经验的人一看就知道 这个就是方法的字节码描述而已
对于不熟悉字节码的同学来说 也没关系,我们下载一个插件 jclasslib即可
通过插件 一样可以拿到关键的方法签名信息
剩下的就是完整实现一下我们的方法
c
int registerNativeMethods(JNIEnv *env, const char *name,
JNINativeMethod *methods, jint nMethods) {
jclass jcls;
// 查一看 能否找到这个类
jcls = env->FindClass(name);
if (jcls == nullptr) {
return JNI_FALSE;
}
// 如果找到这个类了,就动态注册jni方法
if (env->RegisterNatives(jcls, methods, nMethods) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}
// 动态库被系统加载时 会自动回调这个方法
JNIEXPORT int JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
LOGE("jni onload call");
JNIEnv *env;
```
// 获取java虚拟机环境
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_FALSE;
}
registerNativeMethods(env, JAVA_CLASS, gMethods, 2);
return JNI_VERSION_1_6;
}
这里其实关键的就是理解 RegisterNatives 这个方法,
第一个参数就是传入你java的类,就是哪个类 load了你的动态库 第二个参数 就是java类中的 native方法 ,是一个数组, 第三个参数 就是你要动态注册几个方法
JNI中的字符串操作
基本类型操作直接跳过 没啥可说的,字符串稍微复杂一些,
定义一个java方法
arduino
public native String callNativeString(String str);
看下对应的cpp 实现
c
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_ndktest_JNIBasicType_callNativeString(JNIEnv *env, jobject thiz, jstring str) {
// java 一般都是utf的编码 非基本类型 要注意内存泄漏 一定要有release和get 配对
const char *cstr = env->GetStringUTFChars(str, JNI_FALSE);
LOGD("java string is %s", cstr);
// release 千万不要忘记 使用完毕 以后一定记得释放内存
env->ReleaseStringUTFChars(str, cstr);
return env->NewStringUTF("this is C style ");
}
```c
当然也可以在jni中处理字符串
定义一个简单的jni方法
public native void stringMethod(String str);
scss
实现对应的c++方法,其实就是 截取一下传来的字符串
注意内存回收即可
```c
extern "C"
JNIEXPORT void JNICALL
Java_com_example_ndktest_JNIBasicType_stringMethod(JNIEnv *env, jobject thiz, jstring str) {
const char *cstr = env->GetStringUTFChars(str, 0);
LOGD("java string: %s", cstr);
jsize utfLen = env->GetStringUTFLength(str);
LOGD("utf-length : %d", utfLen);
// 返回的是java字符串的长度
int len = env->GetStringLength(str);
LOGD("length %d", len);
// 想截取的字符串
int length = len - 3;
jchar buf[length];
env->GetStringRegion(str, 0, length, buf);
jstring lstr = env->NewString(buf, length);
const char *cstr2 = env->GetStringUTFChars(lstr, 0);
LOGD("region str: %s", cstr2);
env->ReleaseStringUTFChars(lstr, cstr2);
env->ReleaseStringUTFChars(str, cstr);
}
可以看下运行结果
数组类型
c
public native String callNativeStringArray(String[] strArray);
c
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_ndktest_JNIBasicType_callNativeStringArray(JNIEnv *env, jobject thiz,
jobjectArray str_array) {
int len = env->GetArrayLength(str_array);
LOGD("len is %d", len);
// env->GetIntArrayElements() 基础类型 选择对应的就好
// 对象类型
jobject jo = env->GetObjectArrayElement(str_array, 0);
// jstring 无法直接使用
jstring firstStr = static_cast<jstring>(jo);
const char *str = env->GetStringUTFChars(firstStr,0);
LOGD("first str is %s", str);
env->ReleaseStringUTFChars(firstStr,str);
}
jni 方法 java类字段
kotlin
class Person(var name: String, var age: Int) {
companion object {
var score = 5
}
}
看看如何修改Person对象的值,以及 Person的静态变量的值
c
extern "C"
JNIEXPORT void JNICALL
Java_com_example_ndktest_JNIBasicType_changeObjectFieldValue(JNIEnv *env, jobject thiz,
jobject person) {
jclass cls = env->GetObjectClass(person);
jfieldID fid = env->GetFieldID(cls, "name", "Ljava/lang/String;");
jstring str = env->NewStringUTF("lisa");
env->SetObjectField(person, fid, str);
jfieldID sfid = env->GetStaticFieldID(cls, "score", "I");
// 这里的第一个参数 就是jclass 而不是上面field中的jboject了
int num = env->GetStaticIntField(cls, sfid);
env->SetStaticIntField(cls, sfid, ++num);
}
再看下 如何在cpp中调用java的代码
新增一个方法
kotlin
fun callFromNative(num: Int) {
age = num
}
在jni中如何调用
ini
extern "C"
JNIEXPORT void JNICALL
Java_com_example_ndktest_JNIBasicType_callJavaMethodFromNative(JNIEnv *env, jobject thiz,
jobject person) {
jclass cls = env->GetObjectClass(person);
jmethodID mid = env->GetMethodID(cls, "callFromNative", "(I)V");
env->CallVoidMethod(person, mid, 2);
}
子线程 访问方法
在子县城调用方法很常用,毕竟不能阻塞ui主线程的工作
java
public native void nativeThreadCallback(IThreadCallback callback);
interface IThreadCallback{
void callBack();
}
然后我们看下 调用他的方式
bash
jmethod.nativeThreadCallback {
Log.v(
"vivo",
"Thread name is ${Thread.currentThread().name}",
)
}
其实就是 通过jni方法 来执行这个callback回调,并行执行的时候 一定要在独立线程中执行,不能在默认的主线程中执行
考虑到我们是开线程工作 所以 我们为了方便 定义一组静态变量
c
// 方便在子线程中使用
static jclass jc;
static jobject threadObject;
static jmethodID jm;
这里我们能看出来 所谓回调方法 其实也就是一个普通的类中的一个方法,仅此而已
c
extern "C"
JNIEXPORT void JNICALL
Java_com_example_ndktest_JNIInvokeMethod_nativeThreadCallback(JNIEnv *env, jobject thiz,
jobject callback) {
// 这里是全局引用, 且要注意 env 是不可以跨线程使用的
threadObject = env->NewGlobalRef(callback);
jc = env->GetObjectClass(callback);
jm = env->GetMethodID(jc, "callBack", "()V");
pthread_t handle;
pthread_create(&handle, nullptr, threadCallback, nullptr);
}
这里要注意了 JNIEnv是不可以跨线程传递的, 那咋办呢? 还记得之前的
JNI_OnLoad 方法吗吗,我们可以 通过这个方法的JavaVM 参数来在子线程中获取 JNIEnv
c
static JavaVM *gvm = nullptr;
JNIEXPORT int JNICALL JNI_OnLoad(JavaVM *vm, void *reserved){
gvm = vm;
}
最后我们来实现一下这个方法即可
c
void *threadCallback(void *) {
JNIEnv *env = nullptr;
if (gvm->AttachCurrentThread(&env, nullptr) == 0) {
env->CallVoidMethod(threadObject,jm);
gvm->DetachCurrentThread();
}
return 0;
}
最后看下日志
明显可以看出来,使用pthread_t 创建的线程执行的 已经不在主线程了, 而不在主县城执行的,依旧在main线程执行
Jni 方法java类构造方法
Java
public native Person invokePersonConstructors();
public native Person allocObjectConstructors();
给出两种实现方式, 个人觉得第一种方式更好理解一些
c
extern "C"
JNIEXPORT jobject JNICALL
Java_com_example_ndktest_JNIBasicType_invokePersonConstructors(JNIEnv *env, jobject thiz) {
jclass cls = env->FindClass("com/example/ndktest/Person");
jmethodID mid = env->GetMethodID(cls, "<init>", "(Ljava/lang/String;I)V");
jstring str = env->NewStringUTF("vivo");
return env->NewObject(cls, mid, str, 100);
}
extern "C"
JNIEXPORT jobject JNICALL
Java_com_example_ndktest_JNIBasicType_allocObjectConstructors(JNIEnv *env, jobject thiz) {
jclass cls = env->FindClass("com/example/ndktest/Person");
jmethodID mid = env->GetMethodID(cls, "<init>", "(Ljava/lang/String;I)V");
jstring str = env->NewStringUTF("xiaomi");
jobject person = env->AllocObject(cls);
env->CallNonvirtualVoidMethod(person, cls, mid, str, 10000);
return person;
}