IOS 游戏渠道SDK 抽象工程封装

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

猜你喜欢

转载自blog.csdn.net/bobbob32/article/details/79876387
今日推荐