我所理解的jni与ndk

    jni是Java和Android提供的Java和c,c++相互沟通的一种机制;

    ndk是android提供的一套工具集,包含一个交叉编译器和mk文件等,用于将c和c++部分的代码编译成so文件并打包进apk中,从而实现apk应用中可以利用本地代码进行工作;


-------------------------------------------------------------------分割线------------------------------------------------------------
    

    jni的沟通机制简图如下:


    下面的代码都以cocos2dx项目为例。

1,Java代码想调用C++的代码,是通过声明为native的方法进行的,示例如下:

package com.qw.pay; //这是这个类所在的包
public static native void onResult(int result); //这是这个类里面的一个静态方法

    利用javah会自动生成一个com_qw_pay_PayInterface.h的头文件,然后自己创建一个相应的c++文件进行实现,将代码放到ndk编译的地方,相关代码就可以编译进so文件中,实现Java调用本地代码;

2,C++代码想调用Java代码,通过以下类似的方法进行,示例如下:

 #define AppActivityClassName "org/cocos2dx/cpp/JNI" //类和包名
 ocos2d::JniMethodInfo t;
 if (cocos2d::JniHelper::getStaticMethodInfo(t, AppActivityClassName, "showAd", "()V"))
 {
     t.env->CallStaticVoidMethod(t.classID, t.methodID);
     t.env->DeleteLocalRef(t.classID);
 }

    这里的showAd是JNI类的一个方法名,“()V”表示的是它的参数类型和返回值:()表示没有任何参数传递,V表示返回值为空,有关JNI的类型签名,下面将会详细介绍。

    上面这个方法,首先使用cocos2d::JniHelper::getStaticMethodInfo查找该类有没有相关的函数,如果没有的话返回值为false,如果有的话,它会把类id和方法id保存在t变量中,后面在利用env进行相关的函数调用:t.env->CallStaticVoidMethod(t.classID, t.methodID);从而完成C++调用Java代码的整个过程。后面我会贴出完整的c++调用Java的相关代码,包括引用jni头文件等。

    以上就是Java代码和c++代码互调的代码示例。相对来说,c++调用Java比较麻烦一点,涉及env,类型签名等一些概念,而Java代码调用c++则比较简单,只需要利用native关键字,产生对应的头文件,编写实现文件,利用ndk打包就可以实现调用c++代码的功能。


----------------------------------------------------------------具体方法-----------------------------------------------------------------

1,Java调用c++,首先声明native函数,然后利用工具javah生成头文件。以上文的com.qw.pay.PayInterface为例,具体方法如下:

    终端工作路径cd到src文件夹

    

    然后执行命令javah com.qw.pay.PayInterface,就可以产生对应的头文件,接着实现函数功能即可。

    


2,C++调用Java代码:

     在上文中我们提到一些概念,比如env和类型签名等。实际上,jni中涉及到JVM和Env,JVM代表一个虚拟机,android中只允许有一个JVM的实例变量(这点和Java不同),这个变量在应用的整个生命周期不会改变,所以我们可以保存为一个全局变量;而Env在每一个线程调用中的值都会有所不同,所以改变量不能缓存起来使用。

     当然,以上JVM和Env的概念我们也可以不用管,cocos2dx已经帮我们对这些细节进行了封装,所以我们只需要使用cocos2dx提供的JniHelper,利用它提供的几个接口,就可以较轻松地实现jni。利用JniHelper使用如下:


     2.1,首先调用它的查询接口cocos2d::JniHelper::getStaticMethodInfo(t, AppActivityClassName, "showAd", "()V")查询类对应的方法是否存在,如果存在则进行下一个步骤。

     这里的t是一个JniMethodInfo对象,实际上封装了一个env变量,class类的id和方法id:

typedef struct JniMethodInfo_
{
    JNIEnv *    env;
    jclass      classID;
    jmethodID   methodID;
} JniMethodInfo;
    如上文所讲,我们不必关心env变量,因为JniHelper已经帮我们赋予了它正确的值。如果上述的查询函数查询成功,那么classID和methodID会分别被赋予和AppActivityClassName和“showAd”相关的id。

    这里的showAd对应于Java方法如下:

public static void showAd()

    所以它的类型签名即为“()V”,这里提供一份类型签名对应关系的表格如下

类型签名                     C中对应的j类型    Java类型
V                           void            void
Z                           jboolean        boolean
I                           jint            int
J                           jlong           long
D                           jdouble         double
F                           jfloat          float
B                           jbyte           byte
C                           jchar           char
S                           jshort          short 
Ljava/lang/String;          jstring         String
Ljava/net/Socket;           jobject         Socket  //其他类对象,注意类型签名里面的包名要正确
 
数组则以”["开始,用两个字符表示
[I                          jintArray        int[]
[F                          jfloatArray      float[]
[B                          jbyteArray       byte[]
[C                          jcharArray       char[]
[S                          jshortArray      short[]
[D                          jdoubleArray     double[]
[J                          jlongArray       long[]
[Z                          jbooleanArray    boolean[]

    假如你现在有一个函数如下:

public static Boolean test(int, String, int)
    那么它的类型签名应该是(ILjava/lang/String;I)Z,其他情况以此类推。


    2.2,执行相应的Java函数,像上文我们使用的函数是t.env->CallStaticVoidMethod(t.classID, t.methodID);如果有多个参数的话,直接填写在t.methodID即可,这个函数使用的是可变参数。

    如果是静态函数的话,调用的是env的CallStaticXXXMethod函数,这里的XXX表示的是返回值的类型,比如返回值为空,则是CallStaticVoidMethod,返回值为布尔类型,则是CallStaticBooleanMethod;如果是类对象(class),比如String类型,还有数组类型的话,则是CallStaticObjectMethod,具体所有函数如下所示(在jni.h里面可以找到):

#define CALL_STATIC_TYPE_METHOD(_jtype, _jname)                             \
    __NDK_FPABI__                                                           \
    _jtype CallStatic##_jname##Method(jclass clazz, jmethodID methodID,     \
        ...)                                                                \
    {                                                                       \
        _jtype result;                                                      \
        va_list args;                                                       \
        va_start(args, methodID);                                           \
        result = functions->CallStatic##_jname##MethodV(this, clazz,        \
                    methodID, args);                                        \
        va_end(args);                                                       \
        return result;                                                      \
    }
#define CALL_STATIC_TYPE_METHODV(_jtype, _jname)                            \
    __NDK_FPABI__                                                           \
    _jtype CallStatic##_jname##MethodV(jclass clazz, jmethodID methodID,    \
        va_list args)                                                       \
    { return functions->CallStatic##_jname##MethodV(this, clazz, methodID,  \
        args); }
#define CALL_STATIC_TYPE_METHODA(_jtype, _jname)                            \
    __NDK_FPABI__                                                           \
    _jtype CallStatic##_jname##MethodA(jclass clazz, jmethodID methodID,    \
        jvalue* args)                                                       \
    { return functions->CallStatic##_jname##MethodA(this, clazz, methodID,  \
        args); }

#define CALL_STATIC_TYPE(_jtype, _jname)                                    \
    CALL_STATIC_TYPE_METHOD(_jtype, _jname)                                 \
    CALL_STATIC_TYPE_METHODV(_jtype, _jname)                                \
    CALL_STATIC_TYPE_METHODA(_jtype, _jname)

    CALL_STATIC_TYPE(jobject, Object)
    CALL_STATIC_TYPE(jboolean, Boolean)
    CALL_STATIC_TYPE(jbyte, Byte)
    CALL_STATIC_TYPE(jchar, Char)
    CALL_STATIC_TYPE(jshort, Short)
    CALL_STATIC_TYPE(jint, Int)
    CALL_STATIC_TYPE(jlong, Long)
    CALL_STATIC_TYPE(jfloat, Float)
    CALL_STATIC_TYPE(jdouble, Double)

    void CallStaticVoidMethod(jclass clazz, jmethodID methodID, ...)
    {
        va_list args;
        va_start(args, methodID);
        functions->CallStaticVoidMethodV(this, clazz, methodID, args);
        va_end(args);
    }
    上面的CALL_STATIC_TYPE_METHOD是一个宏,由上面的代码可以,CallStaticXXXMethod中的XXX可以包括所有的基本所有的基本类型和Void,以及Object类型。

    这里说的是静态函数的调用,如果是非静态函数呢?其实也差不多,就是需要先查找这个类对象的id,而不是jclass clazz,然后调用CallXXXMethod就可以了,比如:

void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
    这里第二个参数就是类对象,获得这个类对象,可以通过前面的静态函数方法调用Java代码得到。

    不过在cocos2dx中最常用的就是调用静态的Java函数,主要注意点就是类型签名和CallStaticXXXMethod函数的选取,借助JniHelper的封装,可以比较方便的实现jni调用。

    这里注意的是c代码如果要传数据给Java端,必须先转化为jni对应的j类型,当然对于基本类型可以不用转换,因为它们本身只是用typedef包装了一下而已,这里提供jni对应的j类型在头文件中的定义(在jni.h里面可以找到):

/*
 * Primitive types that match up with Java equivalents.
 */
#ifdef HAVE_INTTYPES_H
# include <inttypes.h>      /* C99 */
typedef uint8_t         jboolean;       /* unsigned 8 bits */
typedef int8_t          jbyte;          /* signed 8 bits */
typedef uint16_t        jchar;          /* unsigned 16 bits */
typedef int16_t         jshort;         /* signed 16 bits */
typedef int32_t         jint;           /* signed 32 bits */
typedef int64_t         jlong;          /* signed 64 bits */
typedef float           jfloat;         /* 32-bit IEEE 754 */
typedef double          jdouble;        /* 64-bit IEEE 754 */
#else
typedef unsigned char   jboolean;       /* unsigned 8 bits */
typedef signed char     jbyte;          /* signed 8 bits */
typedef unsigned short  jchar;          /* unsigned 16 bits */
typedef short           jshort;         /* signed 16 bits */
typedef int             jint;           /* signed 32 bits */
typedef long long       jlong;          /* signed 64 bits */
typedef float           jfloat;         /* 32-bit IEEE 754 */
typedef double          jdouble;        /* 64-bit IEEE 754 */
#endif

/* "cardinal indices and sizes" */
typedef jint            jsize;

#ifdef __cplusplus
/*
 * Reference types, in C++
 */
class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jthrowable : public _jobject {};

typedef _jobject*       jobject;
typedef _jclass*        jclass;
typedef _jstring*       jstring;
typedef _jarray*        jarray;
typedef _jobjectArray*  jobjectArray;
typedef _jbooleanArray* jbooleanArray;
typedef _jbyteArray*    jbyteArray;
typedef _jcharArray*    jcharArray;
typedef _jshortArray*   jshortArray;
typedef _jintArray*     jintArray;
typedef _jlongArray*    jlongArray;
typedef _jfloatArray*   jfloatArray;
typedef _jdoubleArray*  jdoubleArray;
typedef _jthrowable*    jthrowable;
typedef _jobject*       jweak;


#else /* not __cplusplus */

/*
 * Reference types, in C.
 */
typedef void*           jobject;
typedef jobject         jclass;
typedef jobject         jstring;
typedef jobject         jarray;
typedef jarray          jobjectArray;
typedef jarray          jbooleanArray;
typedef jarray          jbyteArray;
typedef jarray          jcharArray;
typedef jarray          jshortArray;
typedef jarray          jintArray;
typedef jarray          jlongArray;
typedef jarray          jfloatArray;
typedef jarray          jdoubleArray;
typedef jobject         jthrowable;
typedef jobject         jweak;

#endif /* not __cplusplus */
    可以发现,如果是C类型,jobject实际上是void*类型,如果是C++类型,jobject类型实际上是 _jobject*类型,_jobject定义可由上面代码得知,其他所有的对象类型,比如jclass,jstring,jarray等等,实际上就是对jobject的一个空继承,这也解释了为什么CallStaticObjectMethod函数为什么可以用于返回值为jstring,jobject和其他数组类型的对象。

    对于常用的j类型和std标准库的string之间的转换,就是先把数据转换为char*类型,然后再转换为另一个string类型,可以利用env提供的方法进行转换,注意代码需要使用UTF-8进行编码,否则可能会出现乱码,env提供的方法如下:

jstring   (*NewStringUTF)(JNIEnv*, const char*);   //char*转换为jstring
const jchar* (*GetStringChars)(JNIEnv*, jstring, jboolean*);  //jstring转换为char*
    示例代码如下:
1
const char *cstr = env->GetStringUTFChars(env, jstr, NULL);
...
env->ReleaseStringUTFChars(env, jstr, cstr);  //注意最后需要释放这个新建的字符串

2
jstring jstr = env->NewStringUTF(env, cstr);
…
env->DeleteLocalRef(env, jstr);//注意最后需要释放这个新建的字符串

    然后就可以用char*构造出一个std::string类型,对于GetStringChars的最后一个布尔型参数决定是拷贝一份数据还是仅仅返回JVM中原始数据对应的指针,但是不管采用哪种方式,最后都要调用ReleaseStringChars。根据方法GetStringChars是复制还是直接返回指针,ReleaseStringChars会释放复制对象时所占的内存,或者unpin这个对象。

    cocos2dx中的JniHelper提供了一个函数:

static std::string jstring2string(jstring str);
    利用这个函数可以方便地将jstring类型数据转换为std::string的数据类型,一般用于返回jstring类型的时候使用,示例代码如下:
...
if(JniHelper::getStaticMethodInfo(info, CLASS_NAME, "getUserID", "()Ljava/lang/String;"))
{
    jstring jstr = (jstring)info.env->CallStaticObjectMethod(info.classID,info.methodID);
    std::string text = JniHelper::jstring2string(jstr);
    ...
}
    到这里基本上jni机制的使用方法和注意事项就讲完了,注意我这里主要讲的是对于Java静态函数的访问,如果是非静态函数的访问,具体调用的方法会有点不一样,但是同样要先查询Java类的相关方法存不存在,具体调用的方法可以查看jni.h中提供的相关接口,搜索“CallXXXMethod”应该就可以。

    题外话:需要注意使用jni的时候要注意最好不要直接调用UI组件,防止因为该线程非ui线程导致的渲染异常的问题。对于cocos2dx端的代码,可以使用引擎提供的Director::getInstance()->getScheduler()->performFunctionInCocosThread方法,对于Java端,可以使用Activity的runOnUiThread或其他方法。


--------------------------------------------------------------------代码示例---------------------------------------------------------


    一份简单的C++代码调用Java的示例如下:

#include "MyShare.h"
#include "cocos2d.h"
#include "GameData.h"
USING_NS_CC;

#if CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID
#include <jni.h>
#include "platform/android/jni/JniHelper.h"
#endif

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

void MyShare::share()
{
#if CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID
    JniMethodInfo info;
    std::string msg = GameData::getInstance()->_imagePath;//分享图片地址 
    if(JniHelper::getStaticMethodInfo(info, CLASS_NAME, "share", "(Ljava/lang/String;)V"))
    {
        jstring param = info.env->NewStringUTF(msg.c_str());
        info.env->CallStaticVoidMethod(info.classID, info.methodID, param);
    }
#endif
}


----------------------------------------------------------------------继续探索---------------------------------------------------------

1,前面提到,JVM变量对于一个进程中是唯一的,不变的,所以cocos2dx中的JniHelper已经对它进行了封装,由JniHelper去维护这个变量,所以我们不需要考虑这个细节。

     查看cocos2dx源码,可以发现,在安卓项目的proj.android/jni/hellocpp中,有一个JNI_OnLoad函数,示例如下:

jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
    JniHelper::setJavaVM(vm);
    PluginJniHelper::setJavaVM(vm);

    return JNI_VERSION_1_4;
}
    这个函数是在jni.h中定义的,这里cocos2dx选择在javaactivity-android.cpp(路径为/cocos2d/cocos/platform/android)中进行实现。这个函数在Java端加载cocos2dx的so文件后就会进行调用,在这里可以看到Java把JVM变量示例的指针传了下来,然后JniHelper对这个变量指针保存,而这个变量在应用的生命周期内是不会改变的,所以我们开发者就可以不用管这个JVM变量了,因为JniHelper已经帮我们处理好了。

    而Env在每一个不同线程中的调用都会不同,所以我们不能对这个env变量进行缓存,需要使用JVM进行查询。实际上,如果cocos2d::JniHelper::getStaticMethodInfo这个方法查询成功的话,JniHelper内部就会将正确的Env保存在ocos2d::JniMethodInfo这个实例变量中,示例如下:

bool JniHelper::getStaticMethodInfo(JniMethodInfo &methodinfo,
                                        const char *className, 
                                        const char *methodName,
                                        const char *paramCode) {
    if ((nullptr == className) ||
        (nullptr == methodName) ||
        (nullptr == paramCode)) {
        return false;
    }

    JNIEnv *env = <span style="color:#ff0000;">JniHelper::getEnv()</span>;
    if (!env) {
        LOGE("Failed to get JNIEnv");
        return false;      
    } 
    ...  //省略部分代码
    methodinfo.env = env;
    ...  //省略部分代码
}
    关键是这个getEnv函数,该函数示例如下:
JNIEnv* JniHelper::getEnv() {
    JNIEnv *_env = (JNIEnv *)pthread_getspecific(g_key);
    if (_env == nullptr)
        _env = JniHelper::cacheEnv(_psJavaVM);
    return _env;
}
    这个函数是说,获取该线程下的env缓存值,如果没有的话,使用JVM去获取并在cacheEnv函数中保存这个env值。

    前面我们说过不可以自己去缓存这个env变量值,为什么这里就行的通呢?原因是这里的缓存值是和线程相关的(pthread_getspecific),比如说,现在我有两个线程,其中一个线程已经通过jni进行访问了,现在该线程已经有了一个env的缓存值,但是第二个线程访问的时候一开始env的缓存值还是为空(pthread_getspecific返回Null),第二个线程也需要自己去缓存一个env变量,这样就可以保证每次jni访问使用的env都是与线程相关的。

   说了这么多,其实也就是说,对于JVM和env变量,cocos2dx提供的JniHelper已经帮我们封装和处理好了,所以我们使用jni的时候可以不用考虑这些细节。


2,cocos2dx是使用c++编写的,所以env的调用类似以下形式:

env->CallStaticXXXMethod(参数)
    对于一些c编写的程序,你需要使用下面的调用形式:

(env*)->CallStaticXXXMethod(参数)
    原因就是对于c和c++,JNIEnv的定义不同,示例如下(jni.h文件中):
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif

struct JNINativeInterface {
    void*       reserved0;
    void*       reserved1;
    void*       reserved2;
    void*       reserved3;
    …//省略很多其他方法
    jobject     (*CallStaticObjectMethod)(JNIEnv*, jclass, jmethodID, ...);
    …//省略很多其他方法
}

struct _JNIEnv {
    const struct JNINativeInterface* functions;
#if defined(__cplusplus)
    …//其实就是对JNINativeInterface的简单封装
    void CallStaticVoidMethod(jclass clazz, jmethodID methodID, ...)
    {
        va_list args;
        va_start(args, methodID);
        functions->CallStaticVoidMethodV(this, clazz, methodID, args);
        va_end(args);
    }
    …//省略其他很多方法
}
    从上面的代码中可以看出为什么c和c++的jni函数调用形式有些不同。



---------------------------------------------------------------------------分割线-------------------------------------------------------

    前面讲了这么多关于jni的东西,相对于ndk,感觉可以讲的东西不多,主要就是概念上知道ndk是个交叉编译c++代码的工具集也就差不多了,它可以为我们编译安卓平台上可以运行的c++代码,然后自动将这些代码打包成so文件,打包进apk文件中。

    ndk的出现就是为了简化本地代码交叉编译和打包的流程,大大减轻了开发者利用本地代码进行开发的过程中部署的工作量;而ndk使用的mk文件和自动化脚本才是真正值得我们关注的内容。














  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值