JNI与DNK

  android framework中,JNI将Java层与C++层联系起来,实现java代码与C/C++库进行交互。通常在下列情况下使用JNI
(1)注重处理速度,与本地代码(C/C++)相比,java代码的执行速度慢一些,如果对某程序的执行速度由较高的要求,建议使用本地代码编写,然后再Java中调用给予本地代码的部分。
(2)硬件控制。为了更好的控制硬件,硬件代码通常使用C语言编写,而后借助JNI将其与java层连接起来,从而实现对硬件的控制。
(3)既有C/C++代码的复用
  在实际开发应用开发中,通常使用NDK开发基于C/C++的本地库,然后再通过JNI将java代码与C/C++程序集成在一起。

JNI 允许java代码与基于C/C++编写的应用程序和库进行交互操作。
一.  JNI基本原理
1.1 在java中调用C库函数
      在java代码中,通过JNI调用C函数的步骤
(1)编写java代码  HelloJNI.java, 并编译成HelloJNI.class文件
      首先实现调用C函数的java源代码。若想在java代码中通过JNI调用C函数,首先在java类中声明本地方法,本地方法仅在java中进行声明,在C/C++等本地语言来实现。
***********************************************************
class HelloJNI
{
    native void printHello();//native关键字声明本地方法,与用c/c++编译的JNI本地函数相对应。告知java编译器,此处只是声明。
    native void printString(String str);

    static { //静态块加载库文件
        System.loadLibrary("hellojni");//加载C运行库,在windows下,是dll库;在linux下,是so库
    }

    public static void main(String args[]){
        HelloJNI myJNI = new HelloJNI();
        myJNI.printHello();
        myJNI.printString("aaaaaaaaaaaaaa");
    }
}
***********************************************************
     编译完成后,运行会报错,找不到库文件 hellojni.dll
(2)创建库文件 hellojni.dll
     若想要java文件中的本地方法与C文件中方法的实现在java虚拟机加载库文件时,生成映射。必须先生成函数原型,函数原型在C/C++头文件中,java提供了 javah工具,用于生成函数原型的C/C++头文件,具体方法如下:
               javah  <包含以native关键字声明的方法的java类名称>
     执行完以后,就会在本目录下,生成一个与java类名相同名称的C语言头文件。在C语言头文件中定义了与java本地方法相链接的C函数原型。(在上述例子中,就会生成HelloJNI.h,下面是其具体内容)
***********************************************************
/* DO NOT EDIT THIS FILE - it is machine generated */
#include < jni.h>
/* Header for class HelloJNI */

#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloJNI
 * Method:    printHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloJNI_printHello
  (JNIEnv *, jobject);

/*
 * Class:     HelloJNI
 * Method:    printString
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_HelloJNI_printString
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif
***********************************************************
             注意:查看函数原型的注释,可看到与各函数原型对应的java代码中的本地方法,注释中,说明了了类名, 本地方法名,本地方法签名。
                        只有生成了函数原型,java虚拟机就能把本地库函数与java本地方法正常的连接在一起。
              其中,JNIEXPORT,  JNICALL为JNI关键字,表示此函数要被JNI调用,函数原型必须有这两个关键字,JNI才能正常调用。
              JNI支持的函数命名方法:  Java_类名_本地方法名
              生成的函数原型都有两个参数, JNIEnv *和   jobject 。支持JNI的函数必须包括这两个参数。第一个为JNI接口的指针,用来调用JNI表中的各种JNI函数(指JNI中提供的基本函数集,用来在JNI本地函数中创建java对象或调用相应的方法)。 jobject 为JNI提供的java本地类型,用来在C代码中访问java对象,其保存着调用本地方法的对象的一个引用。
          java类声明的方法     使用javah生成的函数原型
    printHello()                         void Java_HelloJNI_printHello(JNIEnv * , jobject)
              printString(String str)          void Jjava_HelloJNI_printString(JNIEnv * , jobject, jstring)
(3)实现本地方法 hellojni.c
         用来实现JNI本地方法。首先把定义在 HelloJNI.h 头文件中的函数原型复制到 hellojni.c 文件中。
***********************************************************
 #include "HelloJNI.h"
#include <stdio.h>

JNIEXPORT void JNICALL Java_HelloJNI_printHello(JNIEnv *env, jobject obj){
    printf("hello world");
}

JNIEXPORT void JNICALL Java_HelloJNI_printString(JNIEnv *env, jobject obj, jstring string){
    const char *str = (*env)->GetStringUTFChars(env, string, 0);
    printf("%s", str);
}
***********************************************************
(4)生成共享库
       将创建的 hellojni.c 文件编译成 hellojni.dll 库文件
(5)执行(1)中生成的文件。


1.2 调用JNI 函数
      讲解C语言中编写的JNI 本地函数控制java端的代码。
      具体方法:   创建java对象 ,  访问类静态成员域 , 调用类的静态方法, 访问java对象的成员变量,访问java对象的方法
1.2.1 调用JNI函数的示例程序结构
         程序由 JniFuncMain类(包含本地方法声明), JniTest对象, jnifunc.dll(本地方法的具体实现)三部分组成。
           其中Java_JniFuncMain_createJniObject()函数作为函数原型,以创建java对象或调用方法的方式与java代码进行交互。
1.2.2 java层代码(JniFuncMain.java)
     1.  JniFuncMain类
           public class JniFuncMain{
    private static int staticIntField = 30;

    static{
        System.loadLibrary("jnifunc");
    }

    public static native JniTest createJniObject();

    public static void main(String[] args){
        JniTest jniObj = createJniObject();
        jniObj.callTest();
    }
}
     2.  JniTest类
class JniTest{
    private int intField;
    public JniTest(int num){
        intField = num;
    }
    public int callByNative(int num){
        return num;
    }
    public void callTest(){
        System.out.println("jniTest object method called");
    }
}
1.2.3 分析JNI本地函数代码
  1. JniFuncMain.h文件
       对于 JniFuncMain.java 生成 函数原型文件 JniFuncMain.h,具体如下:
              /* DO NOT EDIT THIS FILE - it is machine generated */
#include < jni.h>
/* Header for class JniFuncMain */

#ifndef _Included_JniFuncMain
#define _Included_JniFuncMain
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     JniFuncMain
 * Method:    createJniObject
 * Signature: ()LJniTest;
 */
JNIEXPORT jobject JNICALL Java_JniFuncMain_createJniObject
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

        上述代码中,函数原型的第二个参数为 jclass, 由于此函数在声明时,为静态函数,可以通过类名直接调用而非对象。
        从JniFuncMain.java文件中,可以看到调用 createJniObject() 本地方法是通过 JniFuncMain类进行的,所以,JNI本地函数的第二个参数保存的是JniFuncMain类的引用。
   2.   jnifunc.cpp文件
        实现函数原型。
               JNIEXPORT jobject JNICALL Java_JniFuncMain_createJniObject(JNIEnv *env, jclass clazz){
    jclass targetClass;
    jmethod mid;
    jobject newObject;
    jstring helloStr;
    jfieldID fid;
    jint staticIntField;
    jint result;

    fid = env->GetStaticFieldID(clazz, "staticIntField", "I");
    staticIntField = env->GetStaticIntField(clazz, fid);

    targetClass = env->FindClass("JniTest");

    mid = env->GetMethodID(targetClass, "<init>", "(I)V");

    newObject = env->NewObject(targetClass, mid, 100);

    mid = env->GetMethodID(targetClass, "callByNative", "(I)I");
    result = env->CallIntMethod(newObject, mid, 200);

    fid = env->GetFieldID(targetClass, "intField", "I");
    env->SetIntField(newObject, fid, result);

    return newObject;

}
        通过JNI访问java类/对象的成员变量按如下顺序进行。
       (1)查找含待访问的成员变量的java类的jclass值。
       (2)获得类成员变量的jfieldID值。若成员变量为静态变量,调用名称为 GetStaticFieldID()的JNI函数; 若为普通成员,为GetFieldID()
       (3)根据 jclass和 jfieldID值,获取成员变量值。
          注意:获得成员变量的签名的方法,javap(java反编译器)。方法如下:
                     javap [选项] ‘类名’
                     选项:  -s  输出java签名
                                 -p 输出所有类及成员
 yinazh@Yubuntu:~$ javap -s -p JniFuncMain
Compiled from "JniFuncMain.java"
public class JniFuncMain {
  private static int staticIntField;
    descriptor: I
  public JniFuncMain();
    descriptor: ()V

  public static native JniTest createJniObject();
    descriptor: ()LJniTest;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V

  static {};
    descriptor: ()V
}

                     注意先编译后,然后运行此命令才能得到签名
        (4)在JNI中有两种函数用来获取成员变量的值,分别为 Get<type>Field函数与 GetStatic<type>Field
        (5)在JNI中带哦用函数的形式: CallStatic<type>Method(); 与Call<type>Method();
        (6)在JNI通过  Set<type>Field()和SetStatic<type>Field()来设置成员变量的值。
  完成。

    上面的代码,实现了在JNI本地函数中生成java对象,调用java方法的操作。

 1.3 在C程序中,运行java类
       在C/C++程序中运行java类必须使用java虚拟机。为此,JNI提供了一套 Invocation API, 它允许本地代码在自身内存区域内加载java虚拟机。下面是在C中如何使用 Invocation API,装载java类,并运行指定的方法。
       下面是决定使用 Invocation API 在C/C++代码中调用java代码的几个情况:
    》 需要在C/C++编写本地应用程序中访问用java语言编写的代码或代码库
    》 希望在C/C++编写本地应用程序中使用标准java类库
    》 当需要把已有的C/C++程序与Java程序组织连接在一起,使用Invocation API,可以讲他们组织成一个完整的程序。
 除此之外,在编写并运行java程序时使用的java.exe也是用C写的,该命令通过Invocation API接收命令参数,执行命令参数指定的java类。实际上,android的launcher程序也是通过Invocation API来工作的。
1.3.1 Invocation API应用示例
       示例由 InvokeJava.cpp与 InvocationTest.java两个文件组成。

         上述执行顺序:(1)主程序 InvokeJava.cpp使用Invocation API加载java虚拟机。(2)加载 InvocationTest类至内存中。(3)执行被加载的InvocationTest类的main()方法。
     分析java代码(InvocationApiTest.java
 public class InvocationApiTest{
    public static void main(String[] args){
        System.out.println(args[0]);
    }
}

      分析C代码(invocationApi.c)
      *******************************************************************
#include < jni.h>

int main(){
    JNIEnv * env;
    JavaVM *vm;
    JavaVMInitArgs vm_args;
    JavaVMOption options[1];
    jint res;
    jclass cls;
    jmethodID mid;
    jstring jstr;
    jclass stringClass;
    jobjectArray args;

    //生成java虚拟机选项
    options[0].optionString = "-Djava.class.path=.";
    vm_args.version = 0x00010002;
    vm_args.optons = optons;
    vm_args.nOptions = 1;
    vm_args.ignoreUnrecognized = JNI_TRUE;

    //生成java虚拟机
    res = JNI_CreateJavaVM(&vm, (void**)&env, &vm_args);

    //查找并加载类
    cls = (*env)->FindClass(env, "InvocationApiTest");

    //获取main方法的ID
    mid = (*env)->GetStaticMethodID(env, cls, "main", "([Ljava/lang/String;])V");
    
    //生成字符串对象,用作main()方法的参数
    jstr = (*env)->NewStringUTF(env, "Hello Invocation API");
    jstringClass = (*env)->FindClass(env, "java/lang/String");
    args = (*env)->NewObjectArray(env, 1, stringClass, jstr);

    //调用main()方法
    (*env)->CallStaticVoidMethod(env, cls, mid, args);

    //销毁java虚拟机
    (*vm)->DistoryJavaVM(mv);
}
**********************************************************************
       编译运行,生成InvocationApiTest.java,执行即可。
1.3.2 Invocation API在android中的应用举例: Zygote进程
       Zygote是android系统的核心,本身是一个应用程序,由名称为 app_process的C++本地应用程序调用 JNI Invocation API启动运行。
  当android framework启动时,app_process会先初始化 android runtime, 然后再运行zygote进程。zygote是个特殊的进程,它能替身android framework的性能,所有android应用进程由zygote进程fork出来。zygote进程由称为android zygoteInit类的java程序组成,再android启动时,app_process调用JNI invocationAPI再自身程序域内加载虚拟机,而后调用zygoteInit类的main()方法,从而运行zygote进程。


1.4 直接注册JNI本地函数
         前面1.1节中,java虚拟机在运行包含本地方法的java应用程序时,要有两个步骤:
   (1)调用System.loadLibrary()方法,将包含本地方法具体实现的C/C++运行库加载到内存中。
   (2)java虚拟机检索加载进来的库函数符号,在其中查找与java本地方法拥有相同签名的JNI本地函数符号。若找到一致的, 则将本地方法映射到具体的JNI本地函数。
         上述方法在android Framework复杂的系统下,拥有大量的包含本地方法的java类,java虚拟机加载相应的运行库,在逐一检索,将各个本地方法与相应的函数映射起来,这显然会增加运行时间,减低运行效率。
         为解决这一问题,JNI机制提供了   RegisterNatives()  的JNI函数, 该函数允许C/C++开发者将JNI本地函数与java类的本地方法直接映射在一起当不调用 RegisterNatives() 函数时,java虚拟机会自动检索并将JNI本地函数与相应的java本地方法连接在一起。但当开发者直接调用RegisterNatives()函数时,java虚拟机就不必进行映射处理,这几大的提高了运行速度和效率。
           由于程序员直接将JNI本地函数与Java 本地方法连接在一起,在加载运行库时,java虚拟机不必为了识别JNI本地函数而将JNI本地函数的名称与JNI支持的命名规则进行对比。即任何名称的函数都能直接连接到java本地方法上
1.4.1 加载本地库时,注册JNI本地函数
         在hellojni.c的基础上,进行修改,并调用RegisterNatives()函数,将本地方法与C函数映射在一起,代替java虚拟机进行映射,得到的代码如下:
×××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××
#include " jni.h"
#include <stdio.h>

void printHelloNative(JNIEnv *env, jobject obj);
void printStringNative(JNIEnv *env, jobject obj, jstring string);

JNIEXPORT jint JNICALL JNI_Onload(JavaVM *vm, void *reserved){
    JNIEnv* env = NULL;
    JNINativeMethod nm[2];
    jclass cls;
    jint result = -1;

    if(vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK){
        return JNI_ERR;
    }

    cls = env->FindClass("HelloJNI");
    nm[0].name = "printHello";
    nm[0].signature = "()V";
    nm[0].fnPtr = (void *)printHelloNative;

    nm[1].name = "printString";
    nm[1].signature = "(Ljava/lang/String)V";
    nm[1].fnPtr = (void *)printStringNative;

    env->RegisterNatives(cls, nm, 2);
    return JNI_VERSION_1_4;
}

void printHelloNative(JNIEnv *env, jobject obj){
    return;
}

void printStringNative(JNIEnv *env, jobject obj, jstring string){
    const char *str = env->GetStringUTFChars(string, 0);
    return;
}
×××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××
       
       首先分析 System.loadLibrary()方法的执行过程: 调用此方法时,java虚拟机会加载其参数指定的共享库,然后,java虚拟机检索共享库内的函数符号,检查JNI_OnLoad()函数是否被实现,若共享库中含有相关的函数,则JNI_OnLoad()函数就会被自动调用。若像以前生成的hellojni.dll一样,库中JNI_OnLoad()函数未被是吸纳,则java虚拟机就会自动将本地方法与库内的JNI本地函数符号进行比较匹配。
       在加载指定的库文件时,JNI_OnLoad()函数会被自动调用执行,程序开发者若想手工映射本地方法与JNI本地函数,需要在JNI_OnLoad()函数内调用RegisterNatives()函数进行映射匹配。
       注意:JNI_OnLoad()函数最基本的功能是确定java虚拟机支持的JNI版本。因此其必须返回有关JNI版本的信息。此外通过JNI_OnLoad()函数,java虚拟机在加载本地库时对JNI进行初始化。
       直接注册JNI本地函数的原理,就是重载JNI_OnLoad()函数,手动加载映像匹配的过程。下面介绍在android中的应用。

1.4.2 android中的应用举例
       System Server:在 JNI_OnLoad中注册JNI本地函数
  在android系统中,也是直接使用JNI_OnLoad()映射本地方法。在启动时android时,系统服务器SystemServer用来运行各种服务,加载多种本地库。在framework/base/services/java/com/android/server/SystemServer.java中,包含的下面的代码:
     System.loadLibrary("android_servers");  
        来加载libandroid_servers.so库文件,而编译libandroid_servers.so模块上包含了load.cpp文件,文件将系统启动所需要的JNI本地方法映射到C方法中。
  而app_process进程,是在C中注册JNI本地函数的。并没有使用JNI_OnLoad()方法,而是直接调用RegisterNatives()方法,完成jni本地函数与java类本地方法之间的链接映射。





        

































  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值