从Unity(C#)与Objective-C的互相调用到键者的新年呓语
Posted 大悦天
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从Unity(C#)与Objective-C的互相调用到键者的新年呓语相关的知识,希望对你有一定的参考价值。
话说,又到了新的一年,键者终于又睡了个天昏地暗日月无光,这篇东西应该是一个月前的腹稿,估计是趁做梦的时候码了并梦游发了……
依稀记得去年年底立了flag说今年要出一倍的文章,燃鹅很遗憾地是,确实没做到,算下来,好像立了的flag都没有实现呢……
因为业务需要,需要为已有的以 Objective-C
为主要开发语言库开发一套 Unity(C#)
的接口,说起跨语言,就想起了跨年,就想起了孙悟空……咳咳,就想起了当年为 android
开发 NDK
的日子,还记得在那个汗流浃背的下午,办公室停电,靠着一个小风扇,满头大汗地写着文档……现在来看,那篇文档几乎可以算过期了,因为现在 Android
似乎终于已经放弃 Eclipse
都转到 AndroidStudio
了,不过,思路还是一样的,估计是实现细节上可能会有差异吧。
听说人老了就会变得很啰嗦,所以以上就看做键者老年人的呓语罢……感谢各位童鞋对整天断更的键者的不离不弃,祝愿善良的读者们新的一年身体健康,万事如意!!
一、跨语言的梦呓
其实跨语言调用这东西,更多是同类语言之间的调用,键者这里讨论的也只是以 面向对象
/ 面向过程
为基础设计思路的语言之间的互调。
其实应该说,虽然现代编程语言百花齐放,但由于现代编程语言的理论抽象的高度一致性,基本还是绕不开 变量
、 函数
这两个东西。
亦或者说, 面向过程
/ 面向对象
的设计理念已经深入人心,不同语言的表达或者实现或有不同,但本质却绕不开这一基础的世界观。
在这个抽象的基础上,目前键者接触到的,所有的跨语言互调都有一个通用的思路。
解决变量或者数据结构之间的相互转换
解决函数之间的相互调用。
甚至,有一个万能的胶水语言,
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
的方法:
// hello.cs
using System.Runtime.InteropServices;
class ios
{
[DllImport("__Internal")]
public static extern string Hello(string msg, int code);
}
这里我们声明了一个很经典的Hello方法,并传递了两个很经典的变量,一个 string
和一个 int
作为参数值,并接收一个 string
作为返回值,相信这个场景,已经足够读者举一反三。
首先我们要引入 System.Runtime.InteropServices
这个命名空间,以提供跨平台调用的支持。
我们需要声明一个以 static
和 extern
关键字修饰的函数,并在函数前标注 [DllImport("__Internal")]
。 static
表示该方法为静态方法,不受对象机制约束。
是的,在跨语言调用的场景中,一般都没有可以面向的对象!面向对象的调用,基本都需要处理成面向过程的版本。
extern
关键字表示该方法仅在此处做声明,但不在这里实现。
[DllImport("__Internal")]
可以简单理解为,该方法由内置的全局方法实现,需要导入该默认的动态库后,才能找到具体的实现。
然后是 C语言
的桥:
// hello.m
#if defined (__cplusplus)
extern "C"
{
#endif
const char* Hello(const char* msg, int code){
// 变量落地的时候,最好立即转成当前语言的类型
NSString* ocMsg = [NSString stringWithUTF8String:msg];
NSNumber* ocCode = [NSNumber numberWithInt:code];
NSLog("Hello: %@, %@", ocMsg, code);
// 指针变量返回前,一定要拷贝一份,返回拷贝后的结果
NSString* returnMsg = @"Hello C#";
const char* returnCMsg = [returnMsg UTF8String];
// strdup是C语言的常用函数,用于拷贝指定的字符串。
return strdup(returnCMsg);
}
#if defined (__cplusplus)
}
#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
之间,C
和Objective-C
之间的互动方法。
以下是由Unity引擎提供的基础方法:
UnitySendMessage("game_obj_name","func_name", "param0");
写在最开头的地方, UnitySendMessage
是由 Unity
从引擎的级别提供支持方法,它有很多局限性:
它是基于异步的消息队列实现的,所以,调用之后,无法准确预料方法实际被调用的时机,同样,也难以准确拿到返回值(严格来说不是绝对不行,但开发成本很高)。
它只能调用被包装在
GameObject
上的,只有一个string
作为参数,且没有返回值的方法。它是基于前文提到的字符串的方案通过反射实现,所以性能是有一定欠缺的。
它的优点:简单,且对所有Unity的场景都可以使用,很多场景下已经够用了,这个方法也是大多数搜索到的 iOS/Andorid
调用 Unity(C#)
的文档中给出的解决方案。
例如,在
Andorid
开发的场景中,也有对应的UnitySendMessage方法,因为这个方法是引擎级别提供的支持。
虽然基础和简单,但复杂的东西,都是由简单的东西构成的,所以键者这里还是给出一个原型供各位童鞋参考。
using UnityEngine;
public class Hello : MonoBehaviour
{
// Use this for initialization
void Start() { }
// Update is called once per frame
void Update() { }
// 用于接收UnitySendMessage
void OnMsg(msg string){
System.Console.WriteLine(msg);
}
}
对应的 Objective-C
版:
// 如果是在编写一个通用的库,首先要声明这个函数结构,主要是为了防止编译器报错。
void UnitySendMessage(const char* obj_name, const char* func_name, const char* param);
+(void)SomeFunc{
// 这里只是个栗子,你只要在你期望调用的地方调用即可
UnitySendMessage("Hello","OnMsg", "Hello C#");
}
简单有用,很多时候就能解决问题了,但确实我们有时候会有更复杂的应用场景,那就看下一章咯。
四点三、从 Objective-C
到 Mono(C#)
的进阶操作
总所周知, Unity
是基于 Mono
实现的跨平台支持,所以通过 Mono
的某些特性,我们可以实现更低级别的调用。
嗯,同样出于总所周知的原因(lan),键者不打算介绍
C#
中的结构体与C
中结构体直接传输的方案,一方面复杂,另一方面,有JSON
基本够了,更高性能需求的小伙伴可以搜索关于C#
中关于结构体序列化的内容。
首先是带返回值的调用操作。
using AOT;
using System.Runtime.InteropServices;
class Hello
{
// 声明一个符合希望被调用方法结构一致的Delegate
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate string OnMsgCallDelegate(string msg);
// 声明希望被调用的方法,并标注该方法符合之前声明的Delegate的结构
[MonoPInvokeCallback(typeof(OnMsgCallDelegate))]
static string SayHi (string msg)
{
System.Console.WriteLine(msg);
return "I'm C#";
}
}
首先,我们要声明一个 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(...)
必须是一个静态方法。
using System.Runtime.InteropServices;
class IOS
{
[DllImport("__Internal")]
public static extern string Hello(string msg, Hello. OnMsgCallDelegate callback);
// 示例调用,仅供参考
public static void DemoCall(){
string msg = Hello("Hello C", Hello. SayHi);
System.Console.WriteLine(msg);
}
}
对应的 Objective-C
原型:
// hello.m
#if defined (__cplusplus)
extern "C"
{
#endif
// 这个函数指针其实就是C#中OnMsgCallDelegate的C语言版本。
typedef const char* (*OnMsgCallDelegate) (const char* msg);
// 直接调用的方法
const char* Hello(const char* msg, OnMsgCallDelegate* callback){
// 变量落地的时候,最好立即转成当前语言的类型
NSString* ocMsg = [NSString stringWithUTF8String:msg];
NSNumber* ocCode = [NSNumber numberWithInt:code];
NSLog("Hello: %@, %@", ocMsg, code);
// !!这里就可以直接调用C#的方法,并立即拿到结果了
const char* reply = callback("I'm OC");
NSString* nsReply = [NSString stringWithUTF8String:reply];
// 指针变量返回前,一定要拷贝一份,返回拷贝后的结果
NSString* returnMsg = [NSString stringWithFormat:@"Hi I'm Obj-C, Your reply is: [%@]", nsReply];
const char* returnCMsg = [returnMsg UTF8String];
// strdup是C语言的常用函数,用于拷贝指定的字符串。
return strdup(returnCMsg);
}
#if defined (__cplusplus)
}
#endif
五、键者的呓语与小结
也许,跨语言调用,就像跨年调用一样,让键者就想起《哈利波特》中的 比比多味豆
,每一次编译都是一次全新的冒险,你永远不知道下一颗是什么滋味。
这个时候,自动化的脚本/工具就显得尤为重要了。
匆匆的2018就这样马上要过去了,一年下来有感动有低谷,有兴奋有伤感,立了很多flag,有些做到了,但更多的是没做到。
大概这就是人生吧,相信各位童鞋也收获了自己的感动,如果不介意的话欢迎在评论中与键者分享
以上是关于从Unity(C#)与Objective-C的互相调用到键者的新年呓语的主要内容,如果未能解决你的问题,请参考以下文章
Unity3D 与 objective-c 之间数据交互。iOS SDK接口封装Unity3D接口 .-- 转载