Unity 与 Android 互调用

https://www.jianshu.com/p/b5e3cfcdf081

 

Unity 项目中一些需要访问安卓操作系统的功能,比如获取电量,wifi 状态等,需要 Unity 启动安卓系统的 BroadcastReceiver 监听状态,并在状态更新后通知到 Unity 界面。这就需要一种 Unity 与 Android 互相调用的机制,直观地看就是 C# 与 Java 互相调用的方法。

有 Unity 与 Android 互相调用需求的项目需要在两个开发环境中同时进行,创建两个工程,这时就涉及到如何将两个工程连接起来,有两种方式来连接:

  • Android 工程生成 aar/jar 文件,复制到 Unity 工程中,最终使用 Unity 的 Build 机制生成 apk。
  • Unity 工程将所有内容和代码导出为一个 Android gradle 项目,然后使用 Android Studio 打开项目进行开发,最终使用 Android Studio 打包 apk。

对比一下两者的优缺点:

 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 工程的代价太大。但也遇到了库文件依赖问题,不过由于依赖项不是很多,可以手动解决。以下是解决思路:

  • 运行 gradle task dependencies,可以在 “Gradle projects” 窗口中目标项目的 help 目录中找到,这个 task 会打印出树形结构的依赖关系。
  • 将所有的依赖项单独下载到本地,放到 Unity 工程中。

从这两个步骤可以看出,如果依赖层次比较少、数量比较少,还是可以接受的,但如果有大量深层的依赖就会变得特别麻烦。

 
查看依赖树

Unity 调用 Android

Unity官方文档说明需要通过Plugin的方式调用Java代码,但实际上不需要引入任何Plugin就可以调用Java代码。只是一般情况下需要调用的都是封装好的库,这时才需要将 jar 或者 aar 放到 Unity 项目中,然后通过 C# 来访问其中的内容。

jar 或者 aar 文件可以放在Unity任意目录下,为了方便管理,都放在了 Assets/Plugins/Android 目录下。

C# 调用 Java 方法,获取 Java 字段

C# 调用 Java 的底层原理是使用JNI调用,Unity已经提供了很方便的接口:

  • 创建对象:C#中使用 AndroidJavaObject 类封装 Java 对象,new 一个 AndroidJavaObject 对象相当于调用对应的 Java 对象的构造函数。借助 C# 可变参数列表,可以给 Java 对象的构造函数传递任意数量的参数。
// 第一个参数是 Java 类的完整包名,剩下的其他参数会传递给构造方法。
AndroidJavaObject jo = new AndroidJavaObject("java.lang.String", "some_string"); 
  • 调用对象方法:使用 AndroidJavaObject 类的 Call 方法,有泛型与非泛型的两个版本。
// 泛型版本,目的是指定返回值的类型
int hash = jo.Call<int>("hashCode");
// 非泛型版本,处理返回值是void的情况。
jo.Call("aMethodReturnVoid"); // String中没有返回void的简单方法。。。
  • 获取类,主要用于获取静态字段或调用静态方法,常用来获取 UnityPlayer。
// 传入类的完整包名
AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
  • 获取静态字段,只有泛型版本,因为不会有void类型的字段。。。
AndroidJavaObject jo = jc.GetStatic<AndroidJavaObject>("currentActivity"); 

设置字段、获取对象字段、调用静态方法的代码类似,略。

类型映射

调用 Java 方法时,直接将 C# 变量/常量 传递给 Java 方法,会自动处理 C# 类型到 Java 类型的转换。通过 C# 泛型,可以指定 Java 方法的返回值类型,也就是将 Java 类型转换为了 C# 类型。C# 类型与 Java 类型是可以自动转换的,规则如下:

Java 类型C# 类型
基本类型,比如 int, boolean 对应的值类型 int, bool
String string
数组类型 数组类型 (不能是多维数组)
其他继承自 Object 的类型 AndroidJavaObject

Android 调用 Unity

从 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<AndroidJavaObject>("currentActivity");
        string path = jo.Call<AndroidJavaObject>("getExternalFilesDir", "videos").Call<string>("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<string, string> dictionary) {
        // handle the broadcast
    }
}

可以看到使用广播需要4个步骤:

  1. 实现 UnityBroadcastHelper.IBroadcastListener 接口。
  2. 定义一个 UnityBroadcastHelper 对象并初始化。
  3. 在方法 void OnReceive(string action, Dictionary<string, string> dictionary) 中自定义广播处理代码。
  4. 在合适的时机调用 helper.Stop() 停止监听广播。

可以看出与 Java 代码中自定义 BroadcastReceiver 几乎是相同的步骤,下面分析一下原理。

  1. 先使用一个 Java 对象 UnityBroadcastHelper 来持有 BroadcastReceiver,再通过 Java 代码注册到 Context 中。
  2. 再使用上文提到的接口方式将 UnityBroadcastHelper.BroadcastListener 映射为 C# 中的 UnityBroadcastHelper.IBroadcastListener。这样在 Java 端接收到广播时调用 C# 端的接口,就可以通知 C# 广播已经接收到。
  3. 最后使用数据获取接口将广播中的数据,也就是保存 Extra 的 Bundle,映射为 C# 中的 Dictionary,传递给 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<String[]> keysQueue = new LinkedList<>();
    private Queue<String[]> 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<string, string> 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<bool>("hasKeyValue")) {
                return;
            }
            string[] keys = javaObject.Call<string[]>("getKeys");
            string[] values = javaObject.Call<string[]>("getValues");
            javaObject.Call("pop");
            Dictionary<string, string> dictionary = new Dictionary<string, string>();
            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");
    }
}
posted @ 2019-07-18 12:14  alps_01  阅读(2008)  评论(0编辑  收藏  举报