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文件和自动化脚本才是真正值得我们关注的内容。