前言
大約一個月前收到領(lǐng)導新布置的任務,要用Unity直接接入發(fā)行方ios sdk。當時我一下子就懵了,ios的Object-c沒接觸過啊,Unity和ios該怎么交互呀,完全什么都不懂。接到消息的那一刻整個人狀態(tài)都不好了,查閱了很多資料完全沒有頭緒也看不進去任何有關(guān)OC的基礎(chǔ)知識。還好有我們部門大楊哥耐心的講了一遍怎么弄。經(jīng)過大楊鍋的講解還有Google理解出來的一些知識,現(xiàn)已完整的對接完好幾個IOS 的SDK的接入工作。其實ios sdk接入并沒有你想的那么難,接下來我會舉例說明,跟大家分享一下我學到的東西,讓新手同學不要跟我一樣上來就懵。

文章目錄
Unity與IOS交互層C#代碼編寫
Unity接 iOS SDK你需要了解的Objective-C基礎(chǔ)知識
Unity 與IOS交互工作原理
ios 小7手游sdk接入演示
編譯器版本介紹
Unity :Unity 19.4.2f1 Personal
Xcode : Xcode 12.2
Unity與IOS交互層C#代碼編寫
Unity 廣泛的支持原生插件,即用 C、C++、Objective-C 等編寫的原生代碼庫。插件允許游戲代碼(用 C# 編寫)調(diào)用這些庫中的函數(shù)。
為了使用原生插件,首先需要使用基于 C 的語言編寫函數(shù)來訪問所需的功能并將它們編譯到庫中。在 Unity 中,還需要創(chuàng)建一個 C# 腳本來調(diào)用本機庫中的函數(shù)。
原生插件應提供一個簡單的 C 接口供 C# 腳本隨后向其他用戶腳本公開。當某些低級渲染事件發(fā)生時(例如,創(chuàng)建圖形設(shè)備時),Unity 也可以調(diào)用原生插件導出的函數(shù)?,F(xiàn)在編寫一下C#的腳本,此代碼來源于實際項目部分截取。
using System.Runtime.InteropServices;
namespace GameChannel
{
public class ChannelManager : Singleton<ChannelManager>
{
#if UNITY_IOS //c#中宏的概念 ,意思是當前平臺是iOS
[DllImport("__Internal")]
private static extern void SDk_Login(); //登錄
[DllImport("__Internal")]
private static extern void SDk_Logout();//注銷
[DllImport("__Internal")]
private static extern void SDk_SwitchAccount();//切換賬號(可選參數(shù))
[DllImport("__Internal")]
private static extern void SDk_Pay(string payData);//支付
[DllImport("__Internal")]
private static extern void SDk_Data(string thisdata); //向渠道發(fā)送游戲數(shù)據(jù)
//當前平臺是安卓,交互層插件對外調(diào)用名稱是通用的,但是方式上是略有不同用宏的概念做區(qū)分。
#elif UNITY_ANDROID
}
}
對上述代碼做下說明:在 iOS 上,插件以靜態(tài)方式鏈接到可執(zhí)行文件中,因此我們必須使用“ __Internal” 作為庫名。其他的平臺會通過動態(tài)的方式加載插件 [DllImport ("PluginName")]名稱。C#調(diào)用其他模塊的接口都是通過DllImport的方式來實現(xiàn)的。例如c#定義了void SDk_Login()方法,在ios的object-c中 也一定有void SDk_Login()方法。
Unity接 iOS SDK你需要了解的Objective-C基礎(chǔ)知識
Unity項目開發(fā),iOS平臺要接SDK的話,就需要寫Objective-C原生代碼的,對于沒使用過Objective-C的小伙伴不要慌。我一說你就懂了。
.h : 頭文件作為一種包含功能函數(shù)、數(shù)據(jù)接口聲明的載體文件,主要用于保存程序的聲明,而定義文件用于保存程序的實現(xiàn)
.m : 它是對.h頭文件中方法的實現(xiàn),外部不能訪問
.mm : 源代碼文件。和.m文件類似,唯一的不同點就是,除了可以包含Objective-C和C代碼以外,還可以包含C++代碼。
include與#import
當你需要在源代碼中包含頭文件的時候,你可以使用#include編譯選項也可以使用#import ,但是OC官方更推薦的方法是:#import。這個跟java的import 導包思想上非常相似。
""和<>的區(qū)別
例如 #import "UnityIos.h" 和 #import <Foundation/Foundation.h>兩種。使用""引入的是本地工程的文件,而使用<>引入的是系統(tǒng)庫的文件。
@interface與@implementation
@interface是類為對象提供特性描述(接口),@implementation是對@interface定義接口具體的實現(xiàn)。這兩個跟java中的接口的定義與實現(xiàn)上思想上是一致的。
方法前的+ 和 -
加號(+)的方法為類方法,這類方法是可以直接用類名來調(diào)用的。
減號(-)的方法為實例方法,必須使用這個類的實例才可以調(diào)用它。
打印日志
NSLog打印日志。如 NSlog(@"")
基本數(shù)據(jù)類型
NSString : 字符串
CGfloat : 浮點值的基本類型
NSInteger : 整型
BOOL : 布爾型
json使用
Unity和OC要傳遞數(shù)據(jù),常用的就是json格式。但是OC還和java的不一樣。
//json字符串轉(zhuǎn)化成字典
-(NSDictionary*)getJsonDic:(NSString*)jsonString{
NSData* jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
return [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableLeaves error:nil];
}
//字典轉(zhuǎn)化成json字符串
-(NSString*)arrayToJson:(NSMutableDictionary *)dic{
NSError *parseError = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:&parseError];
return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}
這些知識僅做作為基礎(chǔ)了解,其他詳細的用法大家可以找下Google和度娘
Unity 與IOS交互工作原理
熟悉工作原理前大家不妨看一下之前寫的博客:Unity 導出Xcode 項目的結(jié)構(gòu) 作為一個了解,每個Unity ios Xcode 項目都會有如下結(jié)構(gòu):
UnityFramework 庫文件部分,其中包含源、插件和相關(guān)框架。它還生成 UnityFramework.framework 文件。
Unity-iPhone 主啟動器部分,其中包含應用程序表示數(shù)據(jù)并會運行該庫。Unity-iPhone 目標對 UnityFramework 目標具有單一依賴關(guān)系。
要將 Unity 集成到另一個 Xcode 項目中,必須將兩個 Xcode 項目(原生項目和 Unity 生成的項目)合并到一個 Xcode 工作空間中,并將 UnityFramework.framework 文件添加到原生 Xcode 項目的應用程序 (Application) 目標的嵌入式二進制文件 (Embedded Binaries) 中。完成此操作后,可以使用 UnityFramework 類來控制 Unity 運行時。Unity直接導出Xcode工程目錄結(jié)構(gòu)如下圖:

要想了解原理就要找到程序的入口,熟悉OC或者C的朋友一定知道m(xù)ain方法,這是整個程序的入口。我們先看下MainApp/main.mm,這個文件做了什么呢。


從代碼中大概讀懂的意思將/Frameworks/UnityFramework.framework庫文件加載到應用程序中。然后看下UnityFramework/UnityFramework.h,通過UnityFramework Objective-C 類(該類是 UnityFramework.framework 的主體類)的實例來控制 Unity 運行時:其中的屬性方法我羅列一下。
UnityFramework類
+(UnityFramework*)getInstance :單例類方法,可將實例返回到 UnityFramework。
-(UnityAppController*)appController :返回 UIApplicationDelegate 的 UnityAppController 子類。這是原生端的根 Unity 類,可以訪問應用程序的視圖相關(guān)對象,例如 UIView、UIViewControllers、CADisplayLink 或 DisplayConnection。
-(void)setDataBundleId:(const char*)bundleId:設(shè)置捆綁包,Unity 運行時應在其中查找 Data 文件夾。應在調(diào)用 runUIApplicationMainWithArgc 或 runEmbeddedWithArgc 之前調(diào)用此方法。
-(void)runUIApplicationMainWithArgc:(int)argc argv:(char*[])argv:從沒有其他視圖的主要方法中運行 Unity 的默認方式。
-(void)runEmbeddedWithArgc:(int)argc argv:(char[])argv appLaunchOpts:(NSDictionary)appLaunchOpts:存在其他視圖時,如果需要運行 Unity,需要調(diào)用此方法。
-(void)unloadApplication :調(diào)用此方法可卸載 Unity,并在卸載完成后接收對 UnityFrameworkListener 的回調(diào)。Unity 將釋放占用的大部分內(nèi)存,但不會全部釋放。
-(void)registerFrameworkListener:(id<UnityFrameworkListener>)obj :注冊監(jiān)聽器對象,用于接收 UnityFramework 生命周期相關(guān)事件的回調(diào)。
-(void)unregisterFrameworkListener:(id<UnityFrameworkListener>)obj:取消注冊監(jiān)聽器對象。
-(void)showUnityWindow:在顯示非 Unity 視圖時調(diào)用此方法,也會顯示已經(jīng)在運行的 Unity 視圖。
- -(void)pause:(bool)pause:暫停 Unity
- -(void)setExecuteHeader:(const MachHeader*)header:必須在運行 Unity 之前調(diào)用此命令,CrashReporter 才能正常工作。
-(void)sendMessageToGOWithName:(const char)goName functionName:(const char)name message:(const char*)msg:此方法是 UnitySendMessage 的代理。它通過名稱查找游戲?qū)ο?,并使用單字符串消息參?shù)來調(diào)用 functionName。
(void)quitApplication:(int)exitCode:調(diào)用此方法可完全卸載 Unity,并在 Unity 退出后接收對 UnityFrameworkListener 的回調(diào)。Unity 將釋放所有內(nèi)存。
注意:進行此調(diào)用后,將無法在同一進程中再次運行 Unity??稍?AppController 上設(shè)置 quitHandler 以覆蓋默認進程終止


然后再看下Classes/main.mm,這個文件做了什么,根據(jù)代碼得知(UIApplicaitonMain方法),程序需要創(chuàng)建UnityAppController對象,也就是說UnityAppController.mm才是真正的程序入口。
- 到UnityAppController.mm里先調(diào)用- (BOOL)application:(UIApplication)application didFinishLaunchingWithOptions:(NSDictionary)launchOptions生命周期方法,進行Unity界面初始化

- 然后則調(diào)用- (void)applicationDidBecomeActive:(UIApplication*)application方法,方法中設(shè)置了UnityPause(0);表示Unity為啟動狀態(tài),在方法最后,執(zhí)行[self performSelector:@selector(startUnity:) withObject:application afterDelay:0];.

- 最后調(diào)用到-(void)startUnity:(UIApplication*)application方法,展示Unity游戲界面,完成了原生OC和Unity的交互。

ios 小7手游sdk接入演示
正常情況下要接入小7蘋果sdk,公司商務或者運營會提供相應的參數(shù)和接入文檔。本文演示不提供文檔和參數(shù)。
進入正題把小7 ios對應的庫文件加入進來,首先工程Libraries 創(chuàng)建一個文件夾(例如SDK文件夾),把小7依賴所有庫放到創(chuàng)建的SDK文件夾下,然后Libraries右鍵add files to Unity-iPhone ...把文件添加進來(下圖是添加后的圖),xcode 會自動把小7的文件添加到對應的庫和引用文件上。


按小7的文檔要求配置好info.plist文件然后需要設(shè)置的屬性也都弄好,準備工作就完事,然后進行下一步。接入sdk其實可以在UnityAppController.mm文件中進行的。但是為了清晰,創(chuàng)建一個UnityIos.m(UnityIos.h可忽略)外部引用放在Libraries/SDK文件夾下。
有同學會奇怪我在Unity c#層定義好了例如登陸的方法,也沒看到在OC中調(diào)用?。吭趺蠢鸬顷懓?。年輕人勿要著急繼續(xù)看。在UnityIos.m中我們定義一個和c#層SDk_Login()名一樣的方法體。請看如下代碼:(這一部分代碼是真實項目中部分截取,其中包含了小7 sdk 完整的登陸 支付 切換賬號等功能
#import "UnityIos.h"
#import <Foundation/Foundation.h>
#import <AdSupport/AdSupport.h>
#import <SMSDK/SMSDK.h>
#import "UnityAppController.h"
#import "UnityInterface.h"
@implementation UnityIos
//調(diào)用sdk登陸
void SDk_Login(){
NSLog(@"SDk_Login");
[SMSDK smLogin];
}
//調(diào)用sdk切換賬號功能
void SDk_Logout(){
NSLog(@"SDk_Logout");
[SMSDK smLogout];
}
//這個方法只是為了兼容sdk
void SDk_SwitchAccount(){
NSLog(@"SDk_SwitchAccount");
}
//調(diào)用sdk支付,游戲傳入sdk需要的參數(shù)數(shù)據(jù)(游戲客戶端協(xié)定好字段要統(tǒng)一)
void SDk_pay(void *payData){
NSLog(@"SDk_pay");
//直接傳json,oc無法識別所以要進行一個轉(zhuǎn)化
NSString *idList = [NSString stringWithUTF8String:payData];
NSData *jsonData = [idList dataUsingEncoding:NSUTF8StringEncoding];
NSError *err;
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:jsonData
options:NSJSONReadingMutableContainers error:&err];
if(err) {
NSLog(@"json解析失?。?@",err);
return;
}
NSLog(@"dic解析:%@",dic);
NSString *_price = [NSString stringWithFormat:@"%@",[dic valueForKey:@"price"]];
NSString *_p_level = [NSString stringWithFormat:@"%@",[dic valueForKey:@"playerlevel"]];
NSString *_game_sign= [NSString stringWithFormat:@"%@",[dic valueForKey:@"game_sign"]];
NSString *_subject= [NSString stringWithFormat:@"%@",[dic valueForKey:@"subject"]];
NSString *_game_area= [NSString stringWithFormat:@"%@",[dic valueForKey:@"area"]];
NSString *_game_role_id= [NSString stringWithFormat:@"%@",[dic valueForKey:@"roleId"]];
NSString *_game_role_name= [NSString stringWithFormat:@"%@",[dic valueForKey:@"roleName"]];
NSString *_game_guid= [NSString stringWithFormat:@"%@",[dic valueForKey:@"guid"]];
SMPayInfo *payInfo = [[SMPayInfo alloc] init];
payInfo.game_orderid =[NSString stringWithFormat:@"%@",[dic valueForKey:@"orderid"]]; //游戲訂單號 60個字符
payInfo.game_sign =_game_sign; //服務器返回的簽名?????????????
payInfo.game_price =_price; //價格單位:元
payInfo.subject =_subject; //道具簡介
payInfo.game_area =_game_area; //角色所在區(qū)服
payInfo.game_level =_p_level; //角色等級
payInfo.game_role_id =_game_role_id; //角色ID
payInfo.game_role_name =_game_role_name; //角色名稱
payInfo.notify_id = @"-1"; //回調(diào)通知ID 默認可以在后臺填寫
payInfo.extends_info_data = @""; //自定義擴展數(shù)據(jù)
payInfo.game_guid=_game_guid; //游戲登陸后服務器通過token解析拿到的guid
//調(diào)用支付接口
[SMSDK smPayWithNewPayInfo:payInfo];
}
// 把格式化的JSON格式的字符串轉(zhuǎn)換成字典
// @param jsonString JSON格式的字符串
// @return 返回字典
- (NSDictionary *)dictionaryWithJsonString:(NSString *)jsonString {
if (jsonString == nil) {
return nil;
}
NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
NSError *err;
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:jsonData
options:NSJSONReadingMutableContainers
error:&err];
if(err) {
NSLog(@"json解析失?。?@",err);
return nil;
}
return dic;
}
@end
上面的代碼看完了。還會有點小困惑,既然在Libraries/SDK/UnityIos.m文件下,OC是如何找到登陸方法的呢?UnityIos.m定義了SDk_Login(),這個方法在Classes/Native中能找到解釋,我給大家看一個圖啊.
這個圖就是在Unity c#層定義好的同名方法,編譯成了c++代碼放到了Xcode工程路徑下,成了溝通OC和c#的橋梁。游戲用戶點擊登陸按鈕即可調(diào)用OC的登陸方法完成登陸的客戶端流程。

至于回調(diào)為什么放在UnityAppController.mm里,有兩個點,其一小7 sdk設(shè)計問題,要做個全局回調(diào)我不知道怎么弄,其二是游戲把初始化放在生命周期哪里處理的,不這樣寫我也沒什么好辦法,畢竟我不是一個真正的ios開發(fā)者。正常的接sdk方法和回調(diào)都可以寫在UnityIos.m文件里面的。小7比較特殊啊所以這樣寫。到這里接入結(jié)束嘍,回調(diào)和相關(guān)代碼放在下面了。
#import <SMSDK/SMSDK.h>
#define SMSDKAppKey @"小7后臺申請的appKey"
@implementation UnityAppController //展示部分核心需要部分
- (BOOL)application:(UIApplication*)app openURL:(NSURL*)url options:(NSDictionary<NSString*, id>*)options
{
id sourceApplication = options[UIApplicationOpenURLOptionsSourceApplicationKey], annotation = options[UIApplicationOpenURLOptionsAnnotationKey];
NSMutableDictionary<NSString*, id>* notifData = [NSMutableDictionary dictionaryWithCapacity: 3];
if (url) notifData[@"url"] = url;
if (sourceApplication) notifData[@"sourceApplication"] = sourceApplication;
if (annotation) notifData[@"annotation"] = annotation;
AppController_SendNotificationWithArg(kUnityOnOpenURL, notifData);
return [SMSDK handleApplication:app openURL:url
sourceApplication:[options valueForKey:@"UIApplicationOpenURLOptionsSourceApplicationKey"]
annotation:[options valueForKey:@"UIApplicationOpenURLOptionsAnnotationKey"]];
}
//生命周期啟動時調(diào)用sdk初始化方法(ios的生命周期跟安卓概念差不多)
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
//省略了部分代碼
//設(shè)置全局回調(diào)
//初始化?
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(SMSDKInitCallback:) name:SMSDKInitDidFinishNotification object:nil];
//登陸
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(SMSDKLoginCallback:) name:SMSDKLoginNotification object:nil];
//注銷
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(SMSDKLogoutCallback:) name:SMSDKLogoutNotification object:nil];
//支付
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(SMSDKPayResultCallback:) name:SMSDKPayResultNotification object:nil];
//使用appKey初始化SDK
[SMSDK smInitWithAppKey:SMSDKAppKey];
return YES;
}
//初始化回調(diào)
- (void)SMSDKInitCallback:(NSNotification *)notify {
if (notify.object == kSMSDKSuccessResult) {
NSLog(@"初始化成功");
} else if (notify.object == kSMSDKFailedResult) {
NSLog(@"初始化失敗");
[SMSDK smInitWithAppKey:SMSDKAppKey]; //初始化失敗可以重新初始化
}
}
//登陸回調(diào)
- (void)SMSDKLoginCallback:(NSNotification *)notify {
NSLog(@"SMSDKLoginCallback");
if (notify.object == kSMSDKSuccessResult) {
NSLog(@"login callback success");
NSString *idfa = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];
NSString *deviceUUID = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
NSString *deviceModel = [[UIDevice currentDevice] model];
NSString *token = notify.userInfo[kSMSDKLoginTokenKey];
NSLog(@"解析token========:%@",token);
//這一步是和服務器協(xié)定需要的參數(shù),這里不做真實展示每個游戲邏輯都不一樣
NSDictionary *resultDict=@{@"":@1,@"":@"",@"":"",@"token":token,@"deviceUdid":deviceUUID,@"deviceId":deviceModel,@"idFa":idfa
};
NSData *data = [NSJSONSerialization dataWithJSONObject:resultDict options:NSJSONWritingPrettyPrinted error:nil];
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"json解析:%@",string);
//向 Unity發(fā)送數(shù)據(jù)
// 參數(shù)1綁定的場景 參數(shù)2 為對象上的腳本的一個成員方法名稱(腳本名稱不限制)
//參數(shù)3傳遞的json數(shù)據(jù)。這個跟安卓的一樣。
UnitySendMessage("GameLaunch","OnSdkLoginSuc",[string cStringUsingEncoding:NSASCIIStringEncoding]);
} else {
NSLog(@"login callback fail");
}
}
//切換賬號回調(diào)
- (void)SMSDKLogoutCallback:(NSNotification *)notify {
NSLog(@"LogoutCallback");
}
//支付結(jié)果回調(diào)
- (void)SMSDKPayResultCallback:(NSNotification *)notify {
//支付結(jié)果
if (notify.object == kSMSDKSuccessResult) {
//支付成功,刷新用戶數(shù)據(jù)
NSLog(@"支付成功");
} else if (notify.object == kSMSDKUserCancelResult) {
NSLog(@"支付取消");
} else if (notify.object == kSMSDKFailedResult) {
//支付錯誤,刷新用戶數(shù)據(jù),保障不漏單
NSString *errMsg = notify.userInfo[kSMSDKErrorShowKey];
errMsg = errMsg && [errMsg isEqualToString:@""] ? errMsg : @"支付失敗";
NSLog(@"支付錯誤");
}
}
為了方便只展示結(jié)果,展示初始化成功,意味著 sdk初始化接入是沒問題的

感悟
應標題那句話接入ios SDK沒有你想的那么難,這不是噱頭這是我真實的感受。我是一個搞安卓SDK的程序員,ios里面的很多思想都是和安卓Java 相通的文中我也做過解釋。只要你會用安卓接sdk,那么ios也不是那么難。大家要是有興趣可以看下Android sdk接入Unity 與 Android交互通信 之OPPO篇。
剛開始弄的時候,我承認我非常無助,找了oc語法大全??戳艘粫涂床幌铝?,即使看了一會也就忘了。等真正去操作的時候(實踐才是正道,光看知識點一會就忘),發(fā)現(xiàn)真沒那么難。思想上跟安卓接sdk一樣的,就是語法略微不同。稍微查一下就知道怎么搞了。如果文章對你有幫助留下一個贊唄,你的支持是我繼續(xù)寫下去的動力。

收尾
寫博文不易,希望大家多多支持,如有不對大家多多指正。寫出來就是記錄、學習和成長的過程。