Unity 项目中一些需要访问安卓操作系统的功能,比如获取电量,wifi 状态等,需要 Unity 启动安卓系统的 BroadcastReceiver
监听状态,并在状态更新后通知到 Unity 界面。这就需要一种 Unity 与 Android 互相调用的机制,直观地看就是 C# 与 Java 互相调用的方法。
有 Unity 与 Android 互相调用需求的项目需要在两个开发环境中同时进行,创建两个工程,这时就涉及到如何将两个工程连接起来,有两种方式来连接:
对比一下两者的优缺点:
Unity 使用 jar/aar 库 | Unity 导出 gradle 项目 | |
---|---|---|
Unity 与 Android 依赖性 | Unity 只依赖 Android 库文件,分割清晰,需要同步的文件只有库文件 | Android 依赖 Unity 导出的场景数据,需要同步的文件太多 |
开发调试速度 | Android 库文件比较小,调试较快 | Unity 工程较大,同步较慢,调试周期长 |
Build机制 | Unity 内置的 Android Build 机制,类似于 eclipse 编译 Android 项目 | Android Studio gradle |
Build灵活性 | 较差,无法深度定制,库有依赖时需要将全部依赖显式拷贝到 Unity 工程中 | 非常自由,可以使用最新的 Android Build 机制 |
如何打包apk | Unity Build 机制直接打包 | Android Studio 打包 |
本项目使用的是第一种方法,因为这个项目中 Unity 工程特别大,导出 Unity 工程的代价太大。但也遇到了库文件依赖问题,不过由于依赖项不是很多,可以手动解决。以下是解决思路:
dependencies
,可以在 “Gradle projects” 窗口中目标项目的 help 目录中找到,这个 task 会打印出树形结构的依赖关系。从这两个步骤可以看出,如果依赖层次比较少、数量比较少,还是可以接受的,但如果有大量深层的依赖就会变得特别麻烦。
Unity官方文档说明需要通过Plugin的方式调用Java代码,但实际上不需要引入任何Plugin就可以调用Java代码。只是一般情况下需要调用的都是封装好的库,这时才需要将 jar 或者 aar 放到 Unity 项目中,然后通过 C# 来访问其中的内容。
jar 或者 aar 文件可以放在Unity任意目录下,为了方便管理,都放在了 Assets/Plugins/Android
目录下。
C# 调用 Java 的底层原理是使用JNI调用,Unity已经提供了很方便的接口:
AndroidJavaObject
类封装 Java 对象,new 一个 AndroidJavaObject
对象相当于调用对应的 Java 对象的构造函数。借助 C# 可变参数列表,可以给 Java 对象的构造函数传递任意数量的参数。
// 第一个参数是 Java 类的完整包名,剩下的其他参数会传递给构造方法。
AndroidJavaObject jo = new AndroidJavaObject("java.lang.String", "some_string");
AndroidJavaObject
类的 Call 方法,有泛型与非泛型的两个版本。
// 泛型版本,目的是指定返回值的类型
int hash = jo.Call("hashCode");
// 非泛型版本,处理返回值是void的情况。
jo.Call("aMethodReturnVoid"); // String中没有返回void的简单方法。。。
// 传入类的完整包名
AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
AndroidJavaObject jo = jc.GetStatic("currentActivity");
设置字段、获取对象字段、调用静态方法的代码类似,略。
调用 Java 方法时,直接将 C# 变量/常量 传递给 Java 方法,会自动处理 C# 类型到 Java 类型的转换。通过 C# 泛型,可以指定 Java 方法的返回值类型,也就是将 Java 类型转换为了 C# 类型。C# 类型与 Java 类型是可以自动转换的,规则如下:
Java 类型 | C# 类型 |
---|---|
基本类型,比如 int , boolean |
对应的值类型 int , bool |
String |
string |
数组类型 | 数组类型 (不能是多维数组) |
其他继承自 Object 的类型 |
AndroidJavaObject |
从 Android 端并不能直接调用 Unity 脚本,而是通过消息发送或者接口回调的方式。
消息发送是一个非常简单的调用机制,建立在一个发消息的接口之上:
// objectName: Unity 对象的名称
// methodName: Unity 对象绑定的脚本方法名
// message: 自定义消息
UnityPlayer.UnitySendMessage(String objectName, String methodName, String message);
做一下简单的封装:
import com.unity3d.player.UnityPlayer;
public class UnityPlayerCallback {
public final String objectName;
public final String methodName;
public UnityPlayerCallback(String objectName, String methodName) {
this.objectName = objectName;
this.methodName = methodName;
}
public void invoke(String message) {
UnityPlayer.UnitySendMessage(objectName, methodName, message);
}
}
发送消息需要知道 Unity 对象的名称和方法名,而这些信息在 Android 端是不知道的,也不应该写死在 Java 代码里。因为 Unity 脚本相对于 Android 代码是上层客户代码,调用的是 Android 库文件提供的功能,库文件是不应该知道使用它的客户代码的任何具体信息的。
正确的做法是通过某种方式将这些信息注入到库中,最简单地,使用 C# 调用 Java 端的代码将这两个字符串保存到 Java 对象中。
下面的示例规定了一个简单的消息格式:消息=类型/数据。
// Java 代码
public class Downloader {
private UnityPlayerCallback mUnityCallback;
public void setDownloadCallback(String objectName, String methodName) {
mUnityCallback = new UnityPlayerCallback(objectName, methodName);
}
...
void onDownloaded(File file, String url) {
if (mUnityCallback != null) {
mUnityCallback.invoke("downloaded/" + file.getName());
}
}
}
// C# 脚本:
void OnStart()
{
AndroidJavaObject downloader = new AndroidJavaObject("my.package.Downloader");
downloader.Call("setDownloadCallback", gameObject.name, "OnJavaMessage");
}
void OnJavaMessage(string message)
{
// 这里解析 message,例:"download/filename.txt"
if (message.StartsWith("downloaded/")
{
// 处理下载完成的逻辑...
}
}
由于这种方式比较粗糙,而且绕不开消息处理方法,如果有多个回调方法、传递的数据比较复杂,就需要定义复杂的消息传递格式。
这种方法使用起来比较自然,按照 Java 的风格定义好 Java 的回调接口,然后在 C# 脚本中通过继承 AndroidJavaProxy
类来实现这个 Java 的接口。通过 Java 侧提供的回调设置方法将实现了接口的 C# 对象设置给 Java 代码,就完成了 Java 设置 C# 回调的过程。
下面举例说明这个方法:
Java 代码定义一个下载工具类,使用一个下载进度和状态接口通知调用者:
// 回调接口
public interface DownloadListener {
void onProgress(String name, int progress);
void onDownloaded(String name);
void onError(String name, String message);
}
// 下载工具类
public class DownloadHelper {
public void download(String url, String name) {...}
public void setDownloadListener(DownloadListener listener) {...}
}
C# 代码同样定义一个同名的 DownloadHelper
类,用来封装对 Java 对象的调用:
public class DownloadHelper {
// 定义 C# 端的接口,对外隐藏 Java 相关代码
public interface IDownloadListener {
void OnProgress(string name, int progress);
void OnDownloaded(string name);
void OnError(string name, string message);
}
// 定义个 Adapter 来适配 AndroidJavaProxy 对象和 IDownloadListener
private class ListenerAdapter : AndroidJavaProxy {
private readonly IDownloadListener listener;
public ListenerAdapter(IDownloadListener listener) : base("my.package.DownloadListener") {
this.listener = listener;
}
// 继承自 AndroidJavaProxy 的对象可以直接按照 Java 中的方法签名
// 写出对应的 C# 方法,参数类型遵循上文提到的数据类型转换规则。
// 当 Java 调用接口方法时,对应的 C# 方法会自动调用,非常方便。
void onProgress(string name, int progress) {
listener.OnProgress(name, progress);
}
void onDownloaded(string name) {
listener.OnDownloaded(name);
}
void onError(string name, string message) {
listener.OnError(name, message);
}
}
private readonly AndroidJavaObject javaObject;
private ListenerAdapter listenerAdapter;
public DownloadHelper() {
javaObject = new AndroidJavaObject("my.package.DownloadHelper", DefaultDirectory);
}
public void SetDownloadListener(IDownloadListener listener) {
if (listener != null) {
listenerAdapter = new ListenerAdapter(listener);
javaObject.Call("setDownloadListener", listenerAdapter);
} else {
listenerAdapter = null;
javaObject.Call("setDownloadListener", null);
}
}
public void Download(string url, string name) {
javaObject.Call("download", url, name);
}
// 初始化下载目录
private static string DefaultDirectory;
static DownloadHelper() {
AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
AndroidJavaObject jo = jc.GetStatic("currentActivity");
string path = jo.Call("getExternalFilesDir", "videos").Call("getCanonicalPath");
DefaultDirectory = path;
}
}
使用的时候,直接使用 C# DownloadHelper
类配合 DownloadHelper.IDownloadListener
即可。
后记:
第二种实现的方式交给写 Unity 脚本的同事后发现一个问题:由于下载模块的回调是在安卓UI线程执行的,这个线程并不是 Unity 的主线程,回调到 Unity 环境中不能执行各种对象的操作。因此需要通知 Unity 主线程并在其中执行回调。
C# 代码修改如下,使用了 Loom
类,有点类似于安卓的 Handler
,可以参考这篇文章 Unity Loom 插件使用
private class ListenerAdapter : AndroidJavaProxy {
...
void onProgress(string name, int progress) {
Loom.QueueOnMainThread((param) => {
listener.OnProgress(name, progress);
}, null);
}
void onDownloaded(string name) {
Loom.QueueOnMainThread((param) => {
listener.OnDownloaded(name);
}, null);
}
void onError(string name, string message) {
Loom.QueueOnMainThread((param) => {
listener.OnError(name, message);
}, null);
}
}
虽然可以在安卓层使用 BroadcastReceiver
接收广播,并通过自定义的方法传递给 C# 层。但如果能在 C# 端直接接收就更方便了,于是后来又写了一个通用的广播接收层。
先来看一下如何使用这个广播接收层,设计这个层的主要目的有两个:一是能直接在 C# 代码中注册安卓广播,另一个是使用的代码要足够简单。
先上使用的代码:
public class BTHolder : MonoBehaviour, UnityBroadcastHelper.IBroadcastListener {
UnityBroadcastHelper helper;
void Start() {
if (helper == null) {
helper = UnityBroadcastHelper.Register(
new string[] { "some_action_string" }, this);
}
}
void OnReceive(string action, Dictionary dictionary) {
// handle the broadcast
}
}
可以看到使用广播需要4个步骤:
UnityBroadcastHelper.IBroadcastListener
接口。UnityBroadcastHelper
对象并初始化。void OnReceive(string action, Dictionary dictionary)
中自定义广播处理代码。可以看出与 Java 代码中自定义 BroadcastReceiver
几乎是相同的步骤,下面分析一下原理。
UnityBroadcastHelper
来持有 BroadcastReceiver
,再通过 Java 代码注册到 Context 中。UnityBroadcastHelper.BroadcastListener
映射为 C# 中的 UnityBroadcastHelper.IBroadcastListener
。这样在 Java 端接收到广播时调用 C# 端的接口,就可以通知 C# 广播已经接收到。OnReceive
方法,方便 C# 使用。这里为了简单把所有类型的数据都映射为了 string 类型,这个映射比较繁琐,有需要可以再写详细一些。Java 代码:
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import com.unity3d.player.UnityPlayer;
import java.util.LinkedList;
import java.util.Queue;
public class UnityBroadcastHelper {
private static final String TAG = "UnityBroadcastHelper";
public interface BroadcastListener {
void onReceive(String action);
}
private final BroadcastListener listener;
private Queue keysQueue = new LinkedList<>();
private Queue valuesQueue = new LinkedList<>();
public UnityBroadcastHelper(String[] actions, BroadcastListener listener) {
MyLog.d(TAG, "UnityBroadcastHelper: actions: " + actions);
MyLog.d(TAG, "UnityBroadcastHelper: listener: " + listener);
this.listener = listener;
IntentFilter intentFilter = new IntentFilter();
for (String action : actions) {
intentFilter.addAction(action);
}
Context context = UnityPlayer.currentActivity;
if (context == null) {
return;
}
context.registerReceiver(broadcastReceiver, intentFilter);
}
public boolean hasKeyValue() {
return !keysQueue.isEmpty();
}
public String[] getKeys() {
return keysQueue.peek();
}
public String[] getValues() {
return valuesQueue.peek();
}
public void pop() {
keysQueue.poll();
valuesQueue.poll();
}
public void stop() {
Context context = UnityPlayer.currentActivity;
if (context == null) {
return;
}
context.unregisterReceiver(broadcastReceiver);
}
private BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
MyLog.d(TAG, "UnityBroadcastHelper: action: " + action);
Bundle bundle = intent.getExtras();
if (bundle == null) {
bundle = new Bundle();
}
int n = bundle.size();
String[] keys = new String[n];
String[] values = new String[n];
int i = 0;
for (String key : bundle.keySet()) {
keys[i] = key;
Object value = bundle.get(key);
values[i] = value != null ? value.toString() : null;
MyLog.d(TAG, "UnityBroadcastHelper: key[" + i + "]: " + key);
MyLog.d(TAG, "UnityBroadcastHelper: value[" + i + "]: " + value);
i++;
}
keysQueue.offer(keys);
valuesQueue.offer(values);
listener.onReceive(action);
}
};
}
C# 代码:
using System.Collections.Generic;
using UnityEngine;
public class UnityBroadcastHelper {
public interface IBroadcastListener {
void OnReceive(string action, Dictionary dictionary);
}
private class ListenerAdapter : AndroidJavaProxy {
readonly IBroadcastListener listener;
readonly UnityBroadcastHelper helper;
public ListenerAdapter(IBroadcastListener listener, UnityBroadcastHelper helper) : base("UnityBroadcastHelper$BroadcastListener") {
this.listener = listener;
this.helper = helper;
}
void onReceive(string action) {
AndroidJavaObject javaObject = helper.javaObject;
if (!javaObject.Call("hasKeyValue")) {
return;
}
string[] keys = javaObject.Call("getKeys");
string[] values = javaObject.Call("getValues");
javaObject.Call("pop");
Dictionary dictionary = new Dictionary();
Debug.Log("onReceive: dictionary: " + dictionary);
int n = keys.Length;
for (int i = 0; i < n; i++) {
dictionary[keys[i]] = values[i];
}
listener.OnReceive(action, dictionary);
}
}
private readonly AndroidJavaObject javaObject;
private UnityBroadcastHelper(string[] actions, IBroadcastListener listener) {
ListenerAdapter adapter = new ListenerAdapter(listener, this);
javaObject = new AndroidJavaObject("UnityBroadcastHelper", actions, adapter);
}
public static UnityBroadcastHelper Register(string[] actions, IBroadcastListener listener) {
return new UnityBroadcastHelper(actions, listener);
}
public void Stop() {
javaObject.Call("stop");
}
}