cocos2d-x与原生安卓交互

最近使用cocos2d-x与Android交互时的一些记录,使用的是cocos2d-x提供的JniHelper,基于cocos2d-x v3.15,具体的一些代码也放在了这里

cocos2d-x调用Java

使用JniHelper

JniHelper是cocos2d-x对JNI的一系列接口的封装,cocos2d-x的C++代码和原生Android的java代码的交互可以通过JniHelper来完成。当需要在cocos2d-x中使用JniHelper时,包含头文件即可,注意头文件的位置:

1
2
3
#if(CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
#include "platform/android/jni/JniHelper.h"
#endif

在JniHelper中,有一个最重要的结构体JniMethodInfo,用于定位Java方法的信息,定义如下:

1
2
3
4
5
6
typedef struct JniMethodInfo_
{
JNIEnv * env;
jclass classID;
jmethodID methodID;
} JniMethodInfo;

其中包含三项内容。其中JNIEnv是与线程相关的结构体,由每个线程专属,不能跨线程访问。JNIEnv可以访问存储有JNI函数指针的数组。而jclassjmethodID分别对应调用的类和方法ID。在函数调用时需要用到JniMethodInfo结构体,先看函数调用的步骤:

函数调用

在C++中调用Java函数通常分为两步,首先获取函数信息,然后再调用。

首先获取函数信息,JniHelper中提供了两个静态函数,分别用来获取Java静态函数和非静态函数(实例方法)的信息:

1
2
3
4
5
6
7
8
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的引用,以及类名、方法名、函数签名,返回一个bool值,表示对应的Java方法是否可调用。参数中的三个字符串,类名需要是包含包名的完整类名,方法即Java中被调用方法的名,最后一个函数签名,是根据函数返回值类型和函数参数类型得到的一个独特的字符串。返回值和参数类型对应的字符串标示见下表,

标示类型 Java类型
Z boolean
B byte
C char
S short
I int
J long
F float
D double
Ljava/lang/String; String
[I int[]
[Ljava/lang/object; Object[]

数组类型一般在前边加[,数组之外的引用类型需要在后边加;,以下是三个示例:

函数签名 Java函数
()Ljava/lang/String; string f()
(ILjava/lang/Class;)J long f(int i,Class c)
([B)V void f(byte[] bytes)

除了根据类型拼接函数签名,还可以取Java编译后的class文件,执行:

1
javap –s -p xxx

则会输出包含的函数签名信息。

接着刚才,在获取函数信息之后,如果判断此函数可调用,则可以执行下一步,调用:

1
2
3
4
5
JniMethodInfo t;
// ...
t.env->Call...Method(...);

此时根据调用的Java函数签名不同,所使用的JNIEnv的函数也不一样,如调用一个静态无参返回值为空的Java函数,需要使用CallStaticVoidMethod(...)。OK,现在看一个完整的例子,首先是Java代码:

1
2
3
4
5
6
7
8
9
10
package com.aillieo.cocos2d_x_jni_test;
import org.cocos2dx.cpp.AppActivity;
// ... 略去无关代码
public class SceneTest {
public static void testJni() {
System.out.println("testJni() is called!");
}
}

接下来是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
//
// SceneTest.cpp
//
#include "SceneTest.hpp"
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
#include <jni.h>
#include "platform/android/jni/JniHelper.h"
#define CLASS_NAME "com/aillieo/cocos2d_x_jni_test/SceneTest"
#endif
// ... 略去无关代码
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
void SceneTest::CallJavaMethod()
{
JniMethodInfo t;
cocos2d::log("will try call method");
if(JniHelper::getStaticMethodInfo(t, CLASS_NAME, "testJni", "()V"))
{
cocos2d::log("has this static method");
t.env->CallStaticVoidMethod(t.classID, t.methodID);
}
}
#endif
// ... 略去无关代码

这是一个最简单的例子了,没有参数传入,也没有返回值。

Java类型映射

在C++中调用Java代码时,很多情况下需要传入参数或者接收返回值。Java中的类型在C++中(Native)的映射如下两表所示,分别是Java中的值类型和引用类型。

Java Native类型 描述
boolean jboolean 无符号8位整型(unsigned char)
byte jbyte 带符号8位整型(char)
char jchar 无符号16位整型(unsigned short)
short jshort 带符号16位整型(short)
int jint 带符号32位整型(int)
long jlong 带符号64位整型(long)
float jfloat 32位浮点型(float)
double jdouble 64位浮点型(double)

基本数据类型可以在C++代码中直接使用。

Java引用类型 Native类型
All objects jobject
char[] jcharArray
java.lang.Class实例 jclass
short[] jshortArray
java.lang.String实例 jstring
int[] jintArray
Object[] jobjectArray
long[] jlongArray
boolean[] jbooleanArray
float[] floatArray
byte[] jbyteArray
double[] jdoubleArray
java.lang.Throwable实例 jthrowable

Java引用类型不能在C++中直接使用,必须通过封装在JniMethodInfo中的JNIEnv来操作。

带参数的方法

调用带参数的Java静态方法示例代码如下:

Java方法:

1
2
3
4
5
6
7
8
9
10
11
package com.aillieo.cocos2d_x_jni_test;
import org.cocos2dx.cpp.AppActivity;
// ... 略去无关代码
public class SceneTest {
public static void sendParam(final int index, final String text) {
// ...
}
}

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
//
// SceneTest.cpp
//
#include "SceneTest.hpp"
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
#include <jni.h>
#include "platform/android/jni/JniHelper.h"
#define CLASS_NAME "com/aillieo/cocos2d_x_jni_test/SceneTest"
#endif
// ... 略去无关代码
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
void SceneTest::sendParam(int index, std::string str)
{
JniMethodInfo t;
cocos2d::log("will try call method");
if(JniHelper::getStaticMethodInfo(t, CLASS_NAME, "sendParam", "(ILjava/lang/String;)V"))
{
cocos2d::log("has this static method");
jint jIndex = index;
jstring jText = t.env->NewStringUTF(str.c_str());
t.env->CallStaticVoidMethod(t.classID, t.methodID, jIndex, jText);
t.env->DeleteLocalRef(jText);
}
}
#endif
// ... 略去无关代码

返回值处理

处理返回值,即从Java代码中获取信息。

Java方法:

1
2
3
4
5
6
7
8
9
10
11
package com.aillieo.cocos2d_x_jni_test;
import org.cocos2dx.cpp.AppActivity;
// ... 略去无关代码
public class SceneTest {
public static String getText() {
return "Text from Android";
}
}

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
//
// SceneTest.cpp
//
#include "SceneTest.hpp"
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
#include <jni.h>
#include "platform/android/jni/JniHelper.h"
#define CLASS_NAME "com/aillieo/cocos2d_x_jni_test/SceneTest"
#endif
// ... 略去无关代码
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
std::string SceneTest::getText()
{
JniMethodInfo t;
cocos2d::log("will try call method");
if(JniHelper::getStaticMethodInfo(t, CLASS_NAME, "getText", "()Ljava/lang/String;"))
{
cocos2d::log("has this static method");
jstring outStr = (jstring)t.env->CallStaticObjectMethod(t.classID, t.methodID);
const char* c_str= t.env->GetStringUTFChars(outStr, NULL);
std::string ret(c_str);
t.env->ReleaseStringUTFChars(outStr, c_str);
return ret;
}
}
#endif
// ... 略去无关代码

调用实例方法

之前讨论的都是对Java静态方法的调用,对于实例方法,首先要通过静态方法获取到实例对象。首先有Java静态方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.aillieo.cocos2d_x_jni_test;
import org.cocos2dx.cpp.AppActivity;
// ... 略去无关代码
public class SceneTest {
static AppActivity m_activity;
// ... 略去无关代码
public static AppActivity getJavaActivity() {
return m_activity;
}
}

在类AppActivity里增加一个实例方法testInstanceMethod()

1
2
3
4
5
6
7
8
9
10
11
12
package org.cocos2dx.cpp;
// ... 略去无关代码
public class AppActivity {
// ... 略去无关代码
public void testInstanceMethod() {
System.out.println("testInstanceMethod() is called!");
}
}

接下来在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
//
// SceneTest.cpp
//
#include "SceneTest.hpp"
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
#include <jni.h>
#include "platform/android/jni/JniHelper.h"
#define CLASS_NAME "com/aillieo/cocos2d_x_jni_test/SceneTest"
#define CLASS_NAME_APP "org/cocos2dx/cpp/AppActivity"
#endif
// ... 略去无关代码
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
void SceneTest::testInstanceMethod()
{
JniMethodInfo t;
cocos2d::log("will try call method");
if(JniHelper::getStaticMethodInfo(t, CLASS_NAME, "getJavaActivity", "()Ljava/lang/Object;"))
{
cocos2d::log("has this static method");
jobject activityObj;
activityObj = (jobject)t.env->CallStaticObjectMethod(t.classID, t.methodID);
cocos2d::log("will try call instance method");
if(JniHelper::getMethodInfo(t, CLASS_NAME_APP, "testInstanceMethod", "()V"))
{
cocos2d::log("has this instance method");
t.env->CallVoidMethod(activityObj, t.methodID);
}
}
}
#endif
// ... 略去无关代码

处理自定义类型

某些情况下,需要在C++中使用Java的对象,即处理Java的自定义类型。之前已经可以通过函数调用来获取jobject对象,本节我们将有更进一步的操作:

获取与解析class

获取Java类型有两种方法,可以直接使用FindClass()或根据获取到的jobject对象通过GetObjectClass()来获取:

1
2
3
4
5
6
7
8
JniMethodInfo t;
jobject activityObj;
// ...
// 两种获取jclass的方法
jclass myclass = t.env->FindClass("org/cocos2dx/cpp/MyClass");
jclass myclass = t.env->GetObjectClass(activityObj);

有了jclass之后,就可以获取其中的字段和方法了,根据是否静态,通过JNIEnv获取字段jfieldID和获取方法jmethodID的函数有以下四个:

1
2
3
4
jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);

其中name表示字段或方法的名称,sig表示字段类型或方法函数的签名。

操作字段与方法

获取到字段jfieldID和方法jmethodID之后,可以对字段进行get/set操作,具体的函数有以下的形式:

1
2
3
4
5
6
7
8
9
10
11
//静态属性
//Get
NativeType GetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID);
//Set
void SetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID, NativeType value);
//非静态属性
//Get
NativeType Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID);
//Set
void Set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID, NativeType value);

其中NativeType是Java类型在C++中的映射,<type>是Java中的基本数据类型,见下表:

\ NativeType
Object jobject
Boolean jboolean
Byte jbyte
Char jchar
Short jshort
Long jint
Int jlong
Float jfloat
Double jdouble

对于获取到的jmethodID,可以根据jmethodID调用方法,和上文中的一样,使用t.env->Call...Method(...)

cocos2d-x 调用Android原生控件

通过jni调用到的函数并不能直接作用于Android的UI线程,必须通过消息来实现。如现在想要通过C++调用Java代码,显示一个Android原生的警示框AlertDialog。Java中有类SceneAlert,其有静态成员函数showAlert()接受来自C++的调用,则需要有以下Java代码来实现,分别位于AppActivity类和SceneAlert类中:

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
package org.cocos2dx.cpp;
import org.cocos2dx.lib.Cocos2dxActivity;
import android.app.AlertDialog;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
// ...略去无关代码
import com.aillieo.cocos2d_x_sdk_integration.SceneAlert;
public class AppActivity extends Cocos2dxActivity {
public static final int SHOW_ALERT = 0x0001;
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
SceneAlert.setHandler(m_Handler);
}
private Handler m_Handler = new Handler()
{
public void handleMessage(Message msg) {
switch (msg.what)
{
case SHOW_ALERT: {
AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(AppActivity.this);
AlertDialog alertDialog = alertDialogBuilder.create();
alertDialog.setMessage("message");
alertDialog.show();
}
break;
}
}
};
}

以及

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.aillieo.cocos2d_x_sdk_integration;
import org.cocos2dx.cpp.AppActivity;
import android.os.Handler;
import android.os.Message;
public class SceneAlert {
static private Handler m_handler;
public static void setHandler(Handler handler)
{
m_handler = handler;
};
public static void showAlert() {
Message msg = new Message();
msg.what=AppActivity.SHOW_ALERT;
m_handler.sendMessage(msg);
}
}

Java调用cocos2d-x

调用C++函数

在Java中声明本地方法,需要在方法前加上关键字Native,然后在C++中实现本地方法,注意,声明方法名称和参数的匹配,Java代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.cocos2dx.cpp;
public class AppActivity extends Cocos2dxActivity {
// ...略去无关代码
public static native void sendText(String text);
// ...略去无关代码
// 某个响应函数
public void xxxx()
{
// ...略去无关代码
sendText("text from java);
}
}

在C++中要有对应sendText的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//
// SceneTest.cpp
//
// ...略去无关代码
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
extern "C"
{
void Java_org_cocos2dx_cpp_AppActivity_sendText(JNIEnv *env,jobject thiz,jstring text)
{
std::string str = JniHelper::jstring2string(text);
cocos2d::log("text from java is : %s", str.c_str());
}
}
#endif

cocos2d-x中跨线程处理

C++中,接收到Java调用的函数,无法直接与cocos2d-x主线程直接交互,可以通过cocos2d-x提供的方法performFunctionInCocosThread。如修改刚才的例子,接收到Java对Java_org_cocos2dx_cpp_AppActivity_sendText()的调用之后,发送自定义事件TEXT_FROM_JAVA,则需要将函数修改为:

1
2
3
4
5
6
7
8
void Java_org_cocos2dx_cpp_AppActivity_sendText(JNIEnv *env,jobject thiz,jstring text)
{
std::string str = JniHelper::jstring2string(text);
cocos2d::log("text from java is : %s", str.c_str());
Director::getInstance()->getScheduler()->performFunctionInCocosThread([str]()mutable{
Director::getInstance()->getEventDispatcher()->dispatchCustomEvent("TEXT_FROM_JAVA",&str);
});
}

REFERENCE

http://blog.csdn.net/sgn132/article/details/50511912
http://blog.csdn.net/luxiaoyu_sdc/article/details/15874505
http://www.cs.princeton.edu/courses/archive/fall97/cs461/jdkdocs/guide/jni/spec/functions.doc.html
http://wiki.jikexueyuan.com/project/deep-android-v1/jni.html