运行于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编译),其执行过程如图1(a)图所示。
图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代码
Unity2018之后的版本统一使用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所示。
表1 AndroidJavaClass公共方法列表
公共方法 | 调用对象/属性类型 | 泛型方法 | 描述 |
---|---|---|---|
Call | 非静态 | Call | 调用对象非静态方法 |
CallStatic | 静态 | CallStatic | 调用类静态方法 |
Get | 非静态 | Get | 获取对象非静态属性 |
GetStatic | 静态 | GetStatic | 获取类静态属性 |
Set | 非静态 | Set | 设置对象的非静态属性 |
SetStatic | 静态 | SetStatic | 设置类静态属性 |
GetRawClass | - | GetRawClass | 获取原生Java类型的指针,用于JNI |
GetRawObject | - | GetRawObject | 获取原生Java对象的指针,用于JNI |
Dispose | - | - | IDisposable接口回调 |
AndroidJavaObject是java.lang.Object类在Unity中的表达,而java.lang.Object类是Java中所有类的基类,因此其能接受所有的Java对象,其公共方法如表2所示。
表2 AndroidJavaObject公共方法列表
公共方法 | 调用对象/属性类型 | 泛型方法 | 描述 |
---|---|---|---|
Call | 非静态 | Call | 调用对象非静态方法 |
CallStatic | 静态 | CallStatic | 调用类静态方法 |
Get 非静态 | Get | 获取对象非静态属性 | |
GetStatic | 静态 | GetStatic | 获取类静态属性 |
Set | 非静态 | Set | 设置对象的非静态属性 |
SetStatic | 静态 | SetStatic | 设置类静态属性 |
GetRawClass | - | GetRawClass | 获取原生Java类型的指针,用于JNI |
GetRawObject | - | GetRawObject | 获取原生Java对象的指针,用于JNI |
Dispose | - | - | IDisposable接口回调 |
通过表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()。