iOS 与 Unity 消息传递 (Swift 与 C#)

image

背景知识

Unity 在导出 iOS 工程的时候支持两种模式:mono 和 IL2CPP。

mono 就是传统的虚拟机模式。这种模式只支持 32 位的 cpu,根据官方的答复,以后也不会有更多的支持了。现在 iOS 上架 AppStore 必须要支持 arm64,所以 mono 模式就是看看就好了。

IL2CPP IL(中间语言)转 C++,也就是把 C# 编译成的 IL,再从 IL 转成 C++,然后跟 iOS 代码一起混合编译。所以基于 IL2CPP 的Unity 与 iOS 原生接口的相互调用,本质上就是 C++ 和 Objective C 的相互调用,理解了这个,我们做互相调用的开发就很容易了。

Swift 参数与 C# 转换

函数参数

例如 void UnitySendMessage(const char* obj, const char* method, const char* msg);

String 转 const char* => url.cString(using: String.defaultCStringEncoding)

函数返回值

我们不能在函数内存申请局部变量去返回给 C# 用,因为函数退出的时候,内存就释放了,那么我们需要用 malloc 去分配内存。malloc 分配的内存不需要调用 free,也不用担心会内存泄露,因为合成的代码会自动帮忙处理的。

int 数组

static int *intArr = NULL;
int* getAllBotId() {
    NSArray *array = [SwiftCommon getAllBotId];
    intArr = malloc(array.count);
    for (int i = 0; i < array.count; i++) {
        intArr[i] = [[array objectAtIndex:i] intValue];
    }
    return intArr;
}

char 数组

char* getRobotCardsInfo() {
    NSString *string = [SwiftCommon getRobotCardsInfo];
    char* cStringCopy(const char* string);
    return cStringCopy([string UTF8String]);
}

char* cStringCopy(const char* string){
    if (string == NULL){
        return NULL;
    }
    char* res = (char*)malloc(strlen(string)+1);
    strcpy(res, string);
    return res;
}

通信一:Unity 向 iOS 发消息

Unity 声明一个 [DllImport( "__Internal" )] 函数

iOS .m 文件写函数实现

通信二:iOS 向 Unity 发消息

方法一:UnitySendMessage

Unity 自身提供了一个函数,可以将信息通过字符串或 json 字符串传入

/**
  * iOS 主动发消息给 Unity
  * @param obj     Unity GameObject名称
  * @param method  GameObject绑定脚本的方法
  * @param msg     要传递的消息
  */
void UnitySendMessage(const char* obj, const char* method, const char* msg);

方法二: 指针

原理:C++ 里面有函数指针的概念,我们可以在 iOS 端实现一个函数,接收函数指针,Unity 侧调用,把 Unity 的函数当做指针传过去,在 iOS 端把它存下来,后续使用。

示例:将一个 string 传到 unity, 代码量比 UnitySendMessage 多,但这是为下一个例子做铺垫,通过指针回调以一种更优雅的方式传回数据

TtsPlayeriOSProxy.cs

[MonoPInvokeCallback]
public static int playTtsUrl(string url) {
    // 收到 iOS 传来的 url
}

[DllImport("__Internal")]
private static extern void setTtsFunction(FunctionPlayer urlPlayer);

// ios call c# delegate
public delegate int FunctionPlayer(string url);

...

// 单例构造函数
private TtsPlayeriOSProxy ()
{
    setTtsFunction (playTtsUrl);
}
        
UnityPlugin.m 

// C#设置过来的函数指针类型
typedef int (*PlayTtsUrlFunction)(const char *url);

static PlayTtsUrlFunction playTtsUrlFunc;

void setTtsFucntion(PlayTtsUrlFunction urlFunction) {
    playTtsUrlFunc = urlFunction;
}

void playTtsUrl(const char* url) {
    if (playTtsUrlFunc != nil) {
        playTtsUrlFunc(url);
    }
}

在 .swift 文件中可直接调用 playTtsUrl 函数将 url 传递到 Unity

知识补充:

1. C# delegate

C# 中的委托(Delegate)类似于 C 或 C++ 中函数的指针。委托(Delegate) 是存有对某个方法的引用的一种引用类型变量。
例如,假设有一个委托:

public delegate int MyDelegate (string s);

上面的委托可被用于引用任何一个带有一个单一的 string 参数的方法,并返回一个 int 类型变量。

2. [DllImport( "__Internal" )]
private static extern void sendRequest(int requestId, byte[] data, int dataLength);

[DllImport( "__Internal" )] 表示这个函数位于Dll中,DLL名字是 __Internal,这是固定语法,意思是这个函数是静态链接在 iOS 的 App 中的。extern 表示是一个外部的函数。

3. [MonoPInvokeCallback]

用来标记这个函数会被iOS侧反向调用

通信三:Unity 将任务交付 iOS 处理,结果回调 Unity

示例:Unity 组装数据 encode 成 byte 数组交付 iOS 发起网络请求

image

Unity

NetworkManageriOSProxy.cs 单例类

[DllImport( "__Internal" )]
private static extern void sendRequest(int requestId, byte[] data, int dataLength);
[DllImport("__Internal")]
private static extern int setFunctionPointerOnResponse(FunctionPointerOnResponse pointer);

// ios call c# delegate
public delegate void FunctionPointerOnResponse(int reqeustId, int errorCode, IntPtr responseData, int responseDataLength);
    
[MonoPInvokeCallback]
// 需要 length 重组数据
static public void onResponse(int reqeustId, int errorCode, IntPtr responseData, int responseDataLength)
{            
    if (errorCode == 0)
    {
        byte[] buffer = new byte[responseDataLength];         
        Marshal.Copy(responseData, buffer, 0, responseDataLength);
       NetworkManagerProxy.onResponse(reqeustId, errorCode, buffer);    
    }
}

...
    
private static bool isCallbackSeted = false;
    
public static void sendRequestForiOS(int requestId, byte[] data)
{
    if (!isCallbackSeted) {
        setFunctionPointerOnResponse(onRespone);
        isCallbackSeted = true;
    }
    sendRequest(requestId, data, data.Length);
}


针对 onResponse 函数的 responseData 参数做一个解释

iOS c++: void onRespone(int reqeustId, int errorCode, const void responseData*, int responseDataLength);
Unity c#: static public void onResponse(int reqeustId, int errorCode, IntPtr responseData, int responseDataLength);

iOS侧 调用 C# 的时候,基本类型是可以直接映射的,要注意的是,如果是数组之类的参数,在 iOS 侧用 C++ 的表现是指针,这个其实是内存里面的 rawdata,会以一个 IntPtr 的参数传递到 Unity 侧,我们知道在 C# 中数组是个对象,我们要把它转为“托管的对象”。上面的代码有示例如何转换。

iOS

.m文件

// C#设置过来的函数指针类型
typedef void (*FunctionPointerOnResponse)(int reqeustId, int errorCode, void* responseData, int responseDataLength);
// 用于保存回调指针的
static FunctionPointerOnResponse callbackFunction;

void setFunctionPointerOnResponse(FunctionPointerOnResponse pointer) {
    callbackFunction = pointer;
}

// 需要 length 重组数据
void sendRequest(int requestId, Byte* data, int dataLength) {
    NSData *body = [[NSData alloc] initWithBytes:data length:dataLength];
    [SwiftCommon sendUnityRequest:requestId body:body];
}


// 收到数据的时候回调此接口
void onRespone(int reqeustId, int errorCode, const void* responseData, int responseDataLength) {
    if (callbackFunction) {
        callbackFunction(reqeustId, errorCode, (void *)(responseData), responseDataLength);
    }
}

SwiftCommon.swift

/**
    OC调用swift中不支持的类型的方法
 */
class SwiftCommon: NSObject {
    // 伪代码
    @objc static func sendUnityRequest(_ requestId: Int, body: NSData) {
        let request = xxx
        NetworkEngine.shared.sendRequest(request, success: { response in
            guard var data = response.data else {
                return
            }
            let dataLength = Int32(data.count)
            data.withUnsafeMutableBytes({ (bytes: UnsafeMutablePointer<UInt8>) -> Void in
                // 回传给 unity
                onRespone(Int32(requestId), response.ret, bytes, dataLength)
            })
        }) { error in
        
        }
    }
}

通信四:iOS 将任务交给 Unity 处理,结果回调 iOS

image

Unity

通信三中写了如何将指针传入iOS, 这里不再赘述传递 start 和 stop 指针的 c# 代码 

iOSASRListener.cs

[DllImport("__Internal")]
private static extern void _HandleASRSuccessed(string requestId, bool isFinish, string msgText, string rspMsg, byte[] msgResponse, int msgResponeLength);
[DllImport("__Internal")]
private static extern void _HandleASRFailed(string requestId, int errorCode);

iOS

AsrProxy.m

#ifdef __cplusplus
extern "C" {
#endif

// C#设置过来的函数指针类型
typedef void (*FunctionPointerForAsr)(void);
    
static FunctionPointerForAsr start;
static FunctionPointerForAsr stop;

void setFunctionPointerForAsr(FunctionPointerForAsr startFunc, FunctionPointerForAsr stopFunc)
{
    start = startFunc;
    stop = stopFunc;
}

void StartAsr() 
{
    if (start)
    {
        start();
    }
}

void StopAsr() 
{
    if (stop)
    {
        stop();
    }
}

#ifdef __cplusplus
}
#endif
AsrListenerProxy.m

extern void (^ __nonnull HandleASRSuccessed_SwiftCallBack)(const char* __nonnull requestId, bool isFinish, const char* __nonnull msgText, const char* __nonnull rspMsg, NSData* msgResponseData) = NULL;
extern void (^ __nonnull HandleASRFailed_SwiftCallBack)(const char* __nonnull requestId, int errorCode) = NULL;

void _HandleASRSuccessed(const char* requestId, bool isFinish, const char* msgText, const char* rspMsg, Byte* msgResponse, int msgResponeLength)
{
    if (HandleASRSuccessed_SwiftCallBack != NULL ){
        NSData * data = [[NSData alloc]initWithBytes:msgResponse length:msgResponeLength];
        HandleASRSuccessed_SwiftCallBack(requestId,isFinish,msgText,rspMsg,data);
    }
}

void _HandleASRFailed(const char* requestId, int errorCode)
{
    if (HandleASRFailed_SwiftCallBack != NULL ){
        HandleASRFailed_SwiftCallBack(requestId,errorCode);
    }
}
AsrService.swift

@objc protocol AsrServiceDelegate {
    
    func asrSuccessed(requestId: String, isFinish: Bool, msgText: String, rspMsg: String, response: UniSendMsgResponse?)
    
    func asrFailed(requestId: String, errorCode: Int32)
}

class AsrService {
    
    weak var delegate: AsrServiceDelegate?
    
    init() {
        bridgingCFunction()
    }

    convenience init(delegate: AsrServiceDelegate) {
        self.init()
        self.delegate = delegate
    }
    
    fileprivate func bridgingCFunction() {
        HandleASRSuccessed_SwiftCallBack     = handleASRSuccessed
        HandleASRFailed_SwiftCallBack        = handleASRFailed
    }
    
    func startAsr() {
        StartAsr()
    }
    func stopAsr() {
        StopAsr()
    }

    fileprivate func handleASRSuccessed(requestId: UnsafePointer<Int8> , isFinish: Bool, msgText: UnsafePointer<Int8>, rspMsg: UnsafePointer<Int8>, msgResponseData: Data?) {
    
        var response: UniSendMsgResponse?
        if let `msgResponseData` = msgResponseData {
             response = UniSendMsgResponse.fromData(msgResponseData) as? UniSendMsgResponse
        }
        
        self.delegate?.asrSuccessed(requestId: requestId.stingValue, isFinish: isFinish, msgText: msgText.stingValue, rspMsg: rspMsg.stingValue, response: response)
    }
    
    fileprivate func handleASRFailed(requestId: UnsafePointer<Int8> , errorCode: Int32) {
        self.delegate?.asrFailed(requestId: requestId.stingValue, errorCode: errorCode)
    }
  
}

踩坑

MonoPInvokeCallback属性无法找到

这个是旧版本 Unity 才有的一个属性类,其实就是用来标记这个函数会被 iOS 侧反向调用的,也没有什么实质性的意义。但是新版本去掉了导致编译不过。所以就在工程里面添加一个就行了。

// 参考这个帖子的说明 https://garry.tv/2018/02/15/steamworks-and-il2cpp/
internal class MonoPInvokeCallbackAttribute : Attribute
{
    public MonoPInvokeCallbackAttribute() { }
}

参考

Unity C#和iOS函数互相调用的方法

C# 委托

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,744评论 0 38
  • (译注:P/Invoke,全称是platform invoke service,平台调用服务,简单的说就是允许托管...
    IndieACE阅读 3,324评论 0 7
  • C/C++输入输出流总结 前两天写C++实习作业,突然发现I/O是那么的陌生,打了好长时间的文件都没有打开,今天终...
    LuckTime阅读 1,720评论 0 6
  • __block和__weak修饰符的区别其实是挺明显的:1.__block不管是ARC还是MRC模式下都可以使用,...
    LZM轮回阅读 3,284评论 0 6
  • 史上最全的iOS面试题及答案 iOS面试小贴士———————————————回答好下面的足够了----------...
    Style_伟阅读 2,345评论 0 35