jni_jnienv - CSDN
jni 订阅
JNI是Java Native Interface的缩写,通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植。 [1]  从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,但是它并不妨碍你使用其他编程语言,只要调用约定受支持就可以了。使用java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样做是可以接受的,甚至是必须的。例如,使用一些旧的库,与硬件、操作系统进行交互,或者为了提高程序的性能。JNI标准至少要保证本地代码能工作在任何Java 虚拟机环境。 展开全文
JNI是Java Native Interface的缩写,通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植。 [1]  从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,但是它并不妨碍你使用其他编程语言,只要调用约定受支持就可以了。使用java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样做是可以接受的,甚至是必须的。例如,使用一些旧的库,与硬件、操作系统进行交互,或者为了提高程序的性能。JNI标准至少要保证本地代码能工作在任何Java 虚拟机环境。
信息
简    写
JNI
外文名
Java Native Interface
编程语言
Java
中文名
Java本地接口
学    科
Java和本地代码间的双向交互
目    的
使用 Java 本地接口书写程序
JNI简介
SUN公司发布的Java 本地接口(JNI)提供了将Java与C/C++、汇编等本地代码集成的方案,该规范使得在 Java 虚拟机内运行的 Java 代码能够与其它编程语言互相操作,包括创建本地方法、更新Java对象、调用Java方法,引用 Java类,捕捉和抛出异常等,也允许 Java代码调用 C/C++或汇编语言编写的程序和库。作为一个标准程序接口,它没有对底层 Java虚拟机的实现施加任何限制,并具有以下特点:二进制兼容。本地方法库与同一平台上所有Java 虚拟机之间实现二进制兼容,即对于给定平台开发人员只需要维护一种版本的本地方法库。效率高。为了实现实时系统,JNI 在效率与虚拟机无关性之间进行了优化,以保障高效运行。功能强。JNI 提供了大量的函数及接口让本地方法与Java 虚拟机内核相互操作,增强两者的功能。本地代码与 Java 虚拟机之间是通过 JNI 函数实现相互操作的。JNI 函数通过接口指针来获得,本地方法将 JNI 接口指针当作参数来接受。虚拟机保证在从相同的 Java 线程中对本地方法进行多次调用时,传递给本地方法的接口指针是相同的,本地方法被不同的 Java 线程调用时,它接受不同的 JNI接口指针。 [2] 
收起全文
精华内容
参与话题
  • Android开发中,随着对移动程序的安全性、性能等方面的重视,JNI技术也越发重要。 如今,多数企业在招聘中、高级程序员时,基本上都要求熟悉JNI开发,所以,掌握JNI技术,也是我们迈进心仪企业的必备条件。 本套...
  • Android开发中,随着对移动程序的安全性、性能等方面的重视,JNI技术也越发重要。 如今,多数企业在招聘中、高级程序员时,基本上都要求熟悉JNI开发,所以,掌握JNI技术,也是我们迈进心仪企业的必备条件。 本套课程...
  • JNI详解------完整Demo

    万次阅读 多人点赞 2018-08-07 14:34:53
    为什么要用JNI?因为有些功能JAVA无法提供,比如对扫描仪驱动,我现在就是要搞这个,网上给的例子都是SB.我气不过,便要自己去搞.感觉很悲剧.搜来想去,只能想办法通过C/C++来操作,然后用JAVA去调用C.这就需要JNI了.   ...

    为什么要用JNI?因为有些功能JAVA无法提供,比如对扫描仪驱动,我现在就是要搞这个,网上给的例子都是SB.我气不过,便要自己去搞.感觉很悲剧.搜来想去,只能想办法通过C/C++来操作,然后用JAVA去调用C.这就需要JNI了.

     

    什么是JNI?

    JNI是Java Native Interface的缩写,它提供了若干的API实现了Java和其他语言的通信(主要是C&C++).这是百度百科上说的.通俗来说,就是JAVA调用C/C++函数的接口.如果你要想调用C系列的函数,你就必须遵守这样的约定.

    JNI接口都长什么样?

    就一个native的关键字.

    public  class NativeDemo {
    	{
    		/**
    		 * 系统加载其他的语言的函数
    		 */
    		System.load("C:\\Users\\Administrator\\Desktop\\com\\Hello.dll");
    	}
    	/**
    	 * 就这个natice关键字.标记了这个接口,看起来像是abstract
    	 */
    	public native void sayHello();
    	
    	
    	public static void main(String[] args) {
    		new NativeDemo().sayHello();
    	}
    }

    静态代码块先不讲。先看native方法。

    sayHello()方法加了一个关键字native,就代表是一个native接口.执行这个方法时,会根据jni.h来找到真正的C来编写的sayHello()的实际函数.

    jni.h是什么?

    它实际上就存在%JAVA_HOME%\bin\include下面的一个文件,另外还有个%JAVA_HOME%\bin\include\win32下的jni_md.h.

    这东西不说其他的作用,我也不清楚,只知道它里面存储了大量的函数和对象,它有个很好的方法就是通过native接口名,获取C函数.

    打个比方类似如下:

    public static String getCMethod(String javaMethodName);

    它可以根据你的java接口,找到C函数并调用.

    但这就意味着,你不能在C里随意写函数名,因为如果你写的java方法叫native aaa();C函数也叫aaa();但jni.h通过getCMethod(String javaMethodName)去找的结果是xxx();那这样就无法调用了.

    既然不能随意写,怎么办?

    没事,jdk提供了一个通过java方法生成c函数接口名的工具javah.

    javah是什么?

    就像java是运行main方法一样,javah就是提供具有native method的java对象的c函数接口.

    dos命令如下:

    javac NativeDemo.java
    javah NativeDemo

    这个命令可以提供一个c函数的接口.

    上面那个NativeDemo被javah了之后就生成了一个文件Hello.h(可能我改了名字),就是C函数的接口.里面有方法名和返回值什么的.

    Hello.h长什么样?

    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include "jni.h"
    /* Header for class NativeDemo */
    
    #ifndef _Included_NativeDemo
    #define _Included_NativeDemo
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
     * Class:     NativeDemo
     * Method:    sayHello
     * Signature: ()V
     */
    JNIEXPORT void JNICALL Java_NativeDemo_sayHello
      (JNIEnv *, jobject);
    
    #ifdef __cplusplus
    }
    #endif
    #endif

     

    最重要的C函数接口就是这样:JNIEXPORT void JNICALL Java_NativeDemo_sayHello(JNIEnv *, jobject);

    JNIEXPORT :在Jni编程中所有本地语言实现Jni接口的方法前面都有一个"JNIEXPORT",这个可以看做是Jni的一个标志,至今为止没发现它有什么特殊的用处。

    void :这个学过编程的人都知道,当然是方法的返回值了。

    JNICALL :这个可以理解为Jni 和Call两个部分,和起来的意思就是 Jni调用XXX(后面的XXX就是JAVA的方法名)。

    Java_NativeDemo_sayHello:这个就是被上一步中被调用的部分,也就是Java中的native 方法名,这里起名字的方式比较特别,是:包名+类名+方法名。

    JNIEnv * env:这个env可以看做是Jni接口本身的一个对象,jni.h头文件中存在着大量被封装好的函数,这些函数也是Jni编程中经常被使用到的,要想调用这些函数就需要使用JNIEnv这个对象。例如:env->GetObjectClass()。(详情请查看jni.h)

    jobject obj:代表着native方法的调用者,本例即new NativeDemo();但如果native是静态的,那就是NativeDemo.class .

    也就是说,我们的native sayHello()方法实际上是运行C的Java_NativeDemo_sayHello()这个方法,我们是不能随意写C函数名的的,只能这样写。

    接下来我们可以照着接口去写真正的函数方法了.新建Hello.cpp

    /* Replace "dll.h" with the name of your header */
    #include "Hello.h"
    #include <windows.h>
    #include <iostream>
    
    JNIEXPORT void JNICALL Java_NativeDemo_sayHello(JNIEnv *, jobject){
    	using namespace std;
    	cout << "Hello___________World";
    } 

    这个方法什么都没有做,就打印了一句话"Hello___________World"

    然后将Hello.h和Hello.cpp编译运行出dll文件.生成Hello.dll

    这个过程中可以能编译会出现问题.

    说“jni.h”: No such file or directory:

    你需要把jdk里面的那俩jni.h给拷贝过来,再编译,如果还出错.需要把Hello.h里的头部#include<jni.h> 改写成 #include "jni.h".由尖括号改成双引号,具体咋回事,咱不是C程序员不清楚.

    运行下试试吧

    这样你现在就拥有了下列文件

    实际上你现在就只需要NativeDemo和Hello.dll就行了。

    别急,还有一个。NativeDemo里的静态代码块请准确把dll库给load进去

    System.load("C:\\Users\\Administrator\\Desktop\\com\\Hello.dll");

    然后编译java,运行

    C:\Users\Administrator\Desktop\com>javac NativeDemo.java
    
    C:\Users\Administrator\Desktop\com>java NativeDemo
    Hello___________World
    C:\Users\Administrator\Desktop\com>

    非常完美。C++里的打印被调用了。

    展开全文
  • JNI:Java与C++的美好结合

    万人学习 2019-06-24 13:22:29
    这课程,我们的主题就是对C函数,或是对Shared Library,或是对JNI的本地代码的代码写法,我们要讲求它的稳定性。也就是要让它有更好的设计,是我们对这设计做优化的思考,也是架构师的一个很重要的职责。本节,我...
  • Android JNI机制

    千次阅读 2015-09-14 21:40:24
    由前面基础知识可知,Android的应用层由Java语言编写,Framework框架层则是由Java代码与C/C++语言实现,之所以由两种不同的语言组合开发框架层,是因为Java代码是与硬件环境彻底“隔离”的跨平台语言,Java代码无法...

     

    由前面基础知识可知,Android的应用层由Java语言编写,Framework框架层则是由Java代码与C/C++语言实现,之所以由两种不同的语言组合开发框架层,是因为Java代码是与硬件环境彻底“隔离”的跨平台语言,Java代码无法直接操作硬件。比如:Android系统支持大量传感器,Java运行在虚拟机中,无法直接得到传感器数据,而Android系统基于Linux操作系统,在Linux操作系统中C/C++通过Linux提供的系统调用接口可以直接访问传感器硬件驱动,Java代码可以将自己的请求,交给底层的本地C/C++代码实现间接的对传感器的访问。另外,Java代码的执行效率要比C/C++执行效率要低,在一些对性能要求比较高的场合,也要使用C/C++来实现程序逻辑。

    既然Java代码要请求本地C/C++代码,那么二者必须要通过一种媒介或桥梁联系起来,这种媒介就是Java本地接口(Java NativeInterface),它是Java语言支持的一种本地语言访问方式,JNI提供了一系列接口,它允许Java与C/C++语言之间通过特定的方式相互调用、参数传递等交互操作。

    通常在以下几种情况下考虑使用JNI:

    Ø 对处理速度有要求

    Java代码执行速度要比本地代码(C/C++)执行速度慢一些,如果对程序的执行速度有较高的要求,可以考虑使用C/C++编写代码,然后在通过Java代码调用基于C/C++编写的部分。

    Ø 硬件控制

    如前面所述,Java运行在虚拟机中,和真实运行的物理硬件之间是相互隔离的,通常我们使用本地代码C实现对硬件驱动的控制,然后再通过Java代码调用本地硬件控制代码。

    Ø 复用本地代码

    如果程序的处理逻辑已经由本地代码实现并封装成了库,就没有必要再重新使用Java代码实现一次,直接复用该本地代码,即提高了编程效率,又确保了程序的安全性和健壮性。

     

    4.2 JNI原理

    在计算机中,每种编程语言都有一个执行环境(Runtime),执行环境用来解释执行语言中的语句,不同的编程语言的执行环境就好比如西游记中的“阴阳两界”一样,一般人不能同时生存在阴阳两界中,只有“黑白无常”能自由穿梭在阴阳两界之间,“黑白无常”往返于阴阳两界时手持生死簿,“黑白无常”按生死簿上记录着的人名“索魂”。

    4.2.1 JavaVM与JNIEnv

    Java语言的执行环境是Java虚拟机(JVM),JVM其实是主机环境中的一个进程,每个JVM虚拟机进程在本地环境中都有一个JavaVM结构体,该结构体在创建Java虚拟机时被返回,在JNI中创建JVM的函数为JNI_CreateJavaVM。

    JNI_CreateJavaVM(JavaVM **pvm, void **penv, void*args);


     

    JavaVM结构中封装了一些函数指针(或叫函数表结构),JavaVM中封装的这些函数指针主要是对JVM操作接口,如下面代码所示:

    @jni.h

    struct JNIInvokeInterface_ {

    void *reserved0;

    void *reserved1;

    void *reserved2;

    jint (JNICALL *DestroyJavaVM)(JavaVM *vm);     // 销毁Java虚拟机并回收资源,只有JVM主线程可以销毁

     jint (JNICALL *AttachCurrentThread)(JavaVM *vm,void **penv, void *args); // 连接当前线程为Java线程

      jint (JNICALL *DetachCurrentThread)(JavaVM*vm);                // 释放当前Java线程

      jint (JNICALL *GetEnv)(JavaVM *vm, void**penv, jint version);          // 获得当前线程的Java运行环境

      jint (JNICALL*AttachCurrentThreadAsDaemon)(JavaVM *vm, void **penv, void *args);// 连接当前线程作为守护线程

    };

     

    struct JavaVM_ {

        const struct JNIInvokeInterface_*functions;


      jint DestroyJavaVM() {

            returnfunctions->DestroyJavaVM(this);

        }

    …省略部分代码

        jint GetEnv(void **penv, jintversion) {

            returnfunctions->GetEnv(this, penv, version);

      }

      …省略部分代码

    };

     

    #ifdef __cplusplus


       typedef  JavaVM_  JavaVM;


    #else


       typedef  const structJNIInvokeInterface_  *JavaVM;


    #endif

    通过上面代码分析可知,JNIInvokeInterface_结构封装了几个和JVM相关的功能函数,如销毁JVM,获得当前线程的Java执行环境。另外,在C和C++中JavaVM的定义有所不同,在C中JavaVM是JNIInvokeInterface_类型指针,而在C++中又对JNIInvokeInterface_进行了一次封装,比C中少了一个参数,这也是为什么JNI代码更推荐用C++来编写的原因。

    JNIEnv是当前Java线程的执行环境,一个JVM对应一个JavaVM结构,而一个JVM中可能创建多个Java线程,每个线程对应一个JNIEnv结构,它们保存在线程本地存储(TLS)中。因此,不同的线程的JNIEnv是不同,也不能相互共享使用。

    JNIEnv结构也是一个函数表,在本地代码中通过JNIEnv的函数表来操作Java数据或调用Java方法。也就是说,只要在本地代码中拿到了JNIEnv结构,就可以在本地代码中调用Java代码。

     

    @jni.h中对JNIEnv的定义如下:

    struct JNINativeInterface_ {

            …

            jclass (JNICALL*FindClass) (JNIEnv *env, const char *name);

           …定义大量JNI函数指针

    };

     

    struct JNIEnv_ {

            const structJNINativeInterface_ *functions;

            jclass FindClass(constchar *name) {

                   return functions->FindClass(this, name);      //调用JNINativeInterface_中的函数指针

            }

           …省略部分代码

    };

     

    #ifdef __cplusplus


            typedef JNIEnv_ JNIEnv;


    #else


            typedef const structJNINativeInterface_ *JNIEnv;


    #endif

    由上面代码可知,和JavaVM类似,JNIEnv在C代码和C++代码中的使用方式也是不一样的,在C++中对JNINativeInterface_结构又进行了一次封装,调用起来更方便。

             总体来说,JNI其实就是定义了Java语言与本地语言间的一种沟通方式,这种沟通方式依赖于JavaVM和JNIEnv结构中定义的函数表,这些函数表负责将Java中的方法调用转换成对本地语言的函数调用。

    4.3 JNI中的数据传递

    4.3.1 JNI基本类型

    当Java代码与本地C\C++代码相互调用时,肯定会有参数数据的传递。两者属于不同的编程语言,在数据类型上有很多差别。JNI要保证它们两者之间的数据类型和数据空间大小的匹配。尽管C和Java中都拥有int和char的数据类型,但是他们的长度却不尽相同。在C语言中,int类型的长度取决与平台,char类型为1个字节,而在Java语言中,int类型恒为4字节,char类型为2字节。为了使Java语言和本地语言类型、长度匹配,JNI中定义了jint,jchar等类型,在JNI中定义了一些新的数据类型,如下表所示。

    表xx-xx

    Java Language Type

    JNI Type

    boolean

    jboolean

    byte

    jbyte

    char

    jchar

    short

    jshort

    int

    jint

    long

    jlong

    float

    jfloat

    double

    jdouble

    All Reference type

    jobject

    由Java类型和JNI数据类型的对应关系可以看到,这些新定义的JNI类型名称和Java类型名称具有一致性,只是在前面加了个j,如int对应jint,long对应jlong。我们可以通过JDK目录中的jni.h和jni_md.h来更直观的了解:

    @ jni_md.h

    …省略部分代码

    typedef long        jint;

    typedef __int64    jlong;

    typedef signed char      jbyte;

    …省略部分代码

    @jni.h

    // JNI类型与C/C++类型对应关系声明

    typedef unsigned char   jboolean;

    typedef unsigned short  jchar;

    typedef short                 jshort;

    typedef float                  jfloat;

    typedef double              jdouble;

    typedef jint                     jsize;

    由jni头文件可以看出,jint对应的是C/C++中的long类型,即32位整数,而不是C中的int类型(C中的int类型长度依赖于平台)。所以如果要在本地方法中要定义一个jint类型的数据,规范的写法应该是 jinti=123L;

    再比如jchar代表的是Java类型的char类型,实际上在C/C++中却是unsigned short类型,因为Java中的char类型为两个字节,jchar相当于C/C++中的宽字符。所以如果要在本地方法中要定义一个jchar类型的数据,规范的写法应该是jcharc=L'C';

    实际上,所有带j的类型,都是JNI对应的Java的类型,并且jni中的类型接口与本地代码在类型的空间大小是完全匹配的,而在语言层次却不一定相同。在本地方法中与JNI接口调用时,要在内部都要转换,我们在使用的时候也需要小心。

    4.3.2 JNI引用类型

    在本地代码中为了访问Java运行环境中的引用类型,在JNI中也定义了一套对应的引用类型,它们的对应关系如下:

     

    JNI引用类型

    Java引用类型

    jobject

    所有引用类型父类Object

    jclass

    java.lang.Class类型

    jstring

    java.lang.String类型

    jarray

    数组类型

    jobjectArray

    对象数组类型

    jbooleanArray

    布尔数组类型

    jbyteArray

    字节数组类型

    jcharArray

    字符数组类型

    jshortArray

    短整形数组类型

    jintArray

    整形数组类型

    jlongArray

    长整形数组类型

    jfloatArray

    浮点数组类型

    jdoubleArray

    双精度数组类型

    jthrowable

    java.lang.Throwadble类型

     

    由上表内容可知,JNI引用类型都是以j开头类型。与Java中所有类的父类为Object一样,所有的JNI中的引用类型都是jobject的子类,JNI这些j类型和Java中的类一一对应,只不过名字稍有不同而已。

    4.4 Java访问本地方法

    由4.1节可知,在某些情况下一些功能由本地代码来实现,这时Java代码需要调用这些本地代码,在调用本地代码时,首先要保证本地代码被加载到Java执行环境中并与Java代码链接在一起,这样当Java代码在调用本地方法时能保证找到并调用到正确的本地代码,然后在Java中要显示声明本地方法为native方法。其实现步骤如下:

    ·        编写Java代码,在Java代码中加载本地代码库

    ·        在Java中声明本地native方法

    ·        调用本地native方法

    例如:

    public class HelloJNI{

             static{

                       System.loadLibrary(“hellojni”);  // 通过System.loadLibrary()来加载本地代码库

    }

     

             privatestatic native String getJNIHello();    // 由于该方法的实现在本地代码中,所以加上native关键字进行声明

     

             publicstatic void main(String args[]){

                       System.out.println(HelloJNI.getJNIHello()); // 调用本地方法

    }

    }

    上述代码的执行需要本地代码库hellojni,正常运行的话会在屏幕上打印:Helloworld字符串。由代码可知,在Java中调用本地代码不是很复杂,本地代码库的加载系统方法System.loadLibrary在static静态代码块中实现的,这是因为静态代码块只会在Java类加载时被调用,并且只会被调用一次。本地代码库的名字为hellojni,如果在Windows中则其对应的文件名为:hellojni.dll,如果在Linux中,其对应的文件名为libhellojni.so。

    思考:

    1. 可不可以将本地代码库的加载放到构造方法中?放在非静态代码块中呢?

    2. native方法可以声明为abstract类型的吗?

    Native关键字本身和abstract关键字冲突,他们都是方法的声明,只是一个是把方法实现移交给子类,另一个是移交给本地代码库。如果同时出现,就相当于即把实现移交给子类,又把实现移交给本地操作系统,那到底谁来实现具体方法呢?

     

    4.5 JNI访问Java成员

    在JNI调用中,不仅仅Java可以调用本地方法,本地代码也可以调用Java中的方法和成员变量。在Java1.0中规定:Java和C本地代码绑定后,程序员可以直接访问Java对象数据域。这就要求虚拟机暴露它们之间内部数据的绑定关系,基于这个原因,JNI要求程序员通过特殊的JNI函数来获取和设置数据以及调用java方法。

    Java中的类封装了属性和方法,要想访问Java中的属性和方法,首先要获得Java类或Java对象,然后再访问属性、调用方法。

    在Java中类成员指静态属性和静态方法,它们属于类而不属于对象。而对象成员是指非静态属性和非静态方法,它们属于具体一个对象,不同的对象其成员是不同的。正因为如此,在本地代码中,对类成员的访问和对对象成员的访问是不同的。

    在JNI中通过下面的函数来获得Java运行环境中的类。

    jclass FindClass(const char *name);

    name:类全名,包含包名,其实包名间隔符用“/”代替“.”

    例如:

    jclass jActivity = env->FindClass(“java/lang/String”);

    上述JNI代码获得Android中的Activity类保存在jActivity中。

    在JNI中Java对象一般都是作为参数传递给本地方法的。

    例如:

    Java代码:

    package com.test.exam1;

    class MyClass{

             privateint mNumber;

             privatestatic String mName;

             publicMyClass(){

    }

     

    publicvoid printNum(){

       System.out.println(“Number:” + mNumber);

    }

     

    publicstatic void printNm(){

       System.out.println(“Number:” + mNumber);

    }

    }

     

    class PassJavaObj{

             static{

                       System.loadLibrary(“native_method”);

             }

             privatenative static void passObj(String str);

     

             publicstatic void main(String arg[]){

                       passObj(“HelloWorld”);

    }

    }

    本地代码:

    void Java_com_test_exam1_PassJavaObj_passObj

    (JNIEnv * env, jclassthiz, jobject  str)

    {

    }

    在上述例子中,Java代码中将“Hello World”字符串对象传递给了本地代码,在本地代码对应的方法中,共有三个参数,其中前两个参数是由Java运行环境自动传递过来的,env表示当前Java代码的运行环境,thiz表示调用当前本地方法的对象,这两个参数在每个本地方法中都有。第三个参数str就是我们传递过来的字符串。

    在本地方法中拿到了类或对象后,JNI要求程序员通过特殊的JNI函数来获取和设置Java属性以及调用java方法。

    4.5.1取得Java属性ID和方法ID

    为了在C/C++中表示Java的属性和方法,JNI在jni.h头文件中定义了jfieldID和jmethodID类型来分别代表Java对象的属性和方法。我们在访问或是设置Java属性的时候,首先就要先在本地代码取得代表该Java属性的jfieldID,然后才能对本地代码的Java属性进行操作。同样的,我们需要调用Java方法时,也需要取得代表该方法的jmethodID才能进行Java方法调用。

    使用JNIEnv提供的JNI方法,我们就可以获得属性和方法相对应的jfieldID和jmethodID:

    @jni.h

    // 根据属性签名返回 clazz类中的该属性ID

    jfieldID GetFieldID(jclass clazz, const char*name, const char *sig);

    // 如果获得静态属性ID,则调用下面的函数

    jfieldID GetStaticFieldID(jclass clazz, constchar *name, const char *sig);

     

    // 根据方法签名返回clazz类中该方法ID

    jmethodID GetMethodID(jclass clazz, const char*name, const char *sig);


    // 如果是静态方法,则调用下面的函数实现

    jmethodID GetStaticMethodID(jclass clazz, constchar *name, const char *sig);

    可以看到这四个方法的参数列表都是一模一样的,下面来分析下每个参数的含义:

    ·        jclass clazz:要取得成员对应的类

    ·        const char *name:代表我们要取得的方法名或者属性名

    ·        const char *sig:代表我们要取得的方法或属性的签名

    我们将一个例子进行简单修改:

    package com.test.exam2;

    class MyClass{

             privateint mNumber;

             privatestatic String mName = “Michael”;

             publicMyClass(){

                       mNumber= 1000;

    }

     

    publicvoid printNum(){

       System.out.println(“Number:” + mNumber);

    }

     

    publicstatic void printNm(){

       System.out.println(“Number:” + mNumber);

    }

    }

     

    class NativeCallJava{

             static{

                       System.loadLibrary(“native_callback”);

             }

             privatenative static void callNative(MyClass cls);

     

             publicstatic void main(String arg[]){

                       callNative(newMyClass());

    }

    }

    本地代码:

    void Java_com_test_exam2_NativeCallJava_callNative(JNIEnv* env, jclassthiz, jobject  obj)

    {

             jclass  myCls = env->GetObjectClass(obj);

             jfieldIDmNumFieldID = env->GetFieldID(myCls, “mNumber”, “I”);

             jfieldIDmNameFieldID = env->GetStaticFieldID(myCls, “mName”, “java/lang/String”);

     

             jmethodIDprintNumMethodID = env->GetMethodID(myCls, “printNum”, “(V)V”);

             jmethodIDprintNmMethodID = env->GetStaticMethodID(myCls, “printNm” , “(V)V”);

    }

    在上述Java代码中,我们自己定义了一个类MyClass,里面定义了对应的静态和非静态成员,然后将MyClass对象传递给本地代码。在本地代码中通过GetObjectClass方法取得MyClass对象对应的类,然后依次取得MyClass类中的属性ID和方法ID。

    其中GetObjectClass定义如下:

    jclass GetObjectClass(jobject obj) ;

    jobject obj:如果本地代码拿到一个对象,则通过该方法取得该对象的类类型,其功能如同Object.getClass()方法。

    4.5.2 JNI类型签名

    Java语言是面向对象的语言,它支持重载机制,即:允许多个具有相同的方法名不同的方法签名的方法存在。因此,只通过方法名不能明确的让JNI找到Java里对应的方法,还需要指定方法的签名,即:参数列表和返回值类型。

    JNI中类型签名如下表所示:

    类型签名

    Java类型

    类型签名

    Java类型

    Z

    boolean

    [

    []

    B

    byte

    [I

    int[]

    C

    char

    [F

    float[]

    S

    short

    [B

    byte[]

    I

    int

    [C

    char[]

    J

    long

    [S

    short[]

    F

    float

    [D

    double[]

    D

    double

    [J

    long[]

    L

    [Z

    boolean[]

    V

    void

     

     

    ·        基本类型

    以特定的单个大写字母表示

    ·        Java类类型

    Java类类型以L开头,以“/”分隔包名,在类名后加上“;”分隔符,例如String的签名为:Ljava/lang/String;

    在Java中数组是引用类型,数组以“[”开头,后面跟数组元素类型签名,例如:int[]的签名是[I ,对于二维数组,如int[][]签名就是[[I,object数组签名就是[Ljava/lang/Object;

    对于方法签名,在JNI中也有特定的表示方式。

    (参数1类型签名参数2类型签名参数3类型签名.......)返回值类型签名

    注意:

    ·        方法名在方法签名中没有体现出来

    ·        括号内表示参数列表,参数列表紧密相挨,中间没有逗号,没有空格

    ·        返回值出现在括号后面

    ·        如果函数没有返回值,也要加上V类型

    例如:

    Java方法

    JNI方法签名

    boolean isLedOn(void) ;

    (V)Z

    void setLedOn(int ledNo);

    (I)V

    String substr(String str, int idx, int count);

    (Ljava/lang/String;II)Ljava/lang/String

    char fun (int n, String s, int[] value);

    (ILjava/lang/String;[I)C

    boolean showMsg(android.View v, String msg);

    (Landroid/View;Ljava/lang/String;)Z

     

    4.5.3JNI操作Java属性和方法

    1. 获得、设置属性和静态属性

    取得了代表属性和静态属性的jfieldID,就可以使用JNIEnv中提供的方法来获取和设置属性/静态属性。

    取得Java属性的JNI方法定义如下:

    j<类型> Get<类型>Field(jobjectobj, jfieldID fieldID);


    j<类型> Get Static<类型>Field(jobjectobj, jfieldID fieldID);


    设置Java属性的JNI方法定义如下:

    void Set<类型>Field(jobject obj, jfieldID fieldID, j<类型> val);

    void Set Static<类型>Field(jobject obj, jfieldID fieldID, j<类型> val);

    <类型>表示Java中的基本类型。例如:

    // 获得obj对象中,整形属性ID为fieldID的值

    jint GetIntField(jobject obj, jfieldID fieldID);

     

    // 设置obj对象中,属性ID为fieldID,属性值为val

    void  SetObjectField(jobjectobj, jfieldID fielded,jobject val);  

     

    // 设置clazz类中,静态属性ID为fieldID的属性值为value

    void SetStaticCharField(jclass clazz, jfieldIDfieldID, jchar value)

    我们将上一节中的本地代码进行修改如下:

    void Java_com_test_exam2_NativeCallJava_callNative(JNIEnv* env, jclassthiz, jobject  obj)

    {

             jclass  myCls = env->GetObjectClass(obj);

             jfieldIDmNumFieldID = env->GetFieldID(myCls, “mNumber”, “I”);

             jfieldIDmNameFieldID = env->GetStaticFieldID(myCls, “mName”, “java/lang/String”);

            

             //获得、设置Java成员的属性值

             jintmNum = env->GetIntField(obj, mNumFieldID);

             env->SetIntField(obj,mNumFieldID, mNum+100);

     

             //获得、设置静态属性的值

             jstringmNm = (jstring)(env->GetStaticObjectField(myCls, mNameFieldID));

             printf(“%s\n”,mNm);

             jstringnewStr = env->NewStringUTF(“Hello from Native”);

             env->SetStaticObjectField(myCls,mNumFieldID, newStr);

             …

    }

    上述代码通过JNI提供的Get、Set方法取得和设置Java对象和类的属性,NewStringUTF表示创建一个Java的字符串对象,字符串值使用8位字符初始化。

    2. 通过JNI调用Java中的方法

             在4.5.1节我们通过JNI方法获得了jmethodID,在本地代码中我们就可以通过jmethodID来调用Java中的方法了。

    JNI提供了下面的方法用来调用Java方法:

    // 调用Java成员方法

    Call<Type>Method(jobject obj,jmethodIDmethodID,...);

    Call<Type>MethodV(jobject clazz, jmethodIDmethodID,va_listargs);

    Call<Type>tMethodA(jobject clazz,jmethodID methodID,constjvalue *args);

    // 调用Java静态方法

    CallStatic<Type>Method(jclass clazz,jmethodID methodID,...);

    CallStatic<Type>MethodV(jclass clazz,jmethodID methodID,va_listargs);

    CallStatic<Type>tMethodA(jclass clazz,jmethodID methodID,constjvalue *args);

    上面的Type这个方法的返回值类型,如void,int,char,byte等等。

    第一个参数代表调用的这个方法所属于的对象,或者这个静态方法所属的类。

    第二个参数代表jmethodID。

    后面的表示调用方法的参数列表,…表示是变长参数,以“V”结束的方法名表示以向量表形式提供参数列表,以“A”结束的方法名表示以jvalue数组提供参数列表,这两种调用方式使用较少。

    我们将前面的例子的本地代码继续进行修改:

    void Java_com_test_exam2_NativeCallJava_callNative(JNIEnv* env, jclassthiz, jobject  obj)

    {

             jclass  myCls = env->GetObjectClass(obj);

             jfieldIDmNumFieldID = env->GetFieldID(myCls, “mNumber”, “I”);

             jfieldIDmNameFieldID = env->GetStaticFieldID(myCls, “mName”, “java/lang/String”);

            

             //获得、设置Java成员的属性值

             jintmNum = env->GetIntField(obj, mNumFieldID);

             env->SetIntField(obj,mNumFieldID, mNum+100);

     

             //获得、设置静态属性的值

             jstringmNm = (jstring)(env->GetStaticObjectField(myCls, mNameFieldID));

             printf(“%s\n”,mNm);

             jstringnewStr = env->NewStringUTF(“Hello from Native”);

             env->SetStaticObjectField(myCls,mNumFieldID, newStr);

     

             //取得Java方法ID

             jmethodIDprintNumMethodID = env->GetMethodID(myCls, “printNum”, “(V)V”);

             jmethodIDprintNmMethodID = env->GetStaticMethodID(myCls, “printNm” , “(V)V”);

     

             //调用MyClass对象中的printNum方法

             CallVoidMethod(obj,printNumMethodID);

             //调用MyClass类的静态pinrtNm方法

             CallStaticVoidMethod(myCls,printNmMethodID);

    }

    在Java中构造方法是一种特殊的方法,主要用于对象创建时被回调,我们将在下一节分析。

    4.5.4 在本地代码中创建Java对象

    1. 在本地代码中创建Java对象

    在JNIEnv的函数表中提供了下面几个方法来创建一个Java对象:

    jobject NewObject(jclass clazz, jmethodIDmethodID,...);

    jobject NewObjectV(jclass clazz,jmethodIDmethodID,va_list args);

    jobjectNewObjectA(jclass clazz, jmethodIDmethodID,const jvalue *args) ;

    它们和上一节中介绍的调用Java方法使用起来很相似,他们的参数意义如下:

    clazz:要创建的对象的类。

    jmethodID:创建对象对应的构造方法ID。

    参数列表:…表示是变长参数,以“V”结束的方法名表示向量表表示参数列表,以“A”结束的方法名表示以jvalue数组提供参数列表。

    由于Java的构造方法的特点是方法名与类名一样,并且没有返回值,所以对于获得构造方法的ID的方法env->GetMethodID(clazz,method_name,sig)中的第二个参数是固定为类名(也可以用“<init>”代替类名),第三个参数和要调用的构造方法有关,默认的Java构造方法没有返回值,没有参数。

    我们将上一节的例子进行修改,在本地代码中创建一个新的MyClass对象:

    void Java_com_test_exam2_NativeCallJava_callNative(JNIEnv* env, jclassthiz, jobject  obj)

    {

             jclass  myCls = env->GetObjectClass(obj);

             //当然我们也可以像下面这样写:

             //jclass myCls = env->FindClass("com/test/exam2/MyClass");  

             //取得MyClass的构造方法ID

             jmethodIDmyClassMethodID = env->GetMethodID(myCls, “MyClass”, “(V)V”);

             //创建MyClass新对象

             jobjectnewObj = NewObject(myCls, myClassMethodID);

    }

    2.创建Java String对象

    在Java中,字符串String对象是Unicoode(UTF-16)编码,每个字符不论是中文还是英文还是符号,一个字符总是占用两个字节。在C/C++中一个字符是一个字节, C/C++中的宽字符是两个字节的。

    在本地C/C++代码中我们可以通过一个宽字符串,或是一个UTF-8编码的字符串创建一个Java端的String对象。这种情况通常用于返回Java环境一个String返回值等场合。

    根据传入的宽字符串创建一个Java String对象

    jstring NewString(const jchar *unicode, jsizelen)

    根据传入的UTF-8字符串创建一个Java String对象

    jstring NewStringUTF(const char *utf)

    在Java中String类有很多对字符串进行操作的方法,在本地代码中可以通过JNI接口可以将Java的字符串转换到C/C++的宽字符串(wchar_t*),或是传回一个UTF-8的字符串(char*)到C/C++,在本地进行操作。

    可以看下面的一个例子:

    在Java端有一个字符串 String str="abcde";,在本地方法中取得它并且输出:

    void native_string_operation (JNIEnv * env,  jobject obj)

    {

             //取得该字符串的jfieldID

    jfieldIDid_string=env->GetFieldID(env->GetObjectClass(obj),"str", "Ljava/lang/String;");

             jstringstring=(jstring)(env->GetObjectField(obj, id_string));    //取得该字符串,强转为jstring类型。

             printf("%s",string);

    }

    由上面的代码可知,从java端取得的String属性或者是方法返回值的String对象,对应在JNI中都是jstring类型,它并不是C/C++中的字符串。所以,我们需要对取得的 jstring类型的字符串进行一系列的转换,才能使用。

    JNIEnv提供了一系列的方法来操作字符串:

    将一个jstring对象,转换为(UTF-16)编码的宽字符串(jchar*):

    const jchar *GetStringChars(jstring str,jboolean*isCopy) 

    将一个jstring对象,转换为(UTF-8)编码的字符串(char*):

     const char *GetStringUTFChars(jstringstr,jboolean *isCopy)

    这两个函数的参数中,第一个参数传入一个指向Java 中String对象的jstring引用。第二个参数传入的是一个jboolean的指针,其值可以为NULL、JNI_TRUE、JNI_FLASE。

    如果为JNI_TRUE则表示在本地开辟内存,然后把Java中的String拷贝到这个内存中,然后返回指向这个内存地址的指针。如果为JNI_FALSE,则直接返回指向Java中String的内存指针。这时不要改变这个内存中的内容,这将破坏String在Java中始终是常量的规则。

    如果是NULL,则表示不关心是否拷贝字符串。

    使用这两个函数取得的字符,在不适用的时候(不管String的数据是否拷贝到本地内存),要分别对应的使用下面两个函数来释放内存。

    RealeaseStringChars(jstring jstr,const jchar*str)

    RealeaseStringUTFChars(jstringjstr, constchar* str)

    第一个参数指定一个jstring变量,即要释放的本地字符串的资源

    第二个参数就是要释放的本地字符串

    4.5.5 Java数组在本地代码中的处理

    我们可以使用GetFieldID获取一个Java数组变量的ID,然后用GetObjectFiled取得该数组变量到本地方法,返回值为jobject,然后我们可以强制转换为j<Type>Array类型。

    @jni.h

    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 jarray jobjectArray;

    j<Type>Array类型是JNI定义的一个对象类型,它并不是C/C++的数组,如int[]数组,double[]数组等等。所以我们要把j<Type>Array类型转换为C/C++中的数组来操作。

    JNIEnv定义了一系列的方法来把一个j<Type>Array类型转换为C/C++数组或把C/C++数组转换为j<Type>Array。

    1.      获取数组的长度

    jsize GetArrayLength(jarray array);

     

    2.      对象类型数组的操作

    jobjectArray NewObjectArray(jsize len, jclassclazz, jobject init)                        // 创建对象数组

    jobject GetObjectArrayElement(jobjectArrayarray, jsize index)                      // 获得元素

    void SetObjectArrayElement(jobjectArray array,jsize index, jobject val)          // 设置元素

    参数说明:

    len:新创建对象数组长度

    clazz:对象数组元素类型

    init:对象数组元素的初始值

    array:要操作的数组

    index:要操作数组元素的下标索引

    val:要设置的数组元素的值

    JNI没有提供直接把Java的对象类型数组(Object[ ])直接转到C++中的jobject[ ]数组的函数。而是直接通过Get/SetObjectArrayElement这样的函数来对Java的Object[ ]数组进行操作。

     

    3.       对基本数据类型数组的操作

    基本数据类型数组的操作方法比较多,大致可以分为如下几类:

    获得指定类型的数组:

    j<Type>*Get<Type>ArrayElements(j<Type>Array array, jboolean *isCopy);

    释放数组:

    voidRelease<Type>ArrayElements(j<Type>Array array, j<Type>*elems, jint mode);

    这类函数可以把Java基本类型的数组转换到C/C++中的数组。有两种处理方式,一是拷贝一份传回本地代码,另一种是把指向Java数组的指针直接传回到本地代码,处理完本地化的数组后,通过Realease<Type>ArrayElements来释放数组。处理方式由Get方法的第二个参数isCopied来决定(取值为JNI_TRUE或JNI_FLASE)。

    其第三个参数mode可以取下面的值:

    l  0:对Java的数组进行更新并释放C/C++的数组

    l  JNI_COMMIT:对Java的数组进行更新但是不释放C/C++的数组

    l  JNI_ABORT:对Java的数组不进行更新,释放C/C++的数组

    例如:

    package com.test.exam4_5

    class ArrayTest {

            static{

                       System.loadLibrary("native_array");

             }

     

            privateint [] arrays=new int[]{1,2,3,4,5};

     

            publicnative void show();

     

    publicstatic void main(String[] args) {

                       new ArrayTest ().show();

             }

    }

    本地代码:

    void Java_com_test_exam4_5_ArrayTest_show(JNIEnv * env,  jobject obj)

    {

            jfieldID id_arrsys = env->GetFieldID(env->GetObjectClass(obj),"arrays", "[I");

            jintArrayarr = (jintArray)(env->GetObjectField(obj, id_arrsys));

            jint*int_arr = env->GetIntArrayElements(arr, NULL);

            jsizelen = env->GetArrayLength(arr);

            for(int i = 0; I < len; i++)

             {

                       cout << int_arr[i]<< endl;

             }

            env->ReleaseIntArrayElements(arr, int_arr, JNI_ABORT);

    }

     

     

    4.6 局部引用与全局引用

    Java代码与本地代码里在进行参数传递与返回值复制的时候,要注意数据类型的匹配。对于int, char等基本类型直接进行拷贝即可,对于Java中的对象类型,通过传递引用实现。JVM保证所有的Java对象正确的传递给了本地代码,并且维持这些引用,因此这些对象不会被Java的gc(垃圾收集器)回收。因此,本地代码必须有一种方式来通知JVM本地代码不再使用这些Java对象,让gc来回收这些对象。

            JNI将传递给本地代码的对象分为两种:局部引用和全局引用。

    l  局部引用:只在上层Java调用本地代码的函数内有效,当本地方法返回时,局部引用自动回收。

    l  全局引用:只有显示通知VM时,全局引用才会被回收,否则一直有效,Java的gc不会释放该引用的对象。

    JNI中对于局部引用和全局引用相关的函数如下:

    创建指向某个对象的局部引用,创建失败返回NULL

    jobject NewLocalRef(jobject ref);

    删除局部引用

    void DeleteLocalRef(jobject obj);

    创建指向某个对象的全局引用,创建失败返回NULL

    jobject NewGlobalRef(jobject lobj);

    删除全局引用

    void DeleteGlobalRef(jobject gref);

    4.6.1局部引用

    默认的话,传递给本地代码的引用是局部引用。所有的JNI函数的返回值都是局部引用。

    jstring MyNewString(JNIEnv *env, jchar *chars,jint len)

    {

    staticjclassstringClass = NULL;              //static 不能保存一个局部引用

    jmethodIDcid;

    jcharArrayelemArr;

    jstringresult;

    if(stringClass== NULL) {

    stringClass =env->FindClass("java/lang/String");    // 局部引用

    if(stringClass == NULL) {

    return NULL; /* exception thrown */

                       }

                     }

              /* 本地代码中创建的字符串为局部引用,当函数返回后字符串有可能被gc回收 */

              cid =env->GetMethodID(stringClass,"<init>","([C)V");

             result=env->NewStringUTF(stringClass, cid, “Hello World”);

             returnresult;

    }

    虽然局部引用会在本地代码执行之后自动释放,但是有下列情况时,要手动释放:

    l  本地代码访问一个很大的Java对象时,在使用完该对象后,本地代码要去执行比较复杂耗时的运算时,由于本地代码还没有返回,Java收集器无法释放该本地引用的对象,这时,应该手动释放掉该引用对象。

    l   本地代码创建了大量局部引用,这可能会导致JNI局部引用表溢出,此时有必要及时地删除那些不再被使用的局部引用。比如:在本地代码里创建一个很大的对象数组。

    jni.h头文件中定义了JNI本地方法与Java方法映射关系结构体JNINativeMethod。

    l  创建的工具函数,它会被未知的代码调用,在工具函数里使用完的引用要及时释放。

    l  不返回的本地函数。例如,一个可能进入无限事件分发的循环中的方法。此时在循环中释放局部引用,是至关重要的,这样才能不会无限期地累积,进而导致内存泄露。局部引用只在创建它们的线程里有效,本地代码不能将局部引用在多线程间传递。一个线程想要调用另一个线程创建的局部引用是不被允许的。将一个局部引用保存到全局变量中,然后在其它线程中使用它,这是一种错误的编程。

    4.6.2 全局引用

    在一个本地方法被多次调用时,可以使用一个全局引用跨越它们。一个全局引用可以跨越多个线程,并且在被程序员手动释放之前,一直有效。和局部引用一样,全局引用保证了所引用的对象不会被垃圾回收。

    JNI允许程序员通过局部引用来创建全局引用, 全局引用只能由NewGlobalRef函数创建。下面是一个使用全局引用例子:

    jstringMyNewString(JNIEnv *env, jchar *chars,jint len)

    {

        staticjclassstringClass = NULL;

        ...省略部分代码

       if(stringClass == NULL) {

           jclasslocalRefCls =env->FindClass("java/lang/String");

           if(localRefCls == NULL) {

              return NULL;

            }

            /*创建全局引用并指向局部引用 */

          stringClass = env->NewGlobalRef(localRefCls);

            /*删除局部引用 */

          env->DeleteLocalRef(localRefCls);

            /*判断全局引用是否创建成功 */

           if(stringClass == NULL) {

              return NULL; /* out of memory exception thrown */

            }

        }

    }

    在native代码不再需要访问一个全局引用的时候,应该调用DeleteGlobalRef来释放它。如果调用这个函数失败,Java VM将不会回收对应的对象。

    4.6.3 在Java环境中保存JNI对象

    本地代码在某次被调用时生成的对象,在其他函数调用时是不可见的。虽然可以设置全局变量但那不是好的解决方式,Android中通常是在Java域中定义一个int型的变量,在本地代码生成对象的地方,与这个Java域的变量关联,在别的使用到的地方,再从这个变量中取值。

    以JNICameraContext为例来说明:

    JNICameraContext是android_hardware_camera.cpp中定义的类型,并会在本地代码中生成对象并与Java中定义的android.hardware.Camera类的mNativeContext整形成员关联。

    注:为了简化理解,代码已经做了简单修改

    static voidandroid_hardware_Camera_native_setup(JNIEnv*env, jobject thiz,jobjectweak_this, jintcameraId)

    {

        // 创建JNICameraContext对象

      JNICameraContext *context = new JNICameraContext(env, weak_this,clazz,camera);

    …省略部分代码

     

    // 查找到Camera类

       jclassclazz =env->FindClass("android/hardware/Camera ");

    // 保存Carmera类中mNativeContext成员ID

      jfieldID field = env->GetFieldID(clazz,"mNativeContext","I");

     

        // 保存context对象的地址到了Java中的mNativeContext属性里

      env->SetIntField(thiz,fields, (int)context);

    }

    当要使用在本地代码中创建的JNICameraContext对象时,通过JNIEnv::GetIntField()获取Java对象的属性,并转化为JNICameraContext类型:

    // 查找到Camera类

       jclassclazz =env->FindClass("android/hardware/Camera ");

    // 保存Carmera类中mNativeContext成员ID

      jfieldID field = env->GetFieldID(clazz,"mNativeContext","I");

       // 从Java环境中得到保存在mNativeContext中的对象引用地址

     JNICameraContext*context=(JNICameraContext*)(env->GetIntField(thiz,field));

        if(context!= NULL) {

            //…省略部分代码

        }

     

    4.7本地方法的注册

             前面我们介绍了Java方法和本地方法互相调用过程中用到的JNI接口函数。在Java代码调用本地方法时,JVM又是如何能正确的绑定到本地方法呢?

    当JVM在调用带有native关键字的方法时,JVM在Java运行时环境中查找“一张方法映射表”,根据这张表寻找对应的本地方法,如果本地代码中没有找到对应的函数,则会抛出java.lang.UnsatisfiedLinkError错误,所以,当我们在使用JNI编程时,必须保证本地方法出现在“方法映射表”中。

    4.7.1 JNI_OnLoad方法

             本地代码最终编译成动态库,在Java代码中通过System.loadLibrary方法来加载本地代码库,当本地代码动态库被JVM加载时,JVM会自动调用本地代码中的JNI_OnLoad函数。

    JNI_OnLoad函数的定义如下:

    jint JNI_OnLoad(JavaVM *vm, void *reserved);

    参数说明:

    vm:代表了JVM实例,其实是和JVM相关的一些操作函数指针,详情请查看4.2.1章节。

    reserved:保留

    一般来说JNI_OnLoad函数里主要做以下工作:

    l 调用GetEnv函数,获得JNIEnv,即Java执行环境

    l 通过RegisterNatives函数注册本地方法

    l 返回JNI版本号

    JNI从Java1.0到现在其版本也在发生变化 ,变化主要体现在JNIEnv中支持的函数个数,当调用GetEnv函数时可以指定获得某个版本的JNIEnv函数表。

    jint GetEnv(void **penv, jint version);

    参数说明:

    penv: JNIEnv指针的地址,GetEnv成功调用后,它指向JNIEnv指针。

    version:请求的JNI版本号,如:JNI_VERSION_1_4,表示请求JNI1.4版本的JNIEnv执行环境。

    返回值:当请求的JNI版本号不支持时,返回负值,成功返回JNI_OK。

             RegisterNatives是JNIEnv所提供的功能函数,用于注册本地方法和Java方法的映射关系到JVM中,保证Java代码调用本地代码时能正确调用到本地代码。

             JVM要求JNI_OnLoad函数必须返回一个合法的JNI版本号,表示该库将被JVM加载,因此本地代码的JNI_OnLoad的实现一般如下:

    /*

      * Thisiscalled by the VM when the shared library is first loaded.

      */

     jintJNI_OnLoad(JavaVM* vm, void* reserved) {

        JNIEnv* env= NULL;

         jintresult= -1;

        if(vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {        // 调用GetEnv请求获得指定版本的JNIEnv

    gotofail;

         }

     

    if(env!= NULL)

    gotofail;

        if(registerMethods(env) != 0) {                               //调用子函数注册本地方法

            gotofail;

         }

         /*success-- return valid version number */

        result =JNI_VERSION_1_4;                                          //指定返回值为合法的JNI版本号

     

     fail:

        return result;

     }

    4.7.2 RegisterNatives方法

             RegisterNatives通常在本地代码被加载时被调用,用来将JNI映射关系“告诉”Java执行环境。映射关系其实是在jni.h中定义一个结构体:

    @jni.h

    typedef struct {

    
    char *name;                        // Java方法名

    
    char *signature;                         //方法签名表示字符串

    
    void *fnPtr;
                     // Java方法对应的本地函数指针

    } JNINativeMethod;

    该结构体记录了Java运行环境中Java方法名name,Java方法签名signature以及其对应的本地函数名fnPtr。

             由于Java代码中可能定义多个本地方法,所以JNINativeMethod结构通常放到一个数组中,通过RegisterNatives注册到JVM中:

    @jni.h

    jint RegisterNatives(jclass clazz, constJNINativeMethod *methods,
jint nMethods);

    jint UnregisterNatives(jclass clazz);

    clazz:成员方法属于某个类,clazz指定注册的映射关系所在的类

    methods:JNINativeMethod指针,它通常是一个映射关系数组

    nMethods:映射关系数组元素个数,即:映射关系数量

    当这些映射关系不再需要时,或需要更新映射关系时,则调用UnregisterNatives函数,删除这些映射关系。

    我们现在完整的来看下之前例子:

    @ NativeTest.java

    package com.test.exam2;

    class NativeTest{

             static{

                       System.loadLibrary(“native_call”);

             }

             privatenative static void callNativePrint(String str);

             privatenative static intcallNativeAdd(int n1, int n2);

     

     

             publicstatic void main(String arg[]){

                       callNativePrint(“HelloWorld”);

                       System.out.println(“n1+ n2 = ” + callNativeAdd(10, 20));

     

    }

    }

    @com_test_exam2_NativeTest.cpp:

    #include <stdlib.h>

    #include <string.h>

    #include <unistd.h>

    #include <jni.h>

    #include <jni_md.h>

     

    void  native_print(JNIEnv * env, jclassthiz, jobject obj)

    {

             printf(“Stringfrom java:%s\n”, env->GetStringUTFChars((jstring)obj, JNI_FALSE));

    }

     

    jint native_add(JNIEnv * env, jclassthiz, jintn1, jint n2)

    {

             returnn1 + n2;

    }

     

    /*

    * 定义映射关系结构体数组

    */

    static const JNINativeMethod gMethods[] = {

        {"callNativePrint",  "(Ljava/lang/String;)V",(void*)native_print},

        {"callNativeAdd",  "(II)I",(void*)native_add},

    };

     

    /*

    * 将映射关系结构体数组注册到JVM中

    */

     static int registerMethods(JNIEnv*env) {

         static constchar* const className= " com/test/exam2/NativeTest";

         jclass clazz;

         /* look upthe class */

         clazz =env->FindClass(className);

         if (clazz ==NULL) {

             return-1;

         }

     

         /* registerall the methods */

         if(env->RegisterNatives(clazz,gMethods,

                sizeof(gMethods) /sizeof(gMethods[0])) != JNI_OK)

         {

             return -1;

         }

         /* fill outthe rest of the IDcache */

         return 0;

     }

     

    /*

      * Thisiscalled by the VM when the shared library is first loaded.

      */

     jintJNI_OnLoad(JavaVM* vm, void* reserved) {

        JNIEnv* env= NULL;

         jintresult= -1;

        if(vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {        // 调用GetEnv请求获得指定版本的JNIEnv

    gotofail;

         }

     

    if(env!= NULL)

    gotofail;

        if(registerMethods(env) != 0) {                               //调用子函数注册本地方法

            gotofail;

         }

         /*success-- return valid version number */

        result =JNI_VERSION_1_4;                                          //指定返回值为合法的JNI版本号

     

     fail:

        return result;

     }

     

    4.8 JNI调用实验

    【实验内容】

    在Linux操作系统中硬件通常有:open,read,write,close等相关操作接口,每个设备硬件还有一些自己的属性,我们用Java编写一个Screen“屏幕”设备类,设备的初始化,设备打开,关闭,读/写都交给本地代码去实现。当写屏幕设备时,将写入内容存放在本地代码缓冲区中,当读屏幕设备时,则将数据经过简单处理读取出来。例如:向Screen中写入a-z的小写字母,读出来变成A-Z的大写。

    在Ubuntu系统中编写Java程序和本地C++代码,编写Makefile文件用来编译Java代码和本地代码库,最终正常运行Java与C++本地代码。

    【实验目的】

    通过实验,学员掌握在Linux系统中编写基于JNI的java程序和与Java对应的C++本地代码,熟悉Linux中编译动态库的过程和Makefile的编写,最终掌握JNI编程相关知识。

    【实验平台】

    安装有JDK的Ubuntu操作系统(可以在Windows系统中虚拟Ubuntu系统)。

    【实验步骤】

    1.      设计Screen类,并实现其代码

    @Screen.java

    package com.test.practice4_8;

    class Screen{

    // Load libscreen.so lib

       static{

           System.loadLibrary("screen");

        }

     

       private String mDevName;

       private int mDevNo;

       private boolean isInit = false;

       private int mWidth;

       private int mHeight;

     

        publicScreen(){

           mDevName = null;

           mDevNo = 0;

        }

     

    // check is device inital

        publicboolean isInit(){

           return isInit;

        }

     

    // get the screen width

        publicint getWidth(){

           return mWidth;

        }

     

    // get the screen height

        publicint getHeight(){

           return mHeight;

    }

     

    // print screen informations

        publicvoid printInfo(){

           System.out.println("Screen Name: " + mDevName);

           System.out.println("Device No:   " + mDevNo);

           System.out.println("Screen width: " + mWidth);

           System.out.println("Screen height:" + mHeight);

    }

     

    // define all native methods

        publicnative boolean open();

        publicnative int read(byte[] data, int len);

        publicnative int write(byte[] data, int len);

        publicnative void close();

    }

     

    2.      设计并实现Screen测试类

    package com.test.practice4_8;

    class ScreenTest{

        publicstatic void main(String arg[]){

           Screen dev = new Screen();                  //创建Screen对象

     

           if(!dev.open()){                              //打开Screen设备

               System.out.println("Screen open error");

               return;

            }

     

           dev.printInfo();                              //打印设备信息

     

           byte[] data = new byte[26];                    //定义要写入的数据,不能用双字节的char类型

           for(int i = 0; i < 26; i++){

               data[i] = (byte)(97 + i);

            }

     

           System.out.println("Write a-z to Screen device:");

           dev.write(data, data.length);                  // 写入设备中

     

           byte[] buf = new byte[64];

           int size = dev.read(buf, buf.length);                 //从设备中读取出来

           if(size < 0){

               System.out.println("read data from screen device error");

               return ;

            }

     

           System.out.println("Read data from Screen device:");

           for(int i = 0; i < 26; i++){

               System.out.print((char)buf[i] + ",");            // 打印出读取出的数据

            }

           System.out.println();  

     

           dev.close();                                   //关闭设备

        }

    }

    3.      设计并实现本地代码

    @com_test_practice4_8_ScreenTest.cpp

    #include <unistd.h>

    #include <stdlib.h>

    #include <malloc.h>

    #include <jni.h>

    #include <jni_md.h>

     

    // 定义一个全局结构体,用来保存所有的Screen类的相关ID信息

    struct screen{

        jclassclazz;

       jfieldID id_dev_name;

       jfieldID id_dev_no;

       jfieldID id_is_init;

        jfieldIDid_width;

       jfieldID id_height;

    } *gfieldID;

     

    // 定义读写数据的缓冲区

    char _data[64];

     

    //初始化Screen类的相关ID信息,起到缓存的作用

    static int native_id_init(JNIEnv *env){

        gfieldID = (struct screen*)malloc(sizeof(struct screen));

        if(gfieldID == NULL)

            return -1;

     

        gfieldID->clazz =env->FindClass("com/test/practice4_8/Screen");

        gfieldID->id_dev_name = env->GetFieldID(gfieldID->clazz,"mDevName", "Ljava/lang/String;");

        gfieldID->id_dev_no = env->GetFieldID(gfieldID->clazz,"mDevNo", "I");

         gfieldID->id_is_init= env->GetFieldID(gfieldID->clazz, "isInit", "Z");

        gfieldID->id_width = env->GetFieldID(gfieldID->clazz,"mWidth", "I");

        gfieldID->id_height = env->GetFieldID(gfieldID->clazz,"mHeight", "I");

        return 0;

     }

     

    // Java代码中open方法的本地实现

    static jboolean native_open(JNIEnv * env,jobject thiz) {

        //init the jfieldID in Java env

       if(native_id_init(env) != 0){                               

           return JNI_FALSE;

        }

     

        // 创建设备名字符串

       jstring dev_nm = env->NewStringUTF("Farsight HD LCDScreen");

       if(dev_nm == NULL)

           return JNI_FALSE;

     

       // 写回Screen对象的mDevName属性里

       env->SetObjectField(thiz, gfieldID->id_dev_name, dev_nm);

     

        // 设备设备号

       env->SetIntField(thiz, gfieldID->id_dev_no, 0x1234);

        // 设置初始化标识

       env->SetBooleanField(thiz, gfieldID->id_is_init, JNI_TRUE);

        // 设置Screen宽度

       env->SetIntField(thiz, gfieldID->id_width, 1023);

        // 设置Screen高度

       env->SetIntField(thiz, gfieldID->id_height, 768);

        returnJNI_TRUE;

    }

     

    // Screen类 read方法的本地实现

    static jint native_read(JNIEnv * env, jobjectthiz, jbyteArray arr, jint len)

    {

        if(len<= 0){

           return len;

        }

       // 获得Java层定义的byte数组

        jbyte*byte_arr = env->GetByteArrayElements(arr, NULL);

        int i= 0;

        for(;i < len; i++){

            byte_arr[i]= _data[i] - 32;             // 将处理过的数据写回Java byte数组里

        }

       env->ReleaseByteArrayElements(arr, byte_arr, 0);                // update array data andrelease array

        returni;

    }

     

    // Screen类write方法的本地实现

    static jint native_write(JNIEnv * env, jobjectthiz, jbyteArray arr, jint len)

    {

        if(len> sizeof(_data) && len <= 0){

           return -1;

    }

    // 获得Java层定义的byte数组

        jbyte*byte_arr = env->GetByteArrayElements(arr, NULL);

        int i= 0;

        for(;i < len; i++){

           _data[i] = byte_arr[i];           //将Java byte数组保存在本地缓存区中

           printf("%c,", _data[i]);

        }

       printf("\n");

       env->ReleaseByteArrayElements(arr, byte_arr, JNI_ABORT);        // do not update array data release array

        returni;

    }

     

    // Screen类close方法的本地实现

    static void native_close(JNIEnv * env, jobjectthiz)

    {

        // 修改isInit的值为false

       env->SetBooleanField(thiz, gfieldID->id_is_init, JNI_FALSE);

       free(gfieldID); //释放空间

       gfieldID = NULL;

    }

     

    /*

     * 定义映射关系结构体数组

     */

    static const JNINativeMethod gMethods[] = {

       {"open",   "()Z", (void*)native_open},

       {"read",           "([BI)I", (void*)native_read},

       {"write",          "([BI)I", (void*)native_write},

       {"close",  "()V", (void*)native_close},

    };

     

    /*

     * 将映射关系结构体数组注册到JVM中

     */

    static int registerMethods(JNIEnv* env) {

        staticconst char* const className = "com/test/practice4_8/Screen";

        jclassclazz;

        /*look up the class */

        clazz= env->FindClass(className);

        if(clazz == NULL) {

           printf("FindClass error\n");

           return-1;

        }

     

        /*registerall the methods */

       if(env->RegisterNatives(clazz, gMethods, sizeof(gMethods) /sizeof(gMethods[0])) != JNI_OK)

        {

           return -1;

        }

        /*fill outthe rest of the ID cache */

        return0;                                                                                  

    }

     

    /*

     *  This iscalled by the VM when the sharedlibrary is first loaded.

     */

    jint JNI_OnLoad(JavaVM* vm, void* reserved) {

       JNIEnv* env= NULL;

        jintresult= -1;

       if(vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {  // 调用GetEnv请求获得指定版本的JNIEnv

           printf("GetEnv error\n");

           goto fail;

        }

     

        if(env== NULL)

           goto fail;

       if(registerMethods(env) != 0) {                 // 调用子函数注册本地方法

           printf("registerMethods error\n");

           goto fail;

        }

        /*success-- return valid version number */

        result= JNI_VERSION_1_4;                   // 指定返回值为合法的JNI版本号

     

    fail:

        returnresult;

    }

    我们在本地代码中声明了一个全局结构体指针gfieldID,该结构体里面存放的是Screen类成员ID,因为这些ID要在后面的方法中频繁的使用,如果不缓存起来,意味着每次使用都要Findclass,GetFieldID,这对性能有很大影响。

    4.      为了方便编译,编写Makefile

    libscreen.so:com_test_practice4_8_ScreenTest.cpp ScreenTest.class

        g++-I/home/linux/jdk1.5.0_21/include/ -I/home/linux/jdk1.5.0_21/include/linux/$< -fPIC -shared -o $@

     

    ScreenTest.class: ScreenTest.java

        javac-d ./ $<

     

    clean:

        $(RM)ScreenTest.class libscreen.so

    由于本地代码要编译成so动态库,所以g++的参数要指定-fPIC –shared等选项,另外,在编译本地代码时要用到jni.h和jni_md.h头文件,所以还要加上-I选项,用来指定这两个头文件的位置,它们在我们安装的JDK的目录下。

    细心的同学可能已经注意到,本地C++文件名为Java的包名+类名.cpp,包名不是以“.”作为间隔符,而是以目录间隔符“/”分隔,这也是因为Java中的包名本身就是使用目录名以区分命名空间。这样做还有另外一个好处,即:我们看到本地代码文件时基本上就可以通过文件找到其对应的Java代码,反之亦然。

    5.      执行make命令,并且运行查看实验结果

    $ make

    $ java -Djava.library.path='.'com/test/practice4_8/ScreenTest

    Java命令的“-Djava.library.path”选项表示指定在运行Java代码时,加载本地库时的寻找路径。为了避免每次都输入上述运行命令,我们可以写到一个脚本中

    @run.sh

    #!/bin/bash

    java -Djava.library.path='.'com/test/practice4_8/ScreenTest

    运行结果如下:

    linux@ubuntu:~/jni/practice$ ./run.sh

    Screen Name: Farsight HD LCD Screen

    Device No:   4660

    Screen width: 1023

    Screen height:768

    Write a-z to Screen device:

    a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,

    Read data from Screen device:

    A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,

    6.      常见问题

    l Exception in thread"main" java.lang.NoClassDefFoundError: xxx

    一般是由于FindClass方法查找不到Java类造成的,检查FindClass的参数是否正确。

    l Exception in thread"main" java.lang.NoSuchMethodError: xxx

    Java与本地方法的链接映射时出现错误,先确认下Java中有没有对应xxx方法声明,如果有,确认RegisterNatives注册映射关系的签名是否匹配。

    l Exception in thread"main" java.lang.NoSuchFieldError:xxx

    这表示在本地代码中访问xxx属性时,在java代码中没有该属性,先确认该属性是否定义,如果有定义,看下属性是静态属性还是非静态属性,如果是静态属性,本地方法只能通过Get/SetStatic<Type>Field来访问,如果是非静态属性,本地方法只能通过Get/Set<Type>Field来访问。

    l Exception in thread"main" java.lang.UnsatisfiedLinkError: no xxx in java.library.path

    这表示本地代码库找不到,确认java在执行时,“-Djava.library.path”参数是否正确。

    展开全文
  • JNI入门-第一个实例

    千次阅读 2018-08-26 12:43:24
    NDK简介 官方文档介绍如下: The Android NDK is a toolset that lets you implement parts of your app in native code, using languages such as C and C++. For certain types of apps, this can help you ...

    NDK简介

    官方文档介绍如下:

    The Android NDK is a toolset that lets you implement parts of your app in native code, using languages such as C and C++. For certain types of apps, this can help you reuse code libraries written in those languages.

    大概意思就是NDK它是一个工具集, 它可以让你使用C/C++ 语言来实现APP的某些部分。对于某些类型的应用,NDK可以帮助你使用C/C++重写代码库。

    与NDK密切相关的另一个词汇则是JNI,它是NDK开发中的枢纽,Java与C/C++层交互基本上就是是通过它来完成的,那么什么是JNI?

    JNI:Java Native Interface 也就是java本地接口,它是一个协议,这个协议用来沟通java代码和本地代码(c/c++)。通过这个协议,Java类的某些方法可以使用原生实现,同时让它们可以像普通的Java方法一样被调用和使用,而原生方法也可以使用Java对象,调用和使用Java方法。也就是说,使用JNI这种协议可以实现:java代码调用c/c++ 代码,而c/c++代码也可以调用java代码。

    环境准备

    • Android Studio
    • Android SDK
    • Android NDK

    使用AS创建好一个Project后,需要注意几个地方的配置:

        1.在项目gradle.properties文件中加上以下代码,表示我们要使用NDK进行开发。

    android.useDeprecatedNdk=true

        2.在项目local.properties中加入ndk和sdk的路径

    // 这是我的ndk及sdk安装路径
    ndk.dir=D\:\\software\\JAVA\\android-sdk-windows\\ndk-bundle
    sdk.dir=D\:\\software\\JAVA\\android-sdk-windows

        3.在app文件夹下的build.gradle中的defaultConfig里加入如下代码

    android {
        ......
    
        defaultConfig {
            ......
    
            ndk {
                moduleName "MyFirstJinTest"//指定生成的so文件名
                abiFilters "armeabi-v7a", "x86"//cpu的类型
            }
        }
    
        ......
    }

    第一步:创建好一个类

    创建一个Java类,例如我的是MyFirstJniTest.java,具体代码如下:

    package com.example.androidqunyinhui.jni;
    public class MyFirstJniTest {
    
        static {
            //此名字必须和build.gradle中的ndk下moduleName一致
            try {
                System.loadLibrary("MyFirstJinTest");
                Log.i("JNI", "MyFirstJinTest load success");
            } catch (Exception e) {
                Log.e("JNI", "MyFirstJinTest load error");
                e.printStackTrace();
            }
        }
    
        public static native String getString();
        public static native int add(int a, int b);
    
    }

    第二步:创建对应的.h头文件

    在完成第一步之后,我们在AS的命令行中输入如下命令:

    // 进入到java目录
    cd app/src/main/java
    // javah是进行头文件生成
    // 例如 我的为 avah -jni com.example.androidqunyinhui.jni.MyFirstJniTest
    javah -jni 包名.类名

    此时在java目录下会看到com_example_androidqunyinhui_jni_MyFirstJniTest.h文件

    内容大致如下:

    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class com_example_androidqunyinhui_jni_MyFirstJniTest */
    
    #ifndef _Included_com_example_androidqunyinhui_jni_MyFirstJniTest
    #define _Included_com_example_androidqunyinhui_jni_MyFirstJniTest
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
     * Class:     com_example_androidqunyinhui_jni_MyFirstJniTest
     * Method:    getString
     * Signature: ()Ljava/lang/String;
     */
    JNIEXPORT jstring JNICALL Java_com_example_androidqunyinhui_jni_MyFirstJniTest_getString
      (JNIEnv *, jclass);
    
    /*
     * Class:     com_example_androidqunyinhui_jni_MyFirstJniTest
     * Method:    add
     * Signature: (II)I
     */
    JNIEXPORT jint JNICALL Java_com_example_androidqunyinhui_jni_MyFirstJniTest_add
      (JNIEnv *, jclass, jint, jint);
    
    #ifdef __cplusplus
    }
    #endif
    #endif

    粗略的看,我们会看到两个和我们在MyFirstJniTest类中定义函数名相同的方法:getString和add,只不过前面加了一堆前缀。

    第三步:新建jni文件夹,并拷贝上面生成的.h文件到jni目录

    选中src,右键:New->Folder->JNI Folder

    点击finish,此时我们可以看到在src目录下创建了一个jni目录。

    将上面生成的.h文件拷贝到该jni文件加下。

    第四步:在jni目录下,右键新建C文件,文件名任意

    在jni目录下,右键新建C文件,文件名任意。

    该.C文件的作用就是对.h文件的方法进行实现,例如创建的文件名为MyFirstJniTest.c,具体内容如下:

    #include <com_example_androidqunyinhui_jni_MyFirstJniTest.h>
    
    //返回一个字符串
    JNIEXPORT jstring JNICALL Java_com_example_androidqunyinhui_jni_MyFirstJniTest_getString
      (JNIEnv *env, jclass jobj){
        return (*env)->NewStringUTF(env,"MyFirstJniTest 我是用jni调用出来的字符串");
    }
    //返回 a+b的结果
    JNIEXPORT jint JNICALL Java_com_example_androidqunyinhui_jni_MyFirstJniTest_add
      (JNIEnv *env, jclass jobj, jint a, jint b){
      return a+b;
    }

    第五步:选择 Build->Make Project

    选择 Build->Make Project,这时候可以看到在app/build/intermediates/ndk/debug/lib目录下生成了对于的.so文件。

    如果没有生成,选择 Build->Clean Project,等clean完成后,再Build->Rebuild Project,一般经过上面两步以后都能够解决问题。

    完成以上步骤,基本上算是完成了一个小型的JNI程序,我们编写C/C++代码,Java层如何去调用。

    但是。。但是。。。

    在真实的项目开发中,我们到这里并没有结束,通常情况下,我们不会将C相关代码也写到Android 工程文件下,Android中需要调用C/C++相关代码,一般都是通过so文件加载的,so文件通常是放在app/jniLibs或者是app/libs目录下。这样的好处相信大家都明白,不在说明。

    需要注意的是,当我们把so文件放在app/libs目录下的时候,还需要在app下的build.gradle文件做如下配置

    android {
        ......
        sourceSets {
            main {
                jniLibs.srcDir 'libs'
            }
        }
        ......
    }

    这是因为在加载so文件的时候,默认是app/jniLibs文件夹,因此当改变文件夹的时候,需要对jniLibs.srcDir 指定路径。

    下面看看我的demo最终示例效果

    • activity中代码如下
    public class JniTestActivity extends AppCompatActivity {
    
        public static void startActivity(Context context){
            Intent intent = new Intent(context, JniTestActivity.class);
            context.startActivity(intent);
        }
    
    
        private TextView tvShowInfo;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_jni_test);
    
            initView();
        }
    
        private void initView(){
    
            tvShowInfo = (TextView) findViewById(R.id.tv_show_info);
    
            findViewById(R.id.btn_show_info).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
    
                    String text = MyFirstJniTest.getString();
                    int sum = MyFirstJniTest.add(1, 2);
                    tvShowInfo.setText(text+"   sum="+sum);
                }
            });
        }
    }

     

    参考文献

    https://www.cnblogs.com/guanmanman/p/6769240.html

    https://blog.csdn.net/u011652925/article/details/78272406

    展开全文
  • Android Studio开发JNI示例

    万次阅读 2019-05-04 21:57:54
    JNI和NDK介绍 JNI(Java Native Interface),是方便Java调用C、C++等Native代码所封装的一层接口,相当于一座桥梁。通过JNI可以操作一些Java无法完成的与系统相关的特性,尤其在图像和视频处理中大量用到。 ...

    JNI和NDK介绍

    JNI(Java Native Interface),是方便Java调用C、C++等Native代码所封装的一层接口,相当于一座桥梁。通过JNI可以操作一些Java无法完成的与系统相关的特性,尤其在图像和视频处理中大量用到。

    NDK(Native Development Kit)是Google提供的一套工具,其中一个特性是提供了交叉编译,即C或者C++不是跨平台的,但通过NDK配置生成的动态库却可以兼容各个平台。比如C在Windows平台编译后生成.exe文件,那么源码通过NDK编译后可以生成在安卓手机上运行的二进制文件.so

    在AS中使用ndk-build开发JNI示例

    Android Studio2.2之前对于JNI开发的支持不是很好,开发一般使用Eclipse+插件编写本地动态库。后面Google官方全面增强了对JNI的支持,包括内置NDK。

    1.在AS中新建一个项目

    2.声明一个native方法

    package com.mercury.jnidemo;
    
    public class JNITest {
    
        public native static String getStrFromJNI();
    
    }
    
    

    3.通过javah命令生成头文件

    在AS的Terminal中,先进入要调用本地代码的类所在的目录,也就是在项目中的具体路径,比如这里是cd app\src\main\java。然后通过javah命令生成该类的头文件,注意包名+类名.这里是javah -jni com.mercury.jnidemo.JNITest,生成头文件com_mercury_jnidemo_JNITest.h

    实际项目最终可以不包含此头文件,不熟悉C的语法的开发人员,借助于该头文件可以知道JNI的相关语法:

    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class com_mercury_jnidemo_JNITest */
    
    #ifndef _Included_com_mercury_jnidemo_JNITest
    #define _Included_com_mercury_jnidemo_JNITest
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
     * Class:     com_mercury_jnidemo_JNITest
     * Method:    getStrFromJNI
     * Signature: ()Ljava/lang/String;
     */
    JNIEXPORT jstring JNICALL Java_com_mercury_jnidemo_JNITest_getStrFromJNI
      (JNIEnv *, jclass);
    
    #ifdef __cplusplus
    }
    #endif
    #endif
    

    首先引入jni.h,里面包含了很多宏定义及调用本地方法的结构体。重点是方法名的格式。这里的JNIEXPORT和JNICALL都是jni.h中所定义的宏。JNIEnv *表示一个指向JNI环境的指针,可通过它来访问JNI提供的接口方法。jclass也是jni.h中定义好的,类型是jobject,实际上是一个不确定类型的指针,这里用来接收Java中的this。实际编写中一般只要遵循Java_包名_类名_方法名就好了。

    4.实现JNI方法

    像上面的头文件只是定义了方法,并没有实现,就像一个接口一样。这里就用C写一个简单的无参的JNI方法。
    先创建一个jni目录,我直接在src的父目录下创建的,也可以在其他目录创建,因为最终只需要编译好的动态库。在jni目录下创建Android.mk和demo.c文件。

    Android.mk是一个makefile配置文件,安卓大量采用makefile进行自动化编译。LOCAL_MODULE定义的名称就是编译好的so库名称,比如这里是jni-demo最终生成的动态库名称就叫libjni-demo.so。 LOCAL_SRC_FILES表示参与编译的源文件名称,这里就是demo.c

    LOCAL_PATH := $(call my-dir)
    
    include $(CLEAR_VARS)
    
    LOCAL_MODULE := jni-demo
    LOCAL_SRC_FILES := demo.c
    
    include $(BUILD_SHARED_LIBRARY)
    

    这里的demo.c实现了一个很简单的方法,返回String类型。

    #include<jni.h>
    
    jstring Java_com_mercury_jnidemo_JNITest_getStrFromJNI(JNIEnv *env,jobject thiz){
        return (*env)->NewStringUTF(env,"I am Str from jni libs!");
    }
    
    

    这时候NDK编译生成的动态库会有四个CPU平台:arm64-v8a、armeabi-v7a、x86、x86_64。如果创建Application.mk就可以指定要生成的CPU平台,语法也很简单:

    APP_ABI := all
    

    这样就会生成各个CPU平台下的动态库。

    5.使用ndk-build编程生成.so库

    切回到jni目录的父目录下,在Terminal中运行ndk-build指令,就可以在和jni目录同级生成一个libs文件夹,里面存放相对应的平台的.so库。同时生成的还有一个中间临时的obj文件夹,和jni文件夹可以一起删除。
    需要注意,使用NDK一定要先在build.gradle下要配置ndk-build的相关路径,这样在编写本地代码时才会有相关的提示功能,并且可以关联到相关的头文件

    externalNativeBuild {
            ndkBuild {
                path 'jni/Android.mk'
            }
        }
    
    

    还有一点,网上很多资料都在build.gradle中加入以下代码:

    sourceSets{
            main{
                jniLibs.srcDirs=['libs']
            }
        }
    

    这样就指定了目标.so库的存放位置。但在实际使用中,就算不指定,运行时仍然可以加载正确的.so库文件,并且如果添加该代码后有时会报出以下错误:

     Error:Execution failed for task ':usejava:transformNativeLibsWithMergeJniLibsForDebug'.
    	> More than one file was found with OS independent path 'lib/x86/libjni-calljava.so'
    	> 
    

    6.加载.so库并调用方法

    在类初始化的时候要加载该.so库,一般会写在静态代码块里。名称就是前面的LOCAL_MODULE。

        static {
            System.loadLibrary("jni-demo");
        }
    
    
    

    需要注意的是如果是有参的JNI方法,那么直接在参数列表里补充在jni.h预先typedef好的数据类型就可以了。

    JNI调用Java

    不同于JNI调用C,JNI调用Java的过程不是单独存在的。而是编写native方法,Java先通过JNI调用该方法,在方法内部再去回调类中对应的Java方法。步骤有些类似于Java中的反射。这里写定义三个点击事件,三个Native方法,三种Java的方法类型,根据相关的Log判断是否成功。

    public class MainActivity extends AppCompatActivity {
    
        public static final String TAG = "MainActivity";
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }
    
        static {
            System.loadLibrary("jni-calljava");
        }
    
        public void noParamMethod() {
            Log.i(TAG, "无参的Java方法被调用了");
        }
    
        public void paramMethod(int number) {
            Log.i(TAG, "有参的Java方法被调用了" + number + "次");
        }
    
        public static void staticMethod() {
            Log.i(TAG, "静态的Java方法被调用了");
        }
    
        public void click1(View view) {
            test1();
        }
    
        public void click2(View view) {
            test2();
        }
    
        public void click3(View view) {
            test3();
        }
    
        public native void test1();
    
        public native void test2();
    
        public native void test3();
    
    }
    

    1.调用Java无参方法

    • JNI调用本地方法,根据类名找到类,注意类名用"/"分隔。
    • 找到类后,根据方法名找到方法。该函数GetMethodID最后一个形参是该形参列表的签名。不同于Java,C中是通过签名标识去找方法。
    • 获取方法的签名:首先定位到该类的字节码文件所在的父目录,一般在module\build\intermediates\classes\debug>,通过javap -s com.mercury.usejava.MainActivity获取整个类所有的内部类型签名。无参方法test1()的签名是()V
    • 通过JNIEnv对象的CallVoidMethod来完成方法的回调,最后一个形参是可变参数。
    JNIEXPORT void JNICALL Java_com_mercury_usejava_MainActivity_test1
      (JNIEnv * env, jobject obj){
           //回调MainActivity中的noParamMethod
        jclass clazz = (*env)->FindClass(env, "com/mercury/usejava/MainActivity");
        if (clazz == NULL) {
            printf("find class Error");
            return;
        }
        jmethodID id = (*env)->GetMethodID(env, clazz, "noParamMethod", "()V");
        if (id == NULL) {
            printf("find method Error");
        }
        (*env)->CallVoidMethod(env, obj, id);
      }
    

    2.调用Java有参方法

    类似于无参方法,只是参数签名和可变参数的不同

    3.调用Java静态方法

    注意获取方法名的方法是GetStaticMethodID,调用方法的函数名是CallStaticVoidMethod,并且由于是静态方法,不应该传入jobject参数,而直接是jclass.

    JNIEXPORT void JNICALL Java_com_mercury_usejava_MainActivity_test3
      (JNIEnv * env, jobject obj){
        jclass clazz = (*env)->FindClass(env, "com/mercury/usejava/MainActivity");
        if (clazz == NULL) {
            printf("find class Error");
            return;
        }
        jmethodID id = (*env)->GetStaticMethodID(env, clazz, "staticMethod", "()V");
        if (id == NULL) {
            printf("find method Error");
        }
    
        (*env)->CallStaticVoidMethod(env, clazz, id);
      }
    

    相应日志

    使用CMake开发JNI

    CMake是一个跨平台的安装(编译)工具,通过编写CMakeLists.txt,可以生成对应的makefile或project文件,再调用底层的编译。AS 2.2之后工具中增加了对CMake的支持,官方也推荐用CMake+CMakeLists.txt的方式,代替ndk-build+Android.mk+Application.mk的方式去构建JNI项目.

    1.创建使用CMake构建的项目

    开始前AS要先在SDK Manager中安装SDK Tools->CMake

    只要勾选Include C++ Support。其中会提示配置C++支持的功能.

    一般默认就可以了,各个选项的具体含义:

    • C++ Standard:指定编译库的环境。
    • Exception Support:当前项目支持C++异常处理
    • Runtime Type Information Support:除异常处理外,还支持动态转类型(dynamic casting) 、模块集成、以及对象I/O

    2.工程的目录结构


    创建好的工程主Module下直接就有.externalNativeBuild,多出一个CMakeLists.txt,相当于以前的配置文件。并且在src/main目录下多了一个cpp文件夹,里面存放的是C++文件,相当于以前的jni文件夹。这个是工程创建后AS生成的示例JNI方法,返回了一个字符串。后面开发JNI就可以按照这个目录结构。

    相应的,build.gradle下也增加了一些配置。

    android {
        ...
        defaultConfig {
            ...
            externalNativeBuild {
                cmake {
                    cppFlags "-std=c++14 -frtti -fexceptions"
                }
            }
        }
        buildTypes {
            ...
        }
        externalNativeBuild {
            cmake {
                path "CMakeLists.txt"
            }
        }
    }
    

    defaultConfig中的externalNativeBuild各项属性和前面创建项目时的选项配置有关,外部的externalNativeBuild则定义了CMakeLists.txt的存放路径。
    如果只是在自己的项目中使用,CMake的方式在打包APK的时候会自动将cpp文件编译成so文件拷贝进去。如果要提供给外部使用时,Make Project,之后在libs目录下就可以看到生成的对应配置的相关CPU平台的.so文件。

    CMakeLists.txt

    CMakeLists.txt可以自定义命令、查找文件、头文件包含、设置变量,具体可见 官方文档。项目默认生成的CMakeLists.txt核心内容如下:

    
    # 编译本地库时我们需要的最小的cmake版本
    cmake_minimum_required(VERSION 3.4.1)
    
    # 相当于Android.mk
    add_library( # Sets the name of the library.设置编译生成本地库的名字
                 native-lib
    
                 # Sets the library as a shared library.库的类型
                 SHARED
    
                 # Provides a relative path to your source file(s).编译文件的路径
                 src/main/cpp/native-lib.cpp )
    
    # 添加一些我们在编译我们的本地库的时候需要依赖的一些库,这里是用来打log的库
    find_library( # Sets the name of the path variable.
                  log-lib
    
                  # Specifies the name of the NDK library that
                  # you want CMake to locate.
                  log )
    
    # 关联自己生成的库和一些第三方库或者系统库
    target_link_libraries( # Specifies the target library.
                           native-lib
    
                           # Links the target library to the log library
                           # included in the NDK.
                           ${log-lib} )
    

    使用CMakeLists.txt同样可以指定so库的输出路径,但一定要在add_library之前设置,否则不会生效:

    
    set(CMAKE_LIBRARY_OUTPUT_DIRECTORY 
    	${PROJECT_SOURCE_DIR}/libs/${ANDROID_ABI}) #指定路径
    #生成的so库在和CMakeLists.txt同级目录下的libs文件夹下
    
    

    如果想要配置so库的目标CPU平台,可以在build.gradle中设置

    android {
        ...
        defaultConfig {
            ...
            ndk{
                abiFilters "x86","armeabi","armeabi-v7a"
            }
        }
    	...
      
    }
    

    需要注意的是,如果是多次使用add_library,则会生成多个so库。如果想将多个本地文件编译到一个so库中,只要最后一个参数添加多个C/C++文件的相对路径就可以

    用C语言实现字符串加密

    Java中实现字符串加密的一种比较简单的方法是异或,将字符串转换为字符数组,遍历对其中的每个字符用密钥(可以是字符)进行一次异或运算,生成新的字符串。如果用JNI和C实现,大致步骤如下(jstring是要加密的字符串):

    1 获取jstring的长度

    2 动态开辟一个跟data长度一样的char*

    3 将 jstring类型转换为char数组(用char*接收)

    4 遍历char数组,进行异或运算

    5 将char*转换为jstring类型返回

    6 释放动态开辟的堆内存空间

    效果图

    我是用的是5.0的模拟器,有时会闪退,查看系统日志,会报出一下错误:

    JNI DETECTED ERROR IN APPLICATION: input is not valid Modified UTF-8
    

    网上查了一下,JNI在调用NewStringUTF方法时,遇到不认识的字符就会退出,因为虚拟机dalvik/vm/CheckJni.cpp里面的checkUTFString会对字符类型进行检查。替代方案是在开始转换前,先检查char*中是否含有非UTF-8字符,有的话返回空字符串。完整代码如下:

    #include<jni.h>
    #include <stdlib.h>
    
    jboolean checkUtfBytes(const char* bytes, const char** errorKind) ;
    
    jstring Java_com_mercury_cmakedemo_MainActivity_encryptStr
            (JNIEnv *env, jobject object, jstring data){
        if(data==NULL){
            return (*env)->NewStringUTF(env, "");
        }
        jsize len = (*env)->GetStringLength(env, data);
        char *buffer = (char *) malloc(len * sizeof(char));
        (*env)->GetStringUTFRegion(env, data, 0, len, buffer);
        int i=0;
        for (; i <len ; i++) {
            buffer[i] = (char) (buffer[i] ^ 2);
        }
    
        const char *errorKind = NULL;
        checkUtfBytes(buffer, &errorKind);
        free(buffer);
        if (errorKind == NULL) {
            return (*env)->NewStringUTF(env, buffer);
        } else {
            return (*env)->NewStringUTF(env, "");
        }
    }
    
    //把char*和errorKind传入,如果errorKind不为NULL说明含有非utf-8字符,做相应处理
    jboolean checkUtfBytes(const char* bytes, const char** errorKind) {
        while (*bytes != '\0') {
            jboolean utf8 = *(bytes++);
            // Switch on the high four bits.
            switch (utf8 >> 4) {
                case 0x00:
                case 0x01:
                case 0x02:
                case 0x03:
                case 0x04:
                case 0x05:
                case 0x06:
                case 0x07:
                    // Bit pattern 0xxx. No need for any extra bytes.
                    break;
                case 0x08:
                case 0x09:
                case 0x0a:
                case 0x0b:
                case 0x0f:
                    /*
                     * Bit pattern 10xx or 1111, which are illegal start bytes.
                     * Note: 1111 is valid for normal UTF-8, but not the
                     * modified UTF-8 used here.
                     */
                    *errorKind = "start";
                    return utf8;
                case 0x0e:
                    // Bit pattern 1110, so there are two additional bytes.
                    utf8 = *(bytes++);
                    if ((utf8 & 0xc0) != 0x80) {
                        *errorKind = "continuation";
                        return utf8;
                    }
                    // Fall through to take care of the final byte.
                case 0x0c:
                case 0x0d:
                    // Bit pattern 110x, so there is one additional byte.
                    utf8 = *(bytes++);
                    if ((utf8 & 0xc0) != 0x80) {
                        *errorKind = "continuation";
                        return utf8;
                    }
                    break;
            }
        }
        return 0;
    }
    
    展开全文
  • JNI 入门教程

    2019-06-05 10:25:27
    一、JNI简介 1、什么是JNIJNI(Java Native Interface):java本地开发接口 JNI是一个协议,这个协议用来沟通java代码和外部的本地代码(c/c++) 外部的c/c++代码也可以调用java代码 2、为什么使用JNI: .....
  • JNI 静态注册和动态注册

    千次阅读 2018-08-01 20:32:59
    一、前言  当执行一个 Java 的 native 方法时,虚拟机是怎么知道该调用 so 中的哪个方法呢?这就需要用到注册的概念了,通过注册,将指定的 native 方法和 so 中对应的方法绑定起来... ...   ... 通过 JNIEXPORT...
  • JNI 实战全面解析

    万次阅读 2017-11-01 09:44:54
    项目决定移植一款C++开源项目到Android平台,开始对JNI深入研究。 JNI是什么? JNI(Java Native Interface)意为JAVA本地调用,它允许Java代码和其他语言写的代码进行交互,简单的说,一种在Java虚拟机控制下执行...
  • JNI实现机制

    千次阅读 2018-11-09 12:12:47
    说到JNI都不陌生,它的全称:Java Native Interface,即Java本地接口。 JNI不仅仅是Android特有的,它是属于Java平台的,它允许在Java虚拟机内运行的java代码与其他编程语言(如c, c++和汇编语言)编写的程序和库...
  • Android:JNI 与 NDK到底是什么?(含实例教学)

    万次阅读 多人点赞 2019-04-21 08:17:02
    今天,我将先介绍JNI 与 NDK & 之间的区别,手把手进行 NDK的使用教学,希望你们会喜欢 目录1. JNI介绍1.1 简介 定义:Java Native Interface,即 Java本地接口 作用: 使得Java 与 本地其他类型语言(如C、C++)...
  • jni是什么

    千次阅读 2019-01-05 16:33:52
    JNI是Java Native Interface的缩写,它提供了若干的API实现了Java和其他语言的通信(主要是C&amp;C++)。从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是...
  • 该问题产生与继承的子类没有定义方法,而该方法字父类已定义,但是由于 是private,所以子类无法找到该方法,改为protect 或者 public
  • 代码下载:http://dl.dbank.com/c0c0xs3b24 一、JNI实现回调 通过JNI在Native层调用JAVA层的方法,来实现Native层向JAVA层传递消息。 JNICallback.java public class JNICallb
  • Android JNI(实现自己的JNI_OnLoad函数)

    万次阅读 热门讨论 2010-09-03 18:25:00
    (2)自己重写JNI_OnLoad()函数。(本文介绍)(Android中采用这种) Java端代码:package com.jni; public class JavaHello { public static native String hello(); static { // load ...
  • Javah生成JNI头文件出现找不到类的错误 换个格式即可:javah -classpath . -jni com.harlan.jni.HelloCPP 记住通用格式—— 在工程的bin目录下,输入命令: javah -classpath . -jni 类路径.JNI...
  • Android Studio创建assets、jniLibs目录

    万次阅读 2018-03-17 16:15:58
    android studio中创建assets、jniLibs目录,引入arr文件
  • 使用JNI调用so的时候出现java.lang.UnsatisfiedLinkError: JNI_ERR returned from JNI_OnLoad XXXX.so错误,这是因为无法调用到so。 1.确认在moudle中的gradle写入了 sourceSets { main { jniLibs.srcDirs = ['libs'...
  • 添加了ZXing的依赖,出现64K问题,解决完64k问题之后,报 java.lang.UnsatisfiedLinkError: JNI_ERR returned from JNI_OnLoad in "/data/app-lib/com.example.santintrestaurant-1/libQt5Core.so"
1 2 3 4 5 ... 20
收藏数 145,251
精华内容 58,100
关键字:

jni