映客客户端Unity实践

映客客户端Unity实践我们借鉴 Flutter 插件的原理,在现有 SDK 架构方案上新增了 Unity 部分来实现通用业务能力对所有平台的全覆盖,即不同平台分别使用

大家好,欢迎来到IT知识分享网。

一、背景

目前映客内部的 Unity 创新项目均处于起步阶段,遇到如下问题:

  • 代码拷贝方式降低了项目的可持续迭代能力
  • 无公共仓库导致信息不共享,造成重复工作和无效产出
  • 多团队多标准,认知不统一,造成项目后续维护困难

本文主要和大家分享下映客 Unity SDK 多语言混编、包管理相关技术方案及一些实战经验。

二、映客Unity方案

SDK架构方案

我们借鉴 Flutter 插件的原理,在现有 SDK 架构方案上新增了 Unity 部分来实现通用业务能力对所有平台的全覆盖,即不同平台分别使用对应 Unity 插件,所有插件都依赖 Native SDK 层的逻辑处理,以保证多技术栈混合项目下的业务逻辑与状态的一致性,整体架构如下图:

映客客户端Unity实践

其中 Unity 模块的架构设计如下图所示:

映客客户端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 文件夹)的脚本。

展开结构如下图所示:

映客客户端Unity实践

代码设计原则

  1. 使用抽象门面类,对外 API 使用抽象门面类,具体实现细节落实到各个平台代码内部。
  2. 合理使用条件编译,平台性质的代码,请使用条件编译,能够提高编译速度,降低产物大小。具体的参平台类型,可以参考官方文档:https://docs.unity3d.com/cn/current/Manual/PlatformDependentCompilation.html
  3. 避免暴露平台特定的类,平台专有类,使用 internal 修饰,避免外部直接访问。
  4. 原生端提供桥接层,避免 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和原生通信

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实践

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 字符串之间到底如何选择。

映客客户端Unity实践

通过 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方案落地总结为以下几点:

  1. 详细的架构方案要根据公司技术现状制定
  2. 底层 SDK 团队要有统一的插件研发规范
  3. 为了提升开发效率和使用体验,最好搭建内网私服托管插件
  4. 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

(0)

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信