Cocos2d-x与Android利用JNI相互调用

1.背景知识

1.1什么是JNI

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)

1.2 为什么要用JNI

Cocos2d-x作为一种跨平台的开源游戏引擎,其特点是一次编写,到处部署。如果部署到Android平台,我们可以利用JNI来调用Android的一些类库和控件,加个Android原生对话框什么的。更重要的是,如果项目里有很多组件是用JAVA编写的,我们应该尽量使用现有的组件,避免重复的制造轮子。

应当指出,Cocos2d-x可以调用Android的控件,但是不应该过分依赖这一点,唯一用到这一功能的情景,是引擎实在没法实现,从解耦的角度考虑,界面逻辑应当尽量放在Cocos当中来做。

2.Cocos2d-x调用JAVA代码

Cocos为我们封装了一些JNI调用的接口,这个类叫JniHelper(其实JAVA自己也提供了JNI调用的接口类,叫JniHelp,这两个类没什么差别),这个类的位置在[cocos安装路径]\cocos\platform\android\jni

Cocos2d-x与Android利用JNI相互调用_第1张图片

在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

Cocos2d-x与Android利用JNI相互调用_第2张图片

(关于如何将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
  • getStaticMethodInfo是注册函数
    他有4个参数,前三个很好理解,用来定位JAVA方法,最后一个参数一般称为签名,是用来表示调用的JAVA方法的传递参数和返回值类型的。
  • CallStaticVoidMethod是调用函数
    这个函数也不用多说了,使用来调用指定JAVA方法的。

2.1 JNI怎么传递自定义数据类型

2.1.1 参数、返回值类型组成的字符串——签名

首先要提的是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,就不用死记硬背上面的类型标示了。

2.1.2JAVA类型与C++类型的映射

至此,函数的签名工作就差不多了,但是我们还缺少一个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。

2.1.3 传递、返回自定义类型

举个栗子,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

Cocos2d-x与Android利用JNI相互调用_第3张图片

这时,我在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的对象。

2.2 怎么解析C++通过JNI获取到的自定义类型

到这里,我们已经能够在Cocos中获取到自定义数据类型,也知道所有的自定义类型映射到C++都变成了jobject对象,那么我们如何解析这个jobject对象呢,如何获取到成员变量、如何操作成员函数呢?这就需要jfieldIDjmethodID来帮忙了。

2.2.1 用jfieldID和jmethodID解析jobject

我们知道一个类可以由两个部分组成,分别是成员变量和成员函数,对应jfeldIDjmethodID,通过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");

接下来的问题是,我们获取到了jfieldIDjmethodID了,那么该怎么用他们呢?

2.2.2 解析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的作用和前面所述的一致,最后的参数列表,是方法所需的参数列表。

2.2.3 在C++中创建JAVA类的映射类

从程序运行的效率考虑,最好是在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项目中,你要注意两点

  1. 修改Android.mk文件

    这个文件位于[项目路径]\proj.android\jni\Android.mk

Cocos2d-x与Android利用JNI相互调用_第4张图片

进行如下修改,这样就能添加新的类文件:

Cocos2d-x与Android利用JNI相互调用_第5张图片

这一步修改旨在解决部署到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命名空间

3. 用Cocos调用Android的原生控件

到这里,其实调用原生Android控件已经不是很难的事情了,有很多种方法能够帮助我们实现这一功能。这里就以弹出原生AlertDialog为例,来展示其功能。在cocos上添加一个Menu控件,点击它可以弹出一个Android的AlertDialog对话框,并且可以显示点击了多少次。

3.1 Cocos端设计

首先是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

3.2 Android端设计

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
            }
        }
    }

3.3 效果演示

Cocos2d-x与Android利用JNI相互调用_第6张图片

4 在Java中调用C++本地库

这里通过一个简单的类来介绍具体调用

4.1 编写一个JAVA类

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路径中,不然会找不到文件。

4.2 C++层代码的编写

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

你可能感兴趣的:(android开发,Cocos学习,cocos2d-x)