Android

NDK<第二篇>:JNI编程

2022-10-19  本文已影响0人  NoBugException

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方法,printLogaddFunc

静态注册方法:

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 对象。

[本章完...]

上一篇下一篇

猜你喜欢

热点阅读