赞
踩
在网络上,有许多关于 Unity 和 Android 互相调用的文章,里面的内容大同小异,都给出了相互调用最基本的方法。在这些文章中,有许多文章是很久之前的,里面的代码放到现在已经无法正常运行了,并且基本都说的比较简单。
本篇文章介绍的是以 Android 项目为主,Unity 项目为辅,以实战场景为基准来实现 Unity 和 Android 的桥接。在这其中,需要考虑桥接的功能型、稳定性、可拓展性、以及结合 Android 原生开发的一些特性。
那么究竟是如何实现的呢,请看下方详解!
Unity 和 Android 桥接的原理和网络上大多数文章是一致的。
Unity 调用 Android 使用的是C#脚本所提供的 AndroidJava 系列工具类。
C#代码如下:
var javaClass = new AndroidJavaClass("[Java class package name]");
javaClass.CallStatic<string>("methodName", "params1");
使用AndroidJavaClass调用是即方便又强大的,支持回调和返回值。且性能优秀。
Android 调用 Unity 使用的是 Unity 提供的 Jar包里的方法。代码如下:
UnityPlayer.UnitySendMessage(
"Unity Object Name",
"MethodName",
"message"
)
Unity Object Name 为 Unity 中场景对象的名称。
MethodName 为该对象绑定的脚本中的方法。 message 为发送的内容。
可以看到,Android 调用 Unity 的方式没有返回值,且只能是一个字符串类型的参数。所以如果想要达到两端统一调用,就需要一定的封装。那么具体是怎么封装的呢?请继续往下看!
首先,我们要对 Unity 与 Android 的桥接部分(以下简称:桥接层)进行架构的设计。
注:本篇文章主要讨论以 Android 项目为主的情况,即为,Android 需要为Unity 提供大量功能接口。Unity 为 Android 部分提供少量接口。所以,这里的架构分析主要以 Android 原生部分的桥接层设计来进行后续的讲解。
架构的设计除了桥接层本身提供的业务功能,还需要考虑以下几个点:
这里先解释一下,为什么需要单独列出来回调机制的设计。在上文原理概述中,可以看到,Android 调用 Unity 的场景不支持回调、返回值和非字符串的传参。所以这里我们如果不进行回调机制的设计,那么将无法满足双方回调的场景。 下面,我们来逐一分析每一个点需要如何满足。
由于 Android 和 Unity 底层提供的相互调用接口不一致,所以如果想要达到相同的调用效果。就需要自行封装一套调用协议。这里,我们使用大家所熟知的 JSON 来作为方法调用协议的载体。
在调用的时候,将方法和其参数、回调信息,封装到一个对象中,序列化为JSON后传给对方。 方法调用协议举例:
{
"name": "methodName",
"callback": false,
"args": [
"{\"name\":\"argName1\",\"value\":\"value1\"}",
"{\"name\":\"argName2\",\"value\":\"value2\"}"
],
"callbackId": "callbackID"
}
上方的 JSON 就是方法对象序列化后的内容。方法对象的定义:
data class Command( /** * 指令名称 * 回调情况下name为回调ID */ var name: String, /** * 是否是回调指令 */ var callback: Boolean, /** * 参数List */ var args: List<String>, /** * 回调ID */ var callbackId: String )
方法对象(下文也称指令对象) 方法对象主要分为两类,使用 Command.callback 字段来区分: 方法调用的对象:callback 为 false,代表一次普通的方法调用。 回调的对象:callback 为 true,代表一次回调的调用。 callback 相关的介绍请参考下方「2.2 回调机制」 简单解释一下每个参数的意义:
指令名称代表要调用的方法是什么。对方通过指令名称去执行对应的操作。
注:在回调指令中,指令名称为回调的ID。
参数列表是一个字符串列表。列表每一个元素为一个参数信息的JSON。参数没有顺序要求。
callbackId 代表当次调用所携带的回调。callbackId 由调用方生成并维护。在需要回调时,被调用方根据 callbackId 发送回调指令实现回调的效果。 callbackId 格式32位随机字符串。
因为 Unity 调用 Android 是支持返回值的。所以这里也对返回值进行了一次包装。类似网络请求的返回实体。 实体定义如下:
public class Result { public static final int RESULT_SUCCESS = 0; public static final int RESULT_EXCEPTION = -1; /** * 错误码 */ public int code = 0; /** * 错误信息 */ public String message; /** * 返回结果 */ public Object result; }
Unity 调用获取返回值的方式:
var resultJson = bridgeClass.CallStatic<string>("onUnityCall", commandJson);
var result = JsonUtility.FromJson<Result<T>>(resultJson);
注:这里的 onUnityCall 为 Android 提供的桥接入口方法。
Command 方法对象已经可以满足我们的方法调用需求了。那么回调机制是如何设计的呢?
这里我们统一设计了一套回调机制。Unity 和 Android 均使用这套机制。以 Unity 部分举例,在需要传递回调的时候。会有一个回调的处理器来创建回调,创建之后将回调缓存起来,并为一个回调生成一个ID。这个ID和回调绑定。然后将此回调ID添加到 Command 对象 callbackId 字段上后发送。 附加 callbackId 的指令示例如下:
{
"name": "TipService.dialog",
"callback": false,
"args": ["{\"name\":\"message\",\"value\":\"Hello Unity and Android.\"}"],
"callbackId": "8710a212ffac41b5910462937ed62059"
}
Android 在接收到这个 Command 后执行异步操作。在需要回调的时候发送一条专用于回调指令通知对方。回调指令的 Command.callback 会置为 true。name 为 回调的 ID,示例如下:
{
"name": "baf6b37f562e45b9b0ad7da4f00c91f8",
"callback": true,
"args": ["{\"name\":\"isOK\",\"value\":true}"]
}
Unity 在收到回调指令后会交给回调处理器处理,根据name 找到对应的ID,取出回调,并将参数传递过去,调用回调。这样就形成了回调的闭环。同时,这样的设计是支持回调嵌套的(回调中调用回调)。
注:这样的回调方式不支持一次传递多个回调,多回调的场景,可以单个回调传递多个参数,以参数来区分。
可移植性对桥接层来说同样很重要,我们需要考虑后续接入到其他应用中的情况。这里的可移植性主要针对 Android 的桥接部分。
首先,桥接层单独成一个模块。且尽可能的少依赖第三方库。所以我在这里的设计,仅引入了GSON作为序列化的工具库。以及将 Unity 提供的 jar 包作为编译时依赖。
compileOnly files('libs/unity-classes.jar')
其次,桥接层需要下沉到项目架构的最底部。不依赖任何其他业务模块。也就代表他需要和你的业务逻辑解耦合,提供的服务以接口注册的方式来处理和分发。
所以桥接层提供了服务的基类,注册接口和实例的相关功能。
/** * 注册提供Unity方法的Service实例 * * @param service service实例 */ public static void registerInstance(IUnityService service) { BridgeServiceManager.INSTANCE.register(service); } /** * 注册服务接口 * * @param serviceClass */ public static void register(Class<? extends IUnityService> serviceClass) { BridgeServiceManager.INSTANCE.addInterface(serviceClass); }
有了注册的方式,上层业务模块就可以将自己的服务实现放到业务层,使用之前注册就可以。
接入便捷性主要考虑的方便业务层使用。所以对业务层注册进来的接口和实例,我采用的是注解处理 + 反射的方式进行调用。使得接入方在接口仅关心定义,实现类里仅关心实现。设计方式有点参考 Spring Controller 和 Retrofit Service。
举例:服务接口的定义:
@UnityBridgeService("ToastService")
interface UnityToastService : IUnityService {
@UnityBridgeMethod(name = "show")
fun showToast(@Param("msg") msg: String, @Param("time") time: Long)
}
举例:接口实现:
class UnityToastServiceImpl : UnityToastService {
override fun showToast(msg: String, time: Long) {
QtToast.show(msg, time)
}
}
使用时:
UnityBridge.register(UnityToastService::class.java)
UnityBridge.registerInstance(UnityToastServiceImpl())
注:在接入便捷性上,可以考虑开发 Gradle 插件来实现服务接口和实现的自动注入。
可拓展性和可移植性做的工作是相差不大的。在前面的设计基础上,已经满足了桥接层服务的可拓展性。
这一节主要挑选一些实现时涉及主流程、难点的一些实现来举例说明。的
桥接入口我定义了一个统一入口,也就是 Unity 的调用全部从一个方法进入,代码如下:
/** * Unity调用入口方法 * * @param command 指令序列化json * @return 返回值 */ public static String onUnityCall(String command) { Result result; try { LogUtils.INSTANCE.i("Received command:" + command); Object obj = CommandManager.INSTANCE.onCommandReceived(command); result = ResultUtils.INSTANCE.getSuccessResult(obj); } catch (Throwable thr) { result = ResultUtils.INSTANCE.getErrorResult(thr); } return Warehouse.INSTANCE.getGson().toJson(result); }
注:由于 Unity 调用 Android 是通过底层反射,不支持Kotlin代码,所以入口的类需要使用 Java 编写。
Command(指令) 的处理从入口调用后,会经过以下几步处理:
以上流程代码较为简单,我定义了 CommandManager
来做以上的事情。
try {
command = Warehouse.gson.fromJson(commandJson, Command::class.java)
} catch (e: Throwable) {
throw UnityBridgeRuntimeException("Gson serialized error.Please check command json correctly or not. - ${e.message}")
}
checkCommandAvailable(command)
return if (CallbackController.commandIsCallback(command)) {
CallbackController.executeCallback(command)
} else {
CommandController.executeCommand(command)
}
如果业务层提供的方法需要回调,我提供了一个回调的基类和实现来方便使用。 回调接口如下:
interface ICallbackHandler :
IMethodChainInvoke<ICallbackHandler> {
/**
* 清除参数
*/
fun clearParams()
/**
* 调用回调
*/
fun call()
}
需要提供回调的服务接口举例:
@UnityBridgeService("MessageService")
interface UnityMessageService : IUnityService {
@UnityBridgeMethod(name = "registerSingleMessageListener")
fun registerMessageListener(@Param("cmd") messageCmd: Int, callbackHandler: ICallbackHandler): String
}
在执行 Command 时,会反射调用,反射调用就会判断如果参数列表中包含 ICallbackHandler 就会去实例化一个回调处理的辅助类传递下去。 回调处理的辅助类定义如下:
internal class CallbackHandler(var id: String) : ICallbackHandler { /** * 参数map */ private var params: MutableMap<String, Any?> = ConcurrentHashMap() /** * 回调中的回调对象(用于回调中需要回调的场景) */ private var callback: AbsCallback? = null override fun clearParams() { params.clear() callback = null } override fun putParam(name: String, value: Any?): CallbackHandler { params[name] = value return this } override fun setCallback(callback: AbsCallback): CallbackHandler { this.callback = callback return this } override fun call() { CallbackController.sendCallbackCommand(id, params, callback) } }
注:上方代码中 AbsCallback 相关的代码是用于 Unity 回调 Android 的场景。其他代码是 Android 回调 Unity 的场景。所以如果理解困难时,AbsCallback 相关代码可以去掉。
反射调用是这套设计的核心之一了,反射本身代码没有太多的难点。只是有几点需要注意:
@Param
注解的值与指令对象的参数做映射,所以序列化需要经过2次,第一次根据名字映射,获取到参数的类型,第二次再根据参数类型反序列化具体的值。处理 Kotlin 基础数据类型的代码:
fun kotlinClassConvert(clazz: Class<*>): Class<*> { when (clazz) { Int::class.java -> return Integer::class.java Boolean::class.java -> return java.lang.Boolean::class.java Float::class.java -> return java.lang.Float::class.java Double::class.java -> return java.lang.Double::class.java Byte::class.java -> return java.lang.Byte::class.java Char::class.java -> return java.lang.Character::class.java Short::class.java -> return java.lang.Short::class.java Long::class.java -> return java.lang.Long::class.java else -> return clazz } }
两次反序列化处理调用参数代码:
private fun <T : Any?> findCommandParam(
command: Command,
paramName: String,
paramType: Class<T>
): CommandParam<T> {
command.args.forEach {
val commandParamName = Warehouse.gson.fromJson(it, CommandParamName::class.java)
if (commandParamName.name == paramName) {
//构造带泛型的反序列化type
val type = TypeToken.getParameterized(CommandParam::class.java, paramType).type
return Warehouse.gson.fromJson(it, type)
}
}
return CommandParam(paramName, null)
}
反射调用代码:
private fun reflectInvoke(command: Command, method: Method, instance: Any): Any? { val args = Array<Any?>(method.parameterTypes.size) { null } val commandArgs = 1 CommandUtils.getMethodParamsName(method).forEachIndexed { index, paramName -> val typeClass = CommandUtils.kotlinClassConvert(method.parameterTypes[index]) if (typeClass == ICallbackHandler::class.java) { //方法的参数类型是回调类型 并且command中带有回调的ID,则创建一个handler if (CallbackController.commandHasCallback(command)) { args[index] = CallbackHandler(command.callbackId!!) } } else { val commandParam = findCommandParam(command, paramName, typeClass) args[index] = commandParam.value } } return method.invoke(instance, *args) }
目前暂时没有考虑开源桥接层的模块,因为在我们的项目中,桥接层的应用还没有得到复杂场景的检验,稳定性无法保证得很好。在后需桥接层迭代成熟后,再考虑开源。
本文由博客一文多发平台 OpenWrite 发布!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。