UPL 与 JNI 调用的最佳实践

在使用 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"?>
<!--Unreal Plugin Example-->
<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"/>

<!--Android 的全面屏支持 -->
<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
/* Engine/Source/Programs/UnrealBuildTool/System/UnrealPluginLanguage.cs#L378 
* <!-- optional additions to the GameActivity imports in GameActivity.java -->
* <gameActivityImportAdditions> </gameActivityImportAdditions>
*
* <!-- optional additions to the GameActivity after imports in GameActivity.java -->
* <gameActivityPostImportAdditions> </gameActivityPostImportAdditions>
*
* <!-- optional additions to the GameActivity class implements in GameActivity.java (end each line with a comma) -->
* <gameActivityImplementsAdditions> </gameActivityImplementsAdditions>
*
* <!-- optional additions to the GameActivity class body in GameActivity.java -->
* <gameActivityClassAdditions> </gameActivityOnClassAdditions>
*
* <!-- optional additions to GameActivity onCreate metadata reading in GameActivity.java -->
* <gameActivityReadMetadata> </gameActivityReadMetadata>
*
* <!-- optional additions to GameActivity onCreate in GameActivity.java -->
* <gameActivityOnCreateAdditions> </gameActivityOnCreateAdditions>
*
* <!-- optional additions to GameActivity onDestroy in GameActivity.java -->
* <gameActivityOnDestroyAdditions> </gameActivityOnDestroyAdditions>
*
* <!-- optional additions to GameActivity onStart in GameActivity.java -->
* <gameActivityOnStartAdditions> </gameActivityOnStartAdditions>
*
* <!-- optional additions to GameActivity onStop in GameActivity.java -->
* <gameActivityOnStopAdditions> </gameActivityOnStopAdditions>
*
* <!-- optional additions to GameActivity onPause in GameActivity.java -->
* <gameActivityOnPauseAdditions> </gameActivityOnPauseAdditions>
*
* <!-- optional additions to GameActivity onResume in GameActivity.java -->
* <gameActivityOnResumeAdditions> </gameActivityOnResumeAdditions>
*
* <!-- optional additions to GameActivity onNewIntent in GameActivity.java -->
* <gameActivityOnNewIntentAdditions> </gameActivityOnNewIntentAdditions>
*
* <!-- optional additions to GameActivity onActivityResult in GameActivity.java -->
* <gameActivityOnActivityResultAdditions> </gameActivityOnActivityResultAdditions>
*/

那么,写完了 UPL 的脚本之后,如何来使用它呢?

需要在需要添加该 UPL 的 Module 的 build.cs 中添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
// for Android
if (Target.Platform == UnrealTargetPlatform.Android)
{
PrivateDependencyModuleNames.Add("Launch");
AdditionalPropertiesForReceipt.Add("AndroidPlugin", Path.Combine(ModuleDirectory, "UPL/Android/FGame_Android_UPL.xml"));
}
// for IOS
if (Target.Platform == UnrealTargetPlatform.IOS)
{
AdditionalPropertiesForReceipt.Add("IOSPlugin",Path.Combine(ModuleDirectory,"UPL/IOS/FGame_IOS_UPL.xml"));
}

通过 AdditionalPropertiesForReceipt 来指定我们的 UPL 脚本,注意 AndroidPluginIOSPlugin不可修改,文件路径可以根据 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 值,那么它的签名是什么呢?

1
()Ljava/lang/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
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class GameActivity */

#ifndef _Included_GameActivity
#define _Included_GameActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: GameActivity
* Method: SingnatureTester
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_GameActivity_SingnatureTester
(JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

里面导出了 GameActivity 类成员SingnatureTesterJNI 调用的符号信息,在注释中包含了它的签名()Ljava/lang/String;

Java_ue4game_GameActivity_SingnatureTester是当前函数可以在 C/C++ 中实现的函数名,当我们在 C++ 中实现了这个名字的函数,在 Java 中调用到 GameActivitySingnatureTester时,就会调用到我们 C++ 中的实现。

把函数声明改为:

1
2
3
public class GameActivity {
public static native String SingnatureTester(int ival,double dval,String str);
}

它的签名则是:

1
2
3
4
5
/*
* Class: GameActivity
* Method: SingnatureTester
* Signature: (IDLjava/lang/String;)Ljava/lang/String;
*/

经过上面的两个例子,其实就可以看出来 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

非内置基础类型的签名规则 为:

  1. L 开头
  2. ; 结尾
  3. 中间用 / 隔开包和类名

如 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
/*
* Class: ue4game_GameActivity
* Method: SingnatureTester
* Signature: (Lue4game/GameActivity;)Ljava/lang/String;
*/
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
// Runtime/Core/Public/Android/AndroidPlatform.h
#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
// Runtime/Launch/Public/Android
#include "Android/AndroidJNI.h"
// Runtime/Core/Public/Android
#include "Android/AndroidJavaEnv.h"
// Runtime/Core/Public/Android
#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 并获取返回值了。

结语

参考资料: