JNI C++ 调用 Java 完全指南:Cocos2d-x 集成 Android SDK 实战

引言

n
在 Cocos2d-x 跨平台游戏开发中,虽然核心逻辑使用 C++ 编写,但发布到 Android 平台时经常需要集成第三方 SDK(支付、统计、广告、社交分享等)。这些 SDK 通常只提供 Java 接口,这就需要在 C++ 层通过 JNI(Java Native Interface)调用 Java 方法。本文将从 JNI 基础原理出发,详细介绍在 Cocos2d-x 中使用 JniHelper 进行 C++ 到 Java 调用的完整方案。

JNI 基础原理

什么是 JNI

JNI(Java Native Interface)是 Java 平台的标准机制,允许 Java 代码与 Native 代码(C/C++)进行交互。在 Android 开发中,JNI 被广泛用于以下场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
┌─────────────────────────────────────────────────────────────────────┐
│ JNI 双向调用架构 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Java 层 │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Android SDK (Java) │ │
│ │ • 支付宝/微信支付 │ │
│ │ • 友盟/Flurry 统计 │ │
│ │ • Facebook/Google 广告 │ │
│ │ • 推送服务 (FCM/JPush) │ │
│ └──────────────────────┬──────────────────────────────────────┘ │
│ │ JNI 接口层 │
│ ▼ │
│ Native 层 │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Cocos2d-x Game (C++) │ │
│ │ • 游戏逻辑 │ │
│ │ • UI 系统 │ │
│ │ • 渲染引擎 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 调用方向: │
│ C++ ──JNI──► Java (调用 SDK 功能) │
│ Java ──JNI──► C++ (接收 SDK 回调) │
│ │
└─────────────────────────────────────────────────────────────────────┘

JNI 方法签名规则

JNI 使用特定的字符串格式来描述 Java 方法的参数和返回类型,称为方法签名方法描述符

Java 类型 签名字符 示例
boolean Z Z
byte B B
char C C
short S S
int I I
long J J
float F F
double D D
void V V
对象 L类路径; Ljava/lang/String;
数组 [元素类型 [I (int[]), [Ljava/lang/String; (String[])

方法签名格式: (参数类型签名)返回类型签名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
┌─────────────────────────────────────────────────────────────┐
│ 方法签名解析示例 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Java 方法:public static String pay(String index) │
│ │
│ 签名:"(Ljava/lang/String;)Ljava/lang/String;" │
│ │
│ 解析: │
│ ( - 参数开始 │
│ Ljava/lang/String; - 一个 String 参数 │
│ ) - 参数结束 │
│ Ljava/lang/String; - 返回 String │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ Java 方法:public void init(int id, boolean debug) │
│ │
│ 签名:"(IZ)V" │
│ │
│ 解析: │
│ ( - 参数开始 │
│ I - int 参数 │
│ Z - boolean 参数 │
│ ) - 参数结束 │
│ V - 返回 void │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ Java 方法:public static int[] getScores() │
│ │
│ 签名:"()[I" │
│ │
│ 解析: │
│ () - 无参数 │
│ [I - 返回 int 数组 │
│ │
└─────────────────────────────────────────────────────────────┘

Cocos2d-x JniHelper 详解

JniHelper 简介

Cocos2d-x 封装了 JniHelper 工具类,简化了 JNI 调用的繁琐流程。主要功能包括:

  • getStaticMethodInfo:获取静态方法信息
  • getMethodInfo:获取实例方法信息
  • 自动管理 JNIEnv 指针

调用静态方法

静态方法是最常见的 JNI 调用场景,如调用支付 SDK 的支付接口:

Java 层代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package org.cocos2dx.cpp;

public class AppActivity extends Cocos2dxActivity {

/**
* 支付接口
* @param productId 商品 ID
* @return 支付结果
*/
public static String pay(String productId) {
// 调用第三方支付 SDK
// ...
return "success";
}

/**
* 无参数无返回值的初始化方法
*/
public static void initSDK() {
// 初始化 SDK
}

/**
* 多参数方法
*/
public static boolean reportEvent(String eventName, int value) {
// 上报事件
return true;
}
}

C++ 层调用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include "platform/android/jni/JniHelper.h"
#include <jni.h>

// 定义 Java 类的完整包路径
#define JAVA_CLASS_PATH "org/cocos2dx/cpp/AppActivity"

/**
* 调用 Java 静态方法(String 参数,String 返回)
*/
std::string callJavaPay(const std::string& productId) {
JniMethodInfo minfo;
std::string result = "";

// 第一步:获取方法信息
bool isHave = JniHelper::getStaticMethodInfo(
minfo, // 输出参数:方法信息
JAVA_CLASS_PATH, // Java 类路径
"pay", // Java 方法名
"(Ljava/lang/String;)Ljava/lang/String;" // 方法签名
);

if (!isHave) {
CCLOG("JNI: Method 'pay' not found in %s", JAVA_CLASS_PATH);
return result;
}

CCLOG("JNI: Found method, calling...");

// 第二步:将 C++ string 转换为 jstring
jstring jProductId = minfo.env->NewStringUTF(productId.c_str());

// 第三步:调用静态方法
jstring jResult = (jstring)minfo.env->CallStaticObjectMethod(
minfo.classID, // Java 类
minfo.methodID, // 方法 ID
jProductId // 参数
);

// 第四步:将 jstring 结果转换为 C++ string
if (jResult != nullptr) {
// 获取 String 类的 getBytes 方法
jclass clsString = minfo.env->FindClass("java/lang/String");
jstring strEncode = minfo.env->NewStringUTF("utf-8");
jmethodID mid = minfo.env->GetMethodID(
clsString,
"getBytes",
"(Ljava/lang/String;)[B"
);

// 调用 getBytes("utf-8") 获取字节数组
jbyteArray byteArray = (jbyteArray)minfo.env->CallObjectMethod(
jResult, mid, strEncode
);

// 读取字节数组
jsize len = minfo.env->GetArrayLength(byteArray);
jbyte* bytes = minfo.env->GetByteArrayElements(byteArray, JNI_FALSE);

if (len > 0) {
char* buffer = new char[len + 1];
memcpy(buffer, bytes, len);
buffer[len] = '\0';
result = std::string(buffer);
delete[] buffer;
}

// 释放资源
minfo.env->ReleaseByteArrayElements(byteArray, bytes, 0);
minfo.env->DeleteLocalRef(strEncode);
minfo.env->DeleteLocalRef(clsString);
minfo.env->DeleteLocalRef(jResult);
}

// 清理局部引用
minfo.env->DeleteLocalRef(jProductId);
minfo.env->DeleteLocalRef(minfo.classID);

CCLOG("JNI: Pay result = %s", result.c_str());
return result;
}

调用无返回值方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 调用无参数、无返回值的静态方法
*/
void callJavaInit() {
JniMethodInfo minfo;

bool isHave = JniHelper::getStaticMethodInfo(
minfo,
JAVA_CLASS_PATH,
"initSDK",
"()V" // 无参数,返回 void
);

if (isHave) {
minfo.env->CallStaticVoidMethod(minfo.classID, minfo.methodID);
minfo.env->DeleteLocalRef(minfo.classID);
CCLOG("JNI: initSDK called");
}
}

调用多参数方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 调用多参数静态方法:reportEvent(String, int) -> boolean
*/
bool callJavaReportEvent(const std::string& eventName, int value) {
JniMethodInfo minfo;
bool result = false;

bool isHave = JniHelper::getStaticMethodInfo(
minfo,
JAVA_CLASS_PATH,
"reportEvent",
"(Ljava/lang/String;I)Z" // String + int 参数,返回 boolean
);

if (isHave) {
jstring jEventName = minfo.env->NewStringUTF(eventName.c_str());

jboolean jResult = minfo.env->CallStaticBooleanMethod(
minfo.classID,
minfo.methodID,
jEventName,
value
);

result = (bool)jResult;

minfo.env->DeleteLocalRef(jEventName);
minfo.env->DeleteLocalRef(minfo.classID);
}

return result;
}

JNI 工具类封装

JniHelper 封装

为了简化调用流程,可以封装一个通用的 JNI 工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// JniBridge.h
#ifndef __JNI_BRIDGE_H__
#define __JNI_BRIDGE_H__

#include "platform/android/jni/JniHelper.h"
#include <string>
#include <vector>

class JniBridge {
public:
/**
* 调用 Java 静态 String 方法(String 参数)
*/
static std::string callStaticStringMethod(
const char* classPath,
const char* methodName,
const char* signature,
const std::string& param
);

/**
* 调用 Java 静态 void 方法(无参数)
*/
static void callStaticVoidMethod(
const char* classPath,
const char* methodName,
const char* signature = "()V"
);

/**
* 调用 Java 静态 void 方法(String 参数)
*/
static void callStaticVoidMethod(
const char* classPath,
const char* methodName,
const char* signature,
const std::string& param
);

/**
* 调用 Java 静态 boolean 方法(String + int 参数)
*/
static bool callStaticBoolMethod(
const char* classPath,
const char* methodName,
const char* signature,
const std::string& strParam,
int intParam
);

/**
* jstring 转 std::string
*/
static std::string jstring2string(JNIEnv* env, jstring jstr);

/**
* std::string 转 jstring
*/
static jstring string2jstring(JNIEnv* env, const std::string& str);
};

#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
// JniBridge.cpp
#include "JniBridge.h"

std::string JniBridge::jstring2string(JNIEnv* env, jstring jstr) {
if (jstr == nullptr) return "";

jclass clsString = env->FindClass("java/lang/String");
jstring strEncode = env->NewStringUTF("utf-8");
jmethodID mid = env->GetMethodID(clsString, "getBytes", "(Ljava/lang/String;)[B");
jbyteArray byteArray = (jbyteArray)env->CallObjectMethod(jstr, mid, strEncode);

jsize len = env->GetArrayLength(byteArray);
jbyte* bytes = env->GetByteArrayElements(byteArray, JNI_FALSE);

std::string result;
if (len > 0) {
char* buffer = new char[len + 1];
memcpy(buffer, bytes, len);
buffer[len] = '\0';
result = std::string(buffer);
delete[] buffer;
}

env->ReleaseByteArrayElements(byteArray, bytes, 0);
env->DeleteLocalRef(strEncode);
env->DeleteLocalRef(clsString);

return result;
}

jstring JniBridge::string2jstring(JNIEnv* env, const std::string& str) {
return env->NewStringUTF(str.c_str());
}

std::string JniBridge::callStaticStringMethod(
const char* classPath,
const char* methodName,
const char* signature,
const std::string& param
) {
JniMethodInfo minfo;
std::string result = "";

if (JniHelper::getStaticMethodInfo(minfo, classPath, methodName, signature)) {
jstring jParam = string2jstring(minfo.env, param);
jstring jResult = (jstring)minfo.env->CallStaticObjectMethod(
minfo.classID, minfo.methodID, jParam
);

result = jstring2string(minfo.env, jResult);

minfo.env->DeleteLocalRef(jParam);
if (jResult) minfo.env->DeleteLocalRef(jResult);
minfo.env->DeleteLocalRef(minfo.classID);
}

return result;
}

void JniBridge::callStaticVoidMethod(
const char* classPath,
const char* methodName,
const char* signature
) {
JniMethodInfo minfo;

if (JniHelper::getStaticMethodInfo(minfo, classPath, methodName, signature)) {
minfo.env->CallStaticVoidMethod(minfo.classID, minfo.methodID);
minfo.env->DeleteLocalRef(minfo.classID);
}
}

void JniBridge::callStaticVoidMethod(
const char* classPath,
const char* methodName,
const char* signature,
const std::string& param
) {
JniMethodInfo minfo;

if (JniHelper::getStaticMethodInfo(minfo, classPath, methodName, signature)) {
jstring jParam = string2jstring(minfo.env, param);
minfo.env->CallStaticVoidMethod(minfo.classID, minfo.methodID, jParam);
minfo.env->DeleteLocalRef(jParam);
minfo.env->DeleteLocalRef(minfo.classID);
}
}

bool JniBridge::callStaticBoolMethod(
const char* classPath,
const char* methodName,
const char* signature,
const std::string& strParam,
int intParam
) {
JniMethodInfo minfo;
bool result = false;

if (JniHelper::getStaticMethodInfo(minfo, classPath, methodName, signature)) {
jstring jStrParam = string2jstring(minfo.env, strParam);
jboolean jResult = minfo.env->CallStaticBooleanMethod(
minfo.classID, minfo.methodID, jStrParam, intParam
);
result = (bool)jResult;
minfo.env->DeleteLocalRef(jStrParam);
minfo.env->DeleteLocalRef(minfo.classID);
}

return result;
}

使用封装后的工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 支付调用示例
void GameLogic::onPayButtonClick(int productIndex) {
std::string productId = std::to_string(productIndex);

std::string result = JniBridge::callStaticStringMethod(
"org/cocos2dx/cpp/AppActivity",
"pay",
"(Ljava/lang/String;)Ljava/lang/String;",
productId
);

if (result == "success") {
CCLOG("Payment successful!");
onPaymentSuccess(productIndex);
} else {
CCLOG("Payment failed: %s", result.c_str());
onPaymentFailed(result);
}
}

// 事件上报示例
void GameLogic::reportLevelComplete(int level, int score) {
std::string eventName = "level_complete";

JniBridge::callStaticBoolMethod(
"org/cocos2dx/cpp/AppActivity",
"reportEvent",
"(Ljava/lang/String;I)Z",
eventName,
score
);
}

常见 SDK 集成场景

场景一:支付 SDK 集成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Java 层:支付封装
public class PaymentBridge {

public static void pay(String productId, String orderInfo) {
// 调用支付宝/微信支付 SDK
PayTask payTask = new PayTask(activity);
Map<String, String> result = payTask.payV2(orderInfo, true);

// 支付结果通过回调通知 C++ 层
String resultStatus = result.get("resultStatus");
onPayResult(Integer.parseInt(resultStatus), productId);
}

// Native 回调方法
private static native void onPayResult(int code, String productId);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// C++ 层:接收支付结果回调
extern "C" {
JNIEXPORT void JNICALL
Java_org_cocos2dx_cpp_PaymentBridge_onPayResult(JNIEnv* env, jclass clazz,
jint code, jstring productId) {
std::string id = JniBridge::jstring2string(env, productId);

Director::getInstance()->getScheduler()->performFunctionInCocosThread([=]() {
if (code == 9000) {
// 支付成功
GameData::getInstance()->onPaymentSuccess(id);
} else {
// 支付失败
GameData::getInstance()->onPaymentFailed(id, code);
}
});
}
}

场景二:统计 SDK 集成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// AnalyticsBridge.h
class AnalyticsBridge {
public:
static void init(const std::string& appKey);
static void logEvent(const std::string& eventName);
static void logEvent(const std::string& eventName, const std::string& param);
static void logEvent(const std::string& eventName, int value);
};

// AnalyticsBridge.cpp
void AnalyticsBridge::init(const std::string& appKey) {
JniBridge::callStaticVoidMethod(
"org/cocos2dx/cpp/AnalyticsBridge",
"init",
"(Ljava/lang/String;)V",
appKey
);
}

void AnalyticsBridge::logEvent(const std::string& eventName) {
JniBridge::callStaticVoidMethod(
"org/cocos2dx/cpp/AnalyticsBridge",
"logEvent",
"(Ljava/lang/String;)V",
eventName
);
}

JNI 调用注意事项

内存管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌─────────────────────────────────────────────────────────────┐
│ JNI 局部引用管理 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 必须 DeleteLocalRef 的对象: │
│ • jobject / jclass / jstring │
│ • jbyteArray / jintArray 等数组类型 │
│ • 通过 NewObject 创建的对象 │
│ │
│ 示例: │
│ jstring jstr = env->NewStringUTF("test"); │
│ // ... 使用 jstr │
│ env->DeleteLocalRef(jstr); // 必须释放 │
│ │
│ 注意:JniMethodInfo 的 classID 也需要释放 │
│ env->DeleteLocalRef(minfo.classID); │
│ │
│ 局部引用表上限: │
│ • 默认 512 个,超出会崩溃 │
│ • 大量调用时务必及时释放 │
│ │
└─────────────────────────────────────────────────────────────┘

线程安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* JNI 调用必须在正确的线程执行
*/
void safeJniCall() {
// Android 的 UI 操作必须在主线程
// Cocos2d-x 的调度器操作必须在 GL 线程

if (Director::getInstance()->getRunningScene() == nullptr) {
// 游戏未初始化完成,延迟执行
Director::getInstance()->getScheduler()->schedule(
[&](float dt) {
safeJniCall();
},
this, 0, 0, 0.1f, false, "delayed_jni"
);
return;
}

// 确保在 Cocos 线程中执行回调更新
Director::getInstance()->getScheduler()->performFunctionInCocosThread([]() {
// 更新 UI 或游戏状态
});
}

异常检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* JNI 调用后检查异常
*/
void checkJniException(JNIEnv* env) {
if (env->ExceptionCheck()) {
// 打印异常信息
env->ExceptionDescribe();
// 清除异常,避免后续调用失败
env->ExceptionClear();
}
}

// 使用示例
jstring jResult = (jstring)env->CallStaticObjectMethod(minfo.classID, minfo.methodID, jParam);
checkJniException(minfo.env);

完整调用流程总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
┌─────────────────────────────────────────────────────────────────────┐
│ JNI 调用标准流程 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 获取方法信息 │
│ JniMethodInfo minfo; │
│ bool ok = JniHelper::getStaticMethodInfo( │
│ minfo, classPath, methodName, signature); │
│ │
│ 2. 检查方法是否存在 │
│ if (!ok) { return; } │
│ │
│ 3. 准备参数(C++ 类型转 JNI 类型) │
│ jstring jParam = env->NewStringUTF(param.c_str()); │
│ jint jValue = value; │
│ │
│ 4. 调用方法 │
│ jobject jResult = env->CallStaticObjectMethod( │
│ minfo.classID, minfo.methodID, jParam, jValue); │
│ │
│ 5. 处理返回值(JNI 类型转 C++ 类型) │
│ std::string result = jstring2string(env, (jstring)jResult); │
│ │
│ 6. 释放所有局部引用 │
│ env->DeleteLocalRef(jParam); │
│ env->DeleteLocalRef(jResult); │
│ env->DeleteLocalRef(minfo.classID); │
│ │
│ 7. 检查异常 │
│ if (env->ExceptionCheck()) { env->ExceptionClear(); } │
│ │
└─────────────────────────────────────────────────────────────────────┘

总结

JNI C++ 调用 Java 的关键要点:

  1. 方法签名:准确掌握 JNI 类型签名规则,(参数)返回值 的格式必须完全匹配
  2. JniHelper:使用 Cocos2d-x 封装的 JniHelper 简化 JNIEnv 获取和方法查找
  3. 类型转换:C++ string 和 Java String 之间需要显式转换(NewStringUTF / getBytes)
  4. 内存管理:所有通过 JNI 创建的局部引用(jstring、jclass 等)必须调用 DeleteLocalRef 释放
  5. 线程安全:JNI 调用本身可以在任意线程,但涉及 UI 更新时必须通过 performFunctionInCocosThread 回到主线程
  6. 异常处理:调用后使用 ExceptionCheck/ExceptionClear 防止异常传播

通过封装通用的 JNI 工具类,可以大幅降低 Cocos2d-x 集成 Android SDK 的复杂度,使 C++ 业务代码保持简洁清晰。