当前位置:   article > 正文

浅谈Unity与Android原生的桥接_unity androidjavaclass

unity androidjavaclass

前言

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

一、原理概述

UnityAndroid 桥接的原理和网络上大多数文章是一致的。
Unity 调用 Android 使用的是C#脚本所提供的 AndroidJava 系列工具类。
C#代码如下:

var javaClass = new AndroidJavaClass("[Java class package name]");
javaClass.CallStatic<string>("methodName", "params1");
  • 1

使用AndroidJavaClass调用是即方便又强大的,支持回调和返回值。且性能优秀。
Android 调用 Unity 使用的是 Unity 提供的 Jar包里的方法。代码如下:

UnityPlayer.UnitySendMessage(
            "Unity Object Name",
            "MethodName",
            "message"
        )
  • 1
  • 2
  • 3
  • 4

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"
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

上方的 JSON 就是方法对象序列化后的内容。方法对象的定义:

data class Command(
    /**
     * 指令名称
     * 回调情况下name为回调ID
     */
    var name: String,

    /**
     * 是否是回调指令
     */
    var callback: Boolean,
    /**
     * 参数List
     */
    var args: List<String>,
    /**
     * 回调ID
     */
    var callbackId: String
)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 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 是支持返回值的。所以这里也对返回值进行了一次包装。类似网络请求的返回实体。 实体定义如下:

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

Unity 调用获取返回值的方式:

var resultJson = bridgeClass.CallStatic<string>("onUnityCall", commandJson);
var result = JsonUtility.FromJson<Result<T>>(resultJson);
  • 1

注:这里的 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"
}
  • 1
  • 2
  • 3
  • 4
  • 5

Android 在接收到这个 Command 后执行异步操作。在需要回调的时候发送一条专用于回调指令通知对方。回调指令的 Command.callback 会置为 true。name 为 回调的 ID,示例如下:

{
    "name": "baf6b37f562e45b9b0ad7da4f00c91f8",
    "callback": true,
    "args": ["{\"name\":\"isOK\",\"value\":true}"]
}
  • 1
  • 2
  • 3
  • 4

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);
        }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

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

    2.4 接入便捷性

    接入便捷性主要考虑的方便业务层使用。所以对业务层注册进来的接口和实例,我采用的是注解处理 + 反射的方式进行调用。使得接入方在接口仅关心定义,实现类里仅关心实现。设计方式有点参考 Spring Controller 和 Retrofit Service。
    举例:服务接口的定义:

    @UnityBridgeService("ToastService")
    interface UnityToastService  : IUnityService {
        @UnityBridgeMethod(name = "show")
        fun showToast(@Param("msg") msg: String, @Param("time") time: Long)
    }
    • 1
    • 2
    • 3
    • 4

    举例:接口实现:

    class UnityToastServiceImpl : UnityToastService {
        override fun showToast(msg: String, time: Long) {
            QtToast.show(msg, time)
        }
    }
    • 1
    • 2
    • 3
    • 4

    使用时:

            UnityBridge.register(UnityToastService::class.java)
            UnityBridge.registerInstance(UnityToastServiceImpl())
    • 1

    注:在接入便捷性上,可以考虑开发 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);
        }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    注:由于 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)
            }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3.3 回调控制

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

    interface ICallbackHandler :
        IMethodChainInvoke<ICallbackHandler> {
    
        /**
         * 清除参数
         */
        fun clearParams()
    
        /**
         * 调用回调
         */
        fun call()
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    需要提供回调的服务接口举例:

    @UnityBridgeService("MessageService")
    interface UnityMessageService : IUnityService {
    
        @UnityBridgeMethod(name = "registerSingleMessageListener")
        fun registerMessageListener(@Param("cmd") messageCmd: Int, callbackHandler: ICallbackHandler): String
    }
    • 1
    • 2
    • 3
    • 4
    • 5

    在执行 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)
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    注:上方代码中 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
            }
        }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    两次反序列化处理调用参数代码:

    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)
        }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    反射调用代码:

    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)
        }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    四、关于开源

    目前暂时没有考虑开源桥接层的模块,因为在我们的项目中,桥接层的应用还没有得到复杂场景的检验,稳定性无法保证得很好。在后需桥接层迭代成熟后,再考虑开源。

    本文由博客一文多发平台 OpenWrite 发布!

    声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/96161
    推荐阅读
    相关标签
      

    闽ICP备14008679号