NDK<第二篇>:JNI编程
JNI是一种本地编程接口。它允许运行在JAVA虚拟机中的JAVA代码和用其他编程语言,诸如C语言、C++、汇编,写的应用和库之间的交互操作。
Gradle 3.0之前,AS可以NDK的方式配置JNI环境,Gradle 3.0之后,AS只能用Cmake的方式配置JNI环境。
一、Java调用C++
public class JNI {
static {
// 导入动态库
System.loadLibrary("jniproject");
}
/**
* 加法运算(Java调用C中的方法)
* @param x
* @param y
*/
public native int add(int x, int y);
/**
* 从Java传递字符串,C进行拼接
* @param s
*/
public native String sayHello(String s);
/**
* 让C代码让每个元素加上10
* @param intArray
* @return
*/
public native int[] increaseArrayEles(int[] intArray);
/**
* 校验密码是否正确, 如果正确,则返回200,否则返回400
* @param pwd
* @return
*/
public native int checkPwd(String pwd);
}
#include <jni.h>
#include <string>
using namespace std;
/*
* 加法运算
*
* Class: com_nobug_jniproject_JNI
* Method: add
* Signature: (II)I
*/
extern "C" JNIEXPORT jint JNICALL
Java_com_nobug_jniproject_JNI_add(JNIEnv *, jobject, jint x, jint y) {
return x + y;
}
/*
* Class: com_nobug_jniproject_JNI
* Method: sayHello
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
extern "C" JNIEXPORT jstring JNICALL
Java_com_nobug_jniproject_JNI_sayHello(JNIEnv * env, jobject, jstring oldStr) {
char* fromJava = (char *) env->GetStringChars(oldStr, JNI_FALSE);
char* fromC = "123";
strcat(fromJava, fromC);
return env->NewStringUTF(fromJava);
}
/*
* Class: com_nobug_jniproject_JNI
* Method: increaseArrayEles
* Signature: ([I)[I
*/
extern "C" JNIEXPORT jintArray JNICALL
Java_com_nobug_jniproject_JNI_increaseArrayEles(JNIEnv* env, jobject, jintArray aarFromJava) {
jint* aar = env->GetIntArrayElements(aarFromJava, JNI_FALSE);
for (int i=0;i<env->GetArrayLength(aarFromJava);i++) {
*(aar + i) += 10;
}
return aarFromJava;
}
/*
* Class: com_nobug_jniproject_JNI
* Method: checkPwd
* Signature: (Ljava/lang/String;)I
*/
extern "C" JNIEXPORT jint JNICALL
Java_com_nobug_jniproject_JNI_checkPwd(JNIEnv* env, jobject, jstring pwd) {
int code = strcmp(env->GetStringUTFChars(pwd, JNI_FALSE), "123456");
if (code == 0) {
return 200;
}
return 400;
}
Java调用C++:
// 计算两数之和
int sum = jni.add(1, 3);
tv.setText("两数之和:" + sum);
// 字符串拼接
String newStr = jni.sayHello("Hi");
tv.setText("newStr:" + newStr);
int[] arr = {1, 2, 3};
// 数组元素全部加10
int[] newArr = jni.increaseArrayEles(arr);
tv.setText("newArr[0]="+newArr[0] + " newArr[1]="+newArr[1] + " newArr[2]="+newArr[2]);
int resultCode = jni.checkPwd("123456");
tv.setText(resultCode == 200? "密码正确" : "密码错误");
二、C++调用Java
使用JNI反射,可以实现C++调用Java代码。
public class JNI {
static {
// 导入动态库
System.loadLibrary("jniproject");
}
public native void notifyAdd();
public native void notifyHelloFromJava();
public native void notifyPrintString();
public native void notifySayHello();
public int add(int x, int y) {
Log.d("TAG", "C调用了 add 方法");
return x + y;
}
public void helloFromJava() {
Log.d("TAG", "C调用了 helloFromJava 方法");
}
public void printString(String s) {
Log.d("TAG", "C调用了 printString 方法:" + s);
}
public static void sayHello(String s) {
Log.d("TAG", "C调用了 sayHello 方法:" + s);
}
}
#include <jni.h>
#include <string>
using namespace std;
/*
* Class: com_nobug_jniproject_JNI
* Method: notifyAdd
* Signature: ()V
*/
extern "C" JNIEXPORT void JNICALL
Java_com_nobug_jniproject_JNI_notifyAdd(JNIEnv* env, jobject obj) {
// 得到字节码
jclass clazz = env->GetObjectClass(obj);
// 得到methodId
jmethodID methodId = env->GetMethodID(clazz, "add", "(II)I");
// 执行方法
env->CallIntMethod(obj, methodId, 1, 2);
}
/*
* Class: com_nobug_jniproject_JNI
* Method: notifyHelloFromJava
* Signature: ()V
*/
extern "C" JNIEXPORT void JNICALL
Java_com_nobug_jniproject_JNI_notifyHelloFromJava(JNIEnv* env, jobject obj) {
// 得到字节码
jclass clazz = env->GetObjectClass(obj);
// 得到methodId
jmethodID methodId = env->GetMethodID(clazz, "helloFromJava", "()V");
// 执行方法
env->CallVoidMethod(obj, methodId);
}
/*
* Class: com_nobug_jniproject_JNI
* Method: notifyPrintString
* Signature: ()V
*/
extern "C" JNIEXPORT void JNICALL
Java_com_nobug_jniproject_JNI_notifyPrintString(JNIEnv* env, jobject obj) {
// 得到字节码
jclass clazz = env->GetObjectClass(obj);
// 得到methodId
jmethodID methodId = env->GetMethodID(clazz, "printString", "(Ljava/lang/String;)V");
// 执行方法
env->CallVoidMethod(obj, methodId, env->NewStringUTF("adc"));
}
/*
* Class: com_nobug_jniproject_JNI
* Method: notifySayHello
* Signature: ()V
*/
extern "C" JNIEXPORT void JNICALL
Java_com_nobug_jniproject_JNI_notifySayHello(JNIEnv* env, jobject obj) {
// 得到字节码
jclass clazz = env->GetObjectClass(obj);
// 得到methodId
jmethodID methodId = env->GetStaticMethodID(clazz, "sayHello", "(Ljava/lang/String;)V");
// 执行方法
env->CallStaticVoidMethod(clazz, methodId, env->NewStringUTF("adc"));
}
调用:
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
private JNI jni = new JNI();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
jni.notifyAdd();
jni.notifySayHello();
jni.notifyPrintString();
jni.notifyHelloFromJava();
}
}
三、C++日志打印到AS控制台
【第一步】在cmake中配置日志库
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log)
target_link_libraries( # Specifies the target library.
jniproject
# Links the target library to the log library
# included in the NDK.
${log-lib})
【第二步】在C++中输出日志
#include <android/log.h>
#define LOG_TAG "native-lib"
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, 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__)
输出日志:
LOGD("从C++中打印日志");
【第三步】在AS中查看日志
image.png四、如何在C++中更新UI
UI操作必须含有上下文,native 方法必须在Activity中。
public native void showToastFromC();
public void showToast() {
Toast.makeText(MainActivity.this, "show toast", Toast.LENGTH_SHORT).show();
}
extern "C" JNIEXPORT void JNICALL
Java_com_nobug_jniproject_MainActivity_showToastFromC(JNIEnv *env, jobject obj) {
// 得到字节码
jclass clazz = env->GetObjectClass(obj);
// 得到methodId
jmethodID methodId = env->GetMethodID(clazz, "showToast", "()V");
// 执行方法
env->CallVoidMethod(obj, methodId);
}
五、JNI引用
JNI引用包括:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)
【1】局部引用
大多数JNI函数会创建局部引用。NewObject/FindClass/NewStringUTF 等等都是局部引用。
局部引用只有在创建它的本地方法返回前有效,本地方法返回后,局部引用会被自动释放。
因此无法跨线程、跨方法使用。
释放一个局部引用有两种方式:
1、本地方法执行完毕后VM自动释放;
2、通过DeleteLocalRef手动释放;
一般情况下,我们应该依赖JVM去自动释放 JNI 局部引用;
但下面两种情况必须手动调用 DeleteLocalRef() 去释放:
[1](在循环体或回调函数中)创建大量 JNI 局部引用,即使它们并不会被同时使用,因为 JVM 需要足够的空间去跟踪所有的 JNI 引用,所以可能会造成内存溢出或者栈溢出;
[2] 如果对一个大的 Java 对象创建了 JNI 局部引用,也必须在使用完后手动释放该引用,否则 GC 迟迟无法回收该 Java 对象也会引发内存泄漏;
【2】全局引用
全局引用可以跨方法、跨线程使用,直到它被手动释放才会失效 。
extern "C" JNIEXPORT jstring JNICALL
Java_com_xxx_xxx_xxx(JNIEnv * env, jobject instance) {
// 定义全局变量
static jstring globalStr;
if(globalStr == NULL){
jstring str = env->NewStringUTF("C++字符串");
//删除全局引用调用
DeleteGlobalRef globalStr = static_cast<jstring>(env->NewGlobalRef(str));
//可以释放,因为有了一个全局引用使用str,局部str也不会使用了
env->DeleteLocalRef(str);
}
return globalStr;
}
【3】弱引用
与全局引用类似,弱引用可以跨方法、跨线程使用。与全局引用不同的是,弱引用不会阻止GC回收它所指向的VM内部的对象 。
在对Class进行弱引用是非常合适(FindClass),因为Class一般直到程序进程结束才会卸载。
在使用弱引用时,必须先检查缓存过的弱引用是指向活动的对象,还是指向一个已经被GC的对象。
extern "C" JNIEXPORT jclass JNICALL
Java_com_xxx_xxx_xxx(JNIEnv * env, jobject instance) {
static jclass globalClazz = NULL;
//对于弱引用 如果引用的对象被回收,则返回true,否则返回false
//对于局部和全局引用则判断是否引用java的null对象
jboolean isEqual = env->IsSameObject(globalClazz, NULL);
if (globalClazz == NULL || isEqual) {
jclass clazz = env->GetObjectClass(instance);
//删除使用 DeleteWeakGlobalRef
globalClazz = static_cast<jclass>(env->NewWeakGlobalRef(clazz));
env->DeleteLocalRef(clazz);
}
return globalClazz;
}
六、JNI_OnLoad函数
JNI_Onload在执行system.loadLibrary()函数时被调用,主要用途:
【1】通过JNI_Onload告知VM,当前so库使用的JNI版本,最老的版本问JNI 1.1(JNI_Onload默认返回的是1.1版本)
【2】可以在JNI_Onload中进行数据的初始化
【3】可以在JNI_Onload对java类中的native函数进行注册。java类是通过VM来调用本地方法,调用时需要通过VM在so库中寻找该本地函数,如果该本地函数需要频繁调用的话,会花费很多时间,可以在JNI_Onload调用registerNativeMethods,把native函数注册到VM中,减少寻找花费的时间。
七、静态注册和动态注册
public class JNI {
static {
// 导入动态库
System.loadLibrary("jniproject");
}
public native void printLog();
public native void addFunc(int a,int b);
}
已知存在两个native方法,printLog
和 addFunc
。
静态注册方法:
extern "C" JNIEXPORT void JNICALL
Java_com_nobug_jniproject_JNI_printLog(JNIEnv *env, jobject thiz) {
LOGV("print verbose log");
LOGD("print debug log");
LOGI("print info log");
LOGW("print warn log");
LOGE("print error log");
}
extern "C" JNIEXPORT void JNICALL
Java_com_nobug_jniproject_JNI_addFunc(JNIEnv *env, jobject thiz, jint a, jint b) {
int a1,b1,c1;
a1=a;
b1=b;
c1=a1+b1;
LOGI("addFunc return:%d",c1);
}
使用 Java_com_nobug_jniproject_JNI_addFunc
为函数名叫做静态注册。
我们还可以使用 JNI_OnLoad
函数进行动态注册:
void native_printLog() {
LOGI("native_printLog");
}
void native_addFunc(int x, int y) {
LOGI("native_addFunc");
}
static const JNINativeMethod methods[] = {
{"printLog","()V",(void*)native_printLog},
{"addFunc","(II)V",(void*)native_addFunc}
};
static int registerNativeMethods(JNIEnv *env){ //native函数的注册
jclass clazz;
LOGI("in registerNativeMethods");
clazz = env->FindClass("com/nobug/jniproject/JNI");
if(clazz == NULL){
LOGI("class is null");
return JNI_FALSE;
}
if(env->RegisterNatives(clazz, methods,sizeof(methods)/sizeof(methods[0])) < 0){ //注册函数
LOGI("注册失败");
return JNI_FALSE;
}
LOGI("注册成功");
return JNI_TRUE;
}
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm,void*) {
LOGI("in JNI_Onload");
JNIEnv *env;
jint result = -1;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
if (!registerNativeMethods(env)) {
return -1;
}
result = JNI_VERSION_1_6; //返回JNI 1.4版本信息给VM
return result;
}
静态注册
和 动态注册
从性能上来讲是一样的,动态注册的函数名看起来更加简单。
八、JNI线程
JNIEnv 不支持切换线程,如果在子线程中使用JNIEnv需要特殊处理。
假设,现在需要更新Android的UI:
public native void testThread();
public void updateUI() {
if (Looper.getMainLooper() == Looper.myLooper()) {
Toast.makeText(MainActivity.this, "updateUI", Toast.LENGTH_SHORT).show();
} else {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, "updateUI", Toast.LENGTH_SHORT).show();
}
});
}
}
现在需要做的是,让C++在子线程中调用 updateUI 方法。
C++代码如下:
#include <jni.h>
#include <string>
#include <pthread.h>
#include <android/log.h>
#define LOG_TAG "native-lib"
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, 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__)
using namespace std;
// 全局变量
JavaVM* javaVm;
struct Context {
jobject obj;
};
/**
* 子线程
* @param args
* @return
*/
void* threadTask(void* args) {
LOGI("start thread task");
if (javaVm == NULL) {
LOGE("javaVm is null");
return JNI_FALSE;
}
JNIEnv* env;
// 将native线程添加到JVM中
jint isAttach = javaVm->AttachCurrentThread(&env, 0);
if (isAttach != JNI_OK) {
LOGE("attact thread error");
return JNI_FALSE;
}
Context* context = static_cast<Context *>(args);
// 得到字节码
jclass clazz = env->GetObjectClass(context->obj);
// 得到methodId
jmethodID methodId = env->GetMethodID(clazz, "updateUI", "()V");
// 执行方法
env->CallVoidMethod(context->obj, methodId);
// 分离
javaVm->DetachCurrentThread();
delete context;
context = NULL;
return JNI_FALSE;
}
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm,void*) {
LOGI("in JNI_Onload");
javaVm = vm;
return JNI_VERSION_1_6;
}
extern "C" JNIEXPORT void JNICALL
Java_com_nobug_jniproject_MainActivity_testThread(JNIEnv *env, jobject obj) {
LOGI("testThread from C");
Context* context = new Context;
context->obj = env->NewGlobalRef(obj);
pthread_t pid;
// 启动一个线程
pthread_create(&pid, 0,threadTask, context);
}
加载动态库是,会执行 JNI_OnLoad,将 JavaVM 做为全局变量。
在子线程中,将子线程附加到JVM中:
JNIEnv* env;
// 将native线程添加到JVM中
jint isAttach = javaVm->AttachCurrentThread(&env, 0);
通过这样可以获取 JNIEnv 对象。
[本章完...]