浅谈Unity与Android原生的桥接

前言

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

一、原理概述

UnityAndroid 桥接的原理和网络上大多数文章是一致的。
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 的场景不支持回调、返回值和非字符串的传参。所以这里我们如果不进行回调机制的设计,那么将无法满足双方回调的场景。 下面,我们来逐一分析每一个点需要如何满足。

2.1 方法调用

由于 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 回调机制」 简单解释一下每个参数的意义:

  • Command.name 指令名称

指令名称代表要调用的方法是什么。对方通过指令名称去执行对应的操作。
注:在回调指令中,指令名称为回调的ID。

  • Command.args 参数列表

参数列表是一个字符串列表。列表每一个元素为一个参数信息的JSON。参数没有顺序要求。

  • Command.callbackId 回调ID

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 提供的桥接入口方法。

2.2 回调机制

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,取出回调,并将参数传递过去,调用回调。这样就形成了回调的闭环。同时,这样的设计是支持回调嵌套的(回调中调用回调)。

注:这样的回调方式不支持一次传递多个回调,多回调的场景,可以单个回调传递多个参数,以参数来区分。

2.3 可移植性

可移植性对桥接层来说同样很重要,我们需要考虑后续接入到其他应用中的情况。这里的可移植性主要针对 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);
    }

有了注册的方式,上层业务模块就可以将自己的服务实现放到业务层,使用之前注册就可以。

2.4 接入便捷性

接入便捷性主要考虑的方便业务层使用。所以对业务层注册进来的接口和实例,我采用的是注解处理 + 反射的方式进行调用。使得接入方在接口仅关心定义,实现类里仅关心实现。设计方式有点参考 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 插件来实现服务接口和实现的自动注入。

2.5 可拓展性

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

三、如何实现

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

3.1 桥接入口

桥接入口我定义了一个统一入口,也就是 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 编写。

3.2 Command 处理流程

Command(指令) 的处理从入口调用后,会经过以下几步处理:

  1. 反序列化
  2. 检查指令信息是否合法
  3. 检查本地是否提供该指令的处理服务。
  4. 判断是否为回调指令
  5. 执行指令

以上流程代码较为简单,我定义了 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)
        }

3.3 回调控制

如果业务层提供的方法需要回调,我提供了一个回调的基类和实现来方便使用。 回调接口如下:

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 相关代码可以去掉。

3.4 反射调用

反射调用是这套设计的核心之一了,反射本身代码没有太多的难点。只是有几点需要注意:

  1. Kotlin 基础数据类型 Gson 反序列化不支持。
  2. 由于需要获取 @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 发布!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值