JNI是Java Native Interface的简写,简单来说,就是一种JAVA和C++之间相互调用的一套接口。利用JNI可以在JAVA代码层(manager code)调用C++代码层(native code)的函数,反之亦可。
如果你之前并不熟悉JNI,可以通过官方文档:Java Native Interface Specification ( http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html)先来了解一下。
英文不好的同学,可以看看这篇博客(http://wiki.jikexueyuan.com/project/deep-android-v1/jni.html)
Cocos2d-x作为一种跨平台的开源游戏引擎,其特点是一次编写,到处部署。如果部署到Android平台,我们可以利用JNI来调用Android的一些类库和控件,加个Android原生对话框什么的。更重要的是,如果项目里有很多组件是用JAVA编写的,我们应该尽量使用现有的组件,避免重复的制造轮子。
应当指出,Cocos2d-x可以调用Android的控件,但是不应该过分依赖这一点,唯一用到这一功能的情景,是引擎实在没法实现,从解耦的角度考虑,界面逻辑应当尽量放在Cocos当中来做。
Cocos为我们封装了一些JNI调用的接口,这个类叫JniHelper(其实JAVA自己也提供了JNI调用的接口类,叫JniHelp,这两个类没什么差别),这个类的位置在[cocos安装路径]\cocos\platform\android\jni
中
在Cocos中用JNI调用JAVA程序时,jni.h这个头文件是必须的,好在JniHelper.h中已经帮我们包含了,所以我们只需要包含JniHelper.h这个头文件就可以了。
所以,在需要调用JNI的类中,需要加入
#if(CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
#include "platform/android/jni/JniHelper.h"
#endif
条件编译,是用来说明这个东西是Android平台上用到的。
JniHelper.h当中还包含一个相当重要的结构体:JniMethodInfo,看名字就猜到了,这个结构体是用来定位调用的JAVA方法的,来看看这个结构体的定义
typedef struct JniMethodInfo_
{
JNIEnv * env;
jclass classID;
jmethodID methodID;
} JniMethodInfo;
classID和methodID很好理解,分别代表类和方法,这样就可以定位到具体的方法了。env稍微有点复杂,它表示一个线程相关的结构体,里面保存的是JNI函数指针,不能跨线程访问,其实也不用管这个变量究竟代表什么意思,用起来十分简单。
那么下一个问题是怎么初始化这三个成员变量,或者说怎么注册JAVA方法。
举个例子,待调用的JAVA方法,位于org.cocos2dx.cpp包中,类名叫TestJni,待调用的方法是func1
(关于如何将Cocos项目部署到Eclipse上,参考我前一篇博客:http://blog.csdn.net/sgn132/article/details/50481923)
TestJni.java的代码十分简单,注意这里func1方法是静态函数,具体代码如下:
package org.cocos2dx.cpp;
import android.util.Log;
public class TestJni {
public static void func1(){
Log.d("success","java jni called succeed");
}
}
然后打开Cocos项目,我这里使用VS2015作为开发环境的,cocos版本是v3.8.1,在一个按键响应方法里,添加调用JNI的代码
#if(CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
JniMethodInfo info;
//getStaticMethodInfo判断java定义的静态函数是否存在,返回bool
bool ret = JniHelper::getStaticMethodInfo(info,"org/cocos2dx/cpp/TestJni","func1","()V");
if(ret)
{
log("call void func1() succeed");
//传入类ID和方法ID,小心方法名写错,第一个字母是大写
info.env->CallStaticVoidMethod(info.classID,info.methodID);
}
#endif
首先要提的是JNI的签名机制,签名其实就是函数的参数类型和返回值类型共同组成的字符串,之所以要有签名机制,是因为C++是允许函数重载的,单靠函数名无法准确定位函数的入口,还需要函数的参数信息。
JNI的规范签名格式是:
(参数1类型标示参数2类型标示...参数n类型标示)返回值类型标示。
举个栗子:
JAVA中函数定义为
void play(String singer,String album)
对应的签名
(Ljava/lang/String;Ljava/lang/String;)V
其中L
表示引用类型,后面跟的是包名,需要把”.“换成”/“,V
表示void。
这里给出标示类型对照表:
标示类型 | Java类型 |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
L/java/lang/String; | String |
[I | int[] |
[L/java/lang/object; | Object[] |
如果java类型是数组,在左侧加上[
,另外,引用类型(除基本类型数组外),标示最后有一个;
。
然后再来看几个例子
函数签名 | Java函数 |
---|---|
()Ljava/lang/String; | string f() |
(ILjava/lang/Class;)J | long f(int i,Class c) |
([B)V | void f(byte[] bytes) |
看到这些签名信息,真是难记啊,好在JAVA提供了直接生成签名的方法:
javap –s -p xxx
其中xxx为编译后的class文件,s表示输出内部数据类型的签名信息,p表示打印所有函数和成员的签名信息,而默认只会打印public成员和函数的签名信息。
有了javap,就不用死记硬背上面的类型标示了。
至此,函数的签名工作就差不多了,但是我们还缺少一个C++与JAVA数据类型的映射表,别急,JNI已经为我们设计好了,如下表所示:
基本类型的映射
Java类型 | C++类型 |
---|---|
boolean | jboolean |
byte | jbyte |
char | jchar |
short | jshort |
int | jint |
long | jlong |
float | jfloat |
double | jdouble |
基本类型的映射十分简单,就是在Java类型前面加个j。再来看引用类型的映射。
引用类型的映射
Java类型 | C++类型 |
---|---|
All Objects | jobject |
java.lang.Class | jclass |
java.lang.String | jString |
char[] | jcharArray |
short[] | jshortArray |
int[] | jintArray |
Object[] | jobjectArray |
boolean[] | jbooleanArray |
byte[] | jbyteArray |
long[] | jlongArray |
float[] | jfloatArray |
double[] | jdoubleArray |
java.lang.Throwable | jthrowable |
从上表可知,除了Class和String两个引用类型,其他所有引用类型都映射为jobject。
举个栗子,Context是Android中一个相当重要的对象,现在我们要在Cocos中获取到Context对象,利用Android 自带的方法getContext
可以获取到Context实例,对应的签名为
()Landroid/content/Context;
Cocos中的代码应当为
#if(CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
JniMethodInfo t;
if(JniHelper::getStaticMethodInfo(t,"AppName","getContext","()Landroid/content/Context")
{
jobject ret = t.env->CallStaticObjectMethod(t.classID,t.methodID);
}
#endif
这样,就可以在Cocos中获取到Context实例ret。
再举个栗子,仅仅使用系统自带的数据类型显然是不够我们祸害的,我们需要自定义数据类型,其实也就是一个自定义的类,例如,我在org.cocos2dx.cpp这个包里定义了一个类:MyClass
这时,我在TestJni.java里添加了一个静态方法,用来获取MyClass的一个实例
private static MyClass myclass;
public static MyClass getMyClass(){
Log.d("success", "get Myclass Info");
myclass = new MyClass();
return myclass;
}
这样,在Cocos中,如果我要使用getMyClass获取一个myclass实例,就可以这么写:
#if(CC_TARGET_PLATFORM==CC_PLATFORM_ANDROID)
JniMethodInfo info;
if (JniHelper::getStaticMethodInfo(info, "org/cocos2dx/cpp/TestJni", "getMyClass", "()Lorg/cocos2dx/cpp/MyClass;"))//注意这里函数标签的变化
{
log("call getMyClass() succeed");
jobject ret = info.env->CallStaticObjectMethod(info.classID, info.methodID);
}
#endif
这样,我就得到了一个MyClass的对象。
到这里,我们已经能够在Cocos中获取到自定义数据类型,也知道所有的自定义类型映射到C++都变成了jobject对象,那么我们如何解析这个jobject对象呢,如何获取到成员变量、如何操作成员函数呢?这就需要jfieldID
和jmethodID
来帮忙了。
我们知道一个类可以由两个部分组成,分别是成员变量和成员函数,对应jfeldID
和jmethodID
,通过JNIEnv的两个函数可以获取到:
jfieldID GetFieldID(jclass clazz,const char*name, const char *sig);
jmethodID GetMethodID(jclass clazz, const char*name,const char *sig);
其中,jclass
代表Java类,name
表示成员函数或成员变量的名字,sig
为这个函数和变量的签名信息。
具体怎么用呢,还是来举个栗子吧,在org.cocos2dx.cpp这个包里,有个自定义的类MyClass,定义如下:
public class MyClass{
public static int member;
public void func()//分别演示static和非static情况
{
Log.d("MyClass","member is " + String.valueof(member));
}
}
参照前文所述,如果能够正确的设置JNI调用,假设在Cocos中获取到了MyClass的实例:jobject ret,要想解析它,首先需要获取到jclass
:
jclass myclass = info.env->FindClass("org/cocos2dx/cpp/MyClass");
jclass myclass = info.env->GetObjectClass(ret);//这两种方法都可以
之后,如果我想要获取到member
属性:
jfieldID fid = info.env->GetStaticFieldID(constCls,"member","I");
如果我想要获取到func
方法:
jmethodID mid = info.env->GetMethodID(constCls,"func","()V");
接下来的问题是,我们获取到了jfieldID
和jmethodID
了,那么该怎么用他们呢?
这里,解析jfieldID
是指将它转变为C++类型,解析jmethodID
是指调用这个函数。
首先来看,如何解析jfieldID
。其实也很简单,可以直接利用JNIEnv所提供的Get/Set函数,语法如下所示:
//非静态属性
//Get
NativeType Get<type>Field(jobject&,jfield&);
//Set
void Set<type>Field(jobject&,jfield&,NativeType&);
//静态属性
//Get
NativeType GetStatic<type>Field(jclass&,jfield&);//注意从jobject变成了jclass
//Set
void SetStatic<type>Field(jclass&,jfield&,NativeType&);
NativeType
就是指C++中的数据类型,int
,double
什么的。
对应的就是JAVA的数据类型,可选的type类型如下表所示:
type类型 |
---|
Object |
Boolean |
Byte |
Char |
Short |
Long |
Int |
Float |
Double |
那么如何解析jmethodID
呢,其实也很简单,语法是这样的:
NativeType Call<type>Method(jobject&,jmethodID&,) ;
其中NativeType
和
的作用和前面所述的一致,最后的参数列表,是方法所需的参数列表。
从程序运行的效率考虑,最好是在Cocos上设计一个对应于JAVA类的C++类,这样操作起来更加合理,也提高了程序的运行效率。
比如说,上面的例子中,我们在JAVA层定义了一个类MyClass
,他包含一个成员变量member
,和一个成员函数func()
,那么我们可以在Cocos里面定义一个映射类MyCocosClass
#include "cocos2d.h" //必须包含的库
USING_NS_CC; //JniHelper的各个类的命名空间都在cocos2d中
#if(CC_TARGET_PLATFORM==CC_PLATFORM_ANDROID)
#include "platform/android/jni/JniHelper.h"
/******************************/
//与JAVA层代码对应的C++层类
/******************************/
class MyCocosClass
{
public:
//
//习惯上在构造函数里调用JNI并解析
//
MyCocosClass()
{
//获取MyClass对象
if (JniHelper::getStaticMethodInfo(info, "org/cocos2dx/cpp/TestJni", "getMyClass", "()Lorg/cocos2dx/cpp/MyClass;"))
{
ret = info.env->CallStaticObjectMethod(info.classID, info.methodID);
jclass myclass = info.env->GetObjectClass(ret);
//解析jobject对象
memberID = ret.env->GetStaticFieldID(constCls,"member","I");
methodID = ret.env->GetMethodID(constCls,"func","()V");
}
}
//
//getter & setter
//
int getMember() const
{
return info.env->GetIntField(ret,memberID);//解析Field
}
void setMember(int val)
{
info.env->SetIntField(ret,memberID,val);
}
//
//method
//
void func()
{
info.env->CallVoidMethod(ret,methodID);//解析Method
}
private:
JniMethodInfo info;
jobject ret;
jfieldID memberID;
jmethodID methodID;
};
#endif
大家应该都习惯将声明放到.h
文件,将实现放到.cpp
文件中,对上述代码稍作修改即可,而在cocos项目中,你要注意两点:
修改Android.mk文件
这个文件位于[项目路径]\proj.android\jni\Android.mk
进行如下修改,这样就能添加新的类文件:
这一步修改旨在解决部署到Android项目时可能出现的问题,仅仅是这样简单的修改可能无法解决你所有的问题,如果你确定你的C++代码是没有问题的,但是还是存在部署的问题,你可以看看这两篇博客:
http://www.zaojiahua.com/android-platform.html(总结了Cocos2d-x v3.x版本移植的常见问题)
http://www.cocoachina.com/bbs/read.php?tid=226339(cocos论坛总结的常见问题)
2.包含cocos2d
这个头文件,以及使用USING_NS_CC
即cocos2d命名空间
到这里,其实调用原生Android控件已经不是很难的事情了,有很多种方法能够帮助我们实现这一功能。这里就以弹出原生AlertDialog为例,来展示其功能。在cocos上添加一个Menu控件,点击它可以弹出一个Android的AlertDialog对话框,并且可以显示点击了多少次。
首先是Cocos层的代码,在指定的点击事件回调函数中添加:
#if(CC_TARGET_PLATFORM==CC_PLATFORM_ANDROID)
JniMethodInfo t;
if(JniHelper::getStaticMethodInfo(t,"packageName","showDialog","()V")//注意这里指定的showDialog是静态函数
{
jobject ret = t.env-> CallStaticObjectMethod(t.classID,t.methodID);
}
#endif
Android端的设计中,需要注意的一点是:C++调用JNI的接口所在的线程,不能直接操作UI线程,需要通过Handler机制来捕获这一消息。
具体来说,主要包含两个函数:
静态函数showDialog
,这个函数是C++调用Android的接口,在函数内部抛出一个Message
,再由Handler
捕获这个信息进行处理。
public static showDialog(){
//抛出Message
Message msg = new Message();
msg.what=SHOW_MESSAGE;
handler.sendMessage(msg);
}
创建一个Handler
对象,用以监听Message
对象
private Handler mHandler=new Handler(){
@Override
public void handleMessage(Message msg){
switch(msg.what)
{
case SHOW_DIALOG:
//显示一个dialog
}
}
}
这里通过一个简单的类来介绍具体调用
public class Simple{
//native方法
//native方法表示调用C++实现的方法
public native int intMethod(int n);
public native boolean booleanMethod(boolean flag);
public native String stringMethod(String text);
public native int intArrayMethod(int[] Array);
public static int main(String[] args){
System.loadLibrary("Simple");//加载动态库
Sample sample = new Sample();
int square=sample.intMethod(5);
boolean flag=sample.booleanMethod(true);
String text = sample.stringMethod("java");
int sum = sample.intArrayMethod(new int[]{1,2,3,4,5});
System.out.println("int method is "+square);
System.out.println("boolean method is "+flag);
System.out.println("string method is "+text);
System.out.println("intArray Method is "+sum);
}
}
在main
函数中,首先要做的就是加载动态库Simple
,在Windows下的动态库就是Simple.dll,在Linux下就是Simple.so,而在我们程序编写中,不要加上后缀名,JAVA会根据平台自动添加后缀名,还要保证这个文件在path
路径中,不然会找不到文件。
JAVA调用C++的代码,C++的方法名称必须对应,比如说,JAVA端的方法名为
public class Sample{
public native int method();
}
C++对应的方法名应该为
JNIEXPORT jint Java_Sample_method();
其实这些规则也可以不用记,用javah
这种方法也是可以的,我们在命令行下进入刚才编写的Sample.java所在的文件夹,运行命令
javah Sample
会在相应目录下生成.h头文件
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class Sample */
#ifndef _Included_Sample
#define _Included_Sample
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: Sample
* Method: intMethod
* Signature: (I)I
*/
JNIEXPORT jint JNICALL Java_Sample_intMethod
(JNIEnv *, jobject, jint);
/*
* Class: Sample
* Method: booleanMethod
* Signature: (Z)Z
*/
JNIEXPORT jboolean JNICALL Java_Sample_booleanMethod
(JNIEnv *, jobject, jboolean);
/*
* Class: Sample
* Method: stringMethod
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_Sample_stringMethod
(JNIEnv *, jobject, jstring);
/*
* Class: Sample
* Method: intArrayMethod
* Signature: ([I)I
*/
JNIEXPORT jint JNICALL Java_Sample_intArrayMethod
(JNIEnv *, jobject, jintArray);
#ifdef __cplusplus
}
#endif
#endif
接下来,设计Sample.cpp文件,实现头文件中的函数。
#include "Sample.h"
#include
using namespace std;
JNIEXPORT jint JNICALL Java_Sample_intMethod
(JNIEnv *env, jobject obj, jint num)
{
return num*num;
}
JNIEXPORT jboolean JNICALL Java_Sample_booleanMethod
(JNIEnv *env, jobject obj, jboolean flag)
{
return !flag;
}
JNIEXPORT jstring JNICALL Java_Sample_stringMethod
(JNIEnv *env, jobject obj, jstring text)
{
const char* str=env->GetStringUTFChars(text,0);
char cap[128];
strcpy(cap,str);
env->ReleaseStringUTFChars(text,0);
return env->NewStringUTF(strupr(cap));
}
JNIEXPORT jint JNICALL Java_Sample_intArrayMethod
(JNIEnv *env, jobject obj, jintArray array)
{
int i,sum=0;
jsize len=env->GetArrayLength(array);
jint* body=env->GetIntArrayElements(array,0);
for(int i =0;iReleaseIntArrayElements(array,body,0);
return sum;
}
在Win操作系统下,可以用visual studio生成dll文件,具体的过程可以参考:
https://msdn.microsoft.com/zh-cn/library/ms235636.aspx