Android NDK开发
<p>JNI的全称是Java Native Interface的缩写,通过使用C/C++本地代码,提高代码的性能,或者移植现有代码.</p><p>JNI是Java提供的一种C/C++代码和Java代码进行交互的桥梁.</p>
<p>在Android中使用C/C++代码需要用到NDK,它是Android提供的一个工具开发包.方便开发人员快速开发C/C++的程序,NDK本质上还是通过Java中的JNI来和Java层进行交互的.</p>
<h2 id="1基本使用">1.基本使用</h2>
<p>关于如何创建C++支持项目和NDK配置,可以参考我的另一篇博客</p>
<p>Android配置OpenCV C++开发环境</p>
<h3 id="11-jni函数的结构">1.1 JNI函数的结构</h3>
<pre><code>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主要是了解它提供给的一些常用方法,开发中经常围绕着它进行
}
</code></pre>
<h3 id="12-cc获取java层定义的字段">1.2 C/C++获取Java层定义的字段</h3>
<p>博客末尾有提供完整代码.</p>
<p>首先在MainActivity中定义一个 <code>name</code>字段,然后定义一个 <code>getName()</code>的native方法</p>
<pre><code>private String name = "MainActivity";
</code></pre>
<pre><code>//Alt + Enter可直接在 native-lib.cpp生成对应的函数,不再需要用javah生成头文件.
public native String getName();
</code></pre>
<p>C/C++实现</p>
<pre><code>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);
}
</code></pre>
<p>最后在MainActivity的onCreate方法中显示name的值</p>
<pre><code>Toast.makeText(getApplicationContext(),"name:" + getName(),Toast.LENGTH_SHORT).show();
</code></pre>
<p>JNIEnv提供了一系列用于获取字段值的方法,因为String是Object类型,所以这里调用 <code>GetObjectField()</code>方法获取String的值.</p>
<pre><code>env->GetIntField() //获取int字段的值
env->GetFloatField() //获取float字段的值
env->GetBooleanField() //获取Boolean字段的值
env->GetObjectField()//获取Object类型的值
...
...
env->GetStaticIntField() //获取类中int静态成员值
env->GetStaticFloatField() //获取类中float静态成员值
...
</code></pre>
<p>获取字段值的大体步骤都是一样的,都需要jfieldID对象的参数.</p>
<p><code>env.FindClass()</code>参数组成是包名+ 类名的方式确定一个jclass,用反斜杠隔开</p>
<p><code>env.GetFieldID()</code> 参数1 jclass对象, 参数2 字段名称 参数3 字段的签名</p>
<p>字段的签名统一L开头,后面+所在的包名+类型名称,String就位于java.lang包下,java的基本类型都在这个包下.然后包名的.替换成反斜杠.</p>
<h3 id="13-cc调用java定义的方法">1.3 C/C++调用Java定义的方法</h3>
<p>首先在MainActivity定义一个方法 <code>alertMessage()</code>方法,该方法由C/C++层调用.</p>
<pre><code>public void alertMessage(String msg) {
Toast.makeText(getApplicationContext(),msg,Toast.LENGTH_SHORT).show();
}
</code></pre>
<p>嫌麻烦也可以直接在之前的方法操作,这里再声明一个 <code>alert()</code> native方法来调用Java的 <code>alertMessage()</code>方法,然后在onCreate方法中调用 <code>alert()</code>方法.</p>
<pre><code>public native void alert();
</code></pre>
<p>C/C++实现</p>
<pre><code>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);
}
</code></pre>
<p>获取静态方法id使用 <code>env->GetStaticMethodID()</code></p>
<p><code>env->GetMethodID()</code> 参数1 jclass对象 参数2 方法名称 参数3 方法签名</p>
<p>方法签名组成:</p>
<p>(参数签名+ 分号)+返回值类型 参数签名参考上面的说明 V表示void返回值类型,详细见下图</p>
<p><img src="https://img2022.cnblogs.com/blog/2826579/202204/2826579-20220416173711969-789740496.png" alt="" loading="lazy"></p>
<p>注意 int和Integer不是同一个 int对应I,Integer对应Ljava/lang/Integer,除非要手写,不然也不需要记得很清楚,不过也得知道原理.</p>
<p>float 和Float同理.字段签名和字段签名之间用分号隔开</p>
<p>env->CallxxxMethod()</p>
<pre><code>//无返回值方法
env->CallVoidMethod()
//返回值Boolean
env->CallBooleanMethod()
//返回值Int
env->CallIntMethod()
...
...
//调用静态方法,CallStaticxxxMethod()
//无返回值的静态方法
env->CallStaticVoidMethod()
//Int返回值的静态方法
env->CallStaticIntMethod()
</code></pre>
<p>####################################2022-04-17更新############################</p>
<h3 id="14-cc层创建java对象">1.4 C/C++层创建Java对象</h3>
<p>有时候我们需要在C/C++层创建对象然后返回.可以使用 <code>env->NewObject()</code>方法来创建Java对象.</p>
<p>首先我们在 <code>MainActivity</code>中创建一个 <code>createPoint()</code>的native方法.</p>
<pre><code>public native Point createPoint(int x,int y);
</code></pre>
<p>C/C++实现</p>
<pre><code>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;
}
</code></pre>
<p>最后在onCreate()方法中调用,就可以看到对象以及被初始化了.</p>
<p>其实还可以通过 <code>env->AllocObject()</code>方法来初始化一个对象.它与<code>env->NewObject()</code>方法一个大的区别就是,</p>
<p><code>AllocObject()</code>方法不会调用构造函数,也不初始化任何变量的值,只是分配对象的内存.可以视情况使用.使用也比NewObject()方法要简单一点.</p>
<pre><code>jobjectp = env->AllocObject(pointClass);
</code></pre>
<h3 id="15-cc层设置java对象字段的值">1.5 C/C++层设置Java对象字段的值</h3>
<p>调用env.SetxxField()系列的方法来设置字段的值.比如设置一个Int字段的值</p>
<pre><code>jclass pointClass = env->FindClass("android/graphics/Point");
jfieldID xFieldId = env->GetFieldID(pointClass,"x", "I");
//参考上面的代码
env->SetIntField(pointObj,xFieldId,239);
</code></pre>
<p>设置静态字段的值使用env.SetStaticxxxField()系列的方法.</p>
<p>#####################################2022-04-17####################################</p>
<h2 id="2java对象和cc对象对应">2.Java对象和C/C++对象对应</h2>
<p>有时候我们想让Java层的xx对象和C/C++层的xx对象直接对应起来.操作Java的xx对象就像操作C/C++对象那样.</p>
<p>我们在Java层新建一个Person.java类,将native方法在 <code>native-lib.cpp</code>实现</p>
<pre><code>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);
}
}
</code></pre>
<p>然后在cpp目录下创建一个 <code>C++ Class</code> 的Person类<br>
<img src="https://img2022.cnblogs.com/blog/2826579/202204/2826579-20220416173755620-1492649150.png" alt="" loading="lazy"></p>
<p>注意:新添加的源文件要在CMakeLists.txt中添加,不然用不了.</p>
<p><img src="https://img2022.cnblogs.com/blog/2826579/202204/2826579-20220416173804104-1578670739.png" alt="" loading="lazy"></p>
<p><code>Person.h</code></p>
<pre><code>#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
</code></pre>
<p><code>Person.cpp</code></p>
<pre><code>#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;
}
</code></pre>
<p><code>native-lib.cpp</code> 这里不能直接全部复制,因为我们的创建的应用包名不一样,记得导入 <code>Person.h</code>头文件</p>
<pre><code>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);
}
</code></pre>
<p>最后我们在MainActivity中初始化这个对象.</p>
<pre><code>mPerson = new Person("miku",16);
</code></pre>
<p><img src="https://img2022.cnblogs.com/blog/2826579/202204/2826579-20220416173816452-1243694591.png" alt="" loading="lazy"></p>
<p>到此为止就完成了C++层对应Java层对象的全过程,不难看出,Java层只需要保存C++层对象的首地址,然后再将C++对象的方法用Java方法包装一下而已,真正操作还是通过JNI去操作C++对象.最后,对象用完记得销毁.</p>
<p>完整代码:JNIDemo</p><br><br>
来源:https://www.cnblogs.com/komine/p/16153621.html
頁:
[1]