现在的位置: 首页 > 综合 > 正文

JNI编程详解

2018年05月01日 ⁄ 综合 ⁄ 共 13054字 ⁄ 字号 评论关闭

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();
}

解释:表明程序开始运行的时候会加载testJNI, static区声明的代码会先于onCreate方法执行。
这个名字和/jni/android.mk文件中定义的一样,编译生成的为libtestJNI.so。会自动加前缀lib
同样需要注意的是, 如果你的文件名字为libtestJNI的时候, 他生成的库文件不是liblibtestJNI.so 而一样是libtestJNI.so。


2、JNI层,定义与java native方法对应的JNI方法(这里先讲静态注册JNI方法)

通过java native方法,编译得到.h文件

用eclipse编译该工程(其实就是刷新下就工程,eclipse就会自动编译该工程了),生成相应的.class文件(java字节码),该文件在\TestJNI\bin\classes\com\example\testjni\NativeMethod.class,因为生成.h文件需要用到相应的.class文件。

生成.h文件:
1、cmd进入工程的根目录。
2、 javah -classpath bin/classes -d jni com.example.testjni.NativeMethod

解释一下命令

    -classpath bin/classes:表示类的路径

    -d jni: 表示生成的头文件存放的目录(这里指工程根目录下的jni,没有会自动创建)

   com.example.testjni.NativeMethod则是完整类名 


执行完命令后,工程根目录下自动就多了一个jni的文件夹,里面还有一个com_example_testjni_NativeMethod.h文件

.h文件代码如下

/* 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。


Android.mk详解介绍,参考我的另一篇文章,android.mk语法详解

5、编译工程生成apk了。并运行咯。

项目根目录下Android.mk文件:
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的原理,我在网上看到一篇blog,有简单操作的方法、



第二部分  动态注册JNI方法

(1)简单的示例
练习项目实现了一个简单的四则运算
java层
    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"


JNI层
#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++层

这里主要实现加减乘除运算。

写jni编译需要的jni/Android.mk文件,
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语法详解



(2)原理讲解

从如何载入.so 档案谈起        

 System.loadLibrary(*.so 的档案名);

JNI组件的入口函数——JNI_OnLoad()、JNI_OnUnload()

JNI组件被成功加载和卸载时,会进行函数回调,当VM执行到System.loadLibrary(xxx)函数时,首先会去执行JNI组件中的JNI_OnLoad()函数,而当VM释放该组件时会呼叫JNI_OnUnload()函数。先看示例代码:

  1. //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组件时会呼叫它,因此在该方法中进行善后清理,资源释放的动作最为合适。

java中声明的native方法   与    jni中的方法是怎么对应起来的呢??
这就涉及到JNI函数的注册:(两种)
静态方法:
例如第一部分中那样,java层是:stringFromJNI();   对应JNI层:Java_com_example_testjni_NativeMethod_stringFromJNI(****)
java层调用方法stringFromJNI()时,会从载入的so库中,查找Java_com_example_testjni_NativeMethod_stringFromJNI()函数
如果没有,报错。
如果找到,他们之间建立一种关联,其实就是记下Java_com_example_testjni_NativeMethod_stringFromJNI()函数的函数指针,下次调用stringFromJNI()时,通过函数指针就可以找到了。

动态注册:

使用registerNativeMethods方法

对Java程序员来说,可能我们总是会遵循:1.编写带有native方法的Java类;--->2.使用javah命令生成.h头文件;--->3.编写代码实现头文件中的方法,这样的“官方” 流程,但也许有人无法忍受那“丑陋”的方法名称,

但env->RegisterNatives(clazz, gMethods, numMethods),而无需遵循特定的方法命名格式。来看一段示例代码吧:

  1. //定义目标类名称
    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中,具体定义如下:

  1. typedef struct {
         const char* name;//java方法名称
         const char* signature; //java方法签名
         void* fnPtr;//c/c++的函数指针
    } JNINativeMethod

参照上文示例中初始化该结构的代码:

  1. //定义方法隐射关系
  2. static JNINativeMethod methods[] = {
  3. {   "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);

  1. if ((*env)->EnsureLocalCapacity(env, 2) < 0) {  
  2.         return 0; /* out of memory error */  
  3.     } 

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();

抱歉!评论已关闭.