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文件

进入到对应的\build\intermediates\classes\debug目录下,打开命令行输入以下命令:

javah -jni com.netesae.jnisample.Prompt

后面一定要输入类的全名,包括包名。然后就会生成.h文件了。

2. 但是在jdk1.7及以上,可以直接使用java文件生成.h文件。

进入\src\main\java目录下,打开命令行,敲入和上面一样的命令,就可以了。

两种方式生成的.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:\git\JNISample\app\src\main\java\com\example\jnisample>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

时间: 2024-10-07 07:04:32

JNI学习笔记的相关文章

Android JNI学习笔记(三)-编译文件Android.mk、Application.mk 与camke

1. 前言 在android2.2中,加入了cmake编译,而以前都是用Android.mk.Application.mk的,今天就来记录下,他们的配置选项. 2. Android.mk Android.mk在jni目录下,用于描述构建系统的源文件以及 shared libraries .文件格式如下: 以LOCAL_PATH变量开始 LOCAL_PATH := $(call my-dir) 紧接着是CLEAR_VARS变量 include $(CLEAR_VARS) 接下来LOCAL_MODU

Android JNI学习笔记(四)-数据类型映射以及native调用java

1. 前言 前几篇学习了jni开发的基本流程.动态注册native函数以及相关编译文件的编写,咱们也算是知道了jni开发,但是还不够,今天咱们来学习下,java和jni的数据类型映射(说白了就是对应关系),以及如何在jni层调用java层的一些东西.偷偷告诉你们,这些全在jni.h文件里. 2. 数据类型映射 首先是我们的基本数据类型,其关系如下表描述这样. 上面关系的相关代码在jni.h的44-51行,如下 typedef unsigned char jboolean; /* unsigned

Android JNI 学习笔记

JNI  是 Java Native Interface(Java 本地接口).JNI不是Android 专有的东西,他是从Java继承来的.但是 对于Android来说JNI至关重要,Android 作为一种嵌入式操作系统,有大量和驱动.硬件相关的功能都是用C/C++来实现的.可以说在Android中不管应用级还是系统级的开发都离不开JNI. Java语言的执行,离不开JVM,因此当需要在Java层中调用C/C++层时,需要先告诉JVM那个方法代表本地函数,伊基在哪里能找到这个函数,反之也一样

JNI入门-学习笔记

JNI入门-学习笔记 可执行文件 Windows - *.exe | linux - *.elf c函数库文件 Windows - *.dll | linux - *.so 批处理文件 Windows - *.bat | linux - *.sh 工具 NDK -- native developer kits Cygwin -- Linux系统模拟器 安装过程要点: ——安装时可以联网安装也可以通过本地文件安装(如果有本地文件) ——Select Packages:没必要全部安装,安装Devel

android JNI入门-学习笔记

JNI入门-学习笔记 可执行文件 Windows - *.exe | linux - *.elf c函数库文件 Windows - *.dll | linux - *.so 批处理文件 Windows - *.bat | linux - *.sh 工具 NDK -- native developer kits Cygwin -- Linux系统模拟器 安装过程要点: --安装时可以联网安装也可以通过本地文件安装(如果有本地文件) --Select Packages:没必要全部安装,安装Devel

android学习笔记----JNI中的c控制java

面向对象的底层实现 java作为面向对象高级语言,可对现实世界进行建模.和面向过程不同的是面向对象软件的编写不是流程的堆积,而是对业务逻辑的多视角分解和分类.其过程大致为:      1).将知识分解成不同粒度的小概念.      2).对概念进行分类,形成类.模块.系统      3).用计算机语言将其实现为对象(包含操作和数据) 然而,大多程序员编写代码时很少思考面向对象的体系是如何实现的.因为它属于底层,并不必要.但是,学习过Java反射技术并有所思考的人会意识到,面向对象的种种语法本身也

Android NDK学习笔记(一) 为什么要用NDK?

NDK是什么 NDK是Native Development Kit的简称,即本地开发工具包.通过NDK,Android允许开发人员使用本地代码语言(例如C/C++)来完成应用的部分(甚至全部)功能.注意:由于翻译原因,有些地方也把Native翻译为"原生". NDK是SDK的一个补充,可以帮助你做这些事情: 生成可以在ARM CPU,Android 1.5(及以上)平台运行的JNI兼容的共享库. 将生成的共享库放置在应用程序项目路径的合适位置,使其能自动地添加进你最终的(和经过签名的)

Android深度探索(卷1)HAL与驱动开发学习笔记(2)

Android深度探索(卷1)HAL与驱动开发学习笔记(2) 第二章搭建Android开发环境 书中介绍了两种JDK的安装方法, 方法一: 从官网下载JDK并进行配置,解压后在终端打开profile文件来设置PATH环境变量(# soure /etc/profile),打开profile文件后输入下面的内容 export PATH=.:developer/jdk6/bin:$PATH 保存profile文件以后,有两种方法可以重新加载profile文件. 1.# sourse  /etc/pro

cocos2dx学习笔记(2)

昨天尝试了cocos2dx在win下的开发环境配置,并且运行了cocos的helloword程序,晚上想要尝试一下android开发环境配置,顺便学习cocos在eclipse下的JNI机制,按照cocoa中文论坛的android环境配置弄了NDK,并配置了环境变量,由于想要学习cocos的luabind机制(这个我们公司游戏的引擎用的很多,确实比较有兴趣),一切搞定不明就里的用eclipse导入了cocos3.0rc中的tests目录下的cpp-tests工程(这算android开发久了的毛病