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配置,可以参考我的另一篇博客
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