C++与Java混编通用接口(cocos2d-x Android SDK接入)

 

[注:本文涉及C++代码需C++11支持]

背景

在用c++开发cocos2d-x游戏后期,由于需要大量接入各种SDK,于是出现这种情况:各类Android SDK基本都是Java版本,便涉及到C++调用Java,Java回调C++的问题。虽然我们已经有完善的JNI接口可以实现这种调用,但是经过多次SDK接入后,发现每次接SDK时,都要花大量的时间写JNI调用代码。

那么能不能做一个通用接口呢?所有SDK拿来都能用。这正是本文所要尝试做的事情。

思路

将JNI调用过程封装起来,提供一个类似如下形式的方法。

 

/* C++ */

std::string CallJavaSDK(std::string name, std::string args, std::function<void (std::string)> callback);
extern "C" {
    void Java_com_sample_x_AndroidJNI_OnResult(JNIEnv *env, jobject obj, jstring name, jstring j_result);
}
    
/* Java */
public class JNICall
{
    native private static void OnResult(final String name, final String args);
    
    static public void CallFromCpp(final String name, final String args)
    {
        // ...
    }
}
 

 

实际使用时,在Java部分,我们只需要在CallFromCpp中实现对SDK的调用,然后得到结果后回调OnResult到C++;C++部分则调用CallJavaSDK,然后在callback中处理结果即可。

在这个实现中,所有参数都是通过string传递的,我们可能需要一个处理协议(或序列化的过程),来把所有参数打包成一个字符串形式,具体实现可依各自需求进行处理。

 

原先的SDK接入

以下示例一个平台SDK的初始化,登录,内购的具体接入。

主要涉及MySDK.h, MySDK.cpp, MySDK.java这3个代码文件,简单实现如下:

MySDK.h 声明C++接口,供游戏逻辑调用。

// MySDK.h
void initSDK(std::string key, std::function<void (std::string)> on_result);
void login(int/* 登录相关参数,可能有多个,视具体实现 */, std::function<void (std::string)> on_result);
void buy(int/* 内购的相关参数,可能有多个,视具体实现 */, std::function<void (std::string)> on_result);

MySDK.cpp 具体的C++接口实现

// MySDK.cpp
#include "MySDK.h"
#include "platform/android/jni/JniHelper.h"
#include "cocos2d.h"

using namespace cocos2d;
std::function<void (std::string)> s_OnInitResult = nullptr;

/**
    initSDK 初始化SDK调用,大部分SDK都不会直接返回结果,而需要异步处理,这里on_result用来接收处理结果。
*/
void initSDK(std::string key, std::function<void (std::string)> on_result)
{
    s_OnInitResult = on_result;

    JniMethodInfo m;

    bool isHave = JniHelper::getStaticMethodInfo(m, "com/sample/x/MySDK", "initSDK", "(Ljava/lang/String;)V");

    if(!isHave) throw "java method not found";

    auto j_key = m.env->NewStringUTF(key.data());

    m.env->CallStaticVoidMethod(m.classID, m.methodID, j_key);

    m.env->DeleteLocalRef(j_key);
}

extern "C" {
    void Java_com_sample_x_AndroidJNI_OnInitResult(JNIEnv *env, jobject obj, jstring j_result)
    {
        std::string result = j_result ? env->GetStringUTFChars(jname, nullptr) : "";

        if(s_OnInitResult) s_OnInitResult(result);
    }
}
/**
    其它如登录、购买的操作类似,如下略去实现
*/

/**
    发起登录,内购
    这里on_result都仅使用一个string参数,实际情况可能更复杂(如登录结果、玩家相关信息等)
*/
void login(int/* 登录相关参数,可能有多个,视具体实现 */, std::function<void (std::string)> on_result)
{
    //
}
void buy(int/* 内购的相关参数,可能有多个,视具体实现 */, std::function<void (std::string)> on_result)
{
    //
}

extern "C" {
    void Java_com_sample_x_AndroidJNI_OnLoginResult(JNIEnv *env, jobject obj, jstring j_result)
    {
        //
    }
    void Java_com_sample_x_AndroidJNI_OnBuyResult(JNIEnv *env, jobject obj, jstring j_result)
    {
        //
    }
}

 

Java代码依具体SDK实现相关过程

 

 public class MySDK {
    native private static void OnInitResult(final String data);
    native private static void OnLoginResult(final String data);
    native private static void OnBuyResult(final String data);
    
    static public void initSDK(final String key)
    {
        // ... 完成后 调用 OnInitResult
    } 
    static public void login(final int arg)
    {
        // ... 完成后 调用 OnLoginResult
    } 
    static public void buy(final int arg)
    {
        // ... 完成后 调用 OnBuyResult
    } 
 }

 

由于每个SDK具体实现方式不同(如初始化需要的参数不一样,可能一个的SDK需要两个string,另一个则只需要一个),在实际接入时,我们需要写大量类似的接口代码。

 

通用接口

与原来接入方法类似,同样在三个文件中实现,只是现在我们将SDK的具体方法和参数通过两个string封装起来。

下面涉及AndroidJNI.h/cpp,AndroidJNI.java三个文件。

1、AndroidJNI.h 

// AndroidJNI.h

#pragma once

#include <string>
#include <functional>
#include <map>

namespace AndroidJNI
{
    /*
    name 是调用过程名称(自定义,与java部分实现中匹配即可)
    data 调用过程所需要的参数(打包)
    onCallback 异步回调
    
    这里同步处理返回值设为了空,上面思路中所设计的为string,事实上大多数SDK都不可能同步给出调用结果(由于C++代码在GL线程中执行,而SDK代码大多需要在UI线程中执行),用不上这个返回值
    */
    void callJNI(std::string name, std::string data, std::function<void (std::string)> onCallback);

    /*
    指定一个回调函数,用于接收SDK的主动调用,如消息通知等
    */
    void setKeptCallbacks(std::string name, std::function<void(std::string)> onCallback);
}
 

AndroidJNI.cpp

// AndroidJNI.cpp
#include "AndroidJNI.h"
#include "platform/android/jni/JniHelper.h"
#include "cocos2d.h"

using namespace cocos2d;

namespace AndroidJNI {
    std::function<void (std::string)> popCallbacks(std::string name);
    std::function<void (std::string)> getKeptCallbacks(std::string name);
}

std::string AndroidJNI::callJNI(std::string name, std::string data, std::function<void (std::string)> onCallback)
{
    mOnCallbacks[name] = onCallback;

    JniMethodInfo m;

    bool isHave = JniHelper::getStaticMethodInfo(m, "com/sample/x/AndroidJNI", "Call", "(Ljava/lang/String;Ljava/lang/String;)V");

    if(!isHave) throw "java method not found";

    auto jname = m.env->NewStringUTF(name.data());
    auto jdata = m.env->NewStringUTF(data.data());

    m.env->CallStaticVoidMethod(m.classID, m.methodID, jname, jdata);

    m.env->DeleteLocalRef(jname);
    m.env->DeleteLocalRef(jdata);
}

void AndroidJNI::setKeptCallbacks(std::string name, std::function<void(std::string)> onCallback)
{
    if(onCallback == nullptr)
    {
        mKeptCallbacks.erase(name);
    }
    else
    {
        mKeptCallbacks[name] = onCallback;
    }
}

std::function<void (std::string)> AndroidJNI::popCallbacks(std::string name)
{
    auto iter = mOnCallbacks.find(name);
    if(iter == mOnCallbacks.end()) return nullptr;
    auto func = iter->second;
    mOnCallbacks.erase(iter);
    return func;
}

std::function<void (std::string)> AndroidJNI::getKeptCallbacks(std::string name)
{
    auto iter = mKeptCallbacks.find(name);
    if(iter == mKeptCallbacks.end()) return nullptr;
    auto func = iter->second;
    return func;
}

extern "C" {
    void Java_com_sample_x_AndroidJNI_OnResult(JNIEnv *env, jobject obj, jstring jname, jstring jdata)
    {
        std::string name = jname ? env->GetStringUTFChars(jname, nullptr) : "";
        std::string data = jdata ? env->GetStringUTFChars(jdata, nullptr) : "";

        auto func = AndroidJNI::popCallbacks(name);
        if(!func)
        {
            func = AndroidJNI::getKeptCallbacks(name);
        }

        if(func)
        {
            Director::getInstance()->getScheduler()->performFunctionInCocosThread([func, data]() {
                func(data);
            });
        }
        else
        {
            // log error
        }
    }

AndroidJNI.java 

使用时实现SDKCall,加入具体的SDK调用过程,并在应用初始化时(如Activity的onLaunch)调用 AndroidJNI.Init来指定SDKCall。

package com.sample.x;

public class AndroidJNI
{
    static SDKCall _call;

    static public void Init(SDKCall call)
    {
        _call = call;
    }
 
    native private static void OnResult(final String name, final String data);
     
    static public void Call(final String name, final String data)
    {
        if(_call != NULL) _call(name, data);
    }
    public interface SDKCall {
        public void CallFromCpp(final String name, final String data);
    }
}

OK,有了以上通用接口,我们在接入SDK时就简单多了。

 


 

一个使用示例如下:

首先,实现一个封包和解包的方法来处理多个参数的情况,这个做一次就好,以后所有SDK接入都用它。

#include <string>
#include <vector>

/**
 打包和解包过程,这里只是一个简单的实现,限制是参数中不同有'\n'
 做好了可以封装起来,以后复用
 */
std::vector<std::string> unpack(std::string const & data)
{
    std::vector<std::string> r;
    size_t index = 0;
    for(;;)
    {
        auto n = data.find('\n', index);
        if(n == std::string::npos)
        {//如果最后一项后没有\n 丢掉它
            break;
        }
        r.push_back(data.substr(index, n - index));
        index = n + 1;
    }
    return r;
}
/* args 中的字符串中不能有'\n'**/
std::string pack(std::vector<std::string> args)
{
    std::string r;
    for(auto & i : args)
    {
        r += i + '\n';
    }
    return r;
}

 

以下逻辑代码(C++部分)

 

// 初始化SDK时
std::string sdk_key1 = ".....";
std::string sdk_key2 = ".....";

AndroidJNI::callJNI("initSDK", pack({sdk_key1, sdk_key2}), [](std::string res) {
    auto args = unpack(res);
    if(!args.size()) throw "error";
    if(args[0] == "ok") printf("sdk 初始化成功");
    else ; // ....
});

// 登录时
int login_arg = 0;
auto arg1 = std::to_string(login_arg);
AndroidJNI::callJNI("login", pack({ arg1 }), [](std::string res) {
    // 如上类似
});

 

逻辑代码Java部分

// 实现SDK调用
public class MySDKCall extends AndroidJNI.SDKCall {

    @Override
    public void CallFromCpp(final String name, final String data)
    {
        final String[] args = data.split("\n");
        if(name == "initSDK")
        {
            // check args count here?

            String key1 = args[0];
            String key2 = args[1];

            // call SDK ... 然后调用 AndroidJNI.OnResult('initSDK', <调用结果打包String>)
        }
        else if(name == "login")
        {
            // exception?!

            int login_arg = Integer.parseInt(args[0]);

            // call SDK ... 然后调用 AndroidJNI.OnResult('login', <调用结果打包String>)
        }
    }
}

// 在Activity onLaunch中
AndroidJNI.Init(new MySDKCall());

 


 

部分SDK有主动通知的功能,可通过setKeptCallbacks调用来实现。

C++

设定回调函数,在回调函数内完成处理逻辑

AndroidJNI::setKeptCallbacks("sdk_callback", [](std::string res) {
    // 处理res
};

Java

SDK发起通知时,调用

AndroidJNI::OnResult("sdk_callback", <SDK调用参数打包String>)

 

结束。

 

PS:以上代码仅提供思路和参考,部分地方是没有实现的,需要自行完善后方可运行。

 

posted @ 2017-05-25 18:05  巴巴厘  阅读(905)  评论(0)    收藏  举报