在使用UE4开发Android时,有时需要获取平台相关的信息、或者执行平台相关的操作,在这种情况下,需要在代码中添加Java的代码以及在C++中调用它们。有些需求也需要在游戏中从Java侧接收一些事件,需要处理Java调用C++的流程。
本篇文章主要涉及以下几部分内容:
- UE工程中添加Java代码
- Java函数的签名规则
- Java调用C++的函数
- C++调用Java的函数
如何利用UE的UPL特性、Java的签名规则,以及在UE中进行JNI调用实现方法,会在文章中做详细的介绍。
UPL
UPL全称Unreal Plugin Language,是一个XML-Based的结构化语言,用于介入UE的打包过程(如拷贝so/编辑AndroidManifest.xml,添加IOS的framework/操作plist等),本篇文章主要介绍UPL在Android中的使用,UPL在IOS上的使用,在我之前的文章UE4 开发笔记:Mac/iOS 篇#UPL 在 iOS 中的应用中有介绍。
往UE项目里添加Java代码,需要通过UPL在打包时往GameActivity.java插入代码来实现。
UPL的语法使用XML,文件也需要保存为.xml
格式:
1 2 3 4 5
| <?xml version="1.0" encoding="utf-8"?>
<root xmlns:android="http://schemas.android.com/apk/res/android"> </root>
|
在<root></root>
中可以使用UPL提供的节点来编写逻辑(但是因为它的语法都是XML的形式来实现编程逻辑的,所以写起来循环等控制流程十分麻烦),以添加AndroidManifest.xml
中权限请求为例(以下代码均位于<root></root>
中):
1 2 3 4 5 6 7 8 9 10 11
| <androidManifestUpdates> <addPermission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <addPermission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<addElements tag="application"> <meta-data android:name="notch.config" android:value="portrait|landscape"/> <meta-data android:name="android.notch_support" android:value="true"/> </addElements> </androidManifestUpdates>
|
使用androidManifestUpdates
节点,可以在其中更新AndroidManifest.xml
,UPL为IOS和Android都提供了很多平台相关的节点,在使用时需要注意,不能混用。
UPL还提供了往GameActivity类中添加Java方法的节点:gameActivityClassAdditions
,通过这个节点,可以直接在UPL里编写Java代码,在构建Android包时,会自动把这些代码插入到GameActivity.java
中的GameActivity
类中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <gameActivityClassAdditions> <insert> public String AndroidThunkJava_GetPackageName() { Context context = getApplicationContext(); return context.getPackageName(); } public String AndroidThunkJava_GetInstalledApkPath() { Context context = getApplicationContext(); PackageManager packageManager = context.getPackageManager(); ApplicationInfo appInfo; try{ appInfo = packageManager.getApplicationInfo(context.getPackageName(),PackageManager.GET_META_DATA); return appInfo.sourceDir; }catch (PackageManager.NameNotFoundException e){ return "invalid"; } } </insert> </gameActivityClassAdditions>
|
插入之后生成的文件:
这两个函数就在GameActivity.java
中了,UPL有很多增加GameActivity内容的节点,这部分内容在UE的文档中是不全的,具体还是要去看UBT的代码:UnrealBuildTool/System/UnrealPluginLanguage.cs#L378。
UPL支持对GameActivity的扩展,不仅仅只是添加函数,还可以给OnCreate
/OnDestory
等函数添加额外的代码,方便根据需求介入到不同的时机。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
|
那么,写完了UPL的脚本之后,如何来使用它呢?
需要在需要添加该UPL的Module的build.cs
中添加以下代码:
1 2 3 4 5 6 7 8 9 10 11
| if (Target.Platform == UnrealTargetPlatform.Android) { PrivateDependencyModuleNames.Add("Launch"); AdditionalPropertiesForReceipt.Add("AndroidPlugin", Path.Combine(ModuleDirectory, "UPL/Android/FGame_Android_UPL.xml")); }
if (Target.Platform == UnrealTargetPlatform.IOS) { AdditionalPropertiesForReceipt.Add("IOSPlugin",Path.Combine(ModuleDirectory,"UPL/IOS/FGame_IOS_UPL.xml")); }
|
通过AdditionalPropertiesForReceipt
来指定我们的UPL脚本,注意AndroidPlugin
和IOSPlugin
不可修改,文件路径可以根据UPL文件在项目中的位置指定。
使用这种方式就把UPL添加到了UE的构建系统中,当构建Android/IOS平台时,就会自动执行我们在脚本中的逻辑了。
Java函数签名
JNI是什么?JNI全称Java Native Interface,即Java原生接口。主要用来从Java调用其他语言代码、其他语言来调用Java的代码。
在上一节中,我们通过UPL往GameActivity中添加了Java的代码,在UE中如何通过C++去调用这些Java的函数,需要使用JNI调用来实现。
通过C++去调用Java,首先需要知道,所要调用的Java函数的签名。签名是描述一个函数的参数和返回值类型的信息。
以该函数为例:
1
| public String AndroidThunkJava_GetPackageName(){ return ""; }
|
以这个函数为例,它不接受参数,返回一个Java的String值,那么它的签名是什么呢?
签名的计算是有一个规则的,暂时先按下不表,后面会详细介绍。
JDK提供的javac
具有一个参数可以给Java代码生成C++的头文件,用来方便JNI调用,其中就包含了签名。
写一个测试的Java代码,用来生成JNI调用的.h:
1 2 3
| public class GameActivity { public static native String SingnatureTester(); }
|
生成命令:
1
| javac -h . GameActivity.java
|
会在当前目录下生成.class
和.h
文件,.h
中的内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include <jni.h>
#ifndef _Included_GameActivity #define _Included_GameActivity #ifdef __cplusplus extern "C" { #endif
JNIEXPORT jstring JNICALL Java_GameActivity_SingnatureTester (JNIEnv *, jclass);
#ifdef __cplusplus } #endif #endif
|
里面导出了GameActivity
类成员SingnatureTester
JNI调用的符号信息,在注释中包含了它的签名()Ljava/lang/String;
。
Java_ue4game_GameActivity_SingnatureTester
是当前函数可以在C/C++中实现的函数名,当我们在C++中实现了这个名字的函数,在Java中调用到GameActivity
的SingnatureTester
时,就会调用到我们C++中的实现。
把函数声明改为:
1 2 3
| public class GameActivity { public static native String SingnatureTester(int ival,double dval,String str); }
|
它的签名则是:
经过上面的两个例子,其实就可以看出来Java函数的签名规则:签名包含两部分——参数、返回值。
其中,()
中的是参数的类型签名,按照参数顺序排列,()
后面的是返回值的类型签名。
那么Java中的类型签名规则是怎么样的呢?可以依据下面的Java签名对照表:JNI 调用签名对照表。
Java中的基础类型和签名对照表:
Java |
Native |
Signature |
byte |
jbyte |
B |
char |
jchar |
C |
double |
jdouble |
D |
float |
jfloat |
F |
int |
jint |
I |
short |
jshort |
S |
long |
jlong |
J |
boolean |
jboolean |
Z |
void |
void |
V |
根据上面的规则,void EmptyFunc(int)
的签名为(I)V
。
非内置基础类型的签名规则为:
- 以
L
开头
- 以
;
结尾
- 中间用
/
隔开包和类名
如Java中类类型:
- String:
Ljava/lang/String;
- Object:
Ljava/lang/Object;
给上面的例子加上package时候再测试下:
1 2 3 4
| package ue4game; public class GameActivity { public static native String SingnatureTester(GameActivity activity); }
|
则得到的签名为:
1 2 3 4 5 6 7
|
JNIEXPORT jstring JNICALL Java_ue4game_GameActivity_SingnatureTester (JNIEnv *, jclass, jobject);
|
JNI:Java to C++
UE给我们的游戏生成的GameActivity
中也声明了很多的native
函数,这些函数是在C++实现的,在Java中执行到这些函数会自动调用到引擎的C++代码中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public native int nativeGetCPUFamily(); public native boolean nativeSupportsNEON(); public native void nativeSetAffinityInfo(boolean bEnableAffinity, int bigCoreMask, int littleCoreMask); public native void nativeSetConfigRulesVariables(String[] KeyValuePairs); public native boolean nativeIsShippingBuild(); public native void nativeSetAndroidStartupState(boolean bDebuggerAttached); public native void nativeSetGlobalActivity(boolean bUseExternalFilesDir, boolean bPublicLogFiles, String internalFilePath, String externalFilePath, boolean bOBBInAPK, String APKPath); public native void nativeSetObbFilePaths(String OBBMainFilePath, String OBBPatchFilePath); public native void nativeSetWindowInfo(boolean bIsPortrait, int DepthBufferPreference); public native void nativeSetObbInfo(String ProjectName, String PackageName, int Version, int PatchVersion, String AppType); public native void nativeSetAndroidVersionInformation( String AndroidVersion, String PhoneMake, String PhoneModel, String PhoneBuildNumber, String OSLanguage ); public native void nativeSetSurfaceViewInfo(int width, int height); public native void nativeSetSafezoneInfo(boolean bIsPortrait, float left, float top, float right, float bottom); public native void nativeConsoleCommand(String commandString); public native void nativeVirtualKeyboardChanged(String contents); public native void nativeVirtualKeyboardResult(boolean update, String contents); public native void nativeVirtualKeyboardSendKey(int keyCode); public native void nativeVirtualKeyboardSendTextSelection(String contents, int selStart, int selEnd); public native void nativeVirtualKeyboardSendSelection(int selStart, int selEnd); public native void nativeInitHMDs(); public native void nativeResumeMainInit(); public native void nativeOnActivityResult(GameActivity activity, int requestCode, int resultCode, Intent data); public native void nativeGoogleClientConnectCompleted(boolean bSuccess, String accessToken); public native void nativeVirtualKeyboardShown(int left, int top, int right, int bottom); public native void nativeVirtualKeyboardVisible(boolean bShown); public native void nativeOnConfigurationChanged(boolean bPortrait); public native void nativeOnInitialDownloadStarted(); public native void nativeOnInitialDownloadCompleted(); public native void nativeHandleSensorEvents(float[] tilt, float[] rotation_rate, float[] gravity, float[] acceleration);
|
在上一节Java签名中已经提到过,native
的方法是Java调用其他语言实现,上面这些函数在UE中均有实现,用于在引擎中接收Android设备的不同逻辑,定义分布在下列文件中:
1 2 3 4 5 6 7 8
| Runtime\Android\AndroidLocalNotification\Private\AndroidLocalNotification.cpp Runtime\ApplicationCore\Private\Android\AndroidWindow.cpp Runtime\Core\Private\Android\AndroidPlatformFile.cpp Runtime\Core\Private\Android\AndroidPlatformMisc.cpp Runtime\Core\Private\Android\AndroidPlatformProcess.cpp Runtime\Launch\Private\Android\AndroidEventManager.cpp Runtime\Launch\Private\Android\AndroidJNI.cpp Runtime\Launch\Private\Android\LaunchAndroid.cpp
|
我们也可以自己在GameActivity添加native
的函数,如果有一些SDK中提供了native
这样的函数,也可以用以下方式来实现,我这里写一个简单的例子,使用UPL往GameActivity添加一个native
函数,并在C++端实现。
1 2 3 4 5
| <gameActivityClassAdditions> <insert> public native void nativeDoTester(String Msg); </insert> </gameActivityClassAdditions>
|
在C++中实现一个这样的函数即可:
1 2 3 4 5 6 7
| #if PLATFORM_ANDROID JNI_METHOD void Java_com_epicgames_ue4_GameActivity_nativeDoTester (JNIEnv jenv*, jobject thiz, jstring msg); { } #endif
|
com.epicgames.ue4
是UE生成的GameActivity.java
的包名(package com.epicgames.ue4;
)。
可以看到,在C++中实现JNIMETHOD的函数名是以下规则:
1
| RType Java_PACKAGENAME_CLASSNAME_FUNCNAME(JNIEnv*,jobject thiz,Oher...)
|
注意:这个函数必须是个C函数,不能参与C++的name mangling,不然签名就不对了。
在UE中可以使用JNI_METHOD
宏,它定义在AndroidPlatform.h
中。
1 2
| #define JNI_METHOD __attribute__ ((visibility ("default"))) extern "C"
|
也可以使用extern "C"
。在C++中定义之后,如果Java端调用了该函数,就可以执行到我们在C++里写的逻辑了。
JNI:C++ to Java
通过上一节的内容,可以知道了Java中函数的签名信息,如何在UE中通过函数名和签名信息来在C++中调用到游戏中的Java代码呢。
UE在C++端封装了大量的JNI的辅助函数,可以很方便地进行JNI操作。这些函数大多定义在下面三个头文件中:
1 2 3 4 5 6
| #include "Android/AndroidJNI.h"
#include "Android/AndroidJavaEnv.h"
#include "Android/AndroidJava.h"
|
因为AndroidJNI.h
位于Launch
模块中,所以在需要在Build.cs中为Android
平台添加该模块。
以第一节我们使用UPL往GameActivity类中添加的下面这个函数为例:
1 2 3 4 5
| public String AndroidThunkJava_GetPackageName() { Context context = getApplicationContext(); return context.getPackageName(); }
|
想要在UE中调用到它,首先要获取它的jmethodID
,需要通过函数所属的类、函数名字,签名三种信息来获取:
1 2 3 4
| if (JNIEnv* Env = FAndroidApplication::GetJavaEnv()) { jmethodID GetPackageNameMethodID = FJavaWrapper::FindMethod(Env, FJavaWrapper::GameActivityClassID, "AndroidThunkJava_GetPackageName", "()Ljava/lang/String;", false); }
|
因为我们的代码是插入到GameActivity
类中的,而UE对GameActivity
做了封装,所以可以通过FJavaWrapper
来获取,FJavaWrapper
定义位于Runtime/Launch/Public/Android
。
得到的这个methodID
,有点类似于C++的成员函数指针,想要调用到它,需要通过某个对象来执行调用,UE也做了封装:
1
| jstring JstringResult = (jstring)FJavaWrapper::CallObjectMethod(Env, FJavaWrapper::GameActivityThis,GetPackageNameMethodID);
|
通过CallObjectMethod
来在GameActivity
的实例上调用GetPackageNameMethodID
,得到的值是java中的对象,这个值还不能直接转换为UE中的字符串使用,需要进行转换的流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| namespace FJavaHelperEx { FString FStringFromParam(JNIEnv* Env, jstring JavaString) { if (!Env || !JavaString || Env->IsSameObject(JavaString, NULL)) { return {}; }
const auto chars = Env->GetStringUTFChars(JavaString, 0); FString ReturnString(UTF8_TO_TCHAR(chars)); Env->ReleaseStringUTFChars(JavaString, chars); return ReturnString; } FString FStringFromLocalRef(JNIEnv* Env, jstring JavaString) { FString ReturnString = FStringFromParam(Env, JavaString);
if (Env && JavaString) { Env->DeleteLocalRef(JavaString); }
return ReturnString; } }
|
通过上面定义的FJavaHelperEx::FStringFromLocalRef
可以把jstring
转换为UE的FString:
1
| FString FinalResult = FJavaHelperEx::FStringFromLocalRef(Env,JstringResult);
|
到这里,整个JNI调用的流程就结束了,能够通过C++去调用Java并获取返回值了。
结语
参考资料: