找回密码
 立即注册
查看: 655|回复: 0

[笔记] 浅谈Unity与Android原生的交互

[复制链接]
发表于 2021-3-9 12:41 | 显示全部楼层 |阅读模式
前言

在网络上,有许多关于 UnityAndroid 互相调用的文章,里面的内容大同小异,都给出了相互调用最基本的方法。在这些文章中,有许多文章是很久之前的,里面的代码放到现在已经无法正常运行了,并且基本都说的比较简单。
本篇文章介绍的是以 Android 项目为主,Unity 项目为辅,以实战场景为基准来实现 UnityAndroid 的桥接。在这其中,需要考虑桥接的功能型、稳定性、可拓展性、以及结合 Android 原生开发的一些特性。
那么究竟是如何实现的呢,请看下方详解!
一、原理概述

UnityAndroid 桥接的原理和网络上大多数文章是一致的。
Unity 调用 Android 使用的是C#脚本所提供的 AndroidJava 系列工具类。
C#代码如下:
  1. var javaClass = new AndroidJavaClass("[Java class package name]");
  2. javaClass.CallStatic<string>("methodName", "params1");
复制代码
使用AndroidJavaClass调用是即方便又强大的,支持回调和返回值。且性能优秀。
Android 调用 Unity 使用的是 Unity 提供的 Jar包里的方法。代码如下:
  1. UnityPlayer.UnitySendMessage(
  2.             "Unity Object Name",
  3.             "MethodName",
  4.             "message"
  5.         )
复制代码
Unity Object Name 为 Unity 中场景对象的名称。
MethodName 为该对象绑定的脚本中的方法。 message 为发送的内容。
可以看到,Android 调用 Unity 的方式没有返回值,且只能是一个字符串类型的参数。所以如果想要达到两端统一调用,就需要一定的封装。那么具体是怎么封装的呢?请继续往下看!
二、架构分析

首先,我们要对 Unity 与 Android 的桥接部分(以下简称:桥接层)进行架构的设计。
注:本篇文章主要讨论以 Android 项目为主的情况,即为,Android 需要为Unity 提供大量功能接口。Unity 为 Android 部分提供少量接口。所以,这里的架构分析主要以 Android 原生部分的桥接层设计来进行后续的讲解。
架构的设计除了桥接层本身提供的业务功能,还需要考虑以下几个点:
    方法调用设计回调机制的设计可拓展性接入便捷性可移植性
这里先解释一下,为什么需要单独列出来回调机制的设计。在上文原理概述中,可以看到,Android 调用 Unity 的场景不支持回调、返回值和非字符串的传参。所以这里我们如果不进行回调机制的设计,那么将无法满足双方回调的场景。 下面,我们来逐一分析每一个点需要如何满足。
2.1 方法调用

由于 Android 和 Unity 底层提供的相互调用接口不一致,所以如果想要达到相同的调用效果。就需要自行封装一套调用协议。这里,我们使用大家所熟知的 JSON 来作为方法调用协议的载体。
在调用的时候,将方法和其参数、回调信息,封装到一个对象中,序列化为JSON后传给对方。 方法调用协议举例:
  1. {
  2.         "name": "methodName",
  3.         "callback": false,
  4.         "args": [
  5.                 "{\"name\":\"argName1\",\"value\":\"value1\"}",
  6.                 "{\"name\":\"argName2\",\"value\":\"value2\"}"
  7.         ],
  8.         "callbackId": "callbackID"
  9. }
复制代码
上方的 JSON 就是方法对象序列化后的内容。方法对象的定义:
  1. data class Command(
  2.     /**
  3.      * 指令名称
  4.      * 回调情况下name为回调ID
  5.      */
  6.     var name: String,
  7.     /**
  8.      * 是否是回调指令
  9.      */
  10.     var callback: Boolean,
  11.     /**
  12.      * 参数List
  13.      */
  14.     var args: List<String>,
  15.     /**
  16.      * 回调ID
  17.      */
  18.     var callbackId: String
  19. )
复制代码
方法对象(下文也称指令对象) 方法对象主要分为两类,使用 Command.callback 字段来区分: 方法调用的对象:callback 为 false,代表一次普通的方法调用。 回调的对象:callback 为 true,代表一次回调的调用。 callback 相关的介绍请参考下方「2.2 回调机制」 简单解释一下每个参数的意义:
    Command.name 指令名称
指令名称代表要调用的方法是什么。对方通过指令名称去执行对应的操作。
注:在回调指令中,指令名称为回调的ID。
    Command.args 参数列表
参数列表是一个字符串列表。列表每一个元素为一个参数信息的JSON。参数没有顺序要求。
    Command.callbackId 回调ID
callbackId 代表当次调用所携带的回调。callbackId 由调用方生成并维护。在需要回调时,被调用方根据 callbackId 发送回调指令实现回调的效果。 callbackId 格式32位随机字符串。
因为 Unity 调用 Android 是支持返回值的。所以这里也对返回值进行了一次包装。类似网络请求的返回实体。 实体定义如下:
  1. public class Result {
  2.     public static final int RESULT_SUCCESS = 0;
  3.     public static final int RESULT_EXCEPTION = -1;
  4.     /**
  5.      * 错误码
  6.      */
  7.     public int code = 0;
  8.     /**
  9.      * 错误信息
  10.      */
  11.     public String message;
  12.     /**
  13.      * 返回结果
  14.      */
  15.     public Object result;
  16. }
复制代码
Unity 调用获取返回值的方式:
  1. var resultJson = bridgeClass.CallStatic<string>("onUnityCall", commandJson);
  2. var result = JsonUtility.FromJson<Result<T>>(resultJson);
复制代码
注:这里的 onUnityCall 为 Android 提供的桥接入口方法。
2.2 回调机制

Command 方法对象已经可以满足我们的方法调用需求了。那么回调机制是如何设计的呢?
这里我们统一设计了一套回调机制。Unity 和 Android 均使用这套机制。以 Unity 部分举例,在需要传递回调的时候。会有一个回调的处理器来创建回调,创建之后将回调缓存起来,并为一个回调生成一个ID。这个ID和回调绑定。然后将此回调ID添加到 Command 对象 callbackId 字段上后发送。 附加 callbackId 的指令示例如下:
  1. {
  2.         "name": "TipService.dialog",
  3.         "callback": false,
  4.         "args": ["{\"name\":\"message\",\"value\":\"Hello Unity and Android.\"}"],
  5.         "callbackId": "8710a212ffac41b5910462937ed62059"
  6. }
复制代码
Android 在接收到这个 Command 后执行异步操作。在需要回调的时候发送一条专用于回调指令通知对方。回调指令的 Command.callback 会置为 true。name 为 回调的 ID,示例如下:
  1. {
  2.         "name": "baf6b37f562e45b9b0ad7da4f00c91f8",
  3.         "callback": true,
  4.         "args": ["{\"name\":\"isOK\",\"value\":true}"]
  5. }
复制代码
Unity 在收到回调指令后会交给回调处理器处理,根据name 找到对应的ID,取出回调,并将参数传递过去,调用回调。这样就形成了回调的闭环。同时,这样的设计是支持回调嵌套的(回调中调用回调)。
注:这样的回调方式不支持一次传递多个回调,多回调的场景,可以单个回调传递多个参数,以参数来区分。
2.3 可移植性

可移植性对桥接层来说同样很重要,我们需要考虑后续接入到其他应用中的情况。这里的可移植性主要针对 Android 的桥接部分。
首先,桥接层单独成一个模块。且尽可能的少依赖第三方库。所以我在这里的设计,仅引入了GSON作为序列化的工具库。以及将 Unity 提供的 jar 包作为编译时依赖。
  1. compileOnly files('libs/unity-classes.jar')
复制代码
其次,桥接层需要下沉到项目架构的最底部。不依赖任何其他业务模块。也就代表他需要和你的业务逻辑解耦合,提供的服务以接口注册的方式来处理和分发。
所以桥接层提供了服务的基类,注册接口和实例的相关功能。
  1. /**
  2.      * 注册提供Unity方法的Service实例
  3.      *
  4.      * @param service service实例
  5.      */
  6.     public static void registerInstance(IUnityService service) {
  7.         BridgeServiceManager.INSTANCE.register(service);
  8.     }
  9.     /**
  10.      * 注册服务接口
  11.      *
  12.      * @param serviceClass
  13.      */
  14.     public static void register(Class<? extends IUnityService> serviceClass) {
  15.         BridgeServiceManager.INSTANCE.addInterface(serviceClass);
  16.     }
复制代码
有了注册的方式,上层业务模块就可以将自己的服务实现放到业务层,使用之前注册就可以。
2.4 接入便捷性

接入便捷性主要考虑的方便业务层使用。所以对业务层注册进来的接口和实例,我采用的是注解处理 + 反射的方式进行调用。使得接入方在接口仅关心定义,实现类里仅关心实现。设计方式有点参考 Spring Controller 和 Retrofit Service。
举例:服务接口的定义:
  1. @UnityBridgeSerice("ToastService")
  2. interface UnityToastService  : IUnityService {
  3.     @UnityBridgeMethod(name = "show")
  4.     fun showToast(@Param("msg") msg: String, @Param("time") time: Long)
  5. }
复制代码
举例:接口实现:
  1. class UnityToastServiceImpl : UnityToastService {
  2.     override fun showToast(msg: String, time: Long) {
  3.         QtToast.show(msg, time)
  4.     }
  5. }
复制代码
使用时:
  1. UnityBridge.register(UnityToastService::class.java)
  2.         UnityBridge.registerInstance(UnityToastServiceImpl())
复制代码
注:在接入便捷性上,可以考虑开发 Gradle 插件来实现服务接口和实现的自动注入。
2.5 可拓展性

可拓展性和可移植性做的工作是相差不大的。在前面的设计基础上,已经满足了桥接层服务的可拓展性。
三、如何实现

这一节主要挑选一些实现时涉及主流程、难点的一些实现来举例说明。的
3.1 桥接入口

桥接入口我定义了一个统一入口,也就是 Unity 的调用全部从一个方法进入,代码如下:
  1. /**
  2.      * Unity调用入口方法
  3.      *
  4.      * @param command 指令序列化json
  5.      * @return 返回值
  6.      */
  7.     public static String onUnityCall(String command) {
  8.         Result result;
  9.         try {
  10.             LogUtils.INSTANCE.i("Received command:" + command);
  11.             Object obj = CommandManager.INSTANCE.onCommandReceived(command);
  12.             result = ResultUtils.INSTANCE.getSuccessResult(obj);
  13.         } catch (Throwable thr) {
  14.             result = ResultUtils.INSTANCE.getErrorResult(thr);
  15.         }
  16.         return Warehouse.INSTANCE.getGson().toJson(result);
  17.     }
复制代码
注:由于 Unity 调用 Android 是通过底层反射,不支持Kotlin代码,所以入口的类需要使用 Java 编写。
3.2 Command 处理流程

Command(指令) 的处理从入口调用后,会经过以下几步处理:
    反序列化检查指令信息是否合法检查本地是否提供该指令的处理服务。判断是否为回调指令执行指令
以上流程代码较为简单,我定义了 CommandManager 来做以上的事情。
  1. try {
  2.             command = Warehouse.gson.fromJson(commandJson, Command::class.java)
  3.         } catch (e: Throwable) {
  4.             throw UnityBridgeRuntimeException("Gson serialized error.Please check command json correctly or not. - ${e.message}")
  5.         }
  6.         checkCommandAvailable(command)
  7.         return if (CallbackController.commandIsCallback(command)) {
  8.             CallbackController.executeCallback(command)
  9.         } else {
  10.             CommandController.executeCommand(command)
  11.         }
复制代码
3.3 回调控制

如果业务层提供的方法需要回调,我提供了一个回调的基类和实现来方便使用。 回调接口如下:
  1. interface ICallbackHandler :
  2.     IMethodChainInvoke<ICallbackHandler> {
  3.     /**
  4.      * 清除参数
  5.      */
  6.     fun clearParams()
  7.     /**
  8.      * 调用回调
  9.      */
  10.     fun call()
  11. }
复制代码
需要提供回调的服务接口举例:
  1. @UnityBridgeService("MessageService")
  2. interface UnityMessageService : IUnityService {
  3.     @UnityBridgeMethod(name = "registerSingleMessageListener")
  4.     fun registerMessageListener(@Param("cmd") messageCmd: Int, callbackHandler: ICallbackHandler): String
  5. }
复制代码
在执行 Command 时,会反射调用,反射调用就会判断如果参数列表中包含 ICallbackHandler 就会去实例化一个回调处理的辅助类传递下去。 回调处理的辅助类定义如下:
  1. internal class CallbackHandler(var id: String) : ICallbackHandler {
  2.     /**
  3.      * 参数map
  4.      */
  5.     private var params: MutableMap<String, Any?> = ConcurrentHashMap()
  6.     /**
  7.      * 回调中的回调对象(用于回调中需要回调的场景)
  8.      */
  9.     private var callback: AbsCallback? = null
  10.     override fun clearParams() {
  11.         params.clear()
  12.         callback = null
  13.     }
  14.     override fun putParam(name: String, value: Any?): CallbackHandler {
  15.         params[name] = value
  16.         return this
  17.     }
  18.     override fun setCallback(callback: AbsCallback): CallbackHandler {
  19.         this.callback = callback
  20.         return this
  21.     }
  22.     override fun call() {
  23.         CallbackController.sendCallbackCommand(id, params, callback)
  24.     }
  25. }
复制代码
注:上方代码中 AbsCallback 相关的代码是用于 Unity 回调 Android 的场景。其他代码是 Android 回调 Unity 的场景。所以如果理解困难时,AbsCallback 相关代码可以去掉。
3.4 反射调用

反射调用是这套设计的核心之一了,反射本身代码没有太多的难点。只是有几点需要注意:
    Kotlin 基础数据类型 Gson 反序列化不支持。由于需要获取 @Param 注解的值与指令对象的参数做映射,所以序列化需要经过2次,第一次根据名字映射,获取到参数的类型,第二次再根据参数类型反序列化具体的值。
处理 Kotlin 基础数据类型的代码:
  1. fun kotlinClassConvert(clazz: Class<*>): Class<*> {
  2.         when (clazz) {
  3.             Int::class.java ->
  4.                 return Integer::class.java
  5.             Boolean::class.java ->
  6.                 return java.lang.Boolean::class.java
  7.             Float::class.java ->
  8.                 return java.lang.Float::class.java
  9.             Double::class.java ->
  10.                 return java.lang.Double::class.java
  11.             Byte::class.java ->
  12.                 return java.lang.Byte::class.java
  13.             Char::class.java ->
  14.                 return java.lang.Character::class.java
  15.             Short::class.java ->
  16.                 return java.lang.Short::class.java
  17.             Long::class.java ->
  18.                 return java.lang.Long::class.java
  19.             else ->
  20.                 return clazz
  21.         }
  22.     }
复制代码
两次反序列化处理调用参数代码:
  1. private fun <T : Any?> findCommandParam(
  2.         command: Command,
  3.         paramName: String,
  4.         paramType: Class<T>
  5.     ): CommandParam<T> {
  6.         command.args.forEach {
  7.             val commandParamName = Warehouse.gson.fromJson(it, CommandParamName::class.java)
  8.             if (commandParamName.name == paramName) {
  9.                 //构造带泛型的反序列化type
  10.                 val type = TypeToken.getParameterized(CommandParam::class.java, paramType).type
  11.                 return Warehouse.gson.fromJson(it, type)
  12.             }
  13.         }
  14.         return CommandParam(paramName, null)
  15.     }
复制代码
反射调用代码:
  1. private fun reflectInvoke(command: Command, method: Method, instance: Any): Any? {
  2.         val args = Array<Any?>(method.parameterTypes.size) { null }
  3.         val commandArgs = 1
  4.         CommandUtils.getMethodParamsName(method).forEachIndexed { index, paramName ->
  5.             val typeClass = CommandUtils.kotlinClassConvert(method.parameterTypes[index])
  6.             if (typeClass == ICallbackHandler::class.java) {
  7.                 //方法的参数类型是回调类型 并且command中带有回调的ID,则创建一个handler
  8.                 if (CallbackController.commandHasCallback(command)) {
  9.                     args[index] = CallbackHandler(command.callbackId!!)
  10.                 }
  11.             } else {
  12.                 val commandParam = findCommandParam(command, paramName, typeClass)
  13.                 args[index] = commandParam.value
  14.             }
  15.         }
  16.         return method.invoke(instance, *args)
  17.     }
复制代码
四、关于开源

目前我没有考虑开源桥接层的模块,因为在我们的项目中,桥接层的应用还没有得到复杂场景的检验,稳定性无法保证得很好。在后需桥接层迭代成熟后,再考虑开源。
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-12-23 08:07 , Processed in 0.094030 second(s), 25 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表