使用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;// 获取JNIEnvif (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()) {// 因为函数没有返回值,所以函数签名最后是VjmethodID UninitSDKID = FJavaWrapper::FindMethod(Env, FJavaWrapper::GameActivityClassID, "uinit", "()V", false);if (UninitSDKID != nullptr) {UE_LOG(LogTemp, Warning, TEXT("Success to find method uninit"));// 对于没有返回值的Java方法,调用的是CallVoidMethodFJavaWrapper::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;
}