JNI是一个双向的接口:开发者不仅可以通过JNI在Java代码中访问Native模块,还可以在 Native代码中嵌入一个JVM,并通过JNI访问运行于其中的Java模块。可见,JNI担任了一个桥梁的角色,它将JVM与Native模块联系起 来,从而实现了Java代码与Native代码的互访
第一部分:JNI开发流程
先下载我提过的文件,对jni有个整体的了解。
下载所需要的文件:http://pan.baidu.com/s/10a8Wm
文件夹中包括三个文件:
(1)ndk
(2)ndk使用说明
(3)eclipse+ndk的简单示例
概述
1、java层,定义java native方法
2、JNI层,定义与java native方法对应的JNI native方法(其实也是C/C++的代码)
(1)静态注册JNI方法。也就是通过包名+方法名来建立关联。
(2)动态注册JNI方法。建立一个方法表,来把他们关联起来。
3、C/C++层,在JNI的native方法中调用实现功能的C/C++方法。
4、在jni目录下创建一个Android.mk的文件,并编译JNI代码(最终为so动态库文件)
(1)NDK编译
(2)源码平台上编译
5、编译整个工程(最终为apk文件)
1.java层,定义java native方法
首先在eclipse中创建一个android工程,testJNI
package com.example.testjni;
import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
NativeMethod nm = new NativeMethod();//调用native方法就ok了</span>
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}
定义native方法
package com.example.testjni;
/**
* XXX.c文件怎么写方便呢??
* 答:在C/C++文件编写之前,需要利用javah这个工具生成相应的.h文件,然后根据这个.h文件编写相应的C/C++代码
* 1、cmd进入工程的根目录。
* 2、 javah -classpath bin/classes -d jni com.example.testjni.NativeMethod
* 3、编写c文件(h文件已经没有用了)
* 4、在jni文件夹中建立Android.mk文件</span>
*
* @author Administrator
*
*/
public class NativeMethod {
static {
System.loadLibrary("testJNI");
}
public native String stringFromJNI();
}
2、 javah -classpath bin/classes -d jni com.example.testjni.NativeMethod
-classpath bin/classes:表示类的路径
-d jni: 表示生成的头文件存放的目录(这里指工程根目录下的jni,没有会自动创建)
com.example.testjni.NativeMethod则是完整类名
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_testjni_NativeMethod */
#ifndef _Included_com_example_testjni_NativeMethod
#define _Included_com_example_testjni_NativeMethod
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_testjni_NativeMethod
* Method: stringFromJNI
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_testjni_NativeMethod_stringFromJNI
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
写JNI方法
#include <string.h>
#include <jni.h>/*必须要的*/
JNIEXPORT jstring JNICALL Java_com_example_testjni_NativeMethod_stringFromJNI(JNIEnv* env,
jobject thiz )/*注意拷贝过来的参*/
{
return (*env)->NewStringUTF(env, "Hello Test NDK !");
}
把.h文件的中的声明的方法考过来,并给参数添加上参数变量(如env 、thiz)
JNI方法中,这里只是一个简单的创建了一个字符串
java Native的对应函数名(也就是JNI的Native方法或函数)要以“Java_”开头,后面依次跟上Java的“package名”、“class名”、“函数名”,中间以下划线“_” 分割,在package名中的“.”也要改为“_”。
在JNI的Native函数中,其前两个参数JNIEnv *和jobject 是必需的——前者是一个JNIEnv 结构体的指针,这个结构体中定义了很多JNI的接口函数指针,使开发者可以使用JNI所定义的接口功能;后者指代的是调用这个JNI函数的Java对象,有点类似于C++中的this 指针。在上述两个参数之后,还需要根据Java端的函数声明依次对应添加参数。
怎么调试JNI的native方法呢?
请参考我的另一篇文章:JNI调试C/C++的log打印
3、C/C++层,在JNI的native方法中调用实现功能的C/C++方法。
JNI的native方法中,这里只是一个简单的创建了一个字符串
(*env)->NewStringUTF(env, "Hello Test NDK !");
4、在jni目录下创建一个Android.mk的文件,并编译JNI代码
编译——两种不同的编译环境
以上的C语言代码要编译成最终.so动态库文件,有两种途径:
(一)Android NDK :全称是Native Developer Kit,是用于编译本地JNI源码的工具,为开发人员将本地方法整合到Android应用中提供了方便。事实上NDK和完整源码编译环境一样,都使用Android的编译系统——即通过Android.mk文件控制编译。NDK可以运行在Linux、Mac、Window(+cygwin)三个平台上。有关NDK的使用方法及更多细节请参考以下资料:
我们现在讲的就是这种NDK方式,按照下载文件中ndk说明,把eclipse和ndk关联上,刷新工程,查看console打印窗口,ndk正在把c/c++编译为os文件。生成的位置:libs/armeabi/
(二)源码平台下编译 :Android平台提供有基于make的编译系统,为App编写正确的Android.mk文件就可使用该编译系统。该环境需要通过git从官方网站获取完整源码副本并成功编译,更多细节请参考:http://source.android.com/index.html
通过mm命令编译,生成so文件位置:在out/....../system/lib
不管你选择以上两种方法的哪一个,都必须编写自己的Android.mk文件。
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := testJNI
LOCAL_SRC_FILES := com_example_testjni_NativeMethod.c
include $(BUILD_SHARED_LIBRARY)
#注释:
#1、在NDK环境下编译,输出工程libs/armeabi/libtestJNI.so
#2、不使用NDK编译,放入Android源码平台external目录下编译也可以。输出:out/target/product/generic/system/lib/testJNI.so,然后push到设备system/lib下
我想还需要简单说明一下libxxx.so的命名规则,沿袭Linux传统,lib<something>.so是类库文件名称的格式,但在Java的System.loadLibrary(" something ")方法中指定库名称时,不能包括 前缀—— lib,以及后缀——.so。
5、编译工程生成apk了。并运行咯。
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_JNI_SHARED_LIBRARIES := libarithmetic
LOCAL_PACKAGE_NAME := LongTest
LOCAL_SHARED_LIBRARIES := \
libutils\
liblog
include $(BUILD_PACKAGE)
include $(LOCAL_PATH)/jni/Android.mk
# Also build all of the sub-targets under this one: the shared library.
include $(call all-makefiles-under,$(LOCAL_PATH))
LOCAL_JNI_SHARED_LIBRARIES := libxxx就是把so文件放到apk文件里的libs/armeabi里执行BUILD_PACKAGE
$(call all-makefiles-under, $(LOCAL_PATH))编译器会在编译完当前目录下的文件后再深入子目录编译
第二部分 动态注册JNI方法
public native int add(int x, int y);
public native int substraction(int x, int y);
public native float multiplication(int x, int y);
public native float division(int x, int y);
static{
System.loadLibrary("arithmetic");
}
生成lib的名称为libarithmetic.so.注意load的时候写"arithmetic"
#define LOG_TAG "LongTest2 long.cpp"
#include <utils/Log.h>
#include <stdio.h>
#include "jni.h"
jint add(JNIEnv *env, jobject thiz, jint x, jint y){
return x + y;
}
jint substraction(JNIEnv *env, jobject thiz, jint x, jint y){
return x - y;
}
jfloat multiplication(JNIEnv *env, jobject thiz, jint x, jint y){
return (float)x * (float)y;
}
jfloat division(JNIEnv *env, jobject thiz, jint x, jint y){
return (float)x/(float)y;
}
static const char *classPathName = "com/inspur/test2/MainActivity";
static JNINativeMethod methods[]= {
{"add", "(II)I", (void*)add},
{"substraction", "(II)I", (void*)substraction},
{"multiplication", "(II)F", (void*)multiplication},
{"division", "(II)F", (void*)division},
};
typedef union{
JNIEnv* env;
void* venv;
}UnionJNIEnvToVoid;
static int registerNativeMethods(JNIEnv* env, const char* className,
JNINativeMethod* gMethods, int numMethods){
jclass clazz;
clazz = env->FindClass(className);
if (clazz == NULL)
return JNI_FALSE;
if (env->RegisterNatives(clazz, gMethods, numMethods)<0)
return JNI_FALSE;
return JNI_TRUE;
}
static int registerNatives(JNIEnv *env){
if (!registerNativeMethods(env, classPathName,
methods, sizeof(methods)/sizeof(methods[0])))
{
return JNI_FALSE;
}
return JNI_TRUE;
}
jint JNI_OnLoad(JavaVM* vm, void* reserved){
UnionJNIEnvToVoid uenv;
uenv.venv = NULL;
jint result = -1;
JNIEnv *env = NULL;
if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_4) != JNI_OK){
goto bail;
}
env = uenv.env;
env = uenv.env;
if (registerNatives(env) != JNI_TRUE){
goto bail;
}
result = JNI_VERSION_1_4;
bail:
return result;
}
c/c++层
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE:= libarithmetic
LOCAL_SRC_FILES:= \
long.cpp
LOCAL_SHARED_LIBRARIES := \
libutils
LOCAL_STATIC_LIBRARIES :=
LOCAL_C_INCLUDES += \
$(JNI_H_INCLUDE)
LOCAL_CFLAGS +=
LOCAL_PRELINK_MODULE := false
include $(BUILD_SHARED_LIBRARY)
Android.mk解释,,见 android.mk语法详解
从如何载入.so 档案谈起
System.loadLibrary(*.so 的档案名);
JNI组件的入口函数——JNI_OnLoad()、JNI_OnUnload()
JNI组件被成功加载和卸载时,会进行函数回调,当VM执行到System.loadLibrary(xxx)函数时,首先会去执行JNI组件中的JNI_OnLoad()函数,而当VM释放该组件时会呼叫JNI_OnUnload()函数。先看示例代码:
-
//onLoad方法,在System.loadLibrary()执行时被调用 jint JNI_OnLoad(JavaVM* vm, void* reserved){ LOGI("JNI_OnLoad startup~~!"); return JNI_VERSION_1_4; } //onUnLoad方法,在JNI组件被释放时调用 void JNI_OnUnload(JavaVM* vm, void* reserved){ LOGE("call JNI_OnUnload ~~!!"); }
JNI_OnLoad()有两个重要的作用:
指定JNI版本:告诉VM该组件使用那一个JNI版本(若未提供JNI_OnLoad()函数,VM会默认该使用最老的JNI 1.1版),如果要使用新版本的JNI,例如JNI 1.4版,则必须由JNI_OnLoad()函数返回常量JNI_VERSION_1_4(该常量定义在jni.h中) 来告知VM。
初始化设定,当VM执行到System.loadLibrary()函数时,会立即先呼叫JNI_OnLoad()方法,因此在该方法中进行各种资源的初始化操作最为恰当。
JNI_OnUnload()的作用与JNI_OnLoad()对应,当VM释放JNI组件时会呼叫它,因此在该方法中进行善后清理,资源释放的动作最为合适。
使用registerNativeMethods方法
对Java程序员来说,可能我们总是会遵循:1.编写带有native方法的Java类;--->2.使用javah命令生成.h头文件;--->3.编写代码实现头文件中的方法,这样的“官方” 流程,但也许有人无法忍受那“丑陋”的方法名称,
但env->RegisterNatives(clazz, gMethods, numMethods),而无需遵循特定的方法命名格式。来看一段示例代码吧:
-
//定义目标类名称 static const char *className = "com/okwap/testjni/MyJNI"; //定义方法隐射关系 static JNINativeMethod methods[] = { {"sayHello", "(Ljava/lang/String;)Ljava/lang/String;", (void*)sayHello}, }; jint JNI_OnLoad(JavaVM* vm, void* reserved){ //声明变量 jint result = JNI_ERR; JNIEnv* env = NULL; jclass clazz; int methodsLenght; //获取JNI环境对象 if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) { LOGE("ERROR: GetEnv failed\n"); return JNI_ERR; } assert(env != NULL); //注册本地方法.Load 目标类 clazz = (*env)->FindClass(env,className); if (clazz == NULL) { LOGE("Native registration unable to find class '%s'", className); return JNI_ERR; } //建立方法隐射关系 //取得方法长度 methodsLenght = sizeof(methods) / sizeof(methods[0]); if ((*env)->RegisterNatives(env,clazz, methods, methodsLenght) < 0) { LOGE("RegisterNatives failed for '%s'", className); return JNI_ERR; } result = JNI_VERSION_1_4; return result; }
由于 VM 通常是多执行绪(Multi-threading)的执行环境。每一个执行绪在呼叫
JNI_OnLoad()时,所传递进来的 JNIEnv 指标值都是不同的。为了配合这种多执行绪的环境,C
组件开发者在撰写本地函数时,可藉由 JNIEnv 指标值之不同而避免执行绪的资料冲突问题,才
能确保所写的本地函数能安全地在 Android 的多执行绪 VM 里安全地执行。
建立c/c++方法和Java方法之间映射关系的关键是 JNINativeMethod 结构,该结构定义在jni.h中,具体定义如下:
-
typedef struct { const char* name;//java方法名称 const char* signature; //java方法签名 void* fnPtr;//c/c++的函数指针 } JNINativeMethod
参照上文示例中初始化该结构的代码:
- //定义方法隐射关系
- static JNINativeMethod methods[] = {
- { "sayHello", "(Ljava/lang/String;)Ljava/lang/String;", (void*)sayHello}, };
其中比较难以理解的是第二个参数——signature字段的取值,实际上这些字符与函数的参数类型/返回类型一一对应,其中"()" 中的字符表示参数,后面的则代表返回值。例如"()V" 就表示void func(),"(II)V" 表示 void func(int, int),具体的每一个字符的对应关系如下:
字符 Java类型 C/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
数组则以"["开始,用两个字符表示:
字符 java类型 c/c++类型
[Z jbooleanArray boolean[]
[I jintArray int[]
[F jfloatArray float[]
[B jbyteArray byte[]
[C jcharArray char[]
[S jshortArray short[]
[D jdoubleArray double[]
[J jlongArray long[]
上面的都是基本类型,如果参数是Java类,则以"L"开头,以";"结尾,中间是用"/"隔开包及类名,而其对应的C函数的参数则为jobject,一个例外是String类,它对应C类型jstring,例如:Ljava/lang /String; 、Ljava/net/Socket; 等,如果JAVA函数位于一个嵌入类(也被称为内部类),则用$作为类名间的分隔符,例如:"Landroid/os/FileUtils$FileStatus;"。
使用registerNativeMethods方法不仅仅是为了改变那丑陋的长方法名,最重要的是可以提高效率,因为当Java类别透过VM呼叫到本地函数时,通常是依靠VM去动态寻找.so中的本地函数(因此它们才需要特定规则的命名格式),如果某方法需要连续呼叫很多次,则每次都要寻找一遍,所以使用RegisterNatives将本地函数向VM进行登记,可以让其更有效率的找到函数。
registerNativeMethods方法的另一个重要用途是,运行时动态调整本地函数与Java函数值之间的映射关系,只需要多次调用registerNativeMethods()方法,并传入不同的映射表参数即可。
第三部分 JNI编程
java 、JNI、 C/C++类型对比
1、void
java的void与JNI的void是一致的。
2、基本数据类型
3、对象类型
JNI函数
JNI支持Unicode/UTF-8字符编码互转。Unicode以16-bits值编码;UTF-8是一种以字节为单位变长格式的字符编码,并与7-bitsASCII码兼容。UTF-8字串与C字串一样,以NULL('\0')做结束符, 当UTF-8包含非ASCII码字符时,以'\0'做结束符的规则不变。7-bit ASCII字符的取值范围在1-127之间,这些字符的值域与UTF-8中相同。当最高位被设置时,表示多字节编码。
使用JNI时,最常见的操作是将jstring转换成UTF字符串。JNI提供了几个转换函数:GetStringUTFChars, GetStringUTFRegion。
GetStringUTFChars返回一个指向UTF字符串的指针,该函数会分配内存空间存储该字符串,因此使用完后一定要记得调用对应的释放函数ReleaseStringUTFChars释放分配的空间。
GetStringUTFRegion将UTF字符串存储到预分配的内存空间。相比GetStringUTFChars,它没有重新分配内存空间,因此也无需释放。
开发时,根据需要选择适当的函数。
Android源代码大量使用GetStringUTFChars和ReleaseStringUTFChars。仅在少数几处使用了GetStringUTFRegion。
创建一个jstring对象使用:
1
|
jstring str = (*env)->NewStringUTF(env, your_utf_string);
|
C 和 C++ 函数实现的比较
唯一的差异在于用来访问 JNI 函数的方法。在 C 中,JNI 函数调用由“(*env)->”作前缀,目的是为了取出函数指针所引用的值。在 C++ 中,JNIEnv 类拥有处理函数指针查找的内联成员函数。下面将说明这个细微的差异,其中,这两行代码访问同一函数,但每种语言都有各自的语法。
C 语法:jsize len = (*env)->GetArrayLength(env,array);
C++ 语法:jsize len =env->GetArrayLength(array);
- if ((*env)->EnsureLocalCapacity(env, 2) < 0) {
- return 0; /* out of memory error */
- }
CallObjectMethod
在C++和Java的编程中,异常处理都是一个重要的内容。但是在JNI中,麻烦就来了,native方法是通过C++实现的,如果在native方法中发生了异常,如何传导到Java呢?
JNI提供了实现这种功能的机制。我们可以通过下面这段代码抛出一个Java可以接收的异常,
jclass errCls;
env->ExceptionDescribe();
env->ExceptionClear();
errCls = env->FindClass("java/lang/IllegalArgumentException
env->ThrowNew(errCls, "thrown from C++ code");
如果要抛出其他类型的异常,替换掉FindClass的参数即可。这样,在Java中就可以接收到native方法中抛出的异常。
下面是专用的JNI函数,可以对异常进行处理。
Throw():丢弃一个现有的异常对象;在固有方法中用于重新丢弃一个异常。
ThrowNew():生成一个新的异常对象,并将其丢弃。
ExceptionOccurred():判断一个异常是否已被丢弃,但尚未清除。
ExceptionDescribe():打印一个异常和堆栈跟踪信息。
ExceptionClear():清除一个待决的异常。
FatalError():造成一个严重错误,不返回。
在所有这些函数中,最不能忽视的就是ExceptionOccurred()和ExceptionClear()。大多数JNI函数都能产生异常,而且没有象在Java的try块内的那种语言特性可供利用。所以在每一次JNI函数调用之后,都必须调用ExceptionOccurred(),了解异常是否已被丢弃。若侦测到一个异常,可选择对其加以控制(可能时还要重新丢弃它)。然而,必须确保异常最终被清除。这可以在自己的函数中用ExceptionClear()来实现;若异常被重新丢弃,也可能在其他某些函数中进行。
举个最简单的例子,如果调用Java中的方法后出现异常,忽略。
jobject objectAttr = (*env)->CallObjectMethod(env, objectDocument, createAttributeMid, stoJstring(env, "ABC"));
// deal with exception
jthrowable exc = (*env)->ExceptionOccurred(env);
if(exc) {
(*env)->ExceptionClear(env);
doSomething();
}