从Unity(C#)与Objective-C的互相调用到键者的新年呓语

Posted 大悦天

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从Unity(C#)与Objective-C的互相调用到键者的新年呓语相关的知识,希望对你有一定的参考价值。

话说,又到了新的一年,键者终于又睡了个天昏地暗日月无光,这篇东西应该是一个月前的腹稿,估计是趁做梦的时候码了并梦游发了……

依稀记得去年年底立了flag说今年要出一倍的文章,燃鹅很遗憾地是,确实没做到,算下来,好像立了的flag都没有实现呢……

因为业务需要,需要为已有的以 Objective-C为主要开发语言库开发一套 Unity(C#)的接口,说起跨语言,就想起了跨年,就想起了孙悟空……咳咳,就想起了当年为 android开发 NDK的日子,还记得在那个汗流浃背的下午,办公室停电,靠着一个小风扇,满头大汗地写着文档……现在来看,那篇文档几乎可以算过期了,因为现在 Android似乎终于已经放弃 Eclipse都转到 AndroidStudio了,不过,思路还是一样的,估计是实现细节上可能会有差异吧。

听说人老了就会变得很啰嗦,所以以上就看做键者老年人的呓语罢……感谢各位童鞋对整天断更的键者的不离不弃,祝愿善良的读者们新的一年身体健康,万事如意!!

一、跨语言的梦呓

其实跨语言调用这东西,更多是同类语言之间的调用,键者这里讨论的也只是以 面向对象/ 面向过程为基础设计思路的语言之间的互调。

其实应该说,虽然现代编程语言百花齐放,但由于现代编程语言的理论抽象的高度一致性,基本还是绕不开 变量函数这两个东西。

亦或者说, 面向过程/ 面向对象的设计理念已经深入人心,不同语言的表达或者实现或有不同,但本质却绕不开这一基础的世界观。

在这个抽象的基础上,目前键者接触到的,所有的跨语言互调都有一个通用的思路。

  1. 解决变量或者数据结构之间的相互转换

  2. 解决函数之间的相互调用。

甚至,有一个万能的胶水语言, C语言,键者几乎哪里都能看到……

二、变量的转换梦语

在跨语言调用的时候,我们一般会回避复杂的数据结构,而往往通过简单的三种结构、甚至是两种结构处理我们期望的所有数据类型。

二点一、整数或者小数,或者干脆是数值

在计算机的世界,数字大概是一种万能的解药,亦或是必须是万能的解药,因为在计算机的世界观里,一切都是数字,没有数字表达不了的意思,如果有,那就是 策划??的锅。

写这一段的时候,键者本能地想用其实作为开头,然后又删去了,其实好像其实不想其实了。

一般来说,语言数值支持可以看做 整型浮点型两种,有些语言甚至放弃了整型如 Lua,但并不妨碍它成为性能最好的脚本语言之一,或者可以把“之一”去掉。

二点二、万能的字符串

其实变量这个东西,说起来很复杂,有时候又很简单,在合适的场景下,没有字符串不能描述的变量,因为目前的语言都是用字符串便器的,无论是解释型的也好,编译型的也罢,语法分析器分析的总是字符串本身,而字符串总是可以传达我们希望描述的东西,只要有合适的规则。

就像本章节的标题, 二点二并不妨碍你理解我想表达的是 2.2一样,如果妨碍了,键者在此表示抱歉。

最常用的数据结构表达字符串大概是 JSON了,无论性能上有诟病,语法上有吐槽等等,它确实是实用性最强的字符串级别的数据传输结构,没有之一。

所以每当键者需要处理一种新的跨语言调用时,只要搞定了字符串的转换,键者就感觉妥了一半。

二点三、自动释放与持久化的小坑

很多偏早期的语言,都需要手动管理内存的分配和释放,木有错,说的就是 C语言,而在这个互调的场景下作为胶水语言,就特别要小心这个问题,是个不大不小的坑。

在具体的场景下, C#Objective-C都有自动管理内存的特性,虽然实现机制不同,但会存在,从 C#传给 C的变量,特别是字符串这种往往是通过指针引用的数据结构,可能在不知道什么时候,这个字符串对应的内存就被释放了,或者被修改了,导致使用的时候,系统崩溃,亦或者返回了意料之外的数据。

因为在保险的基础上,从 C拿到所有以指针的形式传递的数据时,最好立即将其以当前语言原生的数据结构拷贝并持有一份。

同理,如果是涉及到函数回调的场景,最好声明一个持久的函数,例如静态的函数,来专门处理某个回调,而不是直接把类似 block的匿名函数直接传入。

这类问题,非常依赖语言特性和运行环境,键者只能说有可能会有这个问题,但不好说。

Android的时候,键者遇到过通过的内存,在有些手机上就自动释放了,有些手机就必须手动释放的情况,最后的结果就是对每一块涉及到的变量,一定要手动释放,且释放的时候做足够的 判断,毕竟有些时候,一个内存不能被释放两次……就像人不能两次踏入同一条河流?

所以遇到问题的时候,有个思路可以排查这方面的问题,也算是排雷了。

三、函数的互调梦魇

函数互相调用的问题往往是跨语言调用中最麻烦又最耗时间的部分,即便有很多文档,往往也需要大量的调试才能实现,就像梦魇一样,要么被打倒,要么征服之,但更多时候,只能在某个维度上取得妥协。

键者根据个人兴趣,把函数互调可以分成几个级别,从低到高,表达是距离开发者的距离,越低的级别,对开发者的心智负担越高,但往往性能也越好;越高的级别,对开发者的心智负担越低,但往往有性能瓶颈。

三点一、字符串翻盘

万能的字符串,可以用于表达我要做什么,只要对应的语言能解析,就能基于这个命令执行操作,最坏的情况大概也只是共享一个异步的消息队列。无论说性能是否充足,至少是能用的。

三点二、函数调用

假设有M种语言,那么如果要能实现两两跨用的话,就需要有 M*M种接口,实际上,没有那个语言会这么有空把所有语言都支持一遍,所以参考很多 M*N业务降维处理思路,我们只需要引入一个中间语言,就可以把支持的难度降到 M+N,即所有语言都支持某个中间语言,那么所有语言都可以互相调用了。

现实中,英语事实上充当了这个中间语言;计算机中,往往是 C语言在干这事,这也是键者为什么说 C语言有时候感觉是万能的胶水,哪怕它明明是静态的,并不怎么流动(^__^) 嘻嘻……比较正式一点的称呼中,我们把这个做法称为 桥接,C语言在这个场景中的作用,就是 。 大概因为 C语言能力,是这类语言最泛用的抽象子集了吧。

可是传入参数有时候会很复杂,怎么办?很多时候,跨语言调用确实没有直接调用那么灵活,不过我们还是可以有折衷的方案,毕竟,有万能的字符串,不是么?

三点三、函数与回调

这大概是最不好处理的地方吧,应该可以看做是变量转换与函数起调问题的组合,如果能解决这个问题,那么,起调的灵活度基本就可以很好的满足日常需求了。

这块很多时候还是很依赖语言的特性以及对跨平台的支持程度。

四、Unity(C#)与Objective-C的醒梦之人

写下这章标题的时候,键者莫名地想起了昨天在微博刷到的一个有点忧桑又有点好笑的梗——面壁者你好,我是你的破壁机!哈哈哈,发明“破壁”这个词的人真是个营销天才。

四点一、从 C#到 Objective-C的基本操作

我们先来一个典型的例子,我们希望在 C#中调用某个 Objective-C的方法:

 
   
   
 
  1. // hello.cs

  2. using System.Runtime.InteropServices;


  3. class ios

  4. {

  5.    [DllImport("__Internal")]

  6.    public static extern string Hello(string msg, int code);

  7. }

这里我们声明了一个很经典的Hello方法,并传递了两个很经典的变量,一个 string和一个 int作为参数值,并接收一个 string作为返回值,相信这个场景,已经足够读者举一反三。

首先我们要引入 System.Runtime.InteropServices这个命名空间,以提供跨平台调用的支持。

我们需要声明一个以 staticextern关键字修饰的函数,并在函数前标注 [DllImport("__Internal")]static表示该方法为静态方法,不受对象机制约束。

是的,在跨语言调用的场景中,一般都没有可以面向的对象!面向对象的调用,基本都需要处理成面向过程的版本。

extern关键字表示该方法仅在此处做声明,但不在这里实现。

[DllImport("__Internal")]可以简单理解为,该方法由内置的全局方法实现,需要导入该默认的动态库后,才能找到具体的实现。

然后是 C语言的桥:

 
   
   
 
  1. // hello.m

  2. #if defined (__cplusplus)

  3. extern "C"

  4. {

  5. #endif


  6. const char* Hello(const char* msg, int code){

  7.        // 变量落地的时候,最好立即转成当前语言的类型

  8.        NSString* ocMsg = [NSString stringWithUTF8String:msg];

  9.        NSNumber* ocCode = [NSNumber numberWithInt:code];

  10.        NSLog("Hello: %@, %@", ocMsg, code);


  11.        // 指针变量返回前,一定要拷贝一份,返回拷贝后的结果

  12.        NSString* returnMsg = @"Hello C#";

  13.        const char* returnCMsg = [returnMsg UTF8String];

  14.        // strdup是C语言的常用函数,用于拷贝指定的字符串。

  15.        return strdup(returnCMsg);

  16. }


  17. #if defined (__cplusplus)

  18. }

  19. #endif

由于Unity导出的iOS工程中,大量代码是基于 C++实现的,所以我们在定义C语言的部分时,需要使用 extern"C"的关键字来确保我们定义的C函数的名称是全局唯一的,以便C#在起调时可以正确的找到。

C语言的桥在这里其实提供了基础的 数据类型函数声明,由于 Objective-C本身与 C之间基本可以说做到无缝混编,所以调用的过程可以说更加简化了,只要函数声明保持一致(函数名全局唯一,函数结构保持一致),就可以实现起调和返回处理结果。

例子中也给出了 C中的字符串、整型与 Objective-C中的整型和字符串的互相转换的写法。要注意的是,为了处理前文提到的自动释放的坑,字符串作为返回值,亦或者需要留作它用的时候,一定要拷贝一份再用。

四点二、从 Objective-C到 Unity(C#)的基本操作

如果说上一节中,键者其实讨论的是 C#Objective-C的操作,这一节就得先加上 Unity(C#)的前缀了。

上一章的方案其实是语言级别提供的支持,也就是通用的 C#C之间, CObjective-C之间的互动方法。

以下是由Unity引擎提供的基础方法:

 
   
   
 
  1. UnitySendMessage("game_obj_name""func_name" "param0");

写在最开头的地方, UnitySendMessage是由 Unity从引擎的级别提供支持方法,它有很多局限性:

  1. 它是基于异步的消息队列实现的,所以,调用之后,无法准确预料方法实际被调用的时机,同样,也难以准确拿到返回值(严格来说不是绝对不行,但开发成本很高)。

  2. 它只能调用被包装在 GameObject上的,只有一个 string作为参数,且没有返回值的方法。

  3. 它是基于前文提到的字符串的方案通过反射实现,所以性能是有一定欠缺的。

它的优点:简单,且对所有Unity的场景都可以使用,很多场景下已经够用了,这个方法也是大多数搜索到的 iOS/Andorid调用 Unity(C#)的文档中给出的解决方案。

例如,在 Andorid开发的场景中,也有对应的UnitySendMessage方法,因为这个方法是引擎级别提供的支持。

虽然基础和简单,但复杂的东西,都是由简单的东西构成的,所以键者这里还是给出一个原型供各位童鞋参考。

 
   
   
 
  1. using UnityEngine;


  2. public class Hello : MonoBehaviour

  3. {

  4.    // Use this for initialization

  5.    void Start() { }

  6.    // Update is called once per frame

  7.    void Update() { }


  8.    // 用于接收UnitySendMessage

  9.    void OnMsg(msg string){

  10.        System.Console.WriteLine(msg);

  11.    }

  12. }

对应的 Objective-C版:

 
   
   
 
  1. // 如果是在编写一个通用的库,首先要声明这个函数结构,主要是为了防止编译器报错。

  2. void UnitySendMessage(const char* obj_name, const char* func_name, const char* param);


  3. +(void)SomeFunc{

  4.    // 这里只是个栗子,你只要在你期望调用的地方调用即可

  5.    UnitySendMessage("Hello""OnMsg" "Hello C#");

  6. }

简单有用,很多时候就能解决问题了,但确实我们有时候会有更复杂的应用场景,那就看下一章咯。

四点三、从 Objective-C到 Mono(C#)的进阶操作

总所周知, Unity是基于 Mono实现的跨平台支持,所以通过 Mono的某些特性,我们可以实现更低级别的调用。

嗯,同样出于总所周知的原因(lan),键者不打算介绍 C#中的结构体与 C中结构体直接传输的方案,一方面复杂,另一方面,有 JSON基本够了,更高性能需求的小伙伴可以搜索关于 C#中关于结构体序列化的内容。

首先是带返回值的调用操作。

 
   
   
 
  1. using AOT;

  2. using System.Runtime.InteropServices;


  3. class Hello

  4. {

  5.    // 声明一个符合希望被调用方法结构一致的Delegate

  6.    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]

  7.    delegate string OnMsgCallDelegate(string msg);


  8.    // 声明希望被调用的方法,并标注该方法符合之前声明的Delegate的结构

  9.    [MonoPInvokeCallback(typeof(OnMsgCallDelegate))]

  10.    static string SayHi (string msg)

  11.    {

  12.        System.Console.WriteLine(msg);

  13.        return "I'm C#";

  14.    }

  15. }

首先,我们要声明一个 delegate,这个 delegate必须与我们希望被 Objective-C调用的 C#方法结构一致。

System.Runtime.InteropServices提供的 UnmanagedFunctionPointer属性,标记该 delegate非托管方法,即不受 C#自己的运行时进行管理(才能给C用);而 CallingConvention.Cdel则表示该 delegate会在被作为 C的接口被调用。

这段可能有些绕,简单说,对于 C来说,这里声明了一个函数结构(函数指针),这样 C在调用 C#方法时,才能知道怎么转换对应数据。

然后,通过 AOT提供的 MonoPInvokeCallback,我们可以指定, SayHi(...)方法是实现了符合 OnMsgCallDelegate结构的回调方法(即由 C#实现的,由 C进行调用的方法)。

注意,这里的 SayHi(...)必须是一个静态方法。

 
   
   
 
  1. using System.Runtime.InteropServices;


  2. class IOS

  3. {

  4.    [DllImport("__Internal")]

  5.    public static extern string Hello(string msg, Hello. OnMsgCallDelegate callback);



  6.    // 示例调用,仅供参考

  7.    public static void DemoCall(){

  8.        string msg = Hello("Hello C", Hello. SayHi);

  9.        System.Console.WriteLine(msg);

  10.    }

  11. }

对应的 Objective-C原型:

 
   
   
 
  1. // hello.m

  2. #if defined (__cplusplus)

  3. extern "C"

  4. {

  5. #endif


  6. // 这个函数指针其实就是C#中OnMsgCallDelegate的C语言版本。

  7. typedef const char* (*OnMsgCallDelegate) (const char* msg);


  8. // 直接调用的方法

  9. const char* Hello(const char* msg, OnMsgCallDelegate* callback){

  10.        // 变量落地的时候,最好立即转成当前语言的类型

  11.        NSString* ocMsg = [NSString stringWithUTF8String:msg];

  12.        NSNumber* ocCode = [NSNumber numberWithInt:code];

  13.        NSLog("Hello: %@, %@", ocMsg, code);


  14.        // !!这里就可以直接调用C#的方法,并立即拿到结果了

  15.        const char* reply = callback("I'm OC");    

  16.        NSString* nsReply = [NSString stringWithUTF8String:reply];


  17.        // 指针变量返回前,一定要拷贝一份,返回拷贝后的结果

  18.        NSString* returnMsg = [NSString stringWithFormat:@"Hi I'm Obj-C, Your reply is: [%@]", nsReply];

  19.        const char* returnCMsg = [returnMsg UTF8String];

  20.        // strdup是C语言的常用函数,用于拷贝指定的字符串。

  21.        return strdup(returnCMsg);

  22. }


  23. #if defined (__cplusplus)

  24. }

  25. #endif

五、键者的呓语与小结

也许,跨语言调用,就像跨年调用一样,让键者就想起《哈利波特》中的 比比多味豆,每一次编译都是一次全新的冒险,你永远不知道下一颗是什么滋味。

这个时候,自动化的脚本/工具就显得尤为重要了。

匆匆的2018就这样马上要过去了,一年下来有感动有低谷,有兴奋有伤感,立了很多flag,有些做到了,但更多的是没做到。

大概这就是人生吧,相信各位童鞋也收获了自己的感动,如果不介意的话欢迎在评论中与键者分享

以上是关于从Unity(C#)与Objective-C的互相调用到键者的新年呓语的主要内容,如果未能解决你的问题,请参考以下文章

Unity3D 与 objective-c 之间数据交互。iOS SDK接口封装Unity3D接口 .-- 转载

Unity与Android Studio互相调用

Unity与Android Studio互相调用

Unity3d Android SDK接入解析Unity3d 与 Android之间的互相调用

C# 对象与JSON串互相转换

从 Android 调用 C# Unity 脚本