使用Unreal引擎做Android端的应用开发,不可避免的会用到第三方Java接口,这就涉及到了JNI调用。Unreal自己有封装一套接口来调用,这里做个总结,主要是在一些自定义的数据类型转换上。其实可以跟到引擎源码的位置具体看实现。
从上一篇文章中我们知道,c++端调用的方法其实是我们定义在xml中的方法,而xml中的方法中真正调用了第三方库的Java接口。
首先假设我们的Java端接口是这么定义的,具体的实现部分我们不关心,它的包名暂定为com.example.test
public class JNITest{
public static int initSDK(int var0, int var1) {
}
public static void uninit() {
}
public static Result process() {
}
}
这里的CustomBuff类和Result类分别如下定义,细节不要纠结,我们只是为了突出各种各样自定的结构体成员
public class Result {
public int m1;
public int m2;
public Point position;
public boolean m3;
}
public class Point {
public float x;
public float y;
public Point () {
}
}
好,有了Java端的定义,我们在Unreal中调用库函数的时候就要把相应的内容做转换。
首先我们在plugin的xml文件中定义好真正调用第三方Java接口的实例
<gameActivityImportAdditions>
<insert>
import com.example.test.*;
</insert>
</gameActivityImportAdditions>
gameActivityClassAdditions>
<insert>
public int init(int v1, int v2) {
return JNITest.init(v1, v2);
}
public void uinit() {
JNITest.uninit();
}
public Result process() {
return JNITest.process();
}
</insert>
</gameActivityClassAdditions>
接下来,我们通过JNI调用的就是上面定义好的init、uinit、process三个方法,大致流程分3步:
- 获取JNIEnv,这是一个指向Java VM的指针,通过它可以获取到所有Java元素。Unreal中通过封装好的FAndroidApplication::GetJavaEnv()获取。相关源码位置:Engine\Source\Runtime\ApplicationCore\Public\Android\AndroidApplication.h
- 找到对应的Java方法,通过方法名、函数签名。Unreal中通过封装好的FJavaWrapper::FindMethod(...)获取。相关源码位置:Engine\Source\Runtime\Launch\Public\Android\AndroidJNI.h
- 调用Java方法,Unreal中通过封装好的FJavaWrapper::CallIntMethod(...)、CallVoidMethod、CallObjectMethod等几个方法。相关源码位置:Engine\Source\Runtime\Launch\Public\Android\AndroidJNI.h
下面以我们定义好的Java接口为例,调用init接口
int32 JNITest::InitSDK()
{
UE_LOG(LogTemp, Warning, TEXT("JNITest InitSDK"));
int32 initResult = -1;
// 获取JNIEnv
if (JNIEnv* Env = FAndroidApplication::GetJavaEnv()) {
// 获取Java方法,第一个参数固定,第二个参数是函数名,第三个是函数签名,第四个是可选项
jmethodID InitSDKID = FJavaWrapper::FindMethod(Env, FJavaWrapper::GameActivityClassID, "init", "()I", false);
if (InitSDKID != nullptr)
{
UE_LOG(LogTemp, Warning, TEXT("Success to find method init"));
// 通过找到的函数ID进行方法调用
initResult = FJavaWrapper::CallIntMethod(Env, FJavaWrapper::GameActivityThis, InitSDKID);
}
}
else
{
UE_LOG(LogTemp, Error, TEXT("Failed to get env"));
}
UE_LOG(LogTemp, Warning, TEXT("JNITest InitSDK result: %d"), initResult);
return initResult;
}
这里会麻烦的是获取Java方法时的函数签名问题。括号里是输入参数,这里init函数没有输入参数所以是空的。括号后面跟的是函数返回值,init函数返回int类型,所以这里是“I”。关于基本类型的函数参数可以参考JNI方法签名 | 以梦为码
如果Java中有我们自己定义的类型,采用”L+包名+类名+;”,注意最后的分号。
有了这个基础,再看下面unit函数的调用就清楚多了。
void JNITest::UnitSDK()
{
UE_LOG(LogTemp, Warning, TEXT("JNITestUnitSDK"));
if (JNIEnv* Env = FAndroidApplication::GetJavaEnv()) {
// 因为函数没有返回值,所以函数签名最后是V
jmethodID UninitSDKID = FJavaWrapper::FindMethod(Env, FJavaWrapper::GameActivityClassID, "uinit", "()V", false);
if (UninitSDKID != nullptr) {
UE_LOG(LogTemp, Warning, TEXT("Success to find method uninit"));
// 对于没有返回值的Java方法,调用的是CallVoidMethod
FJavaWrapper::CallVoidMethod(Env, FJavaWrapper::GameActivityThis, UninitSDKID);
}
}
else
{
UE_LOG(LogTemp, Error, TEXT("Failed to get env"));
}
}
最后看下process函数的调用,这个函数返回值为Result类型,没有输入参数
首先我们要在c++端定义和Java同样类型的结构体,不管是作为函数的输入赋值,还是获取函数返回值都要定义
struct CppPoint
{
float x;
float y;
};
struct CppResult
{
int32 m1;
int32 m2;
Point position;
bool m3;
};
CppResult JNITest::process() {
UE_LOG(LogTemp, Warning, TEXT("JNITest process"));
CppResult ret = new CppResult();
if (JNIEnv* Env = FAndroidApplication::GetJavaEnv()) {
// 通过函数签名查找process方法
jmethodID processID = FJavaWrapper::FindMethod(Env, FJavaWrapper::GameActivityClassID, "process", "()Lcom/example/test/Result;", false);
if (processID != nullptr) {
// 找到Java类型的Result类
jclass resultClass = FAndroidApplication::FindJavaClass("Lcom/example/test/Result");
if (!resultClass) {
UE_LOG(LogTemp, Error, TEXT("Failed to find class Result"));
return ret;
}
// 找到类的构造函数
jmethodID resultConstructor = Env->GetMethodID(resultClass, "<init>", "()V");
if (!resultConstructor) {
UE_LOG(LogTemp, Error, TEXT("Failed to find method Result()"));
return ret;
}
// 构造类对象
jobject resultObj = Env->NewObject(resultClass, resultConstructor);
if (!resultObj)
{
UE_LOG(LogTemp, Error, TEXT("Failed to create resultObj"));
return ret;
}
// 调用process方法获取结果
resultObj = FJavaWrapper::CallObjectMethod(Env, FJavaWrapper::GameActivityThis, processID);
// 将获取到的jobject对象转换到c++类型,这样我们在c++中就能拿到函数返回值
ret = convertJObjectTCppResult(Env, resultObj);
}
}
else
{
UE_LOG(LogTemp, Error, TEXT("Failed to get env"));
}
return ret;
}
CPPResult JNITest::convertJObjectTCppResult(JNIEnv *env, jobject resultObj)
{
UE_LOG(LogTemp, Warning, TEXT("JNITest convertJObjectToCppResult"));
CPPResult result;
jclass detectResultClass = env->GetObjectClass(resultObj);
// 找到Java类中Result类的各个成员并取值
jfieldID m1ID = FJavaWrapper::FindField(env, detectResultClass, "m3", "Z", false);
result.m3 = env->GetBooleanField(resultObj, m1ID);
jfieldID m2ID = FJavaWrapper::FindField(env, detectResultClass, "m1", "I", false);
result.m1 = env->GetIntField(resultObj, m2ID);
jfieldID m3ID = FJavaWrapper::FindField(env, detectResultClass, "m2", "I", false);
result.m2 = env->GetIntField(resultObj, m3ID);
// 因为position成员又是一个自定义的类,所以这里还要进行一次转换
jclass pointClass = FAndroidApplication::FindJavaClass("com/example/test/Point");
if (!pointClass) {
UE_LOG(LogTemp, Error, TEXT("Failed to find class pointClass"));
return result;
}
jfieldID posID = FJavaWrapper::FindField(env, detectResultClass, "position", "Lcom/example/test/Point;", false);
result.position = convertJobjectToPoint(env, env->GetObjectField(resultObj, posID));
return result;
}
Point JNITest::convertJobjectToPoint(JNIEnv *env, jobject posObj) {
CppPoint result;
// 获取 Point 类
jclass PointClass = env->GetObjectClass(posObj);
// 获取 获取 x 和 y 的值
jfieldID xID = FJavaWrapper::FindField(env, PointClass, "x", "F", false);
result.x = env->GetFloatField(arcPointObj, xID);
jfieldID yID = FJavaWrapper::FindField(env, PointClass, "y", "F", false);
result.y = env->GetFloatField(arcPointObj, yID);
return result;
}