zoukankan      html  css  js  c++  java
  • JNI学习笔记

    1为什么使用JNI?

    JNI 的强大特性使我们在使用 JAVA 平台的同一时候,还能够重用原来的本地代码。作为虚拟机 实现的一部分,JNI 同意 JAVA 和本地代码间的双向交互。
    请记住,一旦使用 JNI,JAVA 程序就丧失了 JAVA 平台的两个长处:
    1、 程序不再跨平台。要想跨平台。必须在不同的系统环境下又一次编译本地语言部分。
    2、 程序不再是绝对安全的,本地代码的不当使用可能导致整个程序崩溃。

    一个通用规则是,你应该让本地方法集中在少数几个类其中。

    这样就减少了 JAVA 和 C 之间的耦合性。

    当你開始着手准备一个使用 JNI 的项目时,请确认是否还有替代方案。

    像上一节所提到的, 应用程序使用 JNI 会带来一些副作用。下面给出几个方案,能够避免使用 JNI 的时候,达到 与本地代码进行交互的效果:
    1、JAVA 程序和本地程序使用 TCP/IP 或者 IPC 进行交互。
    2、 当用 JAVA 程序连接本地数据库时,使用 JDBC 提供的 API。
    3、JAVA 程序能够使用分布式对象技术,如 JAVAIDLAPI。

    这些方案的共同点是,JAVA 和 C 处于不同的线程。或者不同的机器上。这样,当本地程序 崩溃时,不会影响到 JAVA 程序。 下面这些场合中,同一进程内 JNI 的使用无法避免:
    1、 程序其中用到了 JAVA API 不提供的特殊系统环境才会有的特征。而跨进程操作又不现 实。


    2、 你可能想訪问一些己有的本地库,但又不想付出跨进程调用时的代价,如效率,内存, 数据传递方面。
    3、JAVA 程序其中的一部分代码对效率要求非常高。如算法计算。图形渲染等。
    总之,仅仅有当你必须在同一进程中调用本地代码时,再使用 JNI。
    Android应用框架层JNI部分源代码主要位于frameworks/base/文件夹下。依照模块组织,不同的模块将被编译为不同的共享库。分别为上层提供不同的服务。这些共享库终于会被放置在目标系统的/system/lib文件夹下。

    注意:NDK与JNI的差别: NDK是为便于开发基于JNI的应用而提供的一套开发和编译工具集;而JNI则是一套编程接口,能够运用在应用层,也能够运用在应用框架层,以实现Java代码与本地代码的互操作。

    2.JNI步骤

    JNI编程模型的结构十分清晰,能够概括为下面三个步骤:

    步骤1 Java层声明Native方法。

    步骤2 JNI层实现Java层声明的Native方法,在JNI层能够调用底层库或者回调Java层方法。这部分将被编译为动态库(SO文件)供系统载入。

    步骤3 载入JNI层代码编译后生成的共享库。

    怎样创建一个支持JNI的项目:https://developer.android.com/studio/projects/add-native-code.html

    创建后的文件夹例如以下:
    这里写图片描写叙述

    3.CMake

    一款外部构建工具。可与 Gradle 搭配使用来构建原生库。简单来说用来将.cpp文件或.c等文件编译生成.so文件的工具,其配置文件就是上述文件夹图中的CMakeLists.txt。

    曾经用的是ndk-build。可是已经弃用,其配置文件是Android.mk。

    CMakeLists.txt的基本配置:
    1. cmake_minimum_required(參数):设置cmake的版本号以决定你将使用到cmake的feature。
    2. add_library(so_file_name [STATIC | SHARED | MODULE] sources):第一个參数是创建的so文件的名字。第二个參数是配置so文件的用途。第三个參数是该so文件包括的c/c++源代码文件。举个样例:

    add_library(native_lib
                SHARED
                src/main/cpp/test.cpp
                src/main/cpp/test2.cpp)

    这个配置的意思是,CMake会创建一个名字叫libnative_lib.so文件,so的命名 = lib + 名字 + .so。

    可是在java层调用System.loadLibrary的时候,还是传入第一个參数就可以,在这个样例中仅仅用传入”native_lib”第二个參数的意思是代表该so文件的类型,static代表静态库,shared代表动态库,module在使用dyid的系统有效,若不支持dyid,等同于shared。
    后面的參数都代表增加到so文件的c/c++源代码,比如这个样例中test.cpp和test2.cpp都会编译到libnative_lib.so这个文件里,根据需求增加你须要的源代码。
    3.find_library:定位NDK的某个库,并将其路径存在某个变量,供其它部分引用。
    其它CMake Commands内容。点击这里

    4.javah, javap

    在java层声明好native方法之后。依照一般的习惯是要生成相应的jni层方法,网上最一般的方法也是通过javah来生成。


    1. 在jdk1.6及下面。使用相应java文件生成的class文件来生成.h文件
    进入到相应的uildintermediatesclassesdebug文件夹下,打开命令行输入下面命令:

    javah -jni com.netesae.jnisample.Prompt

    后面一定要输入类的全名,包括包名。然后就会生成.h文件了。
    2. 可是在jdk1.7及以上。能够直接使用java文件生成.h文件。
    进入srcmainjava文件夹下。打开命令行,敲入和上面一样的命令。就能够了。

    两种方式生成的.h文件名称非常长,当然你能够改。


    在main/下创建cpp文件夹,将该.h文件增加,并创建新的cpp文件或者c文件,include .h文件。实现.h文件的方法就可以,这一部分是c++/c的使用方法就不解释了。

    当然假设你是採用Android官网上的方法创建一个支持c++的项目。它会自己主动帮你生成一个jni的模板。而且发现他事实上仅仅有一个cpp文件,并不须要什么.h文件,当然这也是能够的。
    那为什么还要javah呢?这是由于jni方法规范,java层的方法要相应的native层的方法,为了保证每一个函数的唯一性,所以jni层的方法命名比較长。规则例如以下:

    Java_包名_函数名字

    而且包名之间的.号也要用_来取代。由于名字比較长,为了防止程序猿写错而导致找不到相应的方法,就用javah。

    那么javap是干嘛的,就是用来生成函数签名的,那什么是函数签名呢,后面再解释,如今先看命令。

    看样例:

    public class Prompt {
    
        static {
            System.loadLibrary("prompt-lib");
        }
    
        native String getLine(String prompt);
    
        native String show();
    }

    进入到Prompt.java所在的文件夹,敲入下面命令:

    D:gitJNISampleappsrcmainjavacomexamplejnisample>javap -s -p -classpath . Prompt
    警告: 二进制文件Prompt包括com.example.jnisample.Prompt
    Compiled from "Prompt.java"
    public class com.example.jnisample.Prompt {
      public com.example.jnisample.Prompt();
        descriptor: ()V
    
      native java.lang.String getLine(java.lang.String);
        descriptor: (Ljava/lang/String;)Ljava/lang/String;
    
      native java.lang.String show();
        descriptor: ()Ljava/lang/String;
    
      static {};
        descriptor: ()V
    }

    看prompt中的getLine函数,他的參数是String,返回的是String。所以他的函数签名就是(Ljava/lang/String;)Ljava/lang/String;,括号里的是參数签名,括号外面的就是方法返回值签名。这个后面会用到,先记着吧。

    4.JNIEnv在c和c++中的差别

    一開始看些资料的话。大家可能会有些疑惑,比如我们要调用JNIEnv的同一个函数,会看到有下面两个版本号:

    (*env)->FindClass(env,"com/example/jnisample/Prompt");
    
    env->FindClass("com/example/jnisample/Prompt");

    那这两个有什么差别么?差别就是一个是c++使用方法,一个是c中的使用方法。首先先让我们看下JNIEnv是啥(事实上现是在jni.h中)。

    #if defined(__cplusplus)
    typedef _JNIEnv JNIEnv;
    typedef _JavaVM JavaVM;
    #else
    typedef const struct JNINativeInterface* JNIEnv;
    typedef const struct JNIInvokeInterface* JavaVM;
    #endif

    这段话的意思是在c++中。定义_JNIEnv是JNIEnv,其它情况(c)下,定义const struct JNINativeInterface*是JNIEnv。那么_JNIEnv和JNINativeInterface又是什么呢?

    struct JNINativeInterface {
       ......非常多方法
        jclass      (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*,
    }。
    
    
    struct _JNIEnv {
        /* do not rename this; it does not seem to be entirely opaque */
        const struct JNINativeInterface* functions;
    
    #if defined(__cplusplus)
    
        jint GetVersion()
        { return functions->GetVersion(this); }
    .....其它方法
    }。

    能够看到JNINativeInterface 事实上定义了非常多方法。都是对Java的数据进行操作。而_JNIEnv则封装了一个JNINativeInterface的指针。而且声明与JNINativeInterface中一模一样的方法。而且都是通过JNINativeInterface的指针来调方法,事实上就是对JNINativeInterface做了一层封装,那么为什么这么做呢?
    我的猜想是c++是面向对象的语言。不用在用指针方式来调用。而且_JNIEnv中的每一个方法都比JNINativeInterface少一个參数。就是JNIEnv。详细能够自己看jni.h中的实现。

    5.extern “C” vs JNIExport JNICall

    .cpp文件是c++的语法,.c是c的语法,文件的类型决定了JNIEnv的语法,在上面一小节也提到JNIEnv在c++和c的差别。


    网上的资料中,native方法除了要遵守JNI函数规范。还要加上JNIExport和JNICall,这样才干保证这个native函数是能够注冊在函数列表中,但我后来试了下,在使用cmake的情况下。并不须要JNIExport和JNICall。


    1.c语言情况下,并不须要JNIExport和JNICall。
    2.c++语言情况下。也不须要JNIExport和JNICall。可是须要加上extern “C”{},native函数须要放在这个括号里才干够。

    解释下extern “C”的意思。extern代表声明的方法和变量为全局变量,和java的static一样,可是和c++的static不一样(有关c++语法自行查找)。”c”则代表{}内的内容以c语言方式编译和连接。
    至于c语言下为什么不用JNIExport和JNICall还不是非常清晰。尚未找到原因。但猜想可能是在cmake编译so文件的时候。做了什么手脚。

    6.框架层vs应用层

    下面内容资料来自JNI在Android系统中所处的位置,可自行往下阅读。

    应用框架层:Android定义了一套JNI编程模型,使用函数注冊方式弥补了标准JNI编程模型的不足。Android应用框架层JNI部分源代码主要位于frameworks/base/文件夹下。依照模块组织。不同的模块将被编译为不同的共享库,分别为上层提供不同的服务。这些共享库终于会被放置在目标系统的/system/lib文件夹下。
    在Android应用程序开发中,通常是调用应用框架层的android.util.Log.java提供的Java接口来使用日志系统。

    比方我们会写例如以下代码输出日志:
    Log.d(TAG,"debug log");
    这个Java接口事实上是通过JNI调用系统运行库(即本地库)并终于调用内核驱动程序Logger把Log写到内核空间中的。

    在Android中, Log系统十分通用,而且其JNI结构非常简洁,非常适合作为JNI入门的样例。所涉及的文件包括:
    frameworks/base/core/jni/android_util_Log.cpp(JNI层实现代码)

    frameworks/base/core/java/android/util/Log.java(Java层代码)

    libnativehelper/include/nativehelper/jni.h(JNI规范的头文件)

    libnativehelper/include/nativehelper/JNIHelp.h

    libnativehelper/JNIHelp.cpp

    frameworks/base/core/jni/AndroidRuntime.cpp

    package android.util;  
    public final class Log {  
      ……  
      public static int d(String tag, String msg) {  
    //使用Native方法打印日志。LOG_ID_MAIN表示日志ID,有4种:main、radio、events、system  
    return println_native(LOG_ID_MAIN, DEBUG, tag, msg);  
      }  
      ……  
    //声明Native方法isLoggable  
    public static native boolean isLoggable(String tag, int level);  
      ……  
      /** @hide */ public static final int LOG_ID_MAIN = 0;  
      /** @hide */ public static final int LOG_ID_RADIO = 1;  
      /** @hide */ public static final int LOG_ID_EVENTS = 2;  
      /** @hide */ public static final int LOG_ID_SYSTEM = 3;  
    //声明Native方法println_native  
      /** @hide */ public static native int println_native(int bufID,  
         int priority, String tag, String msg);  
    } 

    native的实现:

    #include "jni.h"  //符合JNI规范的头文件,必须包括进来  
    #include "JNIHelp.h"  //Android为更好地支持JNI提供的头文件  
    #include "utils/misc.h"  
    #include "android_runtime/AndroidRuntime.h"  
    /*这里便是Java层声明的isLoggable方法的实现代码。  
     *JNI方法增加了JNIEnv和jobject两个參数,其余參数和返回值仅仅是将Java參数映射成JNI  
     *的数据类型,然后通过调用本地库和JNIEnv提供的JNI函数处理数据,最后返回给Java层*/  
    static jboolean android_util_Log_isLoggable(JNIEnv* env, jobject clazz,  
       jstring tag, jint level)  
    {  
      ……  
      //这里调用了JNI函数  
    const char* chars = env->GetStringUTFChars(tag, NULL);  
    jboolean result = false;  
    if ((strlen(chars)+sizeof(LOG_NAMESPACE)) > PROPERTY_KEY_MAX) {  
     ……  
    } else {  
      //这里调用了本地库函数  
      result = isLoggable(chars, level);  
    }  
    env->ReleaseStringUTFChars(tag, chars);//调用JNI函数  
    return result;  
    }  
    //下面是Java层声明的println_Native方法的实现代码  
    static jint android_util_Log_println_native(JNIEnv* env, jobject clazz,  
       jint bufID, jint priority, jstring tagObj, jstring msgObj)  
    {  
    const char* tag = NULL;  
    const char* msg = NULL;  
    ……//省略异常处理代码  
    if (tagObj != NULL)  
       tag = env->GetStringUTFChars(tagObj, NULL);//调用JNI函数  
    msg = env->GetStringUTFChars(msgObj, NULL);  
    //调用本地库提供的方法  
    int res = __android_log_buf_write(bufID,(android_LogPriority)priority, tag, msg);  
    if (tag != NULL)  
       env->ReleaseStringUTFChars(tagObj, tag);//调用JNI函数释放资源  
       env->ReleaseStringUTFChars(msgObj, msg);//调用JNI函数释放资源  
    return res; 

    JNI层已经实现了Java层声明的Native方法。可这两个方法又是怎样联系在一起的呢?我们接着分析android_util_Log.cpp的源代码。定位到下面部分:

    static JNINativeMethod gMethods[] = {  
       { "isLoggable",  "(Ljava/lang/String;I)Z",  
          (void*) android_util_Log_isLoggable },  
       { "println_native",  "(IILjava/lang/String;Ljava/lang/String;)I",  
    
          (void*) android_util_Log_println_native },  
    };

    这里定义了一个数组gMethods,用来存储JNINativeMethod类型的数据。

    能够在jni.h文件里找到JNINativeMethod的定义:

    typedef  struct {  
       const char* name;   //Java层声明的Native函数的函数名  
       const char* signature;  //Java函数的签名,根据JNI的签名规则  
       void* fnPtr;   //函数指针,指向JNI层的实现方法  
    } JNINativeMethod; 

    可见,JNINativeMethod是一个结构体类型,保存了声明函数和实现函数的一一相应关系。

    下面分析gMethods[0]中存储的相应信息:

    { "isLoggable", "(Ljava/lang/String;I)Z", (void*) android_util_Log_isLoggable }  
    Java层声明的Native函数名为isLoggable。

    Java层声明的Native函数的签名为(Ljava/lang/String;I)Z。 JNI层实现方法的指针为(void*) android_util_Log_isLoggable。

    这里就能够用到刚刚说到的javap工具,用来生成函数签名。
    至此,我们给出了Java层方法和JNI层方法的相应关系。可怎样告诉虚拟机这样的相应关系呢?

    继续分析android_util_Log.cpp源代码。定位到下面部分:

    int register_android_util_Log(JNIEnv* env)  
    {  
    
    jclass clazz = env->FindClass("android/util/Log");  
       levels.debug = env->GetStaticIntField(clazz, env->GetStaticFieldID(clazz,  
    
          "DEBUG", "I"));  
    ……  
    return AndroidRuntime::registerNativeMethods(env, "android/util/Log",  
           gMethods, NELEM(gMethods));  

    详细细看AndroidRuntime::registerNativeMethods,发现终于调用的是JNIEnv的RegisterNatives方法。其作用是向clazz參数指定的类注冊本地方法。这样,虚拟机就得到了Java层和JNI层之间的相应关系,就能够实现Java和C/C++代码的互操作了。
    register_android_util_Log函数是在哪里调用的?
    这个问题涉及JNI部分代码在系统启动过程中是怎样载入的。这已经超出了本章的知识范围。我们将在启动篇详细介绍这个过程。

    在这里。读者仅仅须要知道这个函数是在系统启动过程中通过AndroidRuntime.cpp的register_jni_procs方法运行的,进而调用到register_android_util_Log将这样的函数映射关系注冊给Dalvik虚拟机的。


    注意 使用JNI有两种方式:一种是遵守JNI规范的函数命名规范。建立声明函数和实现函数之间的相应关系。还有一种是就是Log系统中採用的函数注冊方式。应用层多採用第一种方式,应用框架层多採用另外一种方式。

    那么应用层能够使用上述函数注冊方式来么。不用遵守JNI函数规范?答案是能够的。


    看下面的样例:

    package com.example.jnisample;
    public class Prompt {
    
        static {
            System.loadLibrary("prompt-lib");
        }
    
        native String getLine(String prompt);
    }
    #include <jni.h>
    #include <stdio.h>
    
    jstring
    Prompt_getLine(JNIEnv *env, jobject thiz, jstring params) {
        return params;
    }
    
    static JNINativeMethod gMethods[] = {
            {"getLine", "(Ljava/lang/String;)Ljava/lang/String;", (void *) Prompt_getLine},
    };
    
    /*虚拟机运行System.loadLibrary("native-lib")后,进入libnative-lib.so后  
     *会首先运行这种方法。所以我们在这里做注冊的动作*/  
    jint JNI_OnLoad(JavaVM *vm, void *reserved) {
        JNIEnv *env = NULL;
        jint result = -1;
        if (vm->GetEnv((void **) &env, JNI_VERSION_1_4)) {
            return result;
        }
        jclass clazz = env->FindClass("com/example/jnisample/Prompt");
        if (clazz == NULL) {
            return result;
        }
        if (env->RegisterNatives(clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])) >= 0) {
            result = JNI_VERSION_1_4;
        }
        return result;
    }

    上述样例是仿自android_media_MediaPlayer.cpp

    6.获取当前线程的JNIEnv

    不论进程中有多少个线程,JavaVM仅仅有一份,所以在不论什么地方都能够使用它。能够通过调用JavaVM的attachCurrentThread来得到这个线程的JNIEnv,注意要调用detachCurrentThread来释放相应的资源。

    參考资料:
    http://book.51cto.com/art/201305/395846.htm
    http://androidxref.com/4.2_r1/xref/frameworks/base/media/jni/
    https://developer.android.com/studio/projects/add-native-code.html
    https://cmake.org/cmake/help/latest/manual/cmake-commands.7.html

  • 相关阅读:
    线性分类器之感知机算法
    字符串包含判断
    王家林 云计算分布式大数据Hadoop实战高手之路从零开始 第二讲:全球最详细的从零起步搭建Hadoop单机和伪分布式开发环境图文教程
    王家林 第六讲Hadoop图文训练课程:使用HDFS命令行工具操作Hadoop分布式集群初体验
    王家林的“云计算分布式大数据Hadoop实战高手之路从零开始”的第五讲Hadoop图文训练课程:解决典型Hadoop分布式集群环境搭建问题
    王家林的 第三讲Hadoop图文训练课程:证明Hadoop工作的正确性和可靠性只需4步图文并茂的过程
    王家林 第四讲Hadoop图文训练课程:实战构建真正的Hadoop分布式集群环境
    麻雀GUIv1.0整理好咯,发个开源上来。
    body设置背景色异常
    safari浏览器placeholder垂直居中
  • 原文地址:https://www.cnblogs.com/zhchoutai/p/7402838.html
Copyright © 2011-2022 走看看