Android NDK开发

JNI的全称是Java Native Interface的缩写,通过使用C/C++本地代码,提高代码的性能,或者移植现有代码.

JNI是Java提供的一种C/C++代码和Java代码进行交互的桥梁.

在Android中使用C/C++代码需要用到NDK,它是Android提供的一个工具开发包.方便开发人员快速开发C/C++的程序,NDK本质上还是通过Java中的JNI来和Java层进行交互的.

1.基本使用

关于如何创建C++支持项目和NDK配置,可以参考我的另一篇博客

Android配置OpenCV C++开发环境

1.1 JNI函数的结构

extern "C"  //将函数声明为C风格的函数,防止C++编译器自动修改函数的名称,导致函数链接不到

JNIEXPORT void JNICALL  //JNIEXPORT 表示该函数可由外部调用,作用类似C/C++ dllexport,如果不加JNIEXPORT,调用时会报错. 
                       //JNICALL去掉也不会报错,主要是起一个标记作用,表示它一个由JNI调用的函数,用于与其他函数区分

//方法名是由包名+类名+方法名组成,该方法的名称是唯一的,这样写的作用是让JNI能正确找到定义在Java类中的native修饰的方法
Java_komine_demos_jnidemo_MainActivity_getName(JNIEnv *env, jobject thiz) {
     //JNIEnv是JNI提供的C/C++和Java之间互相调用的桥梁,里面定义了一系列函数给我们使用
     //JNI主要是了解它提供给的一些常用方法,开发中经常围绕着它进行
}

1.2 C/C++获取Java层定义的字段

博客末尾有提供完整代码.

首先在MainActivity中定义一个 name字段,然后定义一个 getName()的native方法

private String name = "MainActivity";
//Alt + Enter可直接在 native-lib.cpp生成对应的函数,不再需要用javah生成头文件.
public native String getName();

C/C++实现

extern "C"
JNIEXPORT jstring JNICALL
Java_komine_demos_jnidemo_MainActivity_getName(JNIEnv *env, jobject thiz) {
    //3.获取Java层的jclass,输入双引号再输入有智能提示
    jclass mainActivityClass = env->FindClass("komine/demos/jnidemo/MainActivity");

    //2.获取Java层字段id 
    jfieldID nameFieldId = env->GetFieldID(mainActivityClass,"mName", "Ljava/lang/String;");

    //1.获取字段的值 thiz表示MainActivity,想象一下反射获取字段值的过程,field.get(this)
    jobject name = env->GetObjectField(thiz,nameFieldId);

    //4.将jobject转换为jstring
    return static_cast<jstring>(name);
}

最后在MainActivity的onCreate方法中显示name的值

Toast.makeText(getApplicationContext(),"name:" + getName(),Toast.LENGTH_SHORT).show();

JNIEnv提供了一系列用于获取字段值的方法,因为String是Object类型,所以这里调用 GetObjectField()方法获取String的值.

env->GetIntField() //获取int字段的值
env->GetFloatField() //获取float字段的值
env->GetBooleanField() //获取Boolean字段的值
env->GetObjectField()  //获取Object类型的值
...
...

env->GetStaticIntField() //获取类中int静态成员值
env->GetStaticFloatField() //获取类中float静态成员值
...

获取字段值的大体步骤都是一样的,都需要jfieldID对象的参数.

env.FindClass()参数组成是包名+ 类名的方式确定一个jclass,用反斜杠隔开

env.GetFieldID() 参数1 jclass对象, 参数2 字段名称 参数3 字段的签名

字段的签名统一L开头,后面+所在的包名+类型名称,String就位于java.lang包下,java的基本类型都在这个包下.然后包名的.替换成反斜杠.

1.3 C/C++调用Java定义的方法

首先在MainActivity定义一个方法 alertMessage()方法,该方法由C/C++层调用.

public void alertMessage(String msg) {
    Toast.makeText(getApplicationContext(),msg,Toast.LENGTH_SHORT).show();
}

嫌麻烦也可以直接在之前的方法操作,这里再声明一个 alert() native方法来调用Java的 alertMessage()方法,然后在onCreate方法中调用 alert()方法.

public native void alert();

C/C++实现

extern "C"
JNIEXPORT void JNICALL
Java_komine_demos_jnidemo_MainActivity_alert(JNIEnv *env, jobject thiz) {

    //3.获取jclass对象
    jclass mainActivityClass = env->FindClass("komine/demos/jnidemo/MainActivity");

    //2.获取一个方法的id
    jmethodID alertMessageMethodId = env->GetMethodID(mainActivityClass,"alertMessage", "(Ljava/lang/String;)V");

    jstring msg = env->NewStringUTF("来自C/C++的方法调用!");

    //1 调用一个无返回值的方法
    env->CallVoidMethod(thiz,alertMessageMethodId,msg);
}

获取静态方法id使用 env->GetStaticMethodID()

env->GetMethodID() 参数1 jclass对象 参数2 方法名称 参数3 方法签名

方法签名组成:

(参数签名+ 分号)+返回值类型 参数签名参考上面的说明 V表示void返回值类型,详细见下图

注意 int和Integer不是同一个 int对应I,Integer对应Ljava/lang/Integer,除非要手写,不然也不需要记得很清楚,不过也得知道原理.

float 和Float同理.字段签名和字段签名之间用分号隔开

env->CallxxxMethod()

//无返回值方法
env->CallVoidMethod()

//返回值Boolean
env->CallBooleanMethod()

//返回值Int
env->CallIntMethod()
...
...


//调用静态方法,CallStaticxxxMethod()

//无返回值的静态方法
env->CallStaticVoidMethod()

//Int返回值的静态方法
env->CallStaticIntMethod()

####################################2022-04-17更新############################

1.4 C/C++层创建Java对象

有时候我们需要在C/C++层创建对象然后返回.可以使用 env->NewObject()方法来创建Java对象.

首先我们在 MainActivity中创建一个 createPoint()的native方法.

public native Point createPoint(int x,int y);

C/C++实现

extern "C"
JNIEXPORT jobject JNICALL
Java_komine_demos_jnidemo_MainActivity_createPoint(JNIEnv *env, jobject thiz, jint x, jint y) {
    //3.获取Java对象的class
    jclass pointClass = env->FindClass("android/graphics/Point");

    //2.获取构造函数方法签名
    jmethodID initMethodId = env->GetMethodID(pointClass,"<init>", "(II)V");

    //1.调用JNIEnv的NewObject()方法创建一个Java对象
    jobject pointObj = env->NewObject(pointClass,initMethodId,x,y);

    //4.返回创建好的对象
    return pointObj;
}

最后在onCreate()方法中调用,就可以看到对象以及被初始化了.

其实还可以通过 env->AllocObject()方法来初始化一个对象.它与env->NewObject()方法一个大的区别就是,

AllocObject()方法不会调用构造函数,也不初始化任何变量的值,只是分配对象的内存.可以视情况使用.使用也比NewObject()方法要简单一点.

jobject  p = env->AllocObject(pointClass);

1.5 C/C++层设置Java对象字段的值

调用env.SetxxField()系列的方法来设置字段的值.比如设置一个Int字段的值

jclass pointClass = env->FindClass("android/graphics/Point");

jfieldID xFieldId = env->GetFieldID(pointClass,"x", "I");

//参考上面的代码
env->SetIntField(pointObj,xFieldId,239);

设置静态字段的值使用env.SetStaticxxxField()系列的方法.

#####################################2022-04-17####################################

2.Java对象和C/C++对象对应

有时候我们想让Java层的xx对象和C/C++层的xx对象直接对应起来.操作Java的xx对象就像操作C/C++对象那样.

我们在Java层新建一个Person.java类,将native方法在 native-lib.cpp实现

public class Person {
    //存放C++层对象的首地址
    private final long mNativeObject;

    public Person(String name,int age){
       //mNativeObject保存的是对象的首地址
       //如果我们某个方法需要传递一个Person的对象,我们可以直接将对象的指针传进去,然后强转为类型就可以了.
        mNativeObject = init(name,age);
    }

    private native long init(String name,int age);

    //实际开发中可能不会这么写,每个方法都传C++对象的首地址,这里只是演示
    private native String getName(long mNativeObject);
    private native int getAge(long mNativeObject);
    private native void setName(long mNativeObject,String name);
    private native void setAge(long mNativeObject,int age);

    public String getName(){
        return getName(mNativeObject);
    }

    public int getAge(){
        return getAge(mNativeObject);
    }

    public void setName(String name){
        setName(mNativeObject,name);
    }

    public void setAge(int age){
        setAge(mNativeObject,age);
    }
}

然后在cpp目录下创建一个 C++ Class 的Person类

注意:新添加的源文件要在CMakeLists.txt中添加,不然用不了.

Person.h

#ifndef JNIDEMO_PERSON_H
#define JNIDEMO_PERSON_H

#include <jni.h>
#include <iostream>

using namespace std;

class Person {
private:
    string name;
    int age;
public:
    Person(string name,int age);
    string getName();
    int getAge();
    void setName(string name);
    void setAge(int age);
};

#endif //JNIDEMO_PERSON_H

Person.cpp

#include "Person.h"

Person::Person(string name, int age) {
    this->name = name;
    this->age = age;
}

string Person::getName() {
    return this->name;
}

int Person::getAge() {
    return this->age;
}

void Person::setName(string name) {
    this->name = name;
}

void Person::setAge(int age) {
    this->age = age;
}

native-lib.cpp 这里不能直接全部复制,因为我们的创建的应用包名不一样,记得导入 Person.h头文件

Person *person = NULL;

extern "C"
JNIEXPORT jlong JNICALL
Java_komine_demos_jnidemo_Person_init(JNIEnv *env, jobject thiz, jstring name, jint age) {

    //将Java层的String转换为C++层的string/const char*
    string nameStr = env->GetStringUTFChars(name,NULL);

    //这里返回的是一个对象指针,new是动态分配的内存,除非你使用delete关键字销毁,不然会一直存在内存中.
    //这里其实不用new创建也没有关系,返回的时候用取地址符获取对象的指针就好了.前提是你要定义在方法外面.
    person = new Person(nameStr,age);

    return reinterpret_cast<jlong>(person);
}

extern "C"
JNIEXPORT jstring JNICALL
Java_komine_demos_jnidemo_Person_getName(JNIEnv *env, jobject thiz, jlong m_native_object) {
    //将对象的首地址强转为Person*
    Person *p = reinterpret_cast<Person *>(m_native_object);

    return env->NewStringUTF(p->getName().c_str());
}

extern "C"
JNIEXPORT jint JNICALL
Java_komine_demos_jnidemo_Person_getAge(JNIEnv *env, jobject thiz, jlong m_native_object) {
    //将对象的首地址强转为Person*
    Person *p = reinterpret_cast<Person *>(m_native_object);

    return p->getAge();
}

extern "C"
JNIEXPORT void JNICALL
Java_komine_demos_jnidemo_Person_setName(JNIEnv *env, jobject thiz, jlong m_native_object,
                                         jstring name) {
    //将对象的首地址强转为Person*
    Person *p = reinterpret_cast<Person *>(m_native_object);
    p->setName(env->GetStringUTFChars(name,NULL));
}

extern "C"
JNIEXPORT void JNICALL
Java_komine_demos_jnidemo_Person_setAge(JNIEnv *env, jobject thiz, jlong m_native_object,
                                        jint age) {
   //将对象的首地址强转为Person*
   Person *p = reinterpret_cast<Person *>(m_native_object);
   p->setAge(age);
}

最后我们在MainActivity中初始化这个对象.

mPerson = new Person("miku",16);

到此为止就完成了C++层对应Java层对象的全过程,不难看出,Java层只需要保存C++层对象的首地址,然后再将C++对象的方法用Java方法包装一下而已,真正操作还是通过JNI去操作C++对象.最后,对象用完记得销毁.

完整代码:JNIDemo

posted @ 2022-04-16 17:38  komine  阅读(69)  评论(0编辑  收藏  举报