android JNI的使用

jni技术用于实现java与c/c++代码之间的连接,在android平台中被大量运用,比如

  • 本地代码启动java虚拟机,创建java的运行环境,如进程zygote启动后创建java虚拟机
  • 系统服务多用c/c++实现,在其中通过jni提供接口供java代码调用

下面主要讲述jni在常见场景下的使用方法,如java应用如何调用本地库函数,本地库函数如何调用java代码,在c程序中如何创建java虚拟机执行java代码。同时也提到如何注册jni本地函数,建立java本地方法和本地库函数之间的映射以加快运行效率。

在java中调用c库函数

  1. 编写java代码,其中需要本地实现的方法用native修饰
        class Hello{
            native void printHello();
            static {
                System.loadLibrary("Hello");
            }
            public static void main(String[] args){
                Hello hello = new Hello();
                hello.printHello();
            }
        }
  1. 编译java文件为class文件
        $ javac Hello.java
  1. 根据java文件生成c语言头文件
        $ javah Hello

此时会生成相应的c语言头文件Hello.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Hello */

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

#ifdef __cplusplus
}
#endif
#endif

自动生成的本地函数名是有规则的,其中JNIEXPORT JNICALL关键字都是在jni.h头文件注册的宏,函数名格式为Java_类名_本地方法名,这样在加载本地函数库时系统可以找到java本地方法对应的本地函数。

在生成的函数原型中带有两个默认参数,第一个JNIEnv*类型参数是jni接口的指针,用来调用jni表中的各种函数,第二个参数jobject类型参数指的是调用本地方法的对象的引用。如果java本地方法为static类型,则第二个参数为jclass类型。
4. 编写c代码,Hello.c
实现头文件中的方法即可,注意添加参数名

        #include "Hello.h"
        #include <stdio.h>

        /*
         * Class:     Hello
         * Method:    printHello
         * Signature: ()V
         */
        JNIEXPORT void JNICALL Java_Hello_printHello
          (JNIEnv *env, jobject obj){
            printf("Hello World!\n");
        }
  1. 生成共享库
        $ gcc -I/usr/lib/jvm/java-7-oracle/include -I/usr/lib/jvm/java-7-oracle/include/linux -fPIC -shared -o libHello.so Hello.c
此处用-I选项指定头文件jni.h位置,最终编译生成libHello.so文件。
  1. 运行java程序,查看结果
        $ java -Djava.library.path=. Hello
        Hello World!

此处Hello.class类执行时需要用-D选项指定动态库的路径。

在c库函数中调用java代码

先写一个Main.java类,该类会调用本地库libmain.so中的库函数,在该库函数中演示如下操作:

  • 创建java对象
  • 访问java类静态变量
  • 访问java类静态方法
  • 访问java对象的变量
  • 访问java对象的方法
  1. 编写Main类,代码很简单,就是调用c库函数
        class Main{
            static native void main();
            public static void main(String args[]) {
                System.loadLibrary("main");
                main();
            }
        }
  1. 按照javah工具生成Main.h文件,实现本地函数

    我们需要在Main.c文件中实现头文件中声明的JNICALL Java_Main_main方法,由于对应的java方法被static修饰,所以此处的第二个函数参数为jclass类型。

        #include "Main.h"
        #include <stdio.h>

        JNIEXPORT void JNICALL Java_Main_main(JNIEnv *env, jclass clazz){

            // 查找JniTest类
            jclass targetClass = (*env)->FindClass(env, "JniTest"); 
            // 查找JniTest类的构造方法
            jmethodID constructorId = (*env)->GetMethodID(env, targetClass, "<init>", "(I)V");
            // 1.生成JniTest对象
            jobject newObject = (*env)->NewObject(env, targetClass, constructorId, 100);
            printf("initiate JniTest object.\n");
            
            // 2.获取JniTest类的静态变量
            jfieldID staticFieldId = (*env)->GetStaticFieldID(env, targetClass, "var1", "I");
            jint var1 = (*env)->GetStaticIntField(env, targetClass, staticFieldId);
            // 3.获取JniTest类的普通变量
            jfieldID fieldId = (*env)->GetFieldID(env, targetClass, "var2", "I");
            jint var2 = (*env)->GetIntField(env, targetClass, fieldId);
            printf("var1: %d , var2: %d\n", var1, var2);
            
            // 4.调用JniTest类的静态方法
            jmethodID staticMethodId = (*env)->GetStaticMethodID(env, targetClass, "getVar1",  "()I");
            jint var11 = (*env)->CallIntMethod(env, newObject, staticMethodId);
            // 5.调用JniTest类的普通方法
            jmethodID methodId = (*env)->GetMethodID(env, targetClass, "getVar2",  "()I");
            jint var22 = (*env)->CallIntMethod(env, newObject, methodId);
            printf("var1: %d , var2: %d\n", var11, var22);

        }
  1. 编译运行效果
        $ javac Main.java
        $ gcc -I/usr/lib/jvm/java-7-oracle/include -I/usr/lib/jvm/java-7-oracle/include/linux -fPIC -shared -o libmain.so Main.c
        $ java -Djava.library.path=. Main
        initiate JniTest object.
        var1: 1 , var2: 0
        var1: 1 , var2: 100

通过上述代码可以看到,Jni接口指针定义了一系列的查找class,实例化object,查找id,获取/设置field,调用method的方法,例如

    jclass FindClass(JNIEnv *env, const char *name)
    jobject NewObject(JNIEnv *env, jclass clazz, jmethodID methodID, ...)
    jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *signature)
    jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *signature)
    <jnitype> Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID)
    void Set<Type>Field(JNIEnv *env, jobject obj, jfeildID feildID, <type> value)
    <jnitype> Call<type>Method(JNIEnv *env, jobject obj, jmethodId methodID, ...)

在调用jni函数时,c与c++调用方式略有不同,例如

    jclass clazz = (*env)->FindClass(env, "JniTest");   // c风格
    jclass clazz = env->FindClass("JniTest");   // c++风格

在查找id时需要传入java成员变量或者方法的signature,用于描述方法的返回值和参数,可以通过javap命令获取

    $javap -s -p JniTest
    Compiled from "JniTest.java"
    class JniTest {
      private static int var1;
        Signature: I
      private int var2;
        Signature: I
      public JniTest(int);
        Signature: (I)V

      public static int getVar1();
        Signature: ()I

      public void setVar2(int);
        Signature: (I)V

      public int getVar2();
        Signature: ()I

      static {};
        Signature: ()V
    }

在c程序中创建java运行环境

jni提供了一套Invocation api允许c/c++程序在自身内存区域加载java虚拟机,创建java运行环境。在android系统中,zygote进程(即app_process)正是通过该方式启动第一个java虚拟机。

先编写本地程序invocation,其代码invocaiotn.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;

        // 1.生成java虚拟机选项
        options[0].optionString = "-Djava.class.path=.";
        vm_args.version = JNI_VERSION_1_2;
        vm_args.options = options;
        vm_args.nOptions = 1;
        vm_args.ignoreUnrecognized = JNI_TRUE;
        
        // 2.生成java虚拟机
        res = JNI_CreateJavaVM(&vm, (void**)&env, &vm_args);
        
        // 3.查找java类
        cls = (*env)->FindClass(env, "Demo");
        
        // 4.获取java类中的main方法id
        mid = (*env)->GetStaticMethodID(env, cls, "main", "([Ljava/lang/String;)V");
        
        // 5.生成字符串对象,用做main方法的参数
        jstr = (*env)->NewStringUTF(env, "Hello World!");
        stringClass = (*env)->FindClass(env, "java/lang/String");
        args = (*env)->NewObjectArray(env, 1, stringClass, jstr);
        
        // 6.调用main方法
        (*env)->CallStaticVoidMethod(env, cls, mid, args);
        
        // 7.销毁虚拟机
        (*vm)->DestroyJavaVM(vm);
    }

编译的时候需要jni.h头文件,同时需要链接libjvm.so库,具体编译指令如下

    $ gcc -o invocation invocation.c -I/usr/lib/jvm/java-7-oracle/include -I/usr/lib/jvm/java-7-oracle/include/linux -L/usr/lib/jvm/java-7-oracle/jre/lib/amd64/server -ljvm

注意gcc在处理符号链接的时候如果发现处理到-l参数时该库文件没有被左边的参数使用,则会忽略该-l参数,所以最好将-l参数放到最后,避免出现链接错误[undefined reference to `JNI_CreateJavaVM’ linux] (http://stackoverflow.com/questions/16860021/undefined-reference-to-jni-createjavavm-linux)

如下是需要调用的java类

    class Demo {
        public static void main(String args[]) {
            System.out.println(args[0]);
        }
    }

编译java类后,我们就可以执行invocation程序看一下效果

    $ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/lib/jvm/java-7-oracle/jre/lib/amd64/server
    $ ./invocation
    Hello World!

在c程序中注册jni本地函数

java虚拟机在运行包含本地方法的java应用程序时,需要经过如下步骤

  • 调用System.loadLibrary()方法,将运行库加载到内存中
  • java虚拟机检索加载进来的库函数符号,在其中查找与java本地方法拥有相同签名的jni本地函数符号。若找到一致的,则将本地方法映射到具体的JNI本地函数。

jni机制提供了名称为RegisterNatives()jni函数,允许c/c++开发者将jni本地函数和java类的本地方法直接映射在一起,提升运行效率。

  1. 加载本地库时注册jni本地函数

    在java代码中调用System.loadLibrary()方法时,java虚拟机加载指定的共享库后会检索共享库内的函数符号,检查JNI_OnLoad()函数是否被实现,是则进行调用,否则自动将本地方法与共享库内的jni本地函数符号进行比较匹配。

    JNI_OnLoad()函数最基本的功能为确定java虚拟机支持的jni版本,因此该函数必须返回有关jni版本的信息。如下代码为本地代码重写JNI_OnLoad()方法

        #include <jni.h>
        #include <stdio.h>

        // 实现本地函数
        void printHelloNative() {
            printf("Hello World!\n");
        }

        JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
            JNIEnv* env = NULL;
            JNINativeMethod nm[1];
            jclass cls;
            
            if((*vm)->GetEnv(vm, (void*)&env, JNI_VERSION_1_4) != JNI_OK) {
                printf("Error!");
                return JNI_ERR;
            }
            
            cls = (*env)->FindClass(env, "Hello");
            nm[0].name = "printHello";
            nm[0].signature = "()V";
            nm[0].fnPtr = (void *)printHelloNative;
            
            // 将Hello.java类中的printHello方法与本地函数printHelloNative绑定
            (*env)->RegisterNatives(env, cls, nm, 1);
            
            return JNI_VERSION_1_4;
        }

按照如下命令编译为libmain.so库文件

        $ gcc -fpic -shared -o libmain.so Main.c -I/usr/lib/jvm/java-7-oracle/include -I/usr/lib/jvm/java-7-oracle/include/linux

    如下为Hello.java类

        class Hello {
            native static void printHello();
            static { System.loadLibrary("main"); }
            
            public static void main(String args[]) {
                printHello();
            }
        }

编译后运行可以看到成功找到本地函数

        $ java -Djava.library.path=. Hello
        Hello World!
  1. 在c程序中注册jni本地函数

    如果开发者在c语言中通过invocation api调用java代码,则不必重写JNI_OnLoad()方法,可以直接调用RegisterNatives()方法完成JNI本地函数与JNI本地方法之间的映射。

    android系统中zygote进程启动过程(由frameworks/base/cmds/app_prosss/app_main.cpp中main方法调用frameworks/base/core/jni/AndroidRuntime.cpp的AndroidRuntime::start方法实现)中,在启动vm后会先调用AndroidRuntime::startReg(JNIEnv* env)方法将android framework中使用到的各种本地方法先注册,之后才调用com.android.internal.os.ZygoteInit.java类的main方法。

    跟踪startReg方法可以看到最终是通过类似如下代码将本地函数与java本地方法建立映射

        int register_com_android_internal_os_RuntimeInit(JNIEnv* env)
        {
            return jniRegisterNativeMethods(env, "com/android/internal/os/RuntimeInit",
                gMethods, NELEM(gMethods));
        }

其中jniRegisterNativeMethods方法位于libnativehelper/JNIHelp.cpp中,最终还是通过调用RegisterNatives方法完成注册。

        extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
            const JNINativeMethod* gMethods, int numMethods)
        {
            // ...
            
            if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
                char* msg;
                asprintf(&msg, "RegisterNatives failed for '%s'; aborting...", className);
                e->FatalError(msg);
            }

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值