iOS 游戏渠道SDK 抽象工程封装(上)
一款手机游戏,要是想挣钱,接入渠道SDK是很重要滴。但是渠道SDK有那么多家,每一家的接口也不一样,那么是否需要每一家渠道SDK都来接入一次呢?游戏的研发同学,每次想到这边,都表示一个头,两个大。
那么为了给研发的同学减轻负担,让他们专心搞研发,给所有渠道SDK封装一个抽象工程,是很有必要的一件事情。这样,游戏接入一次抽象工程就OK了,到时候要接入渠道SDK,只需要把文件替换一下,省时又省力,岂不是美美哒。
使用反射游戏渠道聚合
什么是渠道SDK的抽象工程?
抽象工程,可以说是渠道SDK的驱壳。这个驱壳,可以装下各种各样的渠道SDK。
虽然渠道SDK种类繁多,但是细心一看,他们的接口也是大同小异的。大体上有这么几个:
- 初始化
- 用户登陆
- 用户退出
- 用户支付
- 用户中心
- 工具栏打开关闭
摸清了他们的套路,咱们也可以大大方方地出手了。
抽象工程的总入口
游戏与抽象工程的所有交互,都是由这个类来完成。我们把它命名为SDKAccount。
先来个SDKAccount.h的代码
//获取单例
+ (instancetype)sharedInstance;
//sdk用户初始化
- (void)doInit:(NSString*)gameVersion;
//sdk登陆
- (void)doLogin;
//sdk退出
- (void)doLogout;
//sdk切换用户
- (void)doSwitchAccount;
//sdk支付
- (void)doPay:(SDKPayInfo *)sdkPayInfo;
//调用暂停页面
- (void)doPause;
//设置工具栏 YES打开/NO关闭
- (void)doSetting:(BOOL)visible;
//打开用户中心
- (void)doUserCenter;
为了使我们的抽象工程更方便地使用,我们这边采用单例的模式,使用sharedInstance 来获取抽象工程的单例。
然后大家是不是以为,接下来就是在SDKAccount.m里头来实现渠道SDK的代码啦。no no no,这样会使我们抽象工程的业务代码,和渠道SDK的业务混杂在一起,使代码变得杂乱不堪,这是我所不能容忍的。我们把渠道SDK的代码,统统放在另外一个地方,这个后面再讲。
先来讲讲SDKAccount.m
获取单例,我们用最常见的gcd方式来创建。
+ (instancetype)sharedInstance {
static SDKAccount* instance = nil;
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
instance = [[SDKAccount alloc] init];
});
return instance;
}
初始化
- (void)doInit:(NSString*)gameVersion {
[[SDKContainer sharedInstance] doThirdInit:self gameVersion:gameVersion];
}
登陆
- (void)doLogin {
[[SDKContainer sharedInstance] doLogin];
}
退出
- (void)doLogout {
[[SDKContainer sharedInstance] doLogout];
}
支付
- (void)doPay:(SDKPayInfo *)sdkPayInfo {
SDKUser *sdkUser = [SDKUser sharedInstance];
if(sdkUser.uuid == nil || sdkUser.uuid.length == 0) {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_PAY object:self userInfo:[SDKResponseBase dict:SDK_RESP_NO_LOGIN]];
return;
}
if(sdkPayInfo == nil) {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_PAY object:self userInfo:[SDKResponseBase dict:SDK_RESP_PARAM_ERROR]];
return;
}
if([sdkPayInfo.amount intValue] <= 0 || [sdkPayInfo.roleId length] == 0 || [sdkPayInfo.serverId length] == 0 || [sdkPayInfo.productId length] == 0) {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_PAY object:self userInfo:[SDKResponseBase dict:SDK_RESP_PARAM_ERROR]];
return;
}
SDKPayReq *payReq = [[SDKPayReq alloc] init];
payReq.amount = sdkPayInfo.amount;
payReq.platformUserId = [[SDKUser sharedInstance] uid];
payReq.appOrderId = sdkPayInfo.orderId;
payReq.appProductId = sdkPayInfo.productId;
payReq.appProductName = sdkPayInfo.productName;
payReq.appUserId = sdkPayInfo.userId;
payReq.appUserName = sdkPayInfo.username;
payReq.appRoleId = sdkPayInfo.roleId;
payReq.appRoleName = sdkPayInfo.roleName;
payReq.appRoleLevel = sdkPayInfo.roleLevel;
payReq.appServerId = sdkPayInfo.serverId;
payReq.appServerName = sdkPayInfo.serverName;
payReq.channelId = [self getChannelId];
payReq.deviceId = [self getDeviceId];
payReq.appNotifyUri = sdkPayInfo.notifyUri;
payReq.appExt = sdkPayInfo.ext;
payReq.vipLevel = sdkPayInfo.vipLevel;
[payReq post:^(NSHTTPURLResponse *response, NSDictionary *data) {
SDKLog(@"订单信息---%@", data);
NSNumber *code = data[@"code"];
if([code intValue] == 0) {
NSDictionary *info = data[@"data"];
NSString *orderId = info[@"order_id"];
NSString *amountStr = [NSString stringWithFormat:@"%d", [[payReq amount] intValue]/100];//单位为元
NSString *productId = payReq.appProductId;
NSString *productName = payReq.appProductName;
NSString *roleId = payReq.appRoleId;
NSString *serverId = payReq.appServerId;
NSString *serverName = payReq.appServerName;
NSString *payDesc = [NSString stringWithFormat:@"Product_%@", payReq.appProductId];
NSDictionary *orderMsg = @{
@"orderId":CleanNil(orderId),
@"amountStr":CleanNil(amountStr),
@"productId":CleanNil(productId),
@"productName":CleanNil(productName),
@"roleId":CleanNil(roleId),
@"serverId":CleanNil(serverId),
@"serverName":CleanNil(serverName),
@"payDesc":CleanNil(payDesc)
};
[[SDKContainer sharedInstance] doPayWithOrder:orderMsg];
} else {
[self notifitionCreateOrderError];
}
} failure:^(NSHTTPURLResponse *response, NSDictionary *data, NSError *error) {
[self notifitionCreateOrderError];
}];
}
到这边,有些朋友会问,为什么doPay的代码会多出这么多?那是因为这些代码,是我们自己的业务逻辑。
在调用渠道SDK的支付之前,我们需要先把支付信息传到我们的服务器上面,生成一个订单号,这个订单号,会记录在数据库中,作为以后游戏用户的支付凭据。生成完以后,再回传回来。这时候我们才能用这个订单号,来调用渠道SDK的支付接口。
暂停页面
- (void)doPause {
[[SDKContainer sharedInstance] doPause];
}
有些渠道SDK(比如91助手),要求在按下home键回到主界面,再切换回游戏时,会弹出一个暂停页面,用来展示广告。这时就要在AppDelegate中的applicationWillEnterForeground方法中,调用doPause这个方法。
工具栏(悬浮球)开
- (void)doSetting:(BOOL)visible {
[[SDKContainer sharedInstance] doSetting:visible];
}
打开用户中心
- (void)doUserCenter {
[[SDKContainer sharedInstance] doUserCenter];
}
切换账号
- (void)doSwitchAccount {
[[SDKContainer sharedInstance] doSwitchAccount];
}
切换账号实际上也就是先退出,再调用登陆窗口。
盛放渠道SDK代码的容器
前面讲到,为了不使我们的代码变得杂乱不堪,我们把业务逻辑和渠道SDK的代码分开来。创建一个新的类,用来盛放渠道SDK代码。这个类大家也猜到了,就叫做SDKContainer。
先上SDKContainer.h的代码。
@protocol SDKContainerDelegate <NSObject>
- (void)initFinish:(NSDictionary*)initMsg;
- (void)loginFinished:(NSDictionary*)loginMsg;
- (void)logoutFinished:(NSDictionary*)logoutMsg;
- (void)payFinished:(NSDictionary*)payMsg;
//登录成功
- (void)notifitionLoginSuccess:(SDKUser*)sdkUser;
//登录失败
- (void)notifitionLoginError;
//登录取消
- (void)notifitionLoginCancel;
//注销成功
- (void)notifitionLogoutSuccess;
//创建订单失败
- (void)notifitionCreateOrderError;
//充值用户未登录
- (void)notifitionPayNoLogin;
//充值成功
- (void)notifitionPaySuccess;
//充值失败
- (void)notifitionPayError;
//充值取消
- (void)notifitionPayCancel;
//充值发货中
- (void)notifitionPayShipping;
//充值网络异常
- (void)notifitionPayNetError;
@end
首先一上来是一个协议,有协议就有人遵守。没错,这个协议是为SDKAccount准备的。
先在SDKAccount.h的头部写上
@interface SDKAccount : NSObject
1
然后在SDKAccount.m中实现这些方法
- (void)initFinish:(NSDictionary*)initMsg {
[self doLogin];
}
- (void)loginFinished:(NSDictionary*)loginMsg {
// 登录成功,开发者可继续游戏逻辑
SDKLoginReq *loginReq = [[SDKLoginReq alloc] init];
loginReq.code = loginMsg[@"code"];
loginReq.uid = loginMsg[@"uid"];
loginReq.username = loginMsg[@"username"];
loginReq.nickname = loginMsg[@"nickname"];
[loginReq post:^(NSHTTPURLResponse *response, NSDictionary *data) {
//打印日志
SDKLog(@"登录信息---%@", data);
NSNumber *code = data[@"code"];
if([code intValue] == 0) {
NSDictionary *dataDic = data[@"data"];
SDKUser *sdkUser = [SDKUser sharedInstance];
sdkUser.uuid = dataDic[@"uuid"];
sdkUser.token = dataDic[@"check_token"];
sdkUser.platform = FYSDK_PLATFORM_NAME;
NSDictionary *user = dataDic[@"user"];
NSString *uid = user[@"id"];
NSString *username = user[@"name"];
NSString *nickname = user[@"nickname"];
if (loginReq.uid.length != 0) {
sdkUser.uid = loginReq.uid;
} else {
sdkUser.uid = uid;
}
if (loginReq.username.length != 0) {
sdkUser.username = loginReq.username;
} else {
sdkUser.username = username;
}
if (loginReq.nickname.length != 0) {
sdkUser.nickname = loginReq.nickname;
} else {
sdkUser.nickname = nickname;
}
self.sdkUser = sdkUser;
[self notifitionLoginSuccess:sdkUser];
} else {
[self notifitionLoginError];
}
} failure:^(NSHTTPURLResponse *response, NSDictionary *data, NSError *error) {
[self notifitionLoginError];
}];
}
- (void)logoutFinished:(NSDictionary*)logoutMsg {
[self notifitionLogoutSuccess];
}
- (void)payFinished:(NSDictionary*)payMsg {
}
//-------------------------各种通知-------------------------------
//登录成功
- (void)notifitionLoginSuccess:(SDKUser*)sdkUser{
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_LOGIN object:self userInfo:[SDKResponseBase dict:SDK_RESP_SUCCESS sdkUser:sdkUser]];
}
//登录失败
- (void)notifitionLoginError {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_LOGIN object:self userInfo:[SDKResponseBase dict:SDK_RESP_LOGIN_ERROR]];
}
//登录取消
- (void)notifitionLoginCancel {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_LOGIN object:self userInfo:[SDKResponseBase dict:SDK_RESP_CANCEL_ERROR]];
}
//创建订单失败
- (void)notifitionCreateOrderError {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_PAY object:self userInfo:[SDKResponseBase dict:SDK_RESP_CREATE_ORDER_ORDER]];
}
//注销成功
- (void)notifitionLogoutSuccess {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_LOGOUT object:self userInfo:[SDKResponseBase dict:SDK_RESP_SUCCESS]];
}
//注销失败
- (void)notifitionLogoutError {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_LOGOUT object:self userInfo:[SDKResponseBase dict:SDK_RESP_PARAM_ERROR]];
}
//充值用户未登录
- (void)notifitionPayNoLogin {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_PAY object:self userInfo:[SDKResponseBase dict:SDK_RESP_NO_LOGIN]];
}
//充值成功
- (void)notifitionPaySuccess {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_PAY object:self userInfo:[SDKResponseBase dict:SDK_RESP_SUCCESS]];
}
//充值失败
- (void)notifitionPayError {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_PAY object:self userInfo:[SDKResponseBase dict:SDK_RESP_PAY_ERROR]];
}
//充值取消
- (void)notifitionPayCancel {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_PAY object:self userInfo:[SDKResponseBase dict:SDK_RESP_CANCEL_ORDER]];
}
//充值发货中
- (void)notifitionPayShipping {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_PAY object:self userInfo:[SDKResponseBase dict:SDK_RESP_PAY_ING]];
}
//充值网络异常
- (void)notifitionPayNetError {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_PAY object:self userInfo:[SDKResponseBase dict:SDK_RESP_NET_ERROR]];
}
//暂停页面关闭通知
- (void)notifitionPuasePageClose {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_PAUSE_PAGE_CLOSE object:self userInfo:nil];
}
在SDKAccount的 doInit中,将self传给SDKContainer。这样SDKContainer就可以回调SDKAccount啦。
上面的loginFinished方法,也是调用了我们的业务逻辑。在渠道SDK登陆完以后,需要将token发到我们的服务器中,然后再由我们的服务器,转发给渠道SDK服务器去验证登陆。
我们来继续SDKContainer的内容。
SDKContainer.h
@interface SDKContainer : NSObject<XXXSDKDeletage>
+ (instancetype)sharedInstance ;
- (void)doThirdInit:(id<SDKContainerDelegate>)delegate gameVersion:(NSString*)gameVersion;
- (void)doLogin;
- (void)doLogout;
- (void)doPayWithOrder:(NSDictionary*)orders;
- (void)doPause;
- (void)doSetting:(BOOL)visible;
- (void)doUserCenter;
- (void)doSwitchAccount;
@end
有没有很眼熟,和SDKAccount很像,是吧。
然后是SDKContainer.mm
#import "SDKContainer.h"
@interface SDKContainer()
@property (nonatomic) id<SDKContainerDelegate> delegate;
@end
@implementation SDKContainer
+ (instancetype)sharedInstance {
static SDKContainer* instance = nil;
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
instance = [[SDKContainer alloc] init];
});
return instance;
}
- (void)doThirdInit:(id<SDKContainerDelegate>)delegate gameVersion:(NSString*)gameVersion {
self.delegate = delegate;
//----------------打印平台版本号---------------
NSString *platformVersion = @"";//更新SDK必填
NSLog(@"---Platform Version---%@", platformVersion);
//---------------sdk初始化代码-----------------
//0横屏 1竖屏
if([SDK_CONFIG_ORIENTATION isEqual:@"0"]) {
//-----------sdk横屏设置-----------
} else {
//-----------sdk竖屏设置-----------
}
}
- (void)doLogin {
//-----------sdk登陆接口-----------
}
- (void)doLogout {
//-----------sdk退出接口-----------
}
- (void)doPayWithOrder:(NSDictionary*)orders {
NSString *orderId = orders[@"orderId"]; //订单
NSString *amountStr = orders[@"amountStr"]; //金额,单位为元
NSString *productName = orders[@"productName"]; //商品名
NSString *roleId = orders[@"roleId"]; //角色名
NSString *serverId = orders[@"serverId"]; //区服id
NSString *payDesc = orders[@"payDesc"]; //额外支付信息
//-----------sdk支付接口-----------
}
- (void)doPause {
//------------sdk暂停页面-------------
}
- (void)doSetting:(BOOL)visible {
if (visible) {
//-----------sdk打开工具栏-----------
} else {
//-----------sdk关闭工具栏-----------
}
}
- (void)doUserCenter {
//-----------sdk打开个人中心-----------
}
- (void)doSwitchAccount {
[self doLogout];
[self doLogin];
}
//---------------渠道sdk回调接口----------------
//-----------------------------------------
这边我们为渠道SDK预留了位置,将来要接入渠道SDK的时候,只需要将渠道SDK的代码,放到SDKContainer.mm中对应的位置就行了。是不是很方便呢?
细心的朋友发现,SDKContainer.mm多了一个m出来。这是因为有些渠道SDK是用C++和Obj-C混合写的。所以要求我们要将.m改为.mm。平时我们使用.mm也不妨碍我们的代码。
当渠道SDK需要回调游戏的时候该怎么办?只需要调用一下delegate中的方法,就可以了。
*(1)初始化结束调用以下代码
[self.delegate initFinish:nil];
*(2)登陆结束调用以下代码
NSString *code = ; //登陆时的token
NSString *uid = ; //游戏账号的唯一值
NSString *username = ; //游戏账号名
NSDictionary *loginMsg = @{
@"code":code,
@"uid":uid,
@"username":username
};
[self.delegate loginFinished:loginMsg];
*(3)登陆取消调用以下代码
[self.delegate notifitionLoginCancel];
*(4)登陆失败调用以下代码
[self.delegate notifitionLoginError];
*(5)支付成功调用以下代
[self.delegate notifitionPaySuccess];
(6)支付取消调用以下代码
[self.delegate notifitionPayCancel];
*(7)支付失败调用以下代码
[self.delegate notifitionPayError];
*(8)支付用户未登陆调用以下代码
[self.delegate notifitionPayNoLogin];
*(9)创建订单失败调用以下代码
[self.delegate notifitionCreateOrderError];
*(10)充值发货中调用以下代码
[self.delegate notifitionPayShipping];
*(11)充值网络异常调用以下代码
[self.delegate notifitionPayNetError];
*(12)退出成功调用以下代码
[self.delegate notifitionLogoutSuccess];
然后由SDKAccount统一去回调游戏。
这样是不是将渠道SDK的代码和我们的业务代码,完全地分离开来了呢?
我们将渠道SDK的代码,和我们自己的业务代码分离,一个放在SDKContainer里面,一个放在SDKAccount里面。
这样做的好处,不止在于可以清晰地划分代码之间的界限,更重要的是,这样更加便于管理和维护。
试想一下,我们做抽象工程的目的是什么?是为了游戏可以不用频繁重复地接入渠道SDK嘛。那么怎么才能达到这个目的呢?
抽象工程的目录结构
我们将代码划分成两个部分:
第一部分是不用变动的部分,也就是说所有的渠道SDK,都来共用这部分的代码,是一些基础类。我们统一将他们放在文件夹Base里面。这个Base里面,包含了我们所有自己的业务代码:比如SDKAccount,网络请求类,工具类等等。
第二部分是需要变动的部分,也就是说所有的渠道SDK都有各自自己的一份。我们统一将他们放在Replace文件夹下。这里面包含了SDKContainer和渠道SDK的一些配置信息。
先给出一张目录截图:
这里写图片描述
ThirdLib用来存放渠道SDK的依赖库文件。
为什么我们要把目录的职责分得这么细呢?这是为了更好地在开发中起到一个分工明确地作用。
如何使用抽象工程
1.在实际开发过程中,我们先创建一个空的抽象工程。这时候SDKContainer中是还没有没有渠道SDK代码的。
2.这时研发的同学,来把我们的抽象工程拿过去,然后在游戏代码中调用我们的SDKAccount里面的接口。抽象工程算是接好了。但是现在还不能用,不是吗?你得到了我的驱壳,却没得到我的灵魂。。。不过这时研发的同学,可以把SDK的事情放下了,专心地去做研发。
3.负责专门接入SDK的同学,从商务手中拿到渠道SDK。然后他把抽象工程拷贝一份过来。拷贝完之后,把渠道SDK的依赖库,放到ThirdLib里面。然后在SDKContainer中,填入渠道SDK的代码。接着在SDKConfig中,填入渠道SDK的配置信息。最后测试一下,看有没有问题。没有问题再把它交给研发的同学。
4.研发的同学,拿到接好的渠道SDK的抽象工程,只需要动动手指头,把里面的Replace和ThirdLib文件夹拷贝到原来游戏工程中,将原来的两个文件夹替换一下,OK,接入完成。
5.以后再有新的渠道SDK,只需要重复步骤3和步骤4,就可以了。这个过程中,研发的同学,只需要接入一次抽象工程,接下来的事情,就是动动手指头,替换一下文件。接SDK的同学,也不用关心业务逻辑,只需要将SDK的代码,填入相应的位置就行。两个人都感觉工作量瞬间减小了很多,嘿嘿。。
项目管理
有些朋友发现,说了这么多,只不过是替换文件夹一下而已,为什么要分为Base和Replace?我把所有文件统统放在一个文件夹里面不行啊,到时候我把这个文件夹替换一下就好了啊。
先来看看渠道的管理目录
从这张图可以发现,我们把Base单独提出来了。我们知道,Xcode是可以使用引用的方式,引用一个文件夹里面的内容的,而不必一定要拷贝文件到工程里面去。也就是说,渠道和Base之间,是一个多对一的关系,每个渠道共用一个Base。
为什么要这样做?假设有一天,你的老大叫你修改或者添加一些自家的业务逻辑。哈哈,幸好,我把Base单独拿出来了。这样我只要修改一份代码就可以了~
后记
尽管抽象工程还有很多事情要做,例如网络请求,用户信息记录,数据采集等等,但是这些都是属于业务逻辑的范畴了,这边也不一一介绍了。希望抽象工程,对大家有帮助,让接入渠道SDK不再痛苦。。
转载出处iOS 游戏渠道SDK