搜档网
当前位置:搜档网 › javaJNI编程指南

javaJNI编程指南

javaJNI编程指南
javaJNI编程指南

Android JNI知识简介

Java Native Interface (JNI)标准是java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI 是本地编程接口,它使得在Java 虚拟机(VM) 内部运行的Java 代码能够与用其它编程语言(如C、C++ 和汇编语言)编写的应用程序和库进行交互操作。

1.从如何载入.so档案谈起

由于Android的应用层的类都是以Java写的,这些Java类编译为Dex型式的Bytecode 之后,必须靠Dalvik虚拟机(VM: Virtual Machine)来执行。VM在Android平台里,扮演很重要的角色。

此外,在执行Java类的过程中,如果Java类需要与C组件沟通时,VM就会去载入C 组件,然后让Java的函数顺利地调用到C组件的函数。此时,VM扮演着桥梁的角色,让Java与C组件能通过标准的JNI介面而相互沟通。

应用层的Java类是在虚拟机(VM: Vitual Machine)上执行的,而C件不是在VM上执行,那么Java程式又如何要求VM去载入(Load)所指定的C组件呢? 可使用下述指令:System.loadLibrary(*.so的档案名);

例如,Android框架里所提供的MediaPlayer.java类,含指令:

public class MediaPlayer{

static {

System.loadLibrary("media_jni");

}

}

这要求VM去载入Android的/system/lib/libmedia_jni.so档案。载入*.so之后,Java类与*.so档案就汇合起来,一起执行了。

2.如何撰写*.so的入口函数

---- JNI_OnLoad()与JNI_OnUnload()函数的用途

当Android的VM(Virtual Machine)执行到System.loadLibrary()函数时,首先会去执行C组件里的JNI_OnLoad()函数。它的用途有二:

(1)告诉VM此C组件使用那一个JNI版本。如果你的*.so档没有提供JNI_OnLoad()函数,VM会默认该*.so档是使用最老的JNI 1.1版本。由于新版的JNI做了许多扩充,如果需要使用JNI的新版功能,例如JNI 1.4的java.nio.ByteBuffer,就必须藉由JNI_OnLoad()函数来告知VM。

(2)由于VM执行到System.loadLibrary()函数时,就会立即先呼叫JNI_OnLoad(),所以C组件的开发者可以藉由JNI_OnLoad()来进行C组件内的初期值之设定(Initialization) 。

例如,在Android的/system/lib/libmedia_jni.so档案里,就提供了JNI_OnLoad()函数,其程式码片段为:

//#define LOG_NDEBUG 0

#define LOG_TAG "MediaPlayer-JNI"

jint JNI_OnLoad(JavaVM* vm, void* reserved)

{

JNIEnv* env = NULL;

jint result = -1;

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

LOGE("ERROR: GetEnv failed\n");

goto bail;

}

assert(env != NULL);

if (register_android_media_MediaPlayer(env) < 0) {

LOGE("ERROR: MediaPlayer native registration failed\n");

goto bail;

}

if (register_android_media_MediaRecorder(env) < 0) {

LOGE("ERROR: MediaRecorder native registration failed\n");

goto bail;

}

if (register_android_media_MediaScanner(env) < 0) {

LOGE("ERROR: MediaScanner native registration failed\n");

goto bail;

}

if (register_android_media_MediaMetadataRetriever(env) < 0) {

LOGE("ERROR: MediaMetadataRetriever native registration failed\n");

goto bail;

}

/* success -- return valid version number */

result = JNI_VERSION_1_4;

bail:

return result;

}

此函数回传JNI_VERSION_1_4值给VM,于是VM知道了其所使用的JNI版本了。此外,它也做了一些初期的动作(可呼叫任何本地函数),例如指令:

if (register_android_media_MediaPlayer(env) < 0) {

LOGE("ERROR: MediaPlayer native registration failed\n");

goto bail;

}

就将此组件提供的各个本地函数(Native Function)登记到VM里,以便能加快后续呼叫本地

函数的效率。

JNI_OnUnload()函数与JNI_OnLoad()相对应的。在载入C组件时会立即呼叫JNI_OnLoad()来进行组件内的初期动作;而当VM释放该C组件时,则会呼叫JNI_OnUnload()函数来进行善后清除动作。当VM呼叫JNI_OnLoad()或JNI_Unload()函数时,都会将VM的指针(Pointer)传递给它们,其参数如下:

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

jint JNI_OnUnload(JavaVM* vm, void* reserved){ }

在JNI_OnLoad()函数里,就透过VM之指标而取得JNIEnv之指标值,并存入env指标变数里,如下述指令:

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

JNIEnv* env = NULL;

jint result = -1;

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

LOGE("ERROR: GetEnv failed\n");

goto bail;

}

}

由于VM通常是多执行绪(Multi-threading)的执行环境。每一个执行绪在呼叫JNI_OnLoad()时,所传递进来的JNIEnv指标值都是不同的。为了配合这种多执行绪的环境,C组件开发者在撰写本地函数时,可藉由JNIEnv指标值之不同而避免执行绪的资料冲突问题,才能确保所写的本地函数能安全地在Android的多执行绪VM里安全地执行。基于这个理由,当在呼叫C组件的函数时,都会将JNIEnv指标值传递给它,如下:

jint JNI_OnLoad(JavaVM* vm, void* reserved)

{

JNIEnv* env = NULL;

if (register_android_media_MediaPlayer(env) < 0) {

}

}

这JNI_OnLoad()呼叫register_android_media_MediaPlayer(env)函数时,就将env指标值传递过去。如此,在register_android_media_MediaPlayer()函数就能藉由该指标值而区别不同的执行绪,以便化解资料冲突的问题。

例如,在register_android_media_MediaPlayer()函数里,可撰写下述指令:

if ((*env)->MonitorEnter(env, obj) != JNI_OK) {

}

查看是否已经有其他执行绪进入此物件,如果没有,此执行绪就进入该物件里执行了。还有,也可撰写下述指令:

if ((*env)->MonitorExit(env, obj) != JNI_OK) {

}

查看是否此执行绪正在此物件内执行,如果是,此执行绪就会立即离开。

3.registerNativeMethods()函数的用途

应用层级的Java类别透过VM而呼叫到本地函数。一般是仰赖VM去寻找*.so里的本地函数。如果需要连续呼叫很多次,每次都需要寻找一遍,会多花许多时间。此时,组件开发者可以自行将本地函数向VM进行登记。例如,在Android的/system/lib/libmedia_jni.so档案里的代码段如下:

//#define LOG_NDEBUG 0

#define LOG_TAG "MediaPlayer-JNI"

static JNINativeMethod gMethods[] = {

{"setDataSource", "(Ljava/lang/String;)V",

(void *)android_media_MediaPlayer_setDataSource},

{"setDataSource", "(Ljava/io/FileDescriptor;JJ)V",

(void *)android_media_MediaPlayer_setDataSourceFD},

{"prepare", "()V", (void *)android_media_MediaPlayer_prepare},

{"prepareAsync", "()V", (void *)android_media_MediaPlayer_prepareAsync},

{"_start", "()V", (void *)android_media_MediaPlayer_start},

{"_stop", "()V", (void *)android_media_MediaPlayer_stop},

{"getVideoWidth", "()I", (void *)android_media_MediaPlayer_getVideoWidth}, {"getVideoHeight", "()I", (void *)android_media_MediaPlayer_getVideoHeight}, {"seekTo", "(I)V", (void *)android_media_MediaPlayer_seekTo},

{"_pause", "()V", (void *)android_media_MediaPlayer_pause},

{"isPlaying", "()Z", (void *)android_media_MediaPlayer_isPlaying},

{"getCurrentPosition", "()I", (void *)android_media_MediaPlayer_getCurrentPosition}, {"getDuration", "()I", (void *)android_media_MediaPlayer_getDuration},

{"_release", "()V", (void *)android_media_MediaPlayer_release},

{"_reset", "()V", (void *)android_media_MediaPlayer_reset},

{"setAudioStreamType","(I)V", (void *)android_media_MediaPlayer_setAudioStreamType}, {"setLooping", "(Z)V", (void *)android_media_MediaPlayer_setLooping}, {"setVolume", "(FF)V", (void *)android_media_MediaPlayer_setV olume},

{"getFrameAt", "(I)Landroid/graphics/Bitmap;",

(void *)android_media_MediaPlayer_getFrameAt},

{"native_setup", "(Ljava/lang/Object;)V",

(void *)android_media_MediaPlayer_native_setup},

{"native_finalize", "()V", (void *)android_media_MediaPlayer_native_finalize}, };

static int register_android_media_MediaPlayer(JNIEnv *env){

return AndroidRuntime::registerNativeMethods(env,

"android/media/MediaPlayer", gMethods, NELEM(gMethods));

}

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

if (register_android_media_MediaPlayer(env) < 0) {

LOGE("ERROR: MediaPlayer native registration failed\n");

goto bail;

}

}

当VM载入libmedia_jni.so档案时,就呼叫JNI_OnLoad()函数。接着,JNI_OnLoad()呼叫register_android_media_MediaPlayer()函数。此时,就呼叫到AndroidRuntime::registerNativeMethods()函数,向VM(即AndroidRuntime)登记gMethods[]表格所含的本地函数了。简而言之,registerNativeMethods()函数的用途有二:

(1)更有效率去找到函数。

(2)可在执行期间进行抽换。由于gMethods[]是一个<名称,函数指针>对照表,在程序执行时,可多次呼叫registerNativeMethods()函数来更换本地函数之指针,而达到弹性抽换本地函数之目的。

4.Andoird 中使用了一种不同传统Java JNI的方式来定义其native的函数。其中很重要的区别是Andorid使用了一种Java 和 C 函数的映射表数组,并在其中描述了函数的参数和返回值。这个数组的类型是JNINativeMethod,定义如下:

typedef struct {

const char* name; /*Java中函数的名字*/

const char* signature; /*描述了函数的参数和返回值*/

void* fnPtr; /*函数指针,指向C函数*/

} JNINativeMethod;

其中比较难以理解的是第二个参数,例如

"()V"

"(II)V"

"(Ljava/lang/String;Ljava/lang/String;)V"

实际上这些字符是与函数的参数类型一一对应的。

"()" 中的字符表示参数,后面的则代表返回值。例如"()V" 就表示void Func();

"(II)V" 表示void Func(int, int);

具体的每一个字符的对应关系如下

字符Java类型C类型

V void void

Z jboolean boolean

I jint int

J jlong long

D jdouble double

F jfloat float

B jbyte byte

C jchar char

S jshort short

数组则以"["开始,用两个字符表示

[I jintArray int[]

[F jfloatArray float[]

[B jbyteArray byte[]

[C jcharArray char[]

[S jshortArray short[]

[D jdoubleArray double[]

[J jlongArray long[]

[Z jbooleanArray boolean[]

上面的都是基本类型。如果Java函数的参数是class,则以"L"开头,以";"结尾,中间是用"/" 隔开的包及类名。而其对应的C函数名的参数则为jobject. 一个例外是String类,其对应的类为jstring

Ljava/lang/String; String jstring

Ljava/net/Socket; Socket jobject

如果JA V A函数位于一个嵌入类,则用$作为类名间的分隔符。

例如"(Ljava/lang/String;Landroid/os/FileUtils$FileStatus;)Z"

Android JNI编程实践

一、直接使用java本身jni接口(windows/ubuntu)

1.在Eclipsh中新建一个android应用程序。两个类:一个继承于Activity,UI显示用。另一个包含native方法。编译生成所有类。

jnitest.java文件:

package com.hello.jnitest;

import android.app.Activity;

import android.os.Bundle;

public class jnitest extends Activity {

/** Called when the activity is first created. */

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(https://www.sodocs.net/doc/642026796.html,yout.main);

Nadd cal = new Nadd();

setTitle("The Native Add Result is " + String.valueOf(cal.nadd(10, 19)));

}

}

Nadd.java文件:

package com.hello.jnitest;

public class Nadd {

static {

System.loadLibrary("Nadd");

}

public native int nadd(int a, int b);

}

以上在windows中完成。

2.使用javah命令生成C/C++的.h文件。注意类要包含包名,路径文件夹下要包含所有包中的类,否则会报找不到类的错误。classpath参数指定到包名前一级文件夹,文件夹层次结构要符合java类的组织层次结构。

javah -classpath ../jnitest/bin com.hello.jnitest.Nadd

com_hello_jnitest_Nadd .h文件:

/* DO NOT EDIT THIS FILE - it is machine generated */

#include

/* Header for class com_hello_jnitest_Nadd */

#ifndef _Included_com_hello_jnitest_Nadd

#define _Included_com_hello_jnitest_Nadd

#ifdef __cplusplus

extern "C" {

#endif

/*

* Class: com_hello_jnitest_Nadd

* Method: nadd

* Signature: (II)I

*/

JNIEXPORT jint JNICALL Java_com_hello_jnitest_Nadd_nadd

(JNIEnv *, jobject, jint, jint);

#ifdef __cplusplus

}

#endif

#endif

3.编辑.c文件实现native方法。

com_hello_jnitest_Nadd.c文件:

#include

#include "com_hello_jnitest_Nadd.h"

JNIEXPORT jint JNICALL Java_com_hello_jnitest_Nadd_nadd(JNIEnv * env, jobject c, jint a, jint b)

{

return (a+b);

}

4.编译.c文件生存动态库。

arm-none-linux-gnueabi-gcc -I/home/a/work/android/jdk1.6.0_17/include -I/home/a/work/android/jdk1.6.0_17/include/linux -fpic -c com_hello_jnitest_Nadd.c

arm-none-linux-gnueabi-ld

-T/home/a/CodeSourcery/Sourcery_G++_Lite/arm-none-linux-gnueabi/lib/ldscripts/armelf_linux_ eabi.xsc -share -o libNadd.so com_hello_jnitest_Nadd.o

得到libNadd.so文件。

以上在ubuntu中完成。

5.将相应的动态库文件push到avd的system/lib中:adb push libNadd.so /system/lib。若提示Read-only file system错误,运行adb remount命令,即可。

Adb push libNadd.so /system/lib

6.在eclipsh中运行原应用程序即可。

以上在windows中完成。

对于一中生成的so文件也可采用二中的方法编译进apk包中。只需在工程文件夹中建libs\armeabi文件夹(其他文件夹名无效,只建立libs文件夹也无效),然后将so文件拷入,编译工程即可。

二.使用NDK生成本地方法(ubuntu and windows)

1.安装NDK:解压,然后进入NDK解压后的目录,运行build/host-setup.sh(需要Make 3.81和awk)。若有错,修改host-setup.sh文件:将#!/bin/sh修改为#!/bin/bash,再次运行即可。

2.在apps文件夹下建立自己的工程文件夹,然后在该文件夹下建一文件Application.mk和项project文件夹。

Application.mk文件:

APP_PROJECT_PA TH := $(call my-dir)/project

APP_MODULES := myjni

3.在project文件夹下建一jni文件夹,然后新建Android.mk和myjni.c。这里不需要用javah 生成相应的.h文件,但函数名要包含相应的完整的包、类名。

4.编辑相应文件内容。

Android.mk文件:

# Copyright (C) 2009 The Android Open Source Project

#

# Licensed under the Apache License, Version 2.0 (the "License");

# you may not use this file except in compliance with the License.

# You may obtain a copy of the License at

#

# https://www.sodocs.net/doc/642026796.html,/licenses/LICENSE-2.0

#

# Unless required by applicable law or agreed to in writing, software

# distributed under the License is distributed on an "AS IS" BASIS,

# WITHOUT W ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

# See the License for the specific language governing permissions and

# limitations under the License.

#

LOCAL_PATH := $(call my-dir)

include $(CLEAR_V ARS)

LOCAL_MODULE := myjni

LOCAL_SRC_FILES := myjni.c

include $(BUILD_SHARED_LIBRARY)

myjni.c文件:

#include

#include

jstring

Java_com_hello_NdkTest_NdkTest_stringFromJNI( JNIEnv* env,

jobject thiz )

{

return (*env)->NewStringUTF(env, "Hello from My-JNI !");

}

myjni文件组织:

a@ubuntu:~/work/android/ndk-1.6_r1/apps$ tree myjni

myjni

|-- Application.mk

`-- project

|-- jni

| |-- Android.mk

| `-- myjni.c

`-- libs

`-- armeabi

`-- libmyjni.so

4 directories, 4 files

5.编译:make APP=myjni.

以上内容在ubuntu完成。以下内容在windows中完成。当然也可以在ubuntu中完成。

6.在eclipsh中创建android application。将myjni中自动生成的libs文件夹拷贝到当前工程文件夹中,编译运行即可。

NdkTest.java文件:

package com.hello.NdkTest;

import android.app.Activity;

import android.os.Bundle;

import android.widget.TextView;

public class NdkTest extends Activity {

/** Called when the activity is first created. */

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

TextView tv = new TextView(this);

tv.setText( stringFromJNI() );

setContentView(tv);

}

public native String stringFromJNI();

static {

System.loadLibrary("myjni");

}

}

对于二中生成的so文件也可采用一中的方法push到avd中运行。

这四种情况下你会用到本书:

1、在Java程序中复用以前写过的C/C++代码。

2、自己实现一个java虚拟机

3、学习不同语言如何进行协作,尤其是如何实现垃圾回收和多线程。

4、把一个虚拟机实现整合到用C/C++写的程序中。

本书是写给开发者的。JNI在1997年第一次发布,本书总结了SUN工程师和大量开发者两年来积累的经验。

本书介绍了JNI的设计思想,对这种思想的理解是使用JNI的各种特性的基础。

本书有一部分是JA V A2平台上面的JNI特征的规范说明。JNI程序员可以把这部分用作一个手册。JVM开发者在实现虚拟机的时候必须遵守这些规范。

JNI的部分设计思想来源于Netscape的Java Runtime Interface(JRI)。

简介

JNI是JA V A平台的一个重要特征,使用它我们可以重用以前用C/C++写的大量代码。本书既是一个编程指南也是一个JNI手册。本书共包括三部分:

1、第二章通过一个简单的例子介绍了JNI。它的对象是对JNI不熟悉的初学者。

2、3~10章对JNI的特征进行了系统的介绍。我们会举大量的例子来说明JNI的各个特征,

这些特征都是JNI中重要且常用的。

3、11~13章是关于JNI的技术规范。可以把这两章当作一个手册。

本书尽量去满足各类读者的需要。指南面向初学者,手册面向有经验的人和自己实现JNI 规范的人。大部分读者可能是用JNI来写程序的开发者。本书会假设你有JA V A,C/C++基础。

本章的剩余部分介绍了JNI的背景,扮演的角色和JNI的演化。

1.1 JAV A平台和系统环境(Host Environment)

系统环境代指本地操作系统环境,它有自己的本地库和CPU指令集。本地程序(Native Applications)使用C/C++这样的本地语言来编写,被编译成只能在本地系统环境下运行的二进制代码,并和本地库链接在一起。本地程序和本地库一般地会依赖于一个特定的本地系统环境。比如,一个系统下编译出来的C程序不能在另一个系统中运行。

1.2 JNI扮演的角色

JNI的强大特性使我们在使用JA V A平台的同时,还可以重用原来的本地代码。作为虚拟机实现的一部分,JNI允许JA V A和本地代码间的双向交互。

图1.1 JNI的角色

JNI可以这样与本地程序进行交互:

1、你可以使用JNI来实现“本地方法”(native methods),并在JA V A程序中调用它们。

2、JNI支持一个“调用接口”(invocation interface),它允许你把一个JVM嵌入到本地程序

中。本地程序可以链接一个实现了JVM的本地库,然后使用“调用接口”执行JA V A语言编写的软件模块。例如,一个用C语言写的浏览器可以在一个嵌入式JVM上面执行从网上下载下来的applets

1.3 JNI的副作用

请记住,一旦使用JNI,JA V A程序就丧失了JA V A平台的两个优点:

1、程序不再跨平台。要想跨平台,必须在不同的系统环境下重新编译本地语言部分。

2、程序不再是绝对安全的,本地代码的不当使用可能导致整个程序崩溃。

一个通用规则是,你应该让本地方法集中在少数几个类当中。这样就降低了JA V A和C之间的耦合性。

1.4 什么场合下应该使用JNI

当你开始着手准备一个使用JNI的项目时,请确认是否还有替代方案。像上一节所提到的,应用程序使用JNI会带来一些副作用。下面给出几个方案,可以避免使用JNI的时候,达到与本地代码进行交互的效果:

1、JA V A程序和本地程序使用TCP/IP或者IPC进行交互。

2、当用JA V A程序连接本地数据库时,使用JDBC提供的API。

3、JA V A程序可以使用分布式对象技术,如JA V A IDL API。

这些方案的共同点是,JA V A和C 处于不同的线程,或者不同的机器上。这样,当本地程序崩溃时,不会影响到JA V A程序。

下面这些场合中,同一进程内JNI的使用无法避免:

1、程序当中用到了JA V A API不提供的特殊系统环境才会有的特征。而跨进程操作又不

现实。

2、你可能想访问一些己有的本地库,但又不想付出跨进程调用时的代价,如效率,内存,

数据传递方面。

3、JA V A程序当中的一部分代码对效率要求非常高,如算法计算,图形渲染等。

总之,只有当你必须在同一进程中调用本地代码时,再使用JNI。

1.5 JNI的演化

JDK1.0包含了一个本地方法接口,它允许JA V A程序调用C/C++写的程序。许多第三方的程序和JA V A类库,如:https://www.sodocs.net/doc/642026796.html,ng,java.io,https://www.sodocs.net/doc/642026796.html,等都依赖于本地方法来访问底层系统环境的特征。

不幸的是,JDK1.0中的本地方法有两个主要问题:

1、本地方法像访问C中的结构(structures)一样访问对象中的字段。尽管如此,JVM规范

并没有定义对象怎么样在内存中实现。如果一个给定的JVM实现在布局对象时,和本地方法假设的不一样,那你就不得不重新编写本地方法库。

2、因为本地方法可以保持对JVM中对象的直接指针,所以,JDK1.0中的本地方法采用了

一种保守的GC策略。

JNI的诞生就是为了解决这两个问题,它可以被所有平台下的JVM支持:

1、每一个VM实现方案可以支持大量的本地代码。

2、开发工具作者不必处理不同的本地方法接口。

3、最重要的是,本地代码可以运行在不同的JVM上面。

JDK1.1中第一次支持JNI,但是,JDK1.1仍在使用老风格的本地代码来实现JA V A的API。这种情况在JDK1.2下被彻底改变成符合标准的写法。

1.6 例子程序

本书包含了大量的代码示例,还教我们如何使用javah来构建JNI程序。

第一章开始。。。

本章通过一个简单的例子来示例如何使用JNI。我们写一个JA V A程序,并用它调用一个C

函数来打印“Hello World!”。

2.1 概述

图2.1演示了如何使用JA V A程序调用C函数来打印“Hello World!”。这个过程包含下面几步:

1、创建一个类(HelloWorld.java)声明本地方法。

2、使用javac编译源文件HollowWorld.java,产生HelloWorld.class。使用javah –jni来生成

C头文件(HelloWorld.h),这个头文件里面包含了本地方法的函数原型。

3、用C代码写函数原型的实现。

4、把C函数实现编译成一个本地库,创建Hello-World.dll或者libHello-World.so。

5、使用java命令运行HelloWorld程序,类文件HelloWorld.class和本地库(HelloWorld.dll

或者libHelloWorld.so)在运行时被加载。

图2.1 编写并运行“HelloWorld”程序

本章剩余部分会详细解释这几步。

第二章基本类型、字符串、数组

开发者使用JNI时最常问到的是JA V A和C/C++之间如何传递数据,以及数据类型之间如何互相映射。本章我们从整数等基本类型和数组、字符串等普通的对象类型开始讲述。至于如何传递任意对象,我们将在下一章中进行讲述。

3.1 一个简单的本地方法

JA V A端源代码如下:

class Prompt {

// native method that prints a prompt and reads a line

private native String getLine(String prompt);

public static void main(String args[]) {

Prompt p = new Prompt();

String input = p.getLine("Type a line: ");

System.out.println("User typed: " + input);

}

static {

System.loadLibrary("Prompt");

}

}

3.1.1 本地方法的C函数原型

Prompt.getLine方法可以用下面这个C函数来实现:

JNIEXPORT jstring JNICALL

Java_Prompt_getLine(JNIEnv *env, jobject this, jstring prompt);

其中,JNIEXPORT和JNICALL这两个宏(被定义在jni.h)确保这个函数在本地库外可见,并且C编译器会进行正确的调用转换。C函数的名字构成有些讲究,在11.3中会有一个详细的解释。

3.1.2 本地方法参数

第一个参数JNIEnv接口指针,指向一个个函数表,函数表中的每一个入口指向一个JNI函数。本地方法经常通过这些函数来访问JVM中的数据结构。图3.1演示了JNIEnv这个指针:

图3.1 JNIEnv接口指针

第二个参数根据本地方法是一个静态方法还是实例方法而有所不同。本地方法是一个静态方法时,第二个参数代表本地方法所在的类;本地方法是一个实例方法时,第二个参数代表本

地方法所在的对象。我们的例子当中,Java_Prompt_getLine是一个实例方法,因此jobject 参数指向方法所在的对象。

3.1.3 类型映射

本地方法声明中的参数类型在本地语言中都有对应的类型。JNI定义了一个C/C++类型的集合,集合中每一个类型对应于JA V A中的每一个类型。

JA V A中有两种类型:基本数据类型(int,float,char等)和引用类型(类,对象,数组等)。JNI对基本类型和引用类型的处理是不同的。基本类型的映射是一对一的。例如JA V A中的int类型直接对应C/C++中的jint(定义在jni.h中的一个有符号32位整数)。12.1.1包含了JNI中所有基本类型的定义。

JNI把JA V A中的对象当作一个C指针传递到本地方法中,这个指针指向JVM中的内部数据结构,而内部数据结构在内存中的存储方式是不可见的。本地代码必须通过在JNIEnv中选择适当的JNI函数来操作JVM中的对象。例如,对于https://www.sodocs.net/doc/642026796.html,ng.String对应的JNI类型是jstring,但本地代码只能通过GetStringUTFChars这样的JNI函数来访问字符串的内容。

所有的JNI引用都是jobject类型,对了使用方便和类型安全,JNI定义了一个引用类型集合,集合当中的所有类型都是jobject的子类型。这些子类型和JA V A中常用的引用类型相对应。例如,jstring表示字符串,jobjectArray表示对象数组。

3.2 访问字符串

Java_Prompt_getLine接收一个jstring类型的参数prompt,jstring类型指向JVM内部的一个字符串,和常规的C字符串类型char*不同。你不能把jstring当作一个普通的C字符串。

3.2.1 转换为本地字符串

本地代码中,必须使用合适的JNI函数把jstring转化为C/C++字符串。JNI支持字符串在Unicode和UTF-8两种编码之间转换。Unicode字符串代表了16-bit的字符集合。UTF-8字符串使用一种向上兼容7-bit ASCII字符串的编码协议。UTF-8字符串很像NULL结尾的C 字符串,在包含非ASCII字符的时候依然如此。所有的7-bitASCII字符的值都在1~127之间,这些值在UTF-8编码中保持原样。一个字节如果最高位被设置了,意味着这是一个多字节字符(16-bitUnicode值)。

函数Java_Prompt_getLine通过调用JNI函数GetStringUTFChars来读取字符串的内容。GetStringUTFChars可以把一个jstring指针(指向JVM内部的Unicode字符序列)转化成一个UTF-8格式的C字符串。如何你确信原始字符串数据只包含7-bit ASCII字符,你可以把转化后的字符串传递给常规的C库函数使用,如printf。我们会在8.2中讨论如何处理非ASCII 字符串。

JNIEXPORT jstring JNICALL

Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)

{

char buf[128];

const jbyte *str;

str = (*env)->GetStringUTFChars(env, prompt, NULL);

if (str == NULL) {

return NULL; /* OutOfMemoryError already thrown */

}

printf("%s", str);

(*env)->ReleaseStringUTFChars(env, prompt, str);

/* We assume here that the user does not type more than

* 127 characters */

scanf("%s", buf);

return

不要忘记检查GetStringUTFChars。因为JVM需要为新诞生的UTF-8字符串分配内存,这个操作有可能因为内存太少而失败。失败时,GetStringUTFChars会返回NULL,并抛出一个OutOfMemoryError异常(对异常的处理在第6章)。这些JNI抛出的异常与JAVA中的异常是不同的。一个由JNI抛出的未决的异常不会改变程序执行流,因此,我们需要一个显示的return语句来跳过C函数中的剩余语句。Java_Prompt_getLine函数返回后,异常会在Prompt.main (Prompt.getLine这个发生异常的函数的调用者)中抛出,

3.2.2 释放本地字符串资源

从GetStringUTFChars中获取的UTF-8字符串在本地代码中使用完毕后,要使用ReleaseStringUTFChars告诉JVM这个UTF-8字符串不会被使用了,因为这个UTF-8字符串占用的内存会被回收。

3.2.3 构造新的字符串

你可以通过JNI函数NewStringUTF在本地方法中创建一个新的https://www.sodocs.net/doc/642026796.html,ng.String字符串对象。这个新创建的字符串对象拥有一个与给定的UTF-8编码的C类型字符串内容相同的Unicode编码字符串。

如果一个VM不能为构造https://www.sodocs.net/doc/642026796.html,ng.String分配足够的内存,NewStringUTF会抛出一个OutOfMemoryError异常,并返回一个NULL。在这个例子中,我们不必检查它的返回值,因为本地方法会立即返回。如果NewStringUTF失败,OutOfMemoryError这个异常会被在Prompt.main(本地方法的调用者)中抛出。如果NeweStringUTF成功,它会返回一个JNI引用,这个引用指向新创建的https://www.sodocs.net/doc/642026796.html,ng.String对象。这个对象被Prompt.getLine返回然后被赋值给Prompt.main中的本地input。

3.2.4 其它JNI字符串处理函数

JNI支持许多操作字符串的函数,这里做个大致介绍。

GetStringChars和ReleaseStringChars获取以Unicode格式编码的字符串。当操作系统支持Unicode编码的字符串时,这些方法很有用。

UTF-8字符串以’\0’结尾,而Unicode字符串不是。如果jstring指向一个Unicode编码的字符串,为了得到这个字符串的长度,可以调用GetStringLength。如果一个jstring指向一个UTF-8编码的字符串,为了得到这个字符串的字节长度,可以调用标准C函数strlen。或者直接对jstring调用JNI函数GetStringUTFLength,而不用管jstring指向的字符串的编码格式。

GetStringChars和GetStringUTFChars函数中的第三个参数需要更进一步的解释:

const jchar *

GetStringChars(JNIEnv *env, jstring str, jboolean *isCopy);

当从JNI函数GetStringChars中返回得到字符串B时,如果B是原始字符串https://www.sodocs.net/doc/642026796.html,ng.String的拷贝,则isCopy被赋值为JNI_TRUE。如果B和原始字符串指向的是JVM中的同一份数据,则isCopy被赋值为JNI_FALSE。当isCopy值为JNI_FALSE时,本地代码决不能修改字符串的内容,否则JVM中的原始字符串也会被修改,这会打破JAVA语言中字符串不可变的规则。

通常,因为你不必关心JVM是否会返回原始字符串的拷贝,你只需要为isCopy 传递NULL作为参数。

JVM是否会通过拷贝原始Unicode字符串来生成UTF-8字符串是不可以预测的,程序员最好假设它会进行拷贝,而这个操作是花费时间和内存的。一个典型的JVM会在heap上为对象分配内存。一旦一个JAVA字符串对象的指针被传递给本地代码,GC就不会再碰这个字符串。换言之,这种情况下,JVM必须pin这个对象。可是,大量地pin一个对象是会产生内存碎片的,因为,虚拟机会随意性地来选择是复制还是直接传递指针。

当你不再使用一个从GetStringChars得到的字符串时,不管JVM内部是采用复制还是直接传递指针的方式,都不要忘记调用ReleaseStringChars。根据方法GetStringChars是复制还是直接返回指针,ReleaseStringChars会释放复制对象时所占的内存,或者unpin这个对象。

3.2.5 JDK1.2中关于字符串的新JNI函数

为了提高JVM返回字符串直接指针的可能性,JDK1.2中引入了一对新函数,Get/ReleaseStringCritical。表面上,它们和Get/ReleaseStringChars函数差不多,但实际上这两个函数在使用有很大的限制。

使用这两个函数时,你必须两个函数中间的代码是运行在"critical region"(临界区)的,即,这两个函数中间的本地代码不能调用任何会让线程阻塞或等待JVM中的其它线程的本地函数或JNI函数。

有了这些限制,JVM就可以在本地方法持有一个从GetStringCritical得到的字符串的直接指针时禁止GC。当GC被禁止时,任何线程如果触发GC的话,都会被阻塞。而Get/ReleaseStringCritical这两个函数中间的任何本地代码都不可以执行会导致阻塞的调用或者为新对象在JVM中分配内存。否则,JVM有可能死锁,想象一下这样的场景中:

1、只有当前线程触发的GC完成阻塞并释放GC时,由其它线程触发的GC才可能由阻塞

中释放出来继续运行。

2、在这个过程中,当前线程会一直阻塞。因为任何阻塞性调用都需要获取一个正被其它线

程持有的锁,而其它线程正等待GC。

Get/ReleaseStringCritical的交迭调用是安全的,这种情况下,它们的使用必须有严格的顺序限制。而且,我们一定要记住检查是否因为内存溢出而导致它的返回值是NULL。因为JVM在执行GetStringCritical这个函数时,仍有发生数据复制的可能性,尤其是当JVM内部存储的数组不连续时,为了返回一个指向连续内存空间的指针,JVM必须复制所有数据。

总之,为了避免死锁,在Get/ReleaseStringCritical之间不要调用任何JNI

函数。Get/ReleaseStringCritical和Get/ReleasePrimitiveArrayCritical这两个函数是可以的。

下面代码演示了这对函数的正确用法:

jchar *s1, *s2;

s1 = (*env)->GetStringCritical(env, jstr1);

if (s1 == NULL) {

... /* error handling */

}

s2 = (*env)->GetStringCritical(env, jstr2);

if (s2 == NULL) {

(*env)->ReleaseStringCritical(env, jstr1, s1);

... /* error handling */

}

... /* use s1 and s2 */

(*env)->ReleaseStringCritical(env, jstr1, s1);

(*env)->ReleaseStringCritical(env, jstr2, s2);

JNI不支持Get/ReleaseStringUTFCritical,因为这样的函数在进行编码转换时很可能会促使JVM对数据进行复制,因为JVM内部表示字符串一般都是使用Unicode的。

JDK1.2还一对新增的函数:GetStringRegion和GetStringUTFRegion。这对函数把字符串复制到一个预先分配的缓冲区内。Prompt.getLine这个本地方法可以用GetStringUTFRegion重新实现如下:

JNIEXPORT jstring JNICALL

Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)

{

/* assume the prompt string and user input has less than 128

characters */

char outbuf[128], inbuf[128];

int len = (*env)->GetStringLength(env, prompt);

(*env)->GetStringUTFRegion(env, prompt, 0, len, outbuf);

printf("%s", outbuf);

scanf("%s", inbuf);

return (*env)->NewStringUTF(env, inbuf);

}

GetStringUTFRegion这个函数会做越界检查,如果必要的话,会抛出异常StringIndexOutOfBoundsException。这个方法与GetStringUTFChars比较相似,不同的是,GetStringUTFRegion不做任何内存分配,不会抛出内存溢出异常。

3.2.6 JNI字符串操作函数总结

对于小字符串来说,Get/SetStringRegion和Get/SetString-UTFRegion这两对函数是最佳选择,因为缓冲区可以被编译器提前分配,而且永远不会产生内存溢出的异常。当你需要处理一个字符串的一部分时,使用这对函数也是不错的,因为它们提供了一个开始索引和子字符串的长度值。另外,复制少量字符串的消耗是非常小的。

在使用GetStringCritical时,必须非常小心。你必须确保在持有一个由GetStringCritical获取到的指针时,本地代码不会在JVM内部分配新对象,或者做任何其它可能导致系统死锁的阻塞性调用。

下面的例子演示了使用GetStringCritical时需要注意的一些地方:

/* This is not safe! */

const char *c_str = (*env)->GetStringCritical(env, j_str, 0);

if (c_str == NULL) {

... /* error handling */

}

fprintf(fd, "%s\n", c_str);

(*env)->ReleaseStringCritical(env, j_str, c_str);

上面代码的问题在于,GC被当前线程禁止的情况下,向一个文件写数据不一定安全。例如,另外一个线程T正在等待从文件fd中读取数据。假设操作系统的规则是fprintf会等待线程T完成所有对文件fd的数据读取操作,这种情况下就可能会产生死锁:线程T从文件fd中读取数据是需要缓冲区的,如果当前没有足够内存,线程T就会请求GC来回收一部分,GC一旦运行,就只能等到当前线程运行ReleaseStringCritical时才可以。而ReleaseStringCritical只有在fprintf调用返回时才会被调用。而fprintf这个调用,会一直等待线程T完成文件读取操作。

3.3 访问数组

JNI在处理基本类型数组和对象数组上面是不同的。对象数组里面是一些指向对象实例或者其它数组的引用。

本地代码中访问JVM中的数组和访问JVM中的字符串有些相似。看一个简单的例子。下面的程序调用了一个本地方法sumArray,这个方法对一个int数组里面的元素进行累加:class IntArray {

private native int sumArray(int[] arr);

public static void main(String[] args) {

IntArray p = new IntArray();

int arr[] = new int[10];

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

arr[i] = i;

}

int sum = p.sumArray(arr);

System.out.println("sum = " + sum);

}

static {

System.loadLibrary("IntArray");

}

}

3.3.1 在本地代码中访问数组

数组的引用类型是一般是jarray或者或者jarray的子类型jintArray。就像jstring不是一个C

字符串类型一样,jarray也不是一个C数组类型。所以,不要直接访问jarray。你必须使用合适的JNI函数来访问基本数组元素:

JNIEXPORT jint JNICALL

Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)

{

jint buf[10];

jint i, sum = 0;

(*env)->GetIntArrayRegion(env, arr, 0, 10, buf);

for (i = 0; i < 10; i++) {

sum += buf[i];

}

return sum;

}

3.3.2 访问基本类型数组

上一个例子中,使用GetIntArrayRegion函数来把一个int数组中的所有元素复制到一个C缓冲区中,然后我们在本地代码中通过C缓冲区来访问这些元素。

JNI支持一个与GetIntArrayRegion相对应的函数SetIntArrayRegion。这个函数允许本地代码修改所有的基本类型数组中的元素。

JNI支持一系列的Get/ReleaseArrayElement函数,这些函数允许本地代码获取一个指向基本类型数组的元素的指针。由于GC可能不支持pin操作,JVM可能会先对原始数据进行复制,然后返回指向这个缓冲区的指针。我们可以重写上面的本地方法实现:JNIEXPORT jint JNICALL

Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)

{

jint *carr;

jint i, sum = 0;

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

if (carr == NULL) {

return 0; /* exception occurred */

}

for (i=0; i<10; i++) {

sum += carr[i];

}

(*env)->ReleaseIntArrayElements(env, arr, carr, 0);

return sum;

}

GetArrayLength这个函数返回数组中元素的个数,这个值在数组被首次分配时确定下来。JDK1.2引入了一对函数:Get/ReleasePrimitiveArrayCritical。通过这对函数,可以在本地代码访问基本类型数组元素的时候禁止GC的运行。但程序员使用这对函数时,必须和使用Get/ReleaseStringCritical时一样的小心。在这对函数调用的中间,同样不能调用任何JNI函数,或者做其它可能会导致程序死锁的阻塞性操作。

相关主题