首页 > 代码库 > JNI使用总结

JNI使用总结


相关概念


什么是jni?


JNI(Java Native Interface)是C/C++语言与Java语言通信的中间件,其实就是一套接口规范。

技术分享

技术分享



关于nkd


ndk是一系列工具的集合,主要用于实现交叉编译。


什么是交叉编译?


交叉编译:在一个平台下编译出另一个平台中可以执行的二进制代码


注意:NDK只能在Linux环境下运行。在Windows下可以使用cygwin运行ndk,cygwin是Windows下的Linux环境模拟器。


相关命令:

清除缓存: ndk-build clean

生成动态库:ndk-build


点击查看【Windows下NDK开发环境搭建】


C/C++语言常见术语


库函数

事先编译打包好的函数


为了代码重用,将一些常用的函数(如:输入/输出),这些函数事先被编译好,并生成目标代码,然后将生成的目标代码打包成一个库文件,以供再次使用。库文件中的函数被称为函数库。


在Windows中,库函数的目标代码都是以.obj为后缀的,Linux中是以.o为后缀。


提示:单个目标代码是无法直接执行的,目标代码在运行之前需要使用连接程序将目标代码和其他库函数连接在一起后生成可执行文件。Windows(可执行文件后缀为.exe,动态库文件为.dll),Linux(.so)。


头文件

函数的声明文件。


         头文件中存放的是对某个库中所定义的函数、宏、类型、全局变量等进行的声明,它类似于一份仓库清单。若用户程序中需要使用某个库中的函数,则只需要将该库所对应的头文件include到程序中即可。

 

注意:头文件中定义的是库中所有函数的函数原型。而函数具体实现则是在库文件中。

简单的说:头文件是给编译器用的,库文件是给连接器用的。

 

在连接器连接程序时,会依据用户程序中导入的头文件,将对应的库函数导入到程序中。

 

头文件以.h为后缀名。


函数库

程序员编写的函数

 

 动态库

         在编译用户程序时不会将用户程序内使用的库函数连接到用户程序的目标代码中,只有运行时,且用户程序执行到相关函数时才会调用该函数库里的相应函数,因此动态函数库所产生的可执行文件比较小。

 

Window下的动态库:.dll文件

 

运行时才调用库函数的相关函数

 

静态库

         在编译用户程序时会将其内使用的库函数连接到目标代码中,程序运行时不再需要静态库。使用静态库生成可执行文件比较大。

 

编译时已经把使用的库函数连接到目标代码,运行时不需要再连接库函数。

 

在Linux中:

?  静态库命名一般为:lib+库名+.a。

         如:libHello.a其中lib说明此文件是一个库文件,Hello是库的名称,.a是说明是静态的

?  动态库命名一般为:lib+库名+.so。so说明是动态的。



HelloWorld


1.创建一个Android工程

2.在Java层中声明一个native方法

public native String  callCMethod();	
3.创建jni目录,编写C代码
技术分享

#include <jni.h>
//方法名格式为:返回值  Java_包名_类名_Java代码的方法名(JNIEnv* env,jobject obj)
jstring Java_com_example_hellojni_MainActivity_callCMethod( JNIEnv* env,  jobject obj )
{
    return (*(*env)).NewStringUTF(env,"this is string from c");
}
4.编写Android.mk文件

   LOCAL_PATH := $(call my-dir)

   include $(CLEAR_VARS)
   #模块名称
   LOCAL_MODULE    := Hello
   #源文件名
   LOCAL_SRC_FILES := Hello.c

   include $(BUILD_SHARED_LIBRARY)
5.使用NDK编译生成动态库

打开cygwin,cd到c源文件的目录,使用ndk-build命令生成动态库,生成的.so文件就在工程的libs目录下。

技术分享
6.在Java层加载动态库,并调用native方法

	//加载动态库
	static{
		System.loadLibrary("Hello");
	}
	
	public native String  callCMethod();

	public void sayHello(){
		Toast.makeText(getApplicationContext(), callCMethod() , 0).show();
	}


Java调用C/C++方法


1..在Java类中定义一个native方法


2.在C/C++中按照jni接口规范定义函数声明并编写实现。

函数名格式为:返回值  Java_包名_类名_Java代码的方法名(JNIEnv* env,jobject obj)


也可以使用javah工具生成头文件:

(1).进入到工程的bin目录下的classes目录,命令:javah  类的全限定路径名

技术分享

(2).生成的头文件方法在classes目录下:
(3).把头文件拷贝到工程的jni目录下,在c/c++源文件中引入该头文件,并实现方法


3.使用ndk编译生成动态库。


4.在Java层就可以通过调用native方法来间接调用C/C++的函数了。(需要先加载库文件)



C/C++方法调用Java方法


C/C++调用Java方法的实现是通过反射来完成的。


相关api


步骤:
1. 找到类(FindClass)
//找到Java的String类
jclass clazz = (*env)->FindClass(env, "java/lang/String");
参数说明:
env JNIENV*;
第二个参数是类的全路径名,注意:包名是用“/”隔开,而不是“.”;

2. 找到方法(GetMethodID)
//找到String的getBytes(charset)方法
jmethodID method = (*env)->GetMethodID(env, clazz, "getBytes", "(Ljava/lang/String;)[B");
参数说明:
env JNIENV*
第二个参数是方法名
第三参数是方法的签名(使用javap –s 类全路径名,查看类中所有方法的签名)

3. 调用方法 (CallxxxMethod)
//调用String.getBytes(charset)方法得到byte数组
jbyteArray byteArr = (jbyteArray)(*env)->CallObjectMethod(env, str, method, charset);
参数说明:
env JNIENV*
第二个参数是调用该方法的对象
第三个参数是methodId
后面的参数是调用该方法需要传递的参数

查找静态方法:GetStaticxxxMethod
调用静态方法:CallStaticxxxMethod


JNI创建对象的方法:
jobject  AllocObject(env , clazz)
jobject  NewObject(env , clazz , methodID)


查看方法签名命令:javap -s ClassName

使用命令行进入bin/classes目录下,使用javap命令就可以看到类的所有方法的签名。


提示:C/C++代码最终是在系统进程中执行的,而不是在虚拟机中


示例


示例一:Cocos2d-x的实现示例:

Java中native方法声明如下:
public static native void logout();
C++中调用Java的logout方法:
   #if(CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
   #include "jni.h"
   #include "jni/JniHelper.h"
    //定义Jni函数信息结构体
    JniMethodInfo minfo;
    bool isHave = JniHelper::getStaticMethodInfo(minfo,"org.cocos2dx.testcp.TestCpp","logout", "()V");
    if(isHave){
        CCLOG("logout function exist");
        minfo.env->CallStaticObjectMethod(minfo.classID, minfo.methodID);
        CCLOG("java logout function called");
    }else{
        CCLOG("logout function not exist");
    }
    #endif

示例二:查看下面的jstring转换char*


数据类型的对应


技术分享


在C和C++中是没有对应java中String的类型,但是可以通过反射技术,把char*转换为String。


jstring转换char*


相关api说明:
//在c中获取Java数组的长度
int size = GetArrayLenght(env , arr);

//获取数组的首地址,第三个参数表示是否允许拷贝
jint* arr = GetIntArrayElements(env , arr , 0;)

//释放内存空间
//jArr  Java传递给c的数组
//cArr 数组的首地址
//mode 数组的长度
ReleaseIntArrayElements(env , jArr , cArr , mode);
技术分享

C的实现
char* Jstr2Cstr(JNIEnv * env, jstring str) {
	char* cstr = NULL;
	//找到Java的String类
	jclass clazz = (*env)->FindClass(env, "java/lang/String");
	//找到String的getBytes(charset)方法
	jmethodID method = (*env)->GetMethodID(env, clazz, "getBytes", "(Ljava/lang/String;)[B");
	jstring charset = (*env)->NewStringUTF(env,"UTF-8");
	//调用String.getBytes(charset)方法得到byte数组
	jbyteArray byteArr = (jbyteArray)(*env)->CallObjectMethod(env, str, method, charset);
	//得到byte数组的长度,申请一块内存空间
    int length =  (*env)->GetArrayLength(env,byteArr);
	if (length>0) {
		cstr = malloc(length+1);//最后一个位置放"\0"
		//把byte数组中的数据copy到新建申请的内存空间中
		 char* elements = (*env)->GetByteArrayElements(env,byteArr, 0);

		 strncpy(cstr, elements, length);
		 cstr[length] = 0;//表示字符串的结尾(\0)

		//释放内存并返回
		(*env)->ReleaseByteArrayElements(env, byteArr, elements, 0);
	}
	 return cstr;
}
C++的实现
char* Jstr2Cstr(JNIEnv * env, jstring jstr) {
    char * rtn = NULL;
    jclass clsstring = env->FindClass("java/lang/String");
    jstring strencode = env->NewStringUTF("UTF-8");
    jmethodID mid = env->GetMethodID(clsstring, "getBytes", "(Ljava/lang/String;)[B");
    jbyteArray barr= (jbyteArray)env->CallObjectMethod(jstr,mid,strencode);
    jsize alen = env->GetArrayLength(barr);
    jbyte * ba = env->GetByteArrayElements(barr,JNI_FALSE);
    if(alen > 0)
    {
        rtn = (char*)malloc(alen+1); //new char[alen+1];
        memcpy(rtn,ba,alen);
        rtn[alen]=0;
    }
    env->ReleaseByteArrayElements(barr,ba,0);
    
    return rtn;
}


char*转换jstring


C的实现
(*env)->NewStringUTF(env,"Hello");

C++的实现
env->NewStringUTF("hello")


C与C++中JNI实现的区别


JNIEnv


android-ndk-r9d\platforms\android-4\arch-arm\usr\include目录下的jni.h文件中153行有以下这几代码:

#if defined(__cplusplus)//如果是C++语言 JNIEnv定义为_JNIEnv 这么一个结构体
typedef_JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
//不是c++语言则定义JNINativeInterface这么一个结构体(_JNIEnv指针的指针)
typedef const struct JNIInvokeInterface* JavaVM;
#endif


我们使用javah生成头文件时候至少会生成2个参数,JNIEnv*jobject(如果是静态方法是jclass

比如:JNIEXPORT jint JNICALL Java_com_test_Test (JNIEnv *, jobject);
如果java的native定义的方法中的签名有参数,那么参数则会跟在jobject后面。


在C中,看到JNIEnv 我们实质是取得了JNINativeInterface* (JNIEnv指针的指针),我们得使用**env获取结构体,从而才能使用结构体里面的方法。


在C++中,看到JNIEnv我们实质是取得了JNIEnv*(JNIEnv结构体的指针),我们可以直接使用env->使用结构体里面的方法。


方法调用的区别


C++方法调用的区别:
1.调用JNI函数时,不需要传递env
2.(*env)->调用方法简化成env->

//c语言的实现
 return   (*env)->NewStringUTF(env,"hello from c");
//c++实现
 return env->NewStringUTF("hello in c");



Android.mk说明


Android.mk是编写编译规则的文件。交叉编译器在编译C/C++代码时需要的配置文件,linux下makefile语法的子集。

常见宏说明:
<script src="https://code.csdn.net/snippets/526384.js" type="text/javascript"></script>


绕过JNI直接调用C函数


Android系统的Linux二进制文件在system/bin目录下,该目录已经被配置在系统的环境变量里,我们可以在adb shell下直接执行里面的文件,这样我们也可以通过java的runtime直接执行一个二进制文件了。

 

比如:

ps              获取当前所有进程的信息

ifconfig     获取当前网络信息


示例代码:
//在Java代码中调用Linux下的二进制文件:
Process process = Runtime.getRuntime().exec(“二进制文件全路径名”);
//得到输入流
InputStream is = process.getInputStream();
//包装输入流
DataInputStream dis = new DataInputStream(is);
//读取输入流
StringBuild builder = new StingBuilder();
for(String result ; (result=dis.readLine())!=null ;){
	builder.append(result);
	builder.append(“\n”);
}
//打印信息
System.out.println(builder.toString());


中文乱码问题


如果写C代码的文件是UTF-8编码的才可以直接写中文字符。

低版本的NDK(r4)默认的编码集为ISO8859-1,会显示乱码,高版本的直接挂掉。

如果想使用低版本的NDK,正常显示中文,可以使用以下方式获取正常的中文字符:
C:return (*env)-->NewStringUTF(env,”你好 JNI”)
JAVA:String str = new String(string.getBytes(“ISO8859-1”),”UTF-8”);


解决方法名冲突


如果java中的native方法名带下划线的处理:(在下划线后加1)

示例:
//java层的native方法
public native String call_C_Method();

//C/C++实现
JNIEXPORT jstring JNICALL Java_com_example_hellojni_MainActivity_call_1C_1Method
  (JNIEnv * env, jobject obj)
{
return (*env)->NewStringUTF(env,"这是来自C语言的字符串");
}


注册JNI函数


注册jni函数这个动作作用是建立java层的native方法与jni层的native函数之间的关联。

 

它们之间的关联主要是用一个结构体JNINativeMethod来完成的,结构体定义如下:

typedef struct {

   const char* name;                     //Java层的native方法名称

   const char* signature;              //Java层的native方法的签名(javap –s –p ClassName)

   void*       fnPtr;                   //jni层实现函数的函数指针

} JNINativeMethod;

 

在jni层会建立一个JNINativeMethod数组用于记录java层与jni层的方法调用关系。

 

接着该JNINativeMethod数组会被用作JNIEnv中函数jint RegisterNativeMethod(jclass,JNINativeMethods, len)的第二个参数执行注册动作,jclass是java层定义了native方法的类。


静态注册


实现步骤:

1.编写Java代码,定义native方法,编译生成.class文件;

2.使用javah工具,如javah –o output packageName.className,这样它会生成一个output.h的JNI层头文件;

3.把头文件添加到工程jni目录,在C/C++代码中引用头文件并实现相关函数;


java层中的native方法是如果找到jni层的实现函数的?
当Java层调用xxx方法时,它会从对应的JNI库中寻找Java_packageName_xxx函数,如果没有就会报错。如果找到,则会为这个xxx方法和Java_packageName_xxx函数建立一个关联关系,其实就是保存JNI层函数的函数指针。以后再调用xxx方法时,直接使用这个函数指针就可以了。这项工作是由虚拟机完成的。


静态注册是根据函数名来建立Java方法与JNI函数之间的关联关系的。这种方法有几个弊端

1.需要编译所有声明了native方法的Java类,每个所生成的class文件都得用javah生成一个头文件;

2.javah生成的JNI层函数名特别长,书写起来不方便;

3.初次调用native方法时要根据函数名字搜索对应的JNI层函数来建立关联关系,这样会影响运行效率。


动态注册


在代码中建立java层native方法与jni层实现函数之间的关联。直接让native方法知道JNI层对应函数的函数指针。

 

JNI_Onload函数是在动态库被加载完成时调用,动态注册必须实现JNI_Onload函数。


动态注册示例代码:
//在模块被加载时,建立java层native方法与native层的native方法的关联关系
jint JNI_OnLoad(JavaVM* vm, void* reserved){
	LOGI("loaded mode and call JNI_Onload method");

	//创建关联结构数组
	JNINativeMethod nativeMethods[] = {
			{
					"test",			     //Java层的native方法名
					"()Ljava/lang/String;",	//Java层的native方法名的签名
					(void*)test	         //native层方法的指针
			}
	};
	LOGI("native methods array is created.");

	//动态注册
    //1.得到JNIEnv*
	JNIEnv* env = NULL;
    vm->GetEnv((void**)&env, JNI_VERSION_1_2);
    LOGI("find class is finish.");

//2.注册native方法
//查找java层定义了native方法的类
    jclass clazz = env->FindClass("com/example/jnidynamicreg/MainActivity");
    jint result = env->RegisterNatives( clazz, nativeMethods, 1);
LOGI("after register natives result is %d",result);

   return  JNI_VERSION_1_2;	//注意这里一定要返回JNI_VERSION,不然会报异常
}


JNI使用总结