UVP 价值专家 | Unity与Android交互通信(上)

文摘   游戏   2024-07-05 21:00   上海  
这篇文章来自 2024 年度 Unity 价值专家提名人选 DavidWang。汪老师是 CSDN 博客专家,专注图形学 / XR 开发,著有多本基于 Unity 进行 AR/MR 开发的专业技术书籍,申请发明专利十余项,软件著作权若干。
本文带来汪老师的系列博客《Unity 与 Android 交互通信》前 2 篇,介绍 Unity 和 Android 交互通信原理及在 Unity 中直接调用 Java 代码的方式,并给出代码示例。点击阅读原文,可以访问汪老师的 CSDN 个人主页,阅读更多技术干货。

运行于 Android 平台的原生 App 直接调用 Android 接口,可以享受近水楼台先得月的优势,而使用 Unity 开发的 Android 应用 App 则像是二等公民,使用 Android 原生功能特性就要麻烦得多,比如 WiFi、蓝牙等,特别是一些高级功能特性,Unity 中没有完全覆盖,直接在 Unity 中开发显得力不从心。而且,Unity 为适应跨平台开发部署需求,其引擎架构设计要复杂灵活得多,基于 Unity 引擎开发的 App 应用运行于独立的 VM(Virtual Machine,虚拟机)中(采用 IL2CPP 后端编译的应用,运行时仍然需要虚拟机支持),这给 App 应用与 Android 原生系统代码的交互带来了困难。

在实际应用开发中,基于 Unity 的 App 应用与底层的 Android 平台之间经常有交互需求,本系列我们主要学习 Unity 引擎与 Android 平台的交互通信。

Android 与 Unity 通信原理
Unity 引擎最大的优势和特点是一次制作、多端部署,极大的减轻了多平台游戏的开发和维护成本,而 Unity 引擎实现强大跨平台能力的基础是 Mono / IL2CPP,Mono / IL2CPP 是 Unity 引擎跨平台的核心和根本。
在 2001 年,电信标准组织 ECMA 制定了一个与特定语言无关的跨体系结构的运行环境 CLI(Common Language Infrastructure,公共语言基础)标准规范,只要使用规范定义的高级语言进行开发、应用程序符合 CLI 规范即可以确保在不同的计算机体系结构上实现跨平台运行。在此基础上,微软公司根据标准实现了 .NET Framework 公共语言运行时(Common Language Runtime,CLR),因此 CLR 是 CLI 的一个实现,.NET Framework 即是一个运行于 CLR 基础上的框架,它支持 C#、VB.NET、C++、Python 等语言,但由于 .NET Framework 与 Windows 的深厚渊源,.NET Framework 本身并不能跨平台。
Mono 则是 Xamarin 公司主导的另一个 CLI 实现,它通过内置 C# 语言编译器、CLR 运行时和各类基础类库,可以使应用程序运行在 Windows、Linux、FreeBSD、Android、iOS 等各种平台上,因此,通过 Mono 能使用 C# 语言编写 Android 或 iOS 应用程序。在 CLI 规范中,高级语言并不是直接被编译成机器字节码,而是编译成中间语言(Intermediate Language,IL),这是一种介于高级言与机器字节码之间的与特定底层硬件无关的语言,在真正需要执行的时候,IL 会被加载到 Mono VM 中 [ Unity 支持 C#、Unity Script、Boo 三种脚本开发语言,所有高级语言都会被编译成 IL 中间语言。],由 VM 动态的编译成机器码再执行(Just In Time,JIT 编译),其执行过程如图 (1a) 所示。
图1  Mono 编译运行代码与 IL2CPP 编译运行代码流程示意图
通过 Mono,Unity 引擎能够将应用部署到各类不同的系统平台上,实现跨平台运行。但由于 IL 在 Mono VM 中运行,而 Mono VM 是与系统平台紧密相关的,无法实现跨平台,所以 Unity 需要维护各种平台数量众多的 Mono VM,并且 JIT 编译方式也降低了应用程序的运行速度,因此,Unity 引擎引入了 IL2CPP 后端编译方式,通过将 IL 重新编译成 C++ 代码,再由各类平台的 C++ 编译器直接编译成原生汇编代码(Ahead Of Time,AOT 编译),从而提升代码执行效率,并可利用各平台的 C++ 编译器执行编译期间的优化,但由于高级动态语言的特性,内存管理和回收仍然需要统一维护,即需要 IL2CPP VM 负责 GC、线程等服务性工作,但此时 IL2CPP VM 不再负责 IL 加载和动态解析,维护 VM 的工作量大为降低。
虽然 IL2CPP 最后是采用 AOT 方式直接编译成各平台原生机器码,但其继承了 IL 中间语言的机制,对上层语言几乎没有影响 (因为 C++ 是静态语言,采用 AOT 方式编译,JIT 动态语言的一些特性将不再可用),但它也需要借助 VM 进行内存管理等工作,如图 1(b) 所示。Android 原生应用采用 Java 语言编写,Java 语言也是先编译为 Bytecode IL 中间语言,然后依赖 Android 上的 dalvik / art 虚拟机解释执行。所以 Unity 引擎与 Android 原生代码交互通信实际是两个 VM 之间的通信,如图 2 所示。虽然 Unity 代码与 Android 原生代码在不同的 VM 中执行,但它们都处于同一个进程中,因此可以共享数据。

图 2 Unity 与 Android 交互通信示意图

在图 2 中,Unity 引擎通过 UnityEngine 提供的 API 调用 Android 方法,Android 则借助于 com.unity3d.player 包提供的 API 调用 Unity 方法。通过这种方式,Unity 引擎可以直接调用 Android 类及对象的方法,而 Android 则只能调用 Unity 中指定 GameObject 所挂载的脚本的方法,或者通过动态代理的方式调用 Unity 引擎中的方法。

在执行层面,UnityEngine 封装了 AndroidJavaObject、AndroidJavaClass、AndroidJavaProxy 类,通过这几个类就可以获取 Android 端静态类或者动态对象,从而可以执行其相应方法;Android 则是通过 Unity 应用的 mainActivity 与 C# 代码通信 [ 本节讲述的 Unity 与 Android 的交互通信实质上是指 C# 代码与 Java 代码、JAR 包、AAR 包、SO 包的相互调用,但遵循习惯描述为 Unity 引擎与 Android 操作系统软件之间的通信。]。

Activity 是 Android 应用中最基本和最重要的组件之一,其作用类似于 web 中的 page、winform 中的 form,因此,只有活跃的 Activity 才能响应输入,所以,Android 代码首先要通过 com.unity3d.player.UnityPlayer 包下的 currentActivity 获取到活跃 Activity,利用其与 Unity 代码通信。
Unity 直接调用 Java 代码

Unity 2018 之后的版本统一使用 Gradle 进行 Android 端的编译、构建和打包,而 Android Studio 也使用 Gradle 进行编译、构建和打包,即它们都使用同一种编译构建工具,也即是 Java 代码与 C# 代码都可以在 Unity 中被正确的编译到 Android 端,这就为在 Unity 中直接使用 Java 与 C# 语言打下了基础。

而且为方便映射 Java 数据结构,在 UnityEngine 类中还内置了若干封装好的类,其中最重要的类有:AndroidJavaClass、AndroidJavaObject、AndroidJavaProxy,这些类是进行 Java 端与 C# 端相互调用的基础。AndroidJavaClass 是 java.lang.Class 类在 Unity 中的表达,主要用于类结构反射、获取类静态属性或者调用类静态方法,其公共方法如表 1 所示。

AndroidJavaObject 是 java.lang.Object 类在 Unity 中的表达,而 java.lang.Object 类是 Java 中所有类的基类,因此其能接受所有的 Java 对象,其公共方法如表 2 所示。

通过表 1 和表 2 可以看到,这两个类公共方法完全一样,这就为开发者使用这两个类提供了完全一致的使用外观。在使用中,通过 AndroidJavaClass 类的 forName() 方法、.class 属性生成相应类的对象,由于 Class 类方法常用于反射,所以一般用于调用对应类的静态属性或者方法;AndroidJavaObject 表示对象,通过其 getClass() 方法可以获取该对象的类型,所以一般用于调用对象的实例方法或者属性。例如有一个类 com.davidwang.util,其有一个静态方法 StaticMethod(),一个实例方法 InstanceMethod(),则 new AndroidJavaClass("com.davidwang.util").callstatic("StaticMethod") 等同于调用 util.StaticMethod(); 而 new AndroidJavaObject("com.davidwang.util").call("InstanceMethod") 等同于 new Util().InstanceMethod()。

代码示例
下面通过实际例子演示上文中 AndroidJavaClass、AndroidJavaObject 两个类的基本用法,由于交互通信涉及到两端,我们先使用 Android Studio 创建 Unity2Java 类,Java 代码如下:
//代码片断1package com.example.davidwang;
public class Unity2Java { public static void StaticPrint(String str){ System.out.println(str); } public static int StaticAdd(int a,int b) { return a+b; }
public void DynamicPrint(String str){ System.out.println(str); } public int DynamicAdd(int a,int b) { return a+b; }}

找到该类所在的 .java 文件,并将该文件复制到 Unity 工程 Assets/Plugins/Android 目录或其子目录下,然后在 Unity 工程窗口(Project 窗口)中选中该 java 文件,在属性窗口(Inspector 窗口)中查看其导入设置,确保 Android 平台被选择,如图 3 所示。

图 3 在 Java 文件导入设置中勾选 Android 多选框
为在 Unity 端调用 Java 代码,通过 Unity 创建 Android2Unity.cs 脚本文件,代码如下:
//代码片断2using System.Collections;using System.Collections.Generic;using UnityEngine;public class Android2Unity : MonoBehaviour{    void Start()    {        using (AndroidJavaClass Unity2JavaClass = new AndroidJavaClass("com.example.davidwang.Unity2Java"))        {            Unity2JavaClass.CallStatic("StaticPrint", "Hello World from Android static method ");            int result1 = Unity2JavaClass.CallStatic<int>("StaticAdd", 1, 1);            Debug.Log("结果1:" + result1);
Unity2JavaClass.Call("DynamicPrint", "Hello World from Android dynamic method "); int result2 = Unity2JavaClass.Call<int>("DynamicAdd", 1, 2); Debug.Log("结果2:" + result2); }
using (AndroidJavaObject Unity2JavaObject = new AndroidJavaObject("com.example.davidwang.Unity2Java")) { Unity2JavaObject.CallStatic("StaticPrint", "Hello World from Android static method "); int result3 = Unity2JavaObject.CallStatic<int>("StaticAdd", 1, 3); Debug.Log("结果3:" + result3);
Unity2JavaObject.Call("DynamicPrint", "Hello World from Android dynamic method "); int result4 = Unity2JavaObject.Call<int>("DynamicAdd", 1, 4); Debug.Log("结果4:" + result4); } }}

在 Unity 中,将 Android2Unity 脚本挂载到场景中的任意对象上,连接手机,打包运行[ Java 代码编译后运行于 dalvik / art 虚拟机,其控制台输出不能输出到 Unity Debug 窗口,所以只能通过真机运行,Logcat 查看输出。],输出结果如下:

行号  类型       输出信息     1   System.out Hello World from Android static method 2   Unity       结果123   Unity  4   Unity       结果205   System.out Hello World from Android static method 6   Unity       结果347   System.out Hello World from Android dynamic method 8   Unity       结果45

上述代码首先演示了 Java 端无返回值、有返回值方法的调用,通过泛型方法定义返回值类型获取 Java 端方法执行结果,由于数据类型的不同,Java 端与 C# 端交互支持的类型有 string、int、float、bool、AndroidJavaObject 共 5 类(也可以返回这些数据类型的数组);

其次演示了静态方法和实例方法的调用,类静态方法使用带 static 后辍的方法访问,而对象实例方法则使用不带 Static 后辍的方法调用;再次演示了 AndroidJavaClass 和 AndroidJavaObject 类使用上的区别,通过输出结果,可以看到,AndroidJavaClass 调用对象实例方法既不报错,也不执行;AndroidJavaObject 类调用类静态方法可以正常执行 [ Java 语言支持实例对象调用类静态方法或者获取类静态属性,这与 C# 语言不同。],虽然我们使用时都使用了 new 关键字,但 AndroidJavaClass 类不会生成实例对象,而 AndroidJavaObject 类会实例化对象。

类与实例的属性获取/设置与上述方法使用基本一致,通过这种方式,就可以直接在 C# 代码中调用 Java 端的原生类,代码如下 [ 为了简化排版,后续 Java 代码与 C# 代码将放置于同一个代码片断中,并使用注释进行说明。]:

//代码片断3//Java端代码package com.example.davidwang;import android.app.Activity;import android.widget.Toast;
public class Unity2Java { public boolean ShowToast(Activity activity, String str){ Toast.makeText(activity,str,Toast.LENGTH_SHORT).show(); return true; }}
//C#端代码//获取设备UUIDprivate string GetAndroidID(){ string androidID = "NONE";#if UNITY_ANDROID && !UNITY_EDITOR using (AndroidJavaObject contentResolver = new AndroidJavaClass("com.unity3d.player.UnityPlayer").GetStatic<AndroidJavaObject>("currentActivity").Call<AndroidJavaObject>("getContentResolver")) { using (AndroidJavaClass secure = new AndroidJavaClass("android.provider.Settings$Secure")) { androidID = secure.CallStatic<string>("getString", contentResolver, "android_id"); } }#endif return androidID;}
//调用Andriod端Toastprivate void ShowToast(){ if (Application.platform == RuntimePlatform.Android) using (AndroidJavaObject Unity2JavaObject = new AndroidJavaObject("com.example.davidwang.Unity2Java")) { using (AndroidJavaObject activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer").GetStatic<AndroidJavaObject>("currentActivity")) { bool isSuccess = Unity2JavaObject.Call<bool>("ShowToast", activity, "From Unity"); Debug.Log("ShowToast Status :" + isSuccess); } }}

在上述代码片断 3 中,我们演示了两种直接调用 Android 端原生类的方式,第一种方法是通过直接获取 Android 端的原生类,调用其静态方法;第二种是通过调用自定义的 Java 类间接调用 Android 原生类方法。同时,由于 C# 代码运行平台不确定,为确保代码兼容多平台,我们也使用了两种判断代码执行平台的方法,第一种使用预编译指令区分平台,另一种通过 Application 类直接判断当前运行平台,这也是在多平台开发中经常使用的技巧。

除此之外,代码还演示了获取当前活动 Activity 的方法,即通过 com.unity3d.player.UnityPlayer 类获取当前 Activity,Android 很多类都需要传递活动的 Activity 或者 Context 上下文对象,通过这种方式获取当前 Activity 是一种常用方法。

使用 AndroidJavaClass 和 AndroidJavaObject 类直接调用 Java 代码或 Android 原生类非常方便,但只能是单向由 C# 调用 Java 代码,Java 没办法反向调用 C# 代码,实现反向调用则必须使用 AndroidJavaProxy 类,正如其名,这是个代理类,负责在 Java 和 C# 代码之间桥接,后文我们还会详细介绍该类。

如前文所述,Java 端与 C# 端交互支持的类型有 string、int、float、bool、AndroidJavaObject 共 5 类,AndroidJavaObject 可以表示所有对象类型,因此,除 string、int、float、bool 4 种基本类型,其余对象都可由 AndroidJavaObject 表达。C# 端调用 Java 端方法千差万别,但 Java 端调用 C# 端就以上 5 类(包括数组则共 10 类),因此我们可以编写一个通用的框架,因为结构稍微有点复杂,涉及到 C# 端 AndroidCallbackManager.cs、AndoridCallbackInterface.cs 两个脚本文件,Java 端 CallUnityInterface.java、Java2Unity.java 两个代码文件。

其中,AndroidCallbackManager.cs 文件代码如下:
//代码片断4  using System.Collections;using System.Collections.Generic;using UnityEngine;public class AndroidCallbackManager : MonoBehaviour{    void Start()    {        using (AndroidJavaObject activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer").GetStatic<AndroidJavaObject>("currentActivity"))        {            using (AndroidJavaObject appController = new AndroidJavaObject("com.example.davidwang.Java2Unity"))            {                AndoridCallbackInterface callback = new AndoridCallbackInterface("com.example.davidwang.CallUnityInterface");                callback.stringCallBack = StringProcess;                callback.intCallBack = IntProcess;                callback.floatArrayCallBack = FloatArrayProcess;                appController.Call("Init", activity, callback);                Debug.Log("In Start");            }        }    }
public void StringProcess(string str) { Debug.Log("string callback :"+str); } public void IntProcess(int value) { Debug.Log("int callback :" + value); } public void FloatArrayProcess(float[] arr) { foreach (var value in arr) Debug.Log("float in arr:" + value); }}

在代码片断 4 中,AndroidCallbackManager 类是 Unity 端的使用类,是调用入口,其首先获取到当前 Activity,实例化 Java 端的 Java2Unity 类,并且同时实例化了一个 C# 端的 AndoridCallbackInterface 类。然后设置 AndoridCallbackInterface 类的回调方法之后调用了 Java 端的初始化方法。

AndoridCallbackInterface.cs 文件代码如下:

//代码片断5 using System;using UnityEngine;public class AndoridCallbackInterface : AndroidJavaProxy{    public Action<AndroidJavaObject> javaObjectCallBack;    public Action<bool> boolCallBack;    public Action<string> stringCallBack;     public Action<int> intCallBack;    public Action<float> floatCallBack;    public Action<float[]> floatArrayCallBack;       //构造方法    public AndoridCallbackInterface(string interfaceName) : base(interfaceName)    {    }    public void JavaObjectCallBack(AndroidJavaObject _data)    {        if (javaObjectCallBack != null)            javaObjectCallBack(_data);    }    public void BoolCallBack(bool _data)    {        if (boolCallBack != null)            boolCallBack(_data);    }    public void StringCallBack(string _data)    {        if (stringCallBack != null)            stringCallBack(_data);    }    public void IntCallBack(int _data)    {        if (intCallBack != null)            intCallBack(_data);    }    public void FloatCallBack(float _data)    {        if (floatCallBack != null)            floatCallBack(_data);    }    public void FloatArrayCallBack(float[] _data)    {        if (floatArrayCallBack != null)            floatArrayCallBack(_data);    }}
在代码片断 5 中,AndoridCallbackInterface 类继承自 AndroidJavaProxy 类,定义了回调方法,如前文所述,Java 端与 C# 端交互支持的类型共有 5 种,这里为了通用将这 5 种回调方法都进行了演示(还包括一个 Float 数组的演示方法)。该类构造方法很重要,这里需要通过 AndroidJavaProxy 类将 C# 端实例与 Java 端的 CallUnityInterface 接口对应起来 [ 可以理解为建立了一个从 Java 端到 C# 端 AndoridCallbackInterface 类实例的指针,这样 Java 端就可以通过这个指针访问到 C# 端的实例。],这样 Java 端就可以调用 C# 端实例的方法。
CallUnityInterface.java 文件代码如下:
//代码片断6  package com.example.davidwang;
public interface CallUnityInterface { public void JavaObjectCallBack(Object _data); public void BoolCallBack(boolean _data); public void StringCallBack(String _data); public void IntCallBack(int _data); public void FloatCallBack(float _data); public void FloatArrayCallBack(float[] _data);}
在代码片断 6 中,CallUnityInterface 接口方法签名与 AndoridCallbackInterface 类中对应方法签名需要完全一致,这样才能确保正确相互调用。
CallUnityInterface.java 文件代码如下: 
//代码片断7 package com.example.davidwang;
import android.content.Context;public class Java2Unity { private Context context = null; //上下文对象 private CallUnityInterface callback = null; //缓存回调 public void Init(Context context , CallUnityInterface callback){ this.context = context; this.callback = callback; try { java.lang.Thread.sleep(5000); Run(); } catch (InterruptedException e) { e.printStackTrace(); } } private void Run(){ int i = 100; String str = "From Java"; float[] floatArr = {1.01f,1.02f,1.03f}; this.callback.IntCallBack(i); this.callback.StringCallBack(str); this.callback.FloatArrayCallBack(floatArr); }}
在代码片断 7 中,Java2Unity 类首先通过 Init() 方法获取到当前上下文对象 [ 在本示意中,上下文对象并没有使用,但很多时间都需要上下文对象或者当前 Activity。]和回调实例,通过回调接口即可以调用 C# 端的方法,这里通过延时 5 秒触发回调方法。
在这个通用框架中,这 4 个类的相互关系如图 4 所示。

图 4 Java 端与 C# 端相互调用关系示意图

在 Unity 中,将 AndroidCallbackManager 脚本挂载到场景中的任意对象上,连接手机,打包运行 [ Java 代码编译后运行于 dalvik / art 虚拟机,其控制台输出不能输出到 Unity Debug 窗口,所以只能通过真机运行,Logcat 查看输出。],输出结果如下:

行号 类型       输出信息     1   Unity     int callback :1002   Unity     string callback :From Java3   Unity     float in arr:1.014   Unity     float in arr:1.025   Unity     float in arr:1.036   Unity     In Start
通过结果看到,Java 端正确的向 C# 端回调了相应方法并且参数传递无误。而且也可以看到,这种调用是同步的,会阻塞当前线程。
提示
在开发中,通常我们都是在 Android Studio 中进行 Java 代码编写,然后将代码复制到 Assets/Plugins/Android 目录中供 Unity 使用,如果改动了 Android Studio 中的 Java 代码,则需要再次覆盖 Unity 中的对应文件,而且同时存在两份一样的文件,文件内容同步维护会是非常大的问题,这时我们可以通过创建文件/文件夹链接确保只有一份文件。文件夹链接类似于 Windows 操作系统中的快捷方式,但可供第三方软件使用。
Windows 下创建方式:mklink /D LINK_PATH SOURCE_PATH
Linux 下创建方式:ln -s SOURCE_PATH LINK_PATH
其中参数 LINK_PATH 为链接的路径,这个路径无真实文件夹或者文件;SOURCE_PATH 为源文件/文件夹路径,即真实的文件路径。通过创建文件/文件夹链接,就可以只在 Assets/Plugins/Android 目录下保留一份代码文件,Android Studio 通过链接连接到相应的文件或文件夹,从而可以保持文件同步,无需进行复制代码文件的操作。
DavidWang 是 2024 年度 Unity 价值专家提名人选。Unity 价值专家(UVP)是通过原创作品启发国内创作者的 Unity 专业人员,点击这里提名/自荐
点击阅读原文,可以访问 DavidWang 在 CSDN 的技术专栏,持续学习更多内容。

长按关注
Unity 官方开发者服务平台
第一时间了解 Unity 社区动向,学习开发技巧

 点击“阅读原文”,访问 DavidWang 的主页 



Unity官方开发者服务平台
Unity引擎官方开发者服务平台,分享技术干货、学习课程、产品信息、前沿案例、活动资讯、直播信息等内容。
 最新文章