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:以上代码仅提供思路和参考,部分地方是没有实现的,需要自行完善后方可运行。