1. 關(guān)于Unity3D
Unity3D(以下簡(jiǎn)稱U3D)是由Unity Technologies開發(fā)的一個(gè)讓玩家輕松創(chuàng)建諸如三維視頻游戲、建筑可視化、實(shí)時(shí)三維動(dòng)畫等類型互動(dòng)內(nèi)容的多平臺(tái)的綜合型游戲開發(fā)工具魔眨,是一個(gè)全面整合的專業(yè)游戲引擎程腹。
作為一款跨平臺(tái)開發(fā)工具,難免會(huì)與原生平臺(tái)進(jìn)行一些交互操作來完成一些特定的平臺(tái)功能匈睁。例如:你需要直接操作iOS的IAP來實(shí)現(xiàn)游戲中的內(nèi)付費(fèi)功能卷哩;甚至一些第三方SDK沒有提供U3D版本的情況下蛋辈,你會(huì)直接在原生系統(tǒng)平臺(tái)調(diào)用其提供接口等等。
下面將為大家介紹将谊,在U3D下如何實(shí)現(xiàn)與iOS系統(tǒng)的交互工作冷溶,來滿足一些需要借助原生系統(tǒng)的功能需求。
2. From U3D to iOS
2.1 實(shí)現(xiàn)原理
由于U3D無法直接調(diào)用Objc或者Swift語言聲明的接口尊浓,幸好U3D的主要語言是C#逞频,因此可以利用C#的特性來訪問C語言所定義的接口,然后再通過C接口再調(diào)用ObjC的代碼(對(duì)于Swift代碼則還需要使用OC橋接)眠砾。例如虏劲,有如下C語言方法:
void nativeMethod ()
{
NSLog(@"------- objc method call...\n");
}
在C#中則可以像下面代碼一樣進(jìn)行引入和調(diào)用:
using System.Runtime.InteropServices;
[DllImport("__Internal")]
internal extern static void nativeMethod();
其中DllImport
為一個(gè)Attribute托酸,目的是通過非托管方式將庫中的方法導(dǎo)出到C#中進(jìn)行使用褒颈。而傳入"__Internal"則是表示這個(gè)是一個(gè)靜態(tài)庫或者是一個(gè)內(nèi)部方法。通過上面的聲明励堡,這個(gè)方法就可以在C#里面進(jìn)行調(diào)用了谷丸。如:
public class Sample
{
public void test ()
{
nativeMethod();
}
}
2.2 實(shí)現(xiàn)步驟
下面通過一個(gè)拼接字符串的例子來說明怎么樣從U3D中傳入兩個(gè)字符串到iOS中,然后由iOS拼接后通過NSLog
輸出結(jié)果:
- 首先新建
test.m
和test.h
兩個(gè)文件应结。分別寫入如下內(nèi)容:
/// test.h
extern "C"
{
extern void outputAppendString (char *str1, char *str2);
}
/// test.m
#import <Foundation/Foundation.h>
void outputAppendString (char *str1, char *str2)
{
NSString *string1 = [[NSString alloc] initWithUTF8String:str1];
NSString *string2 = [[NSString alloc] initWithUTF8String:str2];
NSLog(@"###%@", [NSString stringWithFormat:@"%@ %@", string1, string2]);
}
- 然后將上面的兩個(gè)文件放到U3D項(xiàng)目的
Assets
目錄中刨疼。如圖:
- 分別選擇
test.h
和test.m
文件,在Inspector面板中去掉Any Platforms的勾選鹅龄,然后保留iOS這一項(xiàng)選中揩慕。如圖:
- 新建一個(gè)叫Sample的C#腳本文件,并在這個(gè)文件中寫入c接口的聲明扮休,如:
public class Sample : MonoBehaviour
{
//引入聲明
[DllImport("__Internal")]
static extern void outputAppendString (string str1, string str2);
}
- 在Start方法中調(diào)用該方法迎卤,如:
void Start ()
{
#if UNITY_IPHONE
outputAppendString("Hello", "World");
#endif
}
注意:對(duì)于指定平臺(tái)的方法,一定要使用預(yù)處理指令#if來包括起來玷坠。否則在其他平臺(tái)下面執(zhí)行會(huì)導(dǎo)致異常蜗搔。
- 拖動(dòng)Sample腳本到場(chǎng)景的Main Camera對(duì)象中劲藐,讓腳本進(jìn)行掛載。
- 使用快捷鍵Command+Shift+B(或者點(diǎn)擊菜單File -> Build Settings)調(diào)出Build Settings窗口樟凄,將項(xiàng)目導(dǎo)出為iOS項(xiàng)目聘芜。如圖:
- 打開導(dǎo)出的iOS項(xiàng)目,先檢查之前創(chuàng)建的
test.m
和test.h
是否已經(jīng)導(dǎo)出到項(xiàng)目中缝龄。如圖:
- 編譯運(yùn)行應(yīng)用汰现,可以看到控制臺(tái)中會(huì)輸出合并后的字符串信息,如:
2018-01-22 16:17:15.143166+0800 ProductName[29211:4392515] ###Hello World
3. From iOS to U3D
對(duì)于如何從iOS中調(diào)用U3D的接口二拐,分為兩種辦法:一種是通過UnitySendMessage
方法來調(diào)用Unity所定義的方法服鹅。另一種方法則是通過入口參數(shù),傳入一個(gè)U3D的非托管方法百新,然后調(diào)用該方法即可企软。兩種方式的對(duì)比如下:
UnitySendMessage方式 | 非托管方法方式 |
---|---|
接口聲明固定,只能是void method(string message) 饭望。 |
接口靈活仗哨,可以為任意接口。 |
不能帶有返回值 | 可以帶返回值 |
必須要掛載到對(duì)象后才能調(diào)用铅辞。 | 可以不用掛載對(duì)象厌漂,但需要通過接口傳入該調(diào)用方法 |
下面將一一講述兩種方式的實(shí)現(xiàn)。
3.1 UnitySendMessage
- 基于上面調(diào)用iOS接口的例子斟珊,在
Sample.cs
中增加一個(gè)callback
方法苇倡。如:
void callback (string resultStr)
{
Debug.LogFormat ("result string = {0}", resultStr);
}
- 由于項(xiàng)目已經(jīng)掛載
Sample.cs
到Main Camera中,這就不用再進(jìn)行掛載囤踩。然后打開test.m
文件旨椒,在outputAppendString
方法中調(diào)用callback
方法,并將組合字符串返回給U3D堵漱。如:
void outputAppendString (char *str1, char *str2)
{
NSString *string1 = [[NSString alloc] initWithUTF8String:str1];
NSString *string2 = [[NSString alloc] initWithUTF8String:str2];
NSString *resultStr = [NSString stringWithFormat:@"%@ %@", string1, string2];
NSLog(@"###%@", resultStr);
UnitySendMessage("Main Camera", "callback", resultStr.UTF8String);
}
- 導(dǎo)出iOS項(xiàng)目综慎,編譯運(yùn)行看執(zhí)行結(jié)果。
2018-01-22 17:47:00.137259+0800 ProductName[29561:4429040] ###Hello World
Setting up 1 worker threads for Enlighten.
Thread -> id: 170cb3000 -> priority: 1
result string = Hello World
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/DebugBindings.gen.cpp Line: 51)
3.2 非托管方法
- 在
Sample.cs
中建立一個(gè)delegate聲明勤庐,并使用UnmanagedFunctionPointer
特性來標(biāo)識(shí)該delegate是非托管方法示惊。代碼如下:
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void ResultHandler(string resultString);
其中的CallingConvention.Cdel
為調(diào)用時(shí)轉(zhuǎn)換為C聲明接口。
- 然后聲明一個(gè)靜態(tài)方法愉镰,并使用
MonoPInvokeCallback
特性來標(biāo)記為回調(diào)方法米罚,目的是讓iOS中調(diào)用該方法時(shí)可以轉(zhuǎn)換為對(duì)應(yīng)的托管方法。如:
[MonoPInvokeCallback(typeof(ResultHandler))]
static void resultHandler (string resultStr)
{
}
注意:MonoPInvokeCallback
特性參數(shù)是上一步中定義的非托管delegate丈探。方法的聲明一定要與delegate定義一致录择,并且必須為static進(jìn)行修飾(iOS不支持非靜態(tài)方法回調(diào)),否則會(huì)導(dǎo)致異常。
- 打開
test.m
文件糊肠,定義一個(gè)新的接口辨宠,如:
typedef void (*ResultHandler) (const char *object);
void outputAppendString2 (char *str1, char *str2, ResultHandler resultHandler)
{
NSString *string1 = [[NSString alloc] initWithUTF8String:str1];
NSString *string2 = [[NSString alloc] initWithUTF8String:str2];
NSString *resultStr = [NSString stringWithFormat:@"%@ %@", string1, string2];
NSLog(@"###%@", resultStr);
resultHandler (resultStr.UTF8String);
}
上面代碼可見,在C中需要定義一個(gè)與C#的delgate相同的函數(shù)指針ResultHandler
货裹。然后新增的outputAppendString2
方法中多了一個(gè)回調(diào)參數(shù)resultHandler
嗤形。這樣就能夠把C#傳入的方法進(jìn)行調(diào)用了。
- 回到
Sample.cs
文件弧圆,定義outputAppendString2
的聲明赋兵。
[DllImport("__Internal")]
static extern void outputAppendString2 (string str1, string str2, IntPtr resultHandler);
注意:回調(diào)方法的參數(shù)必須是IntPtr類型,表示一個(gè)函數(shù)指針搔预。
- 在
Start
方法中調(diào)用outputAppendString2
霹期,并將回調(diào)方法轉(zhuǎn)換為IntPtr類型傳給方法。如:
ResultHandler handler = new ResultHandler(resultHandler);
IntPtr fp = Marshal.GetFunctionPointerForDelegate(handler);
outputAppendString2 ("Hello", "World", fp);
上面代碼使用Marshal
的GetFunctionPointerForDelegate
來獲取resultHandler
的指針拯田。
- 導(dǎo)出iOS項(xiàng)目历造,編譯運(yùn)行。
2018-01-22 19:02:31.339317+0800 ProductName[29852:4459349] ###Hello World
result string = Hello World
Sample:outputAppendString2(String, String, IntPtr)
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/DebugBindings.gen.cpp Line: 51)
4. 類型傳遞
對(duì)于基礎(chǔ)類型數(shù)據(jù)(如:int船庇、double吭产、string等)是可以直接從U3D中傳遞給iOS的。具體對(duì)應(yīng)關(guān)系如下表所示:
U3D | iOS |
---|---|
short | short |
int | int |
long | long long |
bool | bool |
char | char |
string | char * |
struct | struct |
byte[] | void * |
IntPtr | void * |
注意
- 引用型數(shù)據(jù)不能直接從U3D傳給iOS鸭轮。如果需要傳遞這樣的類型臣淤,可以考慮將對(duì)象序列化成byte數(shù)組,然后在iOS中進(jìn)行反序列化將其還原回來窃爷。
- 對(duì)于string類型邑蒋,會(huì)自動(dòng)轉(zhuǎn)換為c語言中的char *。但是由于C#中的string是托管類型按厘,因此char *是無法直接轉(zhuǎn)換為string的医吊,所以不要直接在返回值中返回char *類型。下一節(jié)會(huì)針對(duì)返回值進(jìn)行詳細(xì)的說明刻剥。
- struct類型數(shù)據(jù)中不能包含引用型數(shù)據(jù)遮咖,否則在調(diào)用接口時(shí)會(huì)報(bào)告類似下面的提示:
MarshalDirectiveException: Cannot marshal field 't' of type 'TestStructType': Reference type field marshaling is not supported.
4.1 關(guān)于Marshal
Marshal
類型主要是用于將C#中托管和非托管類型進(jìn)行一個(gè)轉(zhuǎn)換的橋梁滩字。其提供了一系列的方法造虏,這些方法包括用于分配非托管內(nèi)存、復(fù)制非托管內(nèi)存塊麦箍、將托管類型轉(zhuǎn)換為非托管類型漓藕,此外還提供了在與非托管代碼交互時(shí)使用的其他雜項(xiàng)方法等。
本質(zhì)上U3D與iOS的交互過程就是C#與C的交互過程挟裂,所以Marshal就成了交互的關(guān)鍵享钞,因?yàn)镃#與C的交互正正涉及到托管與非托管代碼的轉(zhuǎn)換。下面將舉例說明,如何將一個(gè)C#的引用類型轉(zhuǎn)換到對(duì)應(yīng)的OC類型栗竖。
- 首先在C#中聲明一個(gè)類型
Person
class Person
{
public string name;
public int age;
}
- 在C中聲明一個(gè)接口
printPersonInfo
用于打印傳遞過來的Person信息暑脆,如:
void printPersonInfo(void *personData);
- 在C#中聲明此接口
[DllImport("__Internal")]
static extern void printPersonInfo (IntPtr personData);
- 創(chuàng)建一個(gè)Person的實(shí)例,然后將其序列化成byte數(shù)組狐肢,這里使用到對(duì)象序列化的一些知識(shí)添吗。
Person person = new Person();
person.name = "vimfung";
person.age = 18;
List<byte> buf = new List<byte>();
//寫入name
byte[] bytes = BitConverter.GetBytes (person.name.Length);
if (BitConverter.IsLittleEndian)
{
Array.Reverse (bytes);
}
buf.AddRange (bytes);
buf.AddRange (Encoding.UTF8.GetBytes (person.name));
//寫入age
bytes = BitConverter.GetBytes (person.age);
if (BitConverter.IsLittleEndian)
{
Array.Reverse (bytes);
}
buf.AddRange(bytes);
byte[] bufBytes = buf.ToArray();
- 將byte數(shù)組通過
Marshal
類轉(zhuǎn)換為IntPtr
類型,并傳入給C接口份名。
//轉(zhuǎn)換成功IntPtr
IntPtr personData = Marshal.AllocHGlobal(bufBytes.Length);
Marshal.Copy(bufBytes, 0, personData, bufBytes.Length);
printPersonInfo(personData);
Marshal.FreeHGlobal(personData);
注意:Marshal
申請(qǐng)的內(nèi)存不是自動(dòng)回收的碟联,因此調(diào)用后需要通過顯示方法FreeHGlobal
調(diào)用釋放。
- 回到C代碼中僵腺,并實(shí)現(xiàn)其內(nèi)部處理邏輯鲤孵,如:
void printPersonInfo(void *personData)
{
int offset = 0;
//獲取name
int nameLen = (((unsigned char *)personData) [offset] << 24)
| (((unsigned char *)personData) [offset + 1] << 16)
| (((unsigned char *)personData) [offset + 2] << 8)
| (((unsigned char *)personData) [offset + 3]);
offset += 4;
char *nameBuf = malloc(sizeof(char) * (nameLen + 1));
memset(nameBuf, 0, nameLen);
memcpy(nameBuf, (char *)personData + offset, nameLen);
offset += nameLen;
NSLog(@"person name = %s", nameBuf);
//獲取age
int age = (((unsigned char *)personData) [offset] << 24)
| (((unsigned char *)personData) [offset + 1] << 16)
| (((unsigned char *)personData) [offset + 2] << 8)
| (((unsigned char *)personData) [offset + 3]);
NSLog(@"person age = %d", age);
}
- 導(dǎo)出iOS項(xiàng)目,編譯運(yùn)行可以看到日志里面的輸出結(jié)果
2018-01-29 14:38:56.378376+0800 ProductName[8584:1163121] person name = vimfung
2018-01-29 14:38:56.378509+0800 ProductName[8584:1163121] person age = 18
5. 返回值
除了基礎(chǔ)類型中的數(shù)值類型可以直接從iOS中返回給U3D外辰如,其他的類型是不能直接進(jìn)行返回的普监,其中理由也很簡(jiǎn)單,因?yàn)榉峭泄茴愋筒荒苤苯愚D(zhuǎn)換成托管類型琉兜。如果你想直接返回一個(gè)字符串給U3D鹰椒,那么在運(yùn)行時(shí)就會(huì)產(chǎn)生異常,因?yàn)檗D(zhuǎn)換成托管類型后他的內(nèi)存由系統(tǒng)管理呕童,一旦對(duì)象銷毀他就會(huì)被釋放內(nèi)存漆际,但它并不知道非托管模式下它是否被釋放。
為了解決返回值的問題夺饲,其實(shí)可以借助上面提到的Marshal
類型配合序列化的方式來進(jìn)行返回值的返回:
- 先定義C代碼中的接口
void* returnString(int *len)
{
NSString *retStr = @"Hello World";
*len = (int)retStr.length;
char *nameBuffer = malloc(sizeof(char) * (retStr.length + 1));
memcpy(nameBuffer, retStr.UTF8String, retStr.length);
return nameBuffer;
}
- 在C#中聲明該接口
[DllImport("__Internal")]
static extern IntPtr returnString (out int len);
- 調(diào)用該接口奸汇,并解析返回參數(shù)值
int strLen = 0;
IntPtr stringData = returnString(out strLen);
if (strLen > 0)
{
byte[] buffer = new byte[strLen];
Marshal.Copy(stringData, buffer, 0, strLen);
Marshal.FreeHGlobal(stringData);
string str = Encoding.UTF8.GetString(buffer);
Debug.Log(str);
}