大家好,欢迎来到IT知识分享网。
一、背景
目前映客内部的 Unity 创新项目均处于起步阶段,遇到如下问题:
- 代码拷贝方式降低了项目的可持续迭代能力
- 无公共仓库导致信息不共享,造成重复工作和无效产出
- 多团队多标准,认知不统一,造成项目后续维护困难
本文主要和大家分享下映客 Unity SDK 多语言混编、包管理相关技术方案及一些实战经验。
二、映客Unity方案
SDK架构方案
我们借鉴 Flutter 插件的原理,在现有 SDK 架构方案上新增了 Unity 部分来实现通用业务能力对所有平台的全覆盖,即不同平台分别使用对应 Unity 插件,所有插件都依赖 Native SDK 层的逻辑处理,以保证多技术栈混合项目下的业务逻辑与状态的一致性,整体架构如下图:
其中 Unity 模块的架构设计如下图所示:
实际开发过程中,部分项目通是过文件夹拷贝的形式将工程组织到一起,频繁的文件移动将大大降低组织的开发及维护效率,同时也存在误删的风险,为彻底解决拷贝替换问题,我们仿照Flutter Plugin Create 创建了Unity Package Plugin Create 工具,包管理最终也是采用Unity官方推荐的 NPM 托管。我们在 Unity Package Plugin Create 工具中对插件工程架构制定了标准规范,研发团队应按照标准统一的流程创建、开发及发布插件,项目工程中依赖标准 Unity Package,至此我们打通了 Unity 项目复用原生能力的整个链路。
插件目录结构
- Android 文件夹:为 Android 项目工程,负责插件 Java 桥接层代码编写以及产物功能验证。
- iOS文件夹:为 iOS 项目工程,负责插件 OC 桥接层代码编写以及产物功能验证。
- UnityProject 文件夹:为 Unity 项目工程,用于插件 C# 脚本、Unity 示例的编写和产物导出,项目内部均内置Unity产物生成后自动导入验证工程(Android、iOS 文件夹)的脚本。
展开结构如下图所示:
代码设计原则
- 使用抽象门面类,对外 API 使用抽象门面类,具体实现细节落实到各个平台代码内部。
- 合理使用条件编译,平台性质的代码,请使用条件编译,能够提高编译速度,降低产物大小。具体的参平台类型,可以参考官方文档:https://docs.unity3d.com/cn/current/Manual/PlatformDependentCompilation.html
- 避免暴露平台特定的类,平台专有类,使用 internal 修饰,避免外部直接访问。
- 原生端提供桥接层,避免 C# 层直接与原生 SDK 通信。
插件托管方案
我们调研了市面上几家 Unity SDK 提供商的产物形式:
- 融云、Bugly、环信是提供 .unitypackage 文件导入到工程;
- 声网、即构是提供代码源文件(zip或者GitHub上下载),拷贝到工程里使用;
- Google Cardboard 提供的是 Git + Package Manager;
.unitypackage和文件拷贝这两种方案有如下缺点:
- 无法维护插件之间的依赖关系,由于原生 SDK 在层级上存在依赖关系,业务在使用 A 插件时,还需要手动引入底层的 B、C、D 等插件,增加了业务使用的复杂度;
- 存在人工导包误操作的潜在风险,导致 SDK 没有完全升级成功,给业务带来风险的同时,也增加了底层 SDK 团队对外服务的成本;
- 管理 SDK 版本成本高,在 SDK 生成之后,发布到文件服务器上提供下载,全程过于人工化,而且版本管理问题也比较多;
- 排查问题不方便,业务无法明确感知 SDK 版本;
- 代码风险高,以源码资源的形式集成到项目中,业务方可以任意修改插件代码逻辑,有升级需求的话会导致代码管理成本增加,同时也增加了排查问题难度。
Git + Package Manager方式,相对于官方的 Unity Register,依赖 Git URL一方面会涉及私有工程权限问题、另外一方面比较难以跟踪 SDK 版本,整体使用体验不够友好。
我们按照官网教程在内网搭建了 Unity 包服务器(Package Registry Server)来托管Unity Package,最后只需在Unity Editor中,打开如下路径菜单:Edit/ProjectSettings,切换到“Package Manager”选项下,添加私有仓库地址,配置好 Name、URL 和 Scopes 即可。
三、Unity和原生通信
Unity 插件中 C# 和双端原生层通信的原理略有区别,实际情况中我们要根据自身需求场景选择不同的方式,例如普通业务从效率的角度考虑就可以选择 UnitySendMessage 方法,偏底层的逻辑型业务 Android 平台就可以选择 AndroidJavaProxy 方式,iOS 平台就可以选择函数指针回调方式,下面我们分平台介绍下 Unity 和原生层的通信方式。
Unity和Android通信
Unity 与原生 Android 之间的通信是一个跨虚拟机调用的过程,Unity 提供了一系列 API ,让二者的通信看起来像是直接通信一样,通信流程如下图所示:
与 Flutter 跨平台通信相比,Unity 不支持 List、Map 的映射,而事实上,Unity 支持使用任意的 Java 数据类型进行通信,Java 层的数据类型,除了最基础的值类型、数组、字符串外,其它的数据类实例最终都会映射成 C# 层的 AndroidJavaObject 类。Java 与 C# 通信一般使用 UnityPlayer.UnitySendMessage(),但是该方法存在若干局限性:
- 需要在 Unity 侧将 C# 脚本挂载在 GameObject 上,业务往往需要考虑使用哪个 GameObject 来挂载脚本,增加业务使用负担。
- 该方法无返回值,需要配合其他手段解决跨平台获取数据的场景。
- 不适合异步回执。
- 只支持传递一个字符串类型的参数。
该方法虽然使用便捷,但是能力有限,适合简单的业务通信场景,如命令式指令,但是并不适合偏底层 SDK 这类大量使用异步、回调的场景。
另一种方式通过 AndroidJavaProxy 来实现,即对 Java 里的接口进行代理,其内部通过动态代理来实现。通过注册监听(Callback)、响应 Callback 方法进行通信,和 Java 的 Callback 模式是一样的。因此,我们可以通过 SDK 初始化的事后,搭建Java call C# 的通信通道。此方案的优势在于,开发人员可以灵活定义通信方法个数、通信协议、通信回执,无需在业务层挂载脚本。
这里需要注意的是,在 Android 平台中,通过 Unity 引擎,C# 层可以直接反射原生 SDK 中的方法。这种方式会带来弊端:原生 aar 内部发生代码变更,如方法签名变化、类删除等,C# 层在编译期无法直接感知,若测试不到位,风险就会带入线上。
Unity和iOS通信
得益于 Unity 与 iOS 对 C 语言的支持,C 语言便成为了 Unity 与 iOS 数据交互的最好桥梁,通信的思路就是将 OC 代码用 C 语言进行封装,Unity 则直接调用 C语言接口,从而实现数据的传递。如果使用 C++ (.cpp) 或 Objective-C++ (.mm) 来实现该插件,则必须使用 extern “C” 来声明函数以免发生名称错用问题(C++符号生成规则与C语言有较大差异,不指定extern “C” C#将无法找到定义的C方法)。
值得注意的是:
- C# 层可以直接调用 C 层方法, 不过前提是这个 C 方法要在 C# 中进行声明 ;
- 在 C# 层声明函数时,函数名最好与 C 层一致, 参数按照上方的映射格式表进行转换即可;
- 函数声明前必须有[DllImport(“__Internal”)]标记;
- 回调方法则必须为静态方法,并使用[MonoPInvokeCallback]进行标记,必须 C# 层先调用 C 层,把回调方法传递过去,类似于一个注册过程,在使用回调方法前进行注册即可;
Unity实际上也提供了OC层直接调用C#的方法,UnitySendMessage(“GameObjectName1”, “MethodName1”, “Message to send”):
- 目标 GameObject 的名称
- 用于调用该对象的脚本方法
- 用于传递给被调用方法的消息字符串
如参数描述,该方法使用时,必须存在对应的模型对象,这就导致其在一些场景下并不适用;如用户数据、日志、网络等,并不需要和模型对象关联。
还有一些其他的限制:
- 通过原生代码,只能调用与以下签名对应的脚本方法:void MethodName(string message);
- 对 UnitySendMessage 的调用是异步的,并有一帧延迟。
- 如果多个模型对象名字相同,不确定最终接收情况。
- 只能传一个参数
该方法更多是用于和模型相关的业务逻辑,只是 OC 与 C# 数据交换的话,更推荐前面提到的函数指针回调。
四、踩坑记录/遇到的问题
通信参数选择
本节主要介绍当需要使用复合类型的数据进行通信时,通信参数在数据类(AndroidJavaObject)和 Json 字符串之间到底如何选择。
通过 Json 方式通信是在 C# 层将数据类序列化成字符串,传递的到 Java 层,Java 层使用 Gson 反序列化成数据类。通过 AndroidJavaObject 方式通信则是在 C# 层反射构造 Java 的对象实例,传递到 Java 层,Java 层直接使用。二者在耗时方面的差异体现在:Json 方式需要进行一次序列化和反序列化,AndroidJavaObject 构建 Java 数据类需要进行多次跨虚拟机的 JNI 调用。
而数据类通信的方式,是在 C# 层直接反射 Java 数据类的。考虑到 Json 的通信方式中,还多了一步 C# 层的序列化耗时,如此看来,很容易的就会得出一个结论:数据类的通信方式性能远高于 Json 字符串的通信方式。实际情况并非如此,以 C# 调用 Java 方法为例,测试设备为 Google pixel 2, 规则为单次循环10000次的累计结果,事件单位为毫秒,数据为20组实验的平均值:
简单Json |
简单JavaObj |
常规Json |
常规JavaObj |
复杂Json |
复杂JavaObj |
|
耗时 |
555 |
966 |
918 |
1220 |
1181 |
9313 |
从测试数据看,无论是数据如何复杂,Json 的通信方式性能都优于使用 AndroidJavaObject 的方式。
为此,我们补充了几组测试,同样都是循环调10000次,20组实验的平均值,耗时时间单位为毫秒:
补充实验一、参数类型对 C# & Java 通信的影响:
无参 |
JavaProxy |
JavaObject |
string |
int |
|
耗时 |
98 |
505 |
505 |
187 |
154 |
不难看出,AndroidJavaObject 数据类的通信性能比使用基础数据类性要差很多。
补充实验二、C# 层反射Java层数据 VS Java 层反射 Java 层(以简单数据类为例,Java层的数据类都是相同的):
C#反射Java构造函数 |
Java反射构造函数 |
C#反射Java成员变量 |
Java反射成员变量 |
|
耗时 |
592 |
26 |
244 |
11 |
可以看出,由于 C# 反射 Java 层,需要跨虚拟机通信,性能比在原生平台使用反射差的多。此外,C# 反射复杂 Java 数据类,往往需要进行多次反射调用,性能就变得很糟糕。
综合以上,我们建议在 C# 和 Java 原生通信过程中,基础类型能解决的,优先使用基础数据类,对于复杂的数据参与通信,我们推荐使用 Json 字符串的形式,而不是AndroidJavaObject(与之对应的是 Java 的数据类)。
五、总结
Unity方案落地总结为以下几点:
- 详细的架构方案要根据公司技术现状制定
- 底层 SDK 团队要有统一的插件研发规范
- 为了提升开发效率和使用体验,最好搭建内网私服托管插件
- Unity 和原生通信基础数据能解决优先基础数据,复杂类型数据通信尽量选择 Json 方式
六、引用
我们对 Unity 官方文档 Unity 插件部分做了调研,并整理出一些干货供大家参考:
- 如何创建自定义包,自定义包需要哪些内容:https://docs.unity3d.com/cn/current/Manual/CustomPackages.html
- Android 平台插件:https://docs.unity3d.com/cn/current/Manual/PluginsForAndroid.html
- iOS 平台插件:https://docs.unity.cn/cn/current/Manual/PluginsForIOS.html
来源:微信公众号:映客技术
出处:https://mp.weixin..com/s/aWf_PKwBtMe5AwCaVC-xvA
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/59943.html