Cocos Creator JNI 开发完全指南:C++ 调用 Java 方法实战与常见问题排查

引言

在 Cocos Creator 游戏开发中,当需要将游戏发布到 Android 平台并集成第三方 SDK(如支付、广告、统计等)时,经常需要在 C++ 层调用 Java 代码。JNI(Java Native Interface)是 Java 平台的标准机制,允许 Java 代码与本地代码(C/C++)进行交互。本文将详细介绍在 Cocos Creator 中使用 JNI 的技术细节、常见问题和最佳实践。

JNI 基础概念

什么是 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 API │ │ │
│ │ │ Activity │ │ (支付/广告) │ │ (系统服务) │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │
│ │ │ │ │ │ │
│ │ └──────────────────┴──────────────────┘ │ │
│ │ │ │ │
│ └────────────────────────────┼─────────────────────────────────┘ │
│ │ JNI 接口 │
│ ┌────────────────────────────┼─────────────────────────────────┐ │
│ │ Native 层 │ │ │
│ │ ┌─────────────────────────┴─────────────────────────────┐ │ │
│ │ │ Cocos2d-x C++ 引擎 │ │ │
│ │ │ ┌─────────────┐ ┌─────────────┐ ┌──────────┐ │ │ │
│ │ │ │ JniHelper │ │ 游戏逻辑 │ │ 渲染模块 │ │ │ │
│ │ │ │ JNI 封装类 │ │ │ │ │ │ │ │
│ │ │ └─────────────┘ └─────────────┘ └──────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘

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
┌─────────────────────────────────────────────────────────────────────┐
│ JNI 调用流程 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 获取 JNIEnv 指针 │
│ ├── 静态方法:JNI_OnLoad 中保存 │
│ └── 动态方法:通过 JniHelper 获取 │
│ │
│ 2. 查找 Java 类 │
│ FindClass("org/cocos2dx/cpp/AppActivity") │
│ │
│ 3. 获取方法 ID │
│ GetStaticMethodID / GetMethodID │
│ │
│ 4. 转换参数 │
│ 字符串: NewStringUTF │
│ 数组: NewByteArray / SetByteArrayRegion │
│ │
│ 5. 调用方法 │
│ CallStaticVoidMethod / CallStaticObjectMethod │
│ │
│ 6. 处理返回值 │
│ 字符串: GetStringUTFChars │
│ 对象: GetObjectClass / GetFieldID │
│ │
│ 7. 释放资源 │
│ DeleteLocalRef / ReleaseStringUTFChars │
│ │
└─────────────────────────────────────────────────────────────────────┘

Cocos Creator 中的 JNI 实现

JniHelper 工具类

Cocos2d-x 提供了 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
// cocos/platform/android/jni/JniHelper.h

class JniHelper {
public:
// 获取 JavaVM 实例
static JavaVM* getJavaVM();

// 设置 JavaVM(在 JNI_OnLoad 中调用)
static void setJavaVM(JavaVM* javaVM);

// 获取 JNIEnv 指针
static JNIEnv* getEnv();

// 获取静态方法信息
static bool getStaticMethodInfo(JniMethodInfo &methodinfo,
const char *className,
const char *methodName,
const char *paramCode);

// 获取实例方法信息
static bool getMethodInfo(JniMethodInfo &methodinfo,
const char *className,
const char *methodName,
const char *paramCode);
};

// JniMethodInfo 结构体
struct JniMethodInfo {
JNIEnv* env; // JNI 接口指针
jclass classID; // Java 类引用
jmethodID methodID; // 方法 ID
};

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
┌─────────────────────────────────────────────────────────────────────┐
│ JNI 方法签名格式 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 格式:(参数类型)返回值类型 │
│ │
│ 基本类型签名: │
│ ┌──────────┬──────────┐ │
│ │ Java 类型 │ JNI 签名 │ │
│ ├──────────┼──────────┤ │
│ │ boolean │ Z │ │
│ │ byte │ B │ │
│ │ char │ C │ │
│ │ short │ S │ │
│ │ int │ I │ │
│ │ long │ J │ │
│ │ float │ F │ │
│ │ double │ D │ │
│ │ void │ V │ │
│ └──────────┴──────────┘ │
│ │
│ 引用类型签名: │
│ ├── String: Ljava/lang/String; │
│ ├── Object: L完整类名; │
│ │ 例: Lorg/cocos2dx/cpp/AppActivity; │
│ ├── 数组: [类型 │
│ │ 例: [I (int[]), [Ljava/lang/String; (String[]) │
│ └── 多维数组: [[类型 │
│ 例: [[I (int[][]) │
│ │
│ 完整示例: │
│ void init(String) → (Ljava/lang/String;)V │
│ String pay(String, int) → (Ljava/lang/String;I)Ljava/lang/String;│
│ boolean check([B) → ([B)Z │
│ │
└─────────────────────────────────────────────────────────────────────┘

实战:支付接口调用

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
// NativePay.cpp
#include "platform/android/jni/JniHelper.h"
#include <jni.h>

#define CLASS_NAME "org/cocos2dx/cpp/AppActivity"

/**
* 调用 Java 支付方法
* @param productId 商品 ID
* @param amount 金额(分)
* @return 订单号
*/
std::string NativePay::pay(const std::string& productId, int amount) {
JniMethodInfo minfo;

// 1. 获取方法信息
bool isHave = JniHelper::getStaticMethodInfo(
minfo,
CLASS_NAME,
"pay", // Java 方法名
"(Ljava/lang/String;I)Ljava/lang/String;" // 签名:(String, int) 返回 String
);

if (!isHave) {
CCLOG("JNI: pay method not found");
return "";
}

// 2. 转换参数
jstring jProductId = minfo.env->NewStringUTF(productId.c_str());
jint jAmount = amount;

// 3. 调用方法
jstring jResult = (jstring)minfo.env->CallStaticObjectMethod(
minfo.classID,
minfo.methodID,
jProductId,
jAmount
);

// 4. 处理返回值
const char* cResult = minfo.env->GetStringUTFChars(jResult, nullptr);
std::string orderId(cResult);

// 5. 释放资源(重要!)
minfo.env->ReleaseStringUTFChars(jResult, cResult);
minfo.env->DeleteLocalRef(jProductId);
minfo.env->DeleteLocalRef(jResult);
minfo.env->DeleteLocalRef(minfo.classID);

return orderId;
}

对应的 Java 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// AppActivity.java
package org.cocos2dx.cpp;

public class AppActivity extends Cocos2dxActivity {

/**
* 支付方法(供 C++ 调用)
* @param productId 商品 ID
* @param amount 金额
* @return 订单号
*/
public static String pay(String productId, int amount) {
Log.d("JNI", "Pay called: productId=" + productId + ", amount=" + amount);

// 调用第三方 SDK 支付
String orderId = ThirdPartySDK.pay(productId, amount);

return orderId;
}
}

实战:字符串处理详解

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
/**
* JNI 字符串处理完整示例
* 演示 Java 字符串和 C++ 字符串的相互转换
*/
void stringOperationsExample() {
JniMethodInfo minfo;

// 调用返回字符串的方法
if (JniHelper::getStaticMethodInfo(minfo, CLASS_NAME, "getUserInfo",
"(I)Ljava/lang/String;")) {

jint userId = 12345;

// 调用 Java 方法获取字符串
jstring jResult = (jstring)minfo.env->CallStaticObjectMethod(
minfo.classID, minfo.methodID, userId
);

// ===== 方式一:直接获取 UTF-8 字符串 =====
const char* utfString = minfo.env->GetStringUTFChars(jResult, nullptr);
std::string cppString(utfString);
CCLOG("User info: %s", cppString.c_str());

// 释放字符串(必须!)
minfo.env->ReleaseStringUTFChars(jResult, utfString);


// ===== 方式二:获取 Unicode 字符串(宽字符)=====
const jchar* unicodeString = minfo.env->GetStringChars(jResult, nullptr);
jsize len = minfo.env->GetStringLength(jResult);

// 转换为 wchar_t(Windows)或 char16_t
std::u16string u16String((char16_t*)unicodeString, len);

minfo.env->ReleaseStringChars(jResult, unicodeString);


// ===== 方式三:创建 Java 字符串 =====
const char* nativeString = "Hello from C++";
jstring jNewString = minfo.env->NewStringUTF(nativeString);

// 使用 jNewString 调用其他 Java 方法...

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

实战:数组处理

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
/**
* JNI 数组处理示例
*/
void arrayOperationsExample() {
JniMethodInfo minfo;

// ===== 传递 byte[] 到 Java =====
if (JniHelper::getStaticMethodInfo(minfo, CLASS_NAME, "sendData", "([B)V")) {

// 准备 C++ 数据
unsigned char data[] = {0x01, 0x02, 0x03, 0x04, 0x05};
int dataLen = sizeof(data);

// 创建 Java byte 数组
jbyteArray jArray = minfo.env->NewByteArray(dataLen);

// 将 C++ 数据复制到 Java 数组
minfo.env->SetByteArrayRegion(jArray, 0, dataLen, (jbyte*)data);

// 调用 Java 方法
minfo.env->CallStaticVoidMethod(minfo.classID, minfo.methodID, jArray);

// 释放
minfo.env->DeleteLocalRef(jArray);
minfo.env->DeleteLocalRef(minfo.classID);
}

// ===== 从 Java 获取 byte[] =====
if (JniHelper::getStaticMethodInfo(minfo, CLASS_NAME, "getData", "()[B")) {

// 调用方法获取数组
jbyteArray jArray = (jbyteArray)minfo.env->CallStaticObjectMethod(
minfo.classID, minfo.methodID
);

// 获取数组长度
jsize len = minfo.env->GetArrayLength(jArray);

// 获取数组元素(复制方式)
jbyte* elements = minfo.env->GetByteArrayElements(jArray, nullptr);

// 复制到 C++ 容器
std::vector<unsigned char> cppData(elements, elements + len);

// 释放数组(模式 0:复制回并释放)
minfo.env->ReleaseByteArrayElements(jArray, elements, 0);

// 或者使用 Get/Set 区域操作(更高效,适合大数组)
std::vector<jbyte> buffer(len);
minfo.env->GetByteArrayRegion(jArray, 0, len, buffer.data());

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

// ===== 处理 int[]、float[] 等其他数组类型 =====
// 类似方式,使用对应的 NewIntArray、SetIntArrayRegion 等方法
}

常见问题与解决方案

问题一:找不到类或方法

1
2
3
4
5
6
7
8
9
10
11
// 错误示例
JniHelper::getStaticMethodInfo(minfo,
"org/cocos2dx/cpp/AppActivity", // 错误:使用 / 而不是 .
"pay",
"()V");

// 正确写法
JniHelper::getStaticMethodInfo(minfo,
"org/cocos2dx/cpp/AppActivity", // 使用 / 作为分隔符
"pay",
"(Ljava/lang/String;)Ljava/lang/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
/**
* JNI 调试辅助函数
*/
void jniDebugHelper() {
JNIEnv* env = JniHelper::getEnv();

// 1. 检查类是否存在
jclass clazz = env->FindClass("org/cocos2dx/cpp/AppActivity");
if (clazz == nullptr) {
CCLOG("Class not found!");
// 检查是否有异常
if (env->ExceptionCheck()) {
env->ExceptionDescribe(); // 输出异常信息到 logcat
env->ExceptionClear();
}
return;
}

// 2. 检查方法是否存在
jmethodID methodId = env->GetStaticMethodID(clazz, "pay", "()V");
if (methodId == nullptr) {
CCLOG("Method not found!");
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
}
env->DeleteLocalRef(clazz);
return;
}

env->DeleteLocalRef(clazz);
}

问题二:签名错误

1
2
3
4
5
6
# 使用 javap 工具获取正确的签名
javap -s org/cocos2dx/cpp/AppActivity

# 输出示例:
# public static java.lang.String pay(java.lang.String);
# Signature: (Ljava/lang/String;)Ljava/lang/String;

常见签名错误:

错误签名 正确签名 说明
(Ljava/lang/String)V (Ljava/lang/String;)V 缺少分号
(String) (Ljava/lang/String;) 必须使用完整类名
(II) (II)I 缺少返回值
()Z ()V 返回值类型不匹配

问题三:内存泄漏

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
/**
* JNI 内存管理最佳实践
*/
class JniResourceGuard {
public:
JniResourceGuard(JNIEnv* env) : _env(env) {}

~JniResourceGuard() {
// 自动释放所有资源
for (auto ref : _refs) {
_env->DeleteLocalRef(ref);
}
}

template<typename T>
T add(T ref) {
if (ref) _refs.push_back((jobject)ref);
return ref;
}

private:
JNIEnv* _env;
std::vector<jobject> _refs;
};

// 使用 RAII 管理资源
void safeJniCall() {
JniMethodInfo minfo;
if (!JniHelper::getStaticMethodInfo(minfo, CLASS_NAME, "getData", "()Ljava/lang/String;")) {
return;
}

JniResourceGuard guard(minfo.env);

jstring jResult = guard.add((jstring)minfo.env->CallStaticObjectMethod(
guard.add(minfo.classID), minfo.methodID
));

const char* cStr = minfo.env->GetStringUTFChars(jResult, nullptr);
std::string result(cStr);
minfo.env->ReleaseStringUTFChars(jResult, cStr);

// guard 析构时自动释放所有引用
}

问题四:线程安全问题

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
/**
* 子线程中使用 JNI
* JNIEnv 是线程相关的,不能跨线程使用
*/
void callJavaFromWorkerThread() {
std::thread([]() {
// 错误:在子线程直接使用 JniHelper
// JniHelper::getStaticMethodInfo(...) // 可能崩溃!

// 正确:附加线程到 JVM
JavaVM* jvm = JniHelper::getJavaVM();
JNIEnv* env = nullptr;

// 附加当前线程
jint attachResult = jvm->AttachCurrentThread(&env, nullptr);
if (attachResult != JNI_OK) {
CCLOG("Failed to attach thread");
return;
}

// 现在可以安全使用 JNI
jclass clazz = env->FindClass(CLASS_NAME);
// ... 执行 JNI 调用

// 分离线程
jvm->DetachCurrentThread();
}).detach();
}

问题五:中文乱码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 正确处理 UTF-8 字符串
*/
std::string jstring2string(JNIEnv* env, jstring jStr) {
if (jStr == nullptr) return "";

const char* cStr = env->GetStringUTFChars(jStr, nullptr);
std::string result(cStr);
env->ReleaseStringUTFChars(jStr, cStr);

return result;
}

// Java 端也要使用 UTF-8
// String result = new String(bytes, StandardCharsets.UTF_8);

完整封装类示例

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
// JniBridge.h
#ifndef __JNI_BRIDGE_H__
#define __JNI_BRIDGE_H__

#include <string>
#include <functional>

class JniBridge {
public:
// 初始化 SDK
static void initSDK(const std::string& appId);

// 支付
static std::string pay(const std::string& productId, int amount);

// 登录
static void login(std::function<void(bool, const std::string&)> callback);

// 分享
static void share(const std::string& title, const std::string& content);

// 获取设备信息
static std::string getDeviceInfo();

// 显示 Toast
static void showToast(const std::string& message);

// 复制到剪贴板
static void copyToClipboard(const std::string& text);
};

#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
// JniBridge.cpp
#include "JniBridge.h"
#include "platform/android/jni/JniHelper.h"
#include <jni.h>

#define CLASS_NAME "org/cocos2dx/cpp/SDKBridge"

void JniBridge::initSDK(const std::string& appId) {
JniMethodInfo minfo;
if (JniHelper::getStaticMethodInfo(minfo, CLASS_NAME, "initSDK",
"(Ljava/lang/String;)V")) {
jstring jAppId = minfo.env->NewStringUTF(appId.c_str());
minfo.env->CallStaticVoidMethod(minfo.classID, minfo.methodID, jAppId);
minfo.env->DeleteLocalRef(jAppId);
minfo.env->DeleteLocalRef(minfo.classID);
}
}

std::string JniBridge::pay(const std::string& productId, int amount) {
JniMethodInfo minfo;
std::string result;

if (JniHelper::getStaticMethodInfo(minfo, CLASS_NAME, "pay",
"(Ljava/lang/String;I)Ljava/lang/String;")) {
jstring jProductId = minfo.env->NewStringUTF(productId.c_str());
jint jAmount = amount;

jstring jResult = (jstring)minfo.env->CallStaticObjectMethod(
minfo.classID, minfo.methodID, jProductId, jAmount
);

if (jResult) {
const char* cResult = minfo.env->GetStringUTFChars(jResult, nullptr);
result = cResult;
minfo.env->ReleaseStringUTFChars(jResult, cResult);
minfo.env->DeleteLocalRef(jResult);
}

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

return result;
}

void JniBridge::showToast(const std::string& message) {
JniMethodInfo minfo;
if (JniHelper::getStaticMethodInfo(minfo, CLASS_NAME, "showToast",
"(Ljava/lang/String;)V")) {
jstring jMessage = minfo.env->NewStringUTF(message.c_str());
minfo.env->CallStaticVoidMethod(minfo.classID, minfo.methodID, jMessage);
minfo.env->DeleteLocalRef(jMessage);
minfo.env->DeleteLocalRef(minfo.classID);
}
}

// ... 其他方法实现

Java 回调 C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 注册 C++ 方法供 Java 调用
extern "C" {

// Java 调用: org.cocos2dx.cpp.SDKBridge.onPayResult
JNIEXPORT void JNICALL
Java_org_cocos2dx_cpp_SDKBridge_onPayResult(JNIEnv* env, jclass clazz,
jboolean success,
jstring orderId) {
const char* cOrderId = env->GetStringUTFChars(orderId, nullptr);
std::string orderIdStr(cOrderId);
env->ReleaseStringUTFChars(orderId, cOrderId);

// 在主线程处理回调
Director::getInstance()->getScheduler()->performFunctionInCocosThread([=]() {
// 通知游戏逻辑层
GameLogic::getInstance()->onPayResult(success, orderIdStr);
});
}

}
1
2
3
4
5
6
7
8
9
10
11
12
// SDKBridge.java
public class SDKBridge {

// C++ 回调方法
public static native void onPayResult(boolean success, String orderId);

// 支付回调处理
public static void handlePayCallback(boolean success, String orderId) {
// 回调到 C++
onPayResult(success, orderId);
}
}

总结

Cocos Creator JNI 开发的核心要点:

  1. 基础概念:理解 JNIEnv、jclass、jmethodID 等核心概念
  2. 方法签名:熟练掌握 JNI 签名格式,使用 javap -s 验证
  3. 参数转换:正确处理字符串、数组等数据类型的转换
  4. 内存管理:及时释放本地引用,避免内存泄漏
  5. 线程安全:子线程需要 Attach/Detach,回调使用主线程
  6. 错误处理:检查返回值,捕获和处理 JNI 异常
  7. 封装设计:封装通用工具类,简化调用代码

通过规范的 JNI 调用方式,可以高效地实现 Cocos Creator 与 Android 平台原生功能的集成。