为 Android 构建插件

本页介绍了 Android 本地代码插件

为 Android 构建插件

若想要为 Android 构建插件, 您首先应该获得 Android NDK,并熟悉包括构建共享库在内的所有步骤。

如果使用 C++ (.cpp) 实现插件,则必须确保所创建的功能使用 C linkage 方式进行声明,以避免出现名称重整问题

extern "C" {
  float FooPluginFunction ();
} 

在 C# 中使用插件

构建之后,共享库将复制到资源 (Assets)->插件 (Plugins)->Android 文件夹。在定义以下 C# 脚本之类的函数时,Unity 将按照名称找到共享库:

[DllImport ("PluginName")]
private static extern float FooPluginFunction (); 

请注意, 插件名称 (PluginName) 不应包含文件名称的前缀 ('lib') 和后缀 ('.so')。 建议将所有本地代码的方法都包含在一个 C# 代码层中。这个代码层需要检查 Application.platform,并且只有在应用程序在真机上运行时才会调用本地代码方法;但在编辑器中运行时将返回虚值。您还可以使用平台定义控制依赖于平台的代码编译。

部署

对于跨平台部署,您的工程应包含支持不同平台的插件(如 Android 的 libPlugin.so 插件、Mac 的 Plugin.bundle 插件以及 Windows 的 Plugin.dll 插件)。 Unity 将自动为目标平台选择正确的插件,并将其包括在播放器中。

使用 Java 插件

Android 的插件原理也允许使用 Java 与 Android OS 进行交互。

为 Android 构建 Java 插件

构建 Java 插件的方法多种多样,但是每种方法的最终结果都是插件包含 .class 文件的 .jar 文件。 其中一种方法是下载 JDK,然后使用 javac 编译 .java 文件命令行。这将创建一个 .class 文件,然后使用 jar 命令行工具将其打包至 .jar 文件。 另外一种选择时使用 Eclipse IDE 和 ADT

在本地代码中使用 Java 插件

构建 Java 插件 (.jar) 之后,您应该将其复制至 Unity 工程的资源 (Assets)->插件 (Plugins)->Android 文件夹内。Unity 会将 .class 文件与其余部分 Java 代码打包在一起,然后使用 Java 本地接口 (JNI) 访问代码。JNI 用于从 Java 调用本地代码以及从本地代码与 Java (或 JavaVM)交互。

若要从本地端找到 Java 代码,则必须访问 Java VM。幸运的是,只需在 /C++ 代码中增加一个函数,便可轻松实现访问,如下所示:

jint JNI_OnLoad(JavaVM* vm, void* reserved) {
  JNIEnv* jni_env = 0;
  vm->AttachCurrentThread(&jni_env, 0);
} 

这是通过 C/C++ 开始 Java 使用的一般方法。它完全超出了本文关于 JNI 的解释范围。但是,使用它通常涉及到寻找类的定义、解决构造 (<init>) 方法并创建一个新的对象实例,如以下示例所示:

jobject createJavaObject(JNIEnv* jni_env) {
  jclass cls_JavaClass = jni_env->FindClass("com/your/java/Class");			// find class definition
  jmethodID mid_JavaClass = jni_env->GetMethodID (cls_JavaClass, "<init>",  "()V");		// find constructor method
  jobject obj_JavaClass = jni_env->NewObject(cls_JavaClass, mid_JavaClass);		// create object instance
  return jni_env->NewGlobalRef(obj_JavaClass);						// return object with a global reference
} 

使用 helper 类的 Java 插件

AndroidJNIHelperAndroidJNI 可以用来缓解原声 JNI 带来的麻烦。

AndroidJavaObjectAndroidJavaClass 可以实现很多任务的自动化,并使用即时缓存更快地调用 Java。AndroidJavaObjectAndroidJavaClass 的组合建立在 AndroidJNIAndroidJNIHelper 的基础上,但也也拥有很多自己独有的逻辑(以处理自动化)。这些类也有在'静态'版本中访问 Java 类的部分静态成员。

您可以选择任何您喜欢的方法,无论是通过 AndroidJNI 类的方法使用原生 JNI,或者是 AndroidJNIHelperAndroidJNI 相结合,并在最终使用 AndroidJavaObject/AndroidJavaClass,以达到最大的自动化和便利。

UnityEngine.AndroidJNI 提供 是 C 中可用的 JNI (如上所述)的封装。这个类中的所有方法都是静态方法,并且与 Java 本地接口有 1:1 的映射关系。UnityEngine.AndroidJNIHelper 提供 了下一级所使用的辅助功能,但因为它可能在某些特殊情况下非常有用,因为作为公共方法。

UnityEngine.AndroidJavaObjectUnityEngine.AndroidJavaClass 的实例与 Java 一方的 java.lang.Object 和 java.lang.Class(或其子类)分别拥有 1:1 的映射关系。基本上提供 了与 Java 端 3 种类型的交互:

  • 调用方法
  • 获得字段值
  • 设置字段值

调用 (Call) 分为两种类别:调用 'void' 方法,以及调用 non-void 返回类型的方法。泛型类型用来表示这些方法返回 non-void 类型的返回类型。GetSet 始终采用泛型类型表示字段类型。

示例 1

//The comments describe what you would need to do if you were using raw JNI
 AndroidJavaObject jo = new AndroidJavaObject("java.lang.String", "some_string"); 
 // jni.FindClass("java.lang.String"); 
 // jni.GetMethodID(classID, "<init>", "(Ljava/lang/String;)V"); 
 // jni.NewStringUTF("some_string"); 
 // jni.NewObject(classID, methodID, javaString); 
 int hash = jo.Call<int>("hashCode"); 
 // jni.GetMethodID(classID, "hashCode", "()I"); 
 // jni.CallIntMethod(objectID, methodID);

在此,我们创建了一个 java.lang.String 实例,初始化我们选择的字符串,并检索该字符串的哈希值

AndroidJavaObject 构造器至少需要一个参数,我们想要构建的实例的类名。在类名称之后的任何参数用于让构造器调用对象,在这个实例中,即字符串 "some_string"。随后调用 hashCode() 返回 “int”,这就是为什么将其作为泛型类型参数的调用方法。

注意:您不能使用句点符号实例化 Java 类。内部类必须使用 $ 分隔符,并能适用于句点和斜线格式。因此可以使用 android.view.ViewGroup$LayoutParamsandroid/view/ViewGroup$LayoutParams,其中 LayoutParams 类是嵌套在 ViewGroup 类中。

示例 2

上述其中一个插件的样例展示了如何获得当前应用程序的缓存目录。而在 C# 中无需操作即可完成所有同样的操作:

 AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); 
 // jni.FindClass("com.unity3d.player.UnityPlayer"); 
 AndroidJavaObject jo = jc.GetStatic<AndroidJavaObject>("currentActivity"); 
 // jni.GetStaticFieldID(classID, "Ljava/lang/Object;"); 
 // jni.GetStaticObjectField(classID, fieldID); 
 // jni.FindClass("java.lang.Object"); 

 Debug.Log(jo.Call<AndroidJavaObject>("getCacheDir").Call<string>("getCanonicalPath")); 
 // jni.GetMethodID(classID, "getCacheDir", "()Ljava/io/File;"); // or any baseclass thereof! 
 // jni.CallObjectMethod(objectID, methodID); 
 // jni.FindClass("java.io.File"); 
 // jni.GetMethodID(classID, "getCanonicalPath", "()Ljava/lang/String;"); 
 // jni.CallObjectMethod(objectID, methodID); 
 // jni.GetStringUTFChars(javaString);

在这个案例中,我们以 AndroidJavaClass 开始,而非 AndroidJavaObject,因为我们要访问 com.unity3d.player.UnityPlayer 的静态成员,而不是创建新的对象(通过 Android UnityPlayer 自动创建示例)。然后,我们将访问静态字段 "currentActivity",但在这里,我们使用 AndroidJavaObject 作为泛型参数。这是由于实际字段类型 (android.app.Activity) 是 java.lang.Object 的子类, 任何 非初级类型都必须作为 AndroidJavaObject 访问。这一规则的例外是字符串,因为就算字符串不代表 Java 中的初级类比,也可以直接访问。

在这之后仅仅是一个 Activity 遍历的问题,通过 getCacheDir() 获得代表缓存目录的文件目标,然后调用 getCanonicalPath() 获得字符串代表。

当然,您现在不需要为了获得缓存目录这样做,因为 Untiy 提供 Application.temporaryCachePathApplication.persistentDataPath 来访问应用程序的缓存和文件目录。

示例 3

最后,这里是一个使用 UnitySendMessage 从 Java 向本地代码传递数据的技巧。

using UnityEngine; 
public class NewBehaviourScript : MonoBehaviour { 

	void Start () { 
		AndroidJNIHelper.debug = true; 
		using (AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) { 
			jc.CallStatic("UnitySendMessage", "Main Camera", "JavaMessage", "whoowhoo"); 
		} 
	} 

	void JavaMessage(string message) { 
		Debug.Log("message from java: " + message); 
	}
} 

Java 类 com.unity3d.player.UnityPlayer 现在有静态方法 UnitySendMessage,相当于 iOS 本地端的 UnitySendMessage

虽然在这里,我们直接从脚本代码调用,这从根本上说是在 Java 端中继该消息。然后回调到本地 /Unity 代码传递消息给名为 “Main Camera” 对象。这个对象附加了包含叫作 “JavaMessage” 方法的脚本。

在 Unity 中使用 Java 插件的最佳方法

本节主要针对没有全面 JNI、Java 和 Android 经验的用户,我们假设 AndroidJavaObject/AndroidJavaClass 方法已经在 Unity 中用来与 Java 代码交互。

首先要注意的是在 AndroidJavaObjectAndroidJavaClass 上执行的任何操作在运算上非常昂贵(原生 JNI 方法)。出于性能和代码简洁等方面考虑,强烈建议您将托管和本地/Java 代码之间的转换数量保持到最低限度。

您应该有一个完成所有实际工作的 Java 方法,然后使用 AndroidJavaObject / AndroidJavaClass 与该方法进行通信,获得结果。需要记住的是,JNI helper 类尝试缓存尽可能多的数据来提高性能。

//The first time you call a Java function like 
AndroidJavaObject jo = new AndroidJavaObject("java.lang.String", "some_string");  // somewhat expensive
int hash = jo.Call<int>("hashCode");  // first time - expensive
int hash = jo.Call<int>("hashCode");  // second time - not as expensive as we already know the java method and can call it directly

在使用之后,Mono 垃圾收集器应释放所有 AndroidJavaObjectAndroidJavaClass 创建的实例,但仍然建议在 using(){} 语句保留这些实例,以确保尽快删除。不这样操作的话,将不能确保实例何时被销毁。如果设置 AndroidJNIHelper.debug 为 true,您将看到垃圾收集器在调试输出的活动纪录。

//Getting the system language with the safe approach
void Start () { 
	using (AndroidJavaClass cls = new AndroidJavaClass("java.util.Locale")) { 
		using(AndroidJavaObject locale = cls.CallStatic<AndroidJavaObject>("getDefault")) { 
			Debug.Log("current lang = " + locale.Call<string>("getDisplayLanguage")); 

		} 
	} 
}

您也可以直接调用 .Dispose() 方法,以确保没有延迟的 Java 对象。实际 C# 目标活动的时间可能更长,但是最终将被 mono 垃圾收集。

扩展 UnityPlayerActivity Java 代码

Unity Andriod 可以扩展标准的 UnityPlayerActivity 类 (在 Android 上用于 Unity 播放器的主 Java 类,类似于 Unity iOS 中的 AppController.mm)。

应用程序可以覆盖 Android OS 和 Unity Android 之间的任何基本交互。您可以通过创建从 UnityPlayerActivity 派生的活动,完成此操作(在 Mac 系统中,UnityPlayerActivity.java 位于 /应用程序 (Applications)/Unity/Unity.app/Contents/PlaybackEngines/AndroidPlayer/src/com/unity3d/player,而在 Windows 中,它一般位于 C:\Program Files\Unity\Editor\Data\PlaybackEngines\AndroidPlayer\src\com\unity3d\player^^)。

要完成此操作,首先要找到 Unity Android 附带的 classes.jar。它位于安装文件夹内(通常,Windows 系统为 C:\Program Files\Unity\Editor\Data,Mac 系统为 PlaybackEngines/AndroidPlayer/bin 子文件夹 /Applications/Unity 中)。然后添加 classes.jar 至用来编译新活动 (Activity) 的类路径中。最终的 .class 文件将压缩成 .jar 文件,且位于资源 (Assets)->插件 (Plugins)->Android 文件夹。 由于清单决定要启动哪个活动,因此还必须建立一个新的 AndroidManifest.xml。AndroidManifest.xml 文件也放置在资源 (Assets)->插件 (Plugins)->Android 文件夹。

新活动可能与如下示例类似,OverrideExample.java

package com.company.product;

import com.unity3d.player.UnityPlayerActivity;

import android.os.Bundle;
import android.util.Log;

public class OverrideExample extends UnityPlayerActivity {

  protected void onCreate(Bundle savedInstanceState) {

    // call UnityPlayerActivity.onCreate()
    super.onCreate(savedInstanceState);

    // print debug message to logcat
    Log.d("OverrideActivity", "onCreate called!");
  }

  public void onBackPressed()
  {
    // instead of calling UnityPlayerActivity.onBackPressed() we just ignore the back button event
    // super.onBackPressed();
  }
} 

并且添加相应的 AndroidManifest.xml,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.company.product">
  <application android:icon="@drawable/app_icon" android:label="@string/app_name">
	<activity android:name=".OverrideExample"
			  android:label="@string/app_name"
			  android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen">
        <intent-filter>
			<action android:name="android.intent.action.MAIN" />
			<category android:name="android.intent.category.LAUNCHER" />
		</intent-filter>
	</activity>
  </application>
</manifest> 

UnityPlayerNativeActivity

您也可以创建自己的 UnityPlayerNativeActivity 子类。这与 UnityPlayerActivity 子类化具有相同的效果,但改善了输入延迟。但还是要注意,NativeActivity 在 Gingerbread 引入,但在老设备并不能运行。因为 touch/motion 事件在本地代码中处理,Java 视图通常不会看到这些事件。然而,Unity 有一个的转发机制,允许事件传递到 DalvikVM。您必须设置清单文件以开启此功能,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.company.product">
  <application android:icon="@drawable/app_icon" android:label="@string/app_name">
	<activity android:name=".OverrideExampleNative"
			  android:label="@string/app_name"
			  android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen">
  <meta-data android:name="android.app.lib_name" android:value="unity" />
  <meta-data android:name="unityplayer.ForwardNativeEventsToDalvik" android:value="true" />
        <intent-filter>
			<action android:name="android.intent.action.MAIN" />
			<category android:name="android.intent.category.LAUNCHER" />
		</intent-filter>
	</activity>
  </application>
</manifest> 

注意,".OverrideExampleNative" 属于活动元素以及两种其他的元数据元素。第一个元数据是使用 Unity 库 libunity.so。第二个元数据允许事件传递至 UnityPlayerNativeActivity 的自定义子类。

示例

本地插件示例

您可以点击此处,查看使用本地代码插件的简单示例

此示例演示如何从 Unity Android 应用程序调用 C 代码。 这个包包括一个显示通过本地插件计算两个值总和的场景。 请注意,您将需要 Android NDK 编译该插件。

Java 插件示例

您可以点击此处,查看使用 Java 代码的示例

此示例演示 Java 代码如何用来与 Android 系统交互,以及 C++ 如何创建 C# 和 Java 之间的桥接。包中的场景显示一个按钮,按照 Android OS 定义,点击此按钮将可以读取应用程序的缓存目录。请注意,您将需要 JDK和 Android NDK 编译此插件。

此处是一个类似的示例,但基于预构建的 JNI 库来打包本地代码到 C#。

Page last updated: 2013-06-02