(4.1.27)JNI

  • 一、概览
    • 1.1 JNI和NDK的区别
    • 1.2 JNI的过程
    • 1.3 JNI的好处
    • 1.4 .so文件是什么?
  • 二、开发流程
    • 第一步、编写声明了native方法的.java类
    • 第二步、生成.class字节码文件
    • 第三步、用javah -jni命令,根据class字节码文件生成.h头文件(-jni参数是可选的)
    • 第四步、用本地代码实现.h头文件中的函数
    • 第五步、将C/C++代码编译成本地动态库文件
      • 2.5.1 基于 Android.mk文件的ndk命令方式
        • 2.5.1.1 Android.mk
      • 2.5.2 基于jdk的单文件打包
      • 2.5.3 配置grable自动打包
    • 第六步、动态库放在指定位置
    • 第七步、动态库加载和调用
  • 三、加载动态库.so
    • 3.1 静态加载
      • 3.1.1 基于动态库名称的静态加载方式
    • 3.2 动态加载
  • 四、查找动态库中指定对应函数
    • 4.1 Native函数标准映射规则
    • 4.2 本地RegisterNatives函数手动注册
  • 五、JNI数据类型及与Java数据类型的映射关系
    • 5.1 JVM函数表操作手柄JNIEnv
      • 5.1.1 线程相关性
    • 5.2 JNI基本类型
    • 5.3 JNI引用类型
      • 5.3.1 jvalue类型
    • 5.4 JNI描述符
      • 5.4.1 类描述符
      • 5.4.2 域描述符
      • 5.4.3 方法描述符
    • 5.5 注意
  • 六、JNI操控Java数据的方式
    • 6.1 对基本类型的操作
    • 6.2 对Object类型的操作
      • 6.2.1 字符串操作示例
  • 七、JNI函数
    • 7.1 *.so的入口函数JNI_OnLoad()与JNI_OnUnload()
    • 7.2 返回值
    • 7.3 操作Java层的类
    • 7.4 回调Java层方法
    • 7.5 传对象到JNI调用
    • 7.6 与C++互转
    • 7.7 C/C++访问Java实例方法和静态方法
    • 7.8 C/C++访问Java实例变量和静态变量
    • 7.9 调用构造方法和父类实例方法
    • 7.10 JNI局部引用、全局引用和弱全局引用
    • 7.11 JNI异常处理
    • 7.12 JNI调用性能测试及优化
    • 7.13 JNI操作函数一览
    • 7.14 注意
      • 7.14.1 UTF-8编码
      • 7.14.2 错误
  • 参考文献

一、概览

1.1 JNI和NDK的区别

  • JNI

JNI是java语言提供的Java和C/C++相互沟通的机制,Java可以通过JNI调用本地的C/C++代码,本地的C/C++的代码也可以调用java代码。

JNI 是本地编程接口,Java和C/C++互相通过的接口,Java通过C/C++使用本地的代码的一个关键性原因在于C/C++代码的高效性。

  • NDK

NDK是一系列工具的集合。

它提供了一系列的工具,帮助开发者快速开发C(或C++)的动态库,并能自动将so和java应用一起打包成apk。

这些工具对开发者的帮助是巨大的。它集成了交叉编译器,并提供了相应的mk文件隔离CPU、平台、ABI等差异,开发人员只需要简单修改mk文件(指出“哪些文件需要编译”、“编译特性要求”等),就可以创建出so。

它可以自动地将so和Java应用一起打包,极大地减轻了开发人员的打包工作。

AndroidStudio甚至不需要配置mk文件,它将根据gradle中的相关配置,自动生成mk文件。也就说将配置过程移动到了gradle中集中管理
当然,如果一定要用mk手动处理,AS也是支持的

  • 总结

JNI是Java调用Native机制,是Java语言自己的特性全称为Java Native Interface,类似的还有微软.Net Framework上的p/invoke,可以让C#或Visual Basic.Net可以调用C/C++的API,所以说JNI和Android没有关系,在PC上开发Java的应用,如果运行在Windows平台使用JNI是是经常的,比如说读写Windows的注册表

NDK是Google公司推出的帮助Android开发者通过C/C++本地语言编写应用的开发包,包含了C/C++的头文件、库文件

1.2 JNI的过程

  • Java通过JNI机制和C/C++沟通的具体步骤

    1. 编写包含native本地方法的java类
    2. 将Java源代码编译成.class字节码文件[可选]
    3. 通过javah工具生成C/C++语言的头文件
    4. 使用C/C++语言实现头文件
    5. 使用交叉编译工具对C/C++本地代码进行编译,最后通过链接生成*.so可执行的C/C++库
    6. 加载动态库,并调用native函数,实际执行Java代码去和本地的C/C++代码互相沟通
  • 在Android的NDK中,Java、C/C++、Dalvik VM关系如下:

    • java的dex字节码和C/C++的*.so同时运行DalvikVM之内,共同使用一个进程空间。每次使用jni调用c/c++开辟一个线程去处理
    • java和C/C++可以相互调用,调用的关键是DalvikVM
    • 一般而言,比较经典的模式是Java通过JNI和C/C++组件相互沟通,一般业务处理放在C/C++中
    • C++代码处于核心控制地位更具价值

1.3 JNI的好处

  • 高效性

C作为底层代码,在某些底层处理上,可能比Java更具备效率

  • 站在巨人的肩膀上

由于JNI是JVM规范中的一部份,因此可以将我们写的JNI程序在任何实现了JNI规范的Java虚拟机中运行。

C语言体系已经有丰富的基础资源,利用jni使我们可以复用以前用C/C++写的大量代码。

  • 应用的安全性

为了应用的安全性,会将一些复杂的逻辑和算法通过本地代码(C或C++)来实现,然后打包成so动态库文件,并提供Java接口供应用层调用,这么做的目的主要就是为了提供应用的安全性,防止被反编译后被不法分子分析应用的逻辑。

然打包成so也不能说完全安全了,只是相对反编译Java的class字节码文件来说,反汇编so动态库来分析程序的逻辑要复杂得多,没那么容易被破解。比如百度开放平台提供的定位服务、搜索服务、LBS服务、推送服务的Android SDK,除了Java接口的jar包之外,还有一个.so文件,这个so就是实现了Java层定义的native接口的动态库

  • 复用性

依托C++的跨平台特性,只需用C++编写一次逻辑,就可以将游戏打包发布到不同的平台(IOS、Android、WinPhone、黑莓、Linux、Windows)

  • 与硬件交流的便捷性

当各种物联网设备接入互联网的同时,自然少不了人机交互的应用程序,当应用程序需要调用硬件特定的功能时,此时只能通过C或C++封装对应功能的JNI接口来供上层应用使用。

比如要用手机中的app控制家里的电灯、窗帘、冰箱、空调等一切智能的电子设备时,自然少不了应用要和底层硬件进行通讯,至于各种智能设备的运行控制,自然是由厂商来实现,他们只需提供操作设备相关功能的接口即可。

虽然厂商会封装好JNI接口,但我们也要了解下jni与java通讯的原理,以便我们在开发过程当中遇到问题时,能够快速定位到问题

1.4 .so文件是什么?

  • 研发期,我们使用C/C++语言编写出来的代码,将会被变异成不同的动态库文件。
  • 使用期,对应的系统平台会将动态库连接入系统中,从而可以使用其中的函数资源。

开发JNI程序会受到系统环境的限制,因为用C/C++语言写出来的代码或模块,编译过程当中要依赖当前操作系统环境所提供的一些库函数,并和本地库链接在一起

而且编译后生成的二进制代码只能在本地操作系统环境下运行,因为不同的操作系统环境,有自己的本地库和CPU指令集,而且各个平台对标准C/C++的规范和标准库函数实现方式也有所区别

这就造成使用了JNI接口的JAVA程序,不再像以前那样自由的跨平台。如果要实现跨平台,就必须将本地代码在不同的操作系统平台下编译出相应的动态库

  • .so后缀动态库文件对应linux/unix
  • .dll后缀动态库文件对应windows
  • .jnilib后缀动态库文件对应mac

android作为linxu系统,使用的就是.so文件作为动态库

二、开发流程

(4.1.27)JNI_第1张图片

  1. 编写声明了native方法的.java类
  2. 将Java源代码编译成.class字节码文件[可选]
  3. 用javah -jni命令生成.h头文件(javah是jdk自带的一个命令,-jni参数表示将class中用native声明的函数生成jni规则的函数)
  4. 用本地代码实现.h头文件中的函数,编写.c/cpp文件
  5. 将本地代码编译成动态库(windows:.dll,linux/unix:.so,mac os x:*.jnilib)
    • Eclipse手动创建mk,并定义规则
    • AS通过Gradle中的配置,自动创建mk,在.gradle中定义相关规则
  6. 拷贝动态库至 java.library.path 本地库搜索目录下,并运行Java程序(加载动态库,并调用函数)

如果操作熟练,了解.h的生成规则,那么就不需要步骤2.3,直接手动写出.h文件并开始第4步即可,效果是一样的

以下实例我是在app中直接建的,但是最好方式是自己做一个新的module(android library)专门处理JNI操作,步骤是相同的:new module—–android library—-MyJni

第一步、编写声明了native方法的.java类

在module中,编写JniTest.java类,源码如下:

使用native关键字说明这个函数是java和其他语言(c/c++)协作时用的,并用其他语言实现的

package test.com.MyJni;  

public class HelloWorld {  

    public static native String sayHello(String name);  // 1.声明这是一个native函数,由本地代码实现  

   static {  
        System.loadLibrary("HelloWorld");   // 2.加载实现了native函数的动态库,只需要写动态库的名字
    }  

    public static void main(String[] args) {  
        String text = sayHello("yangxin");  // 3.调用本地函数  
        System.out.println(text);  
    }  
}  

或者

package test.com.MyJni;

public class JniTest {

    //使用JNI的关键字native
    //这个关键字决定我们那些方法能在我们的C文件中使用
    //只须声明,不必实现
    public native void yu();
    public native double sum(double x,double y);
}

第二步、生成.class字节码文件

  • 方法1:用javac命令将.java源文件编译成.class字节码文件
    • 命令行中输入指定命令,可以在AS自带的命令行工具中,可以直接定位到目录位置
    • 也可以在系统的命令行中,但是需要手动定位到根目录位置
    • 需要配置jdk环境
javac src/test/com/MyJni/HelloWorld.java -d ./bin  

//注意:HelloWorld放在test.com.MyJni包下面
//-d 表示将编译后的class文件放到指定的目录下,这里我把它放到和src同级的bin目录下
  • 方法2:使用AS的Build全量编译所有.java文件为.class文件
    • 执行Build->Make Project
    • 这一步骤执行一下,验证工程中并无其它错误,并对工程进行了编译,生成了.class文件.生成路径是在 Module/build/intermediates/classes/debug下的

第三步、用javah -jni命令,根据class字节码文件生成.h头文件(-jni参数是可选的)

需要注意的是,如果你操作熟练,了解.h的生成规则,那么就不需要步骤2、3,直接手动写出.h文件并开始第4步即可,效果是一样的

  • 在AS自带的命令终端(直接定位到工程根目录),或者自带cmd中执行命令(需要手动定位到根目录)
    • 点击”View->Tool Windows->Terminal”
    • 进入当前文件夹cd app/src/main /java/test/com/MyJni
  • 命令javah -d jni -classpath
//默认生成的.h头文件名为:test_com_MyJni_HelloWorld.h(包名+类名.h)
javah -jni -classpath ./bin -d ./jni test.com.MyJni.HelloWorld  
javah -d jni -classpath D:\Users\sf\AppData\Local\Android\sdk\platforms\android-21\android.jar;..\..\..\..\..\..\build\intermediates\classes\debug test.com.MyJni.JniTest

//也可以通过-o参数指定生成头文件名称:
javah -jni -classpath ./bin -o HelloWorld.h test.com.MyJni.HelloWorld  

我们可以看看自动生成的头文件,具体的转换规则请参看《Native标准映射规则》

(4.1.27)JNI_第2张图片

test_com_MyJni_HelloWorld.h:

/* DO NOT EDIT THIS FILE - it is machine generated */  
#include   
/* Header for class test_com_MyJni_HelloWorld */  

#ifndef _Included_test_com_MyJni_HelloWorld  
#define _Included_test_com_MyJni_HelloWorld  
#ifdef __cplusplus  
extern "C" {  
#endif  
/* 
 * Class:     test_com_MyJni_HelloWorld 
 * Method:    sayHello 
 * Signature: (Ljava/lang/String;)Ljava/lang/String; 
 */  
JNIEXPORT jstring JNICALL Java_test_com_MyJni_HelloWorld_sayHello  
  (JNIEnv *, jclass, jstring);  

#ifdef __cplusplus  
}  
#endif  
#endif  

/* DO NOT EDIT THIS FILE - it is machine generated */
#include 
/* Header for class test_com_MyJni_JniTest */

#ifndef _Included_test_com_MyJni_JniTest
#define _Included_test_com_MyJni_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     test_com_MyJni_JniTest
 * Method:    yu
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_test_com_MyJni_JniTest_yu
  (JNIEnv *, jobject);

/*
 * Class:     test_com_MyJni_JniTest
 * Method:    sum
 * Signature: (DD)D
 */
JNIEXPORT jdouble JNICALL Java_test_com_MyJni_JniTest_sum
  (JNIEnv *, jobject, jdouble, jdouble);

#ifdef __cplusplus
}
#endif
#endif

第四步、用本地代码实现.h头文件中的函数

(4.1.27)JNI_第3张图片

  • C/C++代码的头部和实现需要放在AS的默认位置:[module]/src/main/包名/jni/路径下
  • 自定义需要在步骤当前的module中的gradle配置自定义路径,以告知AS需要编译的C/C++代码在哪里,详情请看《gradle配置》

HelloWorld.c:

// HelloWorld.c  

#include "test_com_MyJni_HelloWorld.h"  

#ifdef __cplusplus  
extern "C"  
{  
#endif  

/* 
 * Class:     test_com_MyJni_HelloWorld 
 * Method:    sayHello 
 * Signature: (Ljava/lang/String;)Ljava/lang/String; 
 */  
JNIEXPORT jstring JNICALL Java_test_com_MyJni_HelloWorld_sayHello(  
        JNIEnv *env, jclass cls, jstring j_str)  
{  
    const char *c_str = NULL;  
    char buff[128] = { 0 };  
    c_str = (*env)->GetStringUTFChars(env, j_str, NULL);  
    if (c_str == NULL)  
    {  
        printf("out of memory.\n");  
        return NULL;  
    }  
    printf("Java Str:%s\n", c_str);  
    sprintf(buff, "hello %s", c_str);  
        (*env)->ReleaseStringUTFChars(env, j_str, c_str);  
    return (*env)->NewStringUTF(env, buff);  
}  
#ifdef __cplusplus  
}  
#endif  

//必须的头文件jni.h
#include 
//导入我们需要实现的本地方法
#include "test_com_MyJni_JniTest.h"
#include 

/*
 * Class:     test_com_MyJni_JniTest
 * Method:    yu
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_test_com_MyJni_JniTest_yu
   (JNIEnv *env, jobject obj){
    LOGE("log string from ndk.");
    printf("Hello World tom!!");
        return;
  }

  /*
   * Class:     test_com_MyJni_JniTest
   * Method:    sum
   * Signature: (DD)D
   */
  JNIEXPORT jdouble JNICALL Java_test_com_MyJni_JniTest_sum(JNIEnv *env, jobject obj, jdouble a, jdouble b)

   return a+b;
    }

第五步、将C/C++代码编译成本地动态库文件

动态库文件名命名规则:lib+动态库文件名+后缀(操作系统不一样,后缀名也不一样)如:

  • Mac OS X : libHelloWorld.jnilib
  • Windows :HelloWorld.dll(不需要lib前缀)
  • Linux/Unix:libHelloWorld.so

我们接下来讲一下linux下的编译动态库的两种方法:

2.5.1 基于 Android.mk文件的ndk命令方式

Android.mk文件和.c文件同级目录

我们实现好.c或者.cpp文件后,编写Android.mk文件,来生成动态库,一般使用NDK工具进行生成,首先是下载ndk包,然后设计全局变量,进入Android.mk文件夹中执行ndk编译命令即可

ndk编译命令使用参考:

  • http://www.cnblogs.com/lipeil/archive/2012/08/27/2659378.html

  • http://blog.csdn.net/laczff21/article/details/7542236

我们进入.c,.h, Android.mk所在的文件下面,然后执行ndk编译命令:

ndk-build

信息如下:

abc@abc:~/workspace/NativeTest/jni$ndk-build

AndroidNDK: WARNING: APP_PLATFORM android-19 is larger thanandroid:minSdkVersion 8 in/home/archermind/workspace/NativeTest/AndroidManifest.xml

[armeabi]Compile++ thumb: NativeClassJni <=com_example_nativetest_NativeClass.cpp

[armeabi]StaticLibrary : libstdc++.a

[armeabi]SharedLibrary : libNativeClassJni.so

[armeabi]Install : libNativeClassJni.so =>libs/armeabi/libNativeClassJni.so

2.5.1.1 Android.mk

Android.mk文件是GNU Makefile的一小部分,它用来对Android程序进行编译,Android.mk中的变量都是全局的,解析过程会被定义。

一个Android.mk文件可以编译多个模块,模块包括:APK程序、JAVA库、C\C++应用程序、C\C++静态库、C\C++共享库。

简单实例如下:

LOCAL_PATH := $(call my-dir)  #表示是当前文件的路径  
include $(CLEAR_VARS)       #指定让GNU MAKEFILE该脚本为你清除许多 LOCAL_XXX 变量  
LOCAL_MODULE:= helloworld   #标识你在 Android.mk 文件中描述的每个模块  
MY_SOURCES := foo.c         #自定义变量  
ifneq ($(MY_CONFIG_BAR),)  
 MY_SOURCES += bar.c  
endif  
LOCAL_SRC_FILES += $(MY_SOURCES)    #包含将要编译打包进模块中的 C 或 C++源代码文件  
include $(BUILD_SHARED_LIBRARY) #根据LOCAL_XXX系列变量中的值,来编译生成共享库(动态链接库) 

更多字段参看(4.1.27.7)JNI/NDK开发指南(四)——JNI 实战全面解析

2.5.2 基于jdk的单文件打包

gcc -I$JAVA_HOME/include -I$JAVA_HOME/include/linux -fPIC -shared HelloWorld.c -o libHelloWorld.so  
  • I:包含编译JNI必要的头文件
  • fPIC:编译成与位置无关的独立代码
  • shared:编译成动态库
  • o:指定编译后动态库生成的路径和文件名

2.5.3 配置grable自动打包

  • 执行”Build->Make Project”,如果未安装ndk,会发现”Messages Gradle Build”会给出提示如下:
    • 提示了NDK未配置,并且需要在工程中的local.properties文件中配置NDK路径
Error:Execution failed for task ':app:compileDebugNdk'.   
> NDK not configured.   
Download the NDK from http://developer.android.com/tools/sdk/ndk/.Then add ndk.dir=path/to/ndk in local.properties.   
(On Windows, make sure you escape backslashes, e.g. C:\\ndk rather than C:\ndk)  
  • 【配置NDK】

(4.1.27)JNI_第4张图片

  • 【配置NDK】修改local.properties配置
    • 在 local.properties 文件中设置ndk的路径,一般会自动生成
    • gradle.properties中 官方已有新的方式使用,在这里true,表示还是用旧方式
      android.useDeprecatedNdk=true

(4.1.27)JNI_第5张图片

  • 修改build.gradle配置

工程中共有两个build.gradle配置文件,我们要修改的是在\app\build.gradle这个文件。为其在defaultConfig分支中增加上

sourceSets.main {
          jni.srcDirs = []
          // 默认情况下,你需要把C/C++源代码放在[module]/src/main/jni/路径下
          //自定义源代码路径
          jni.srcDirs 'src/main/java/test/com/MyJni'
          //设置你的.so文件的实际路径,用于调用System.loadLibrary( libName );
         //默认是再app/src/main/jniLibs中
         jniLibs.srcDir "src/main/libs"
        }

ndk {  
    moduleName "JniTest"   //生成的so名字
    ldLibs "log", "z", "m"  
    abiFilters "armeabi", "armeabi-v7a", "x86"  //输出指定三种abi体系结构下的so库。目前可有可无。
}  

以上配置代码指定的so库名称为JniTest,链接时使用到的库,对应android.mk文件中的LOCAL_LDLIBS,及最终输出指定三种abi体系结构下的so库。

(4.1.27)JNI_第6张图片

  • 这时,再执行”Build->Rebuild Project”,就可以编译出so文件了
    • android Studio不需要配置MK文件,是因为可以自动生成,生成的so文件路径如图所示:

(4.1.27)JNI_第7张图片

  • Window平台上可能会出现一个问题:
    • 在Windows下NDK一个bug,当仅仅编译一个文件时出现会出现此问题,解决方法就是再往jni文件夹加入一个空util.c文件即可。
Error:Execution failed for task ':app:compileDebugNdk'.  
> com.android.ide.common.internal.LoggedErrorException: Failed to run command:  

第六步、动态库放在指定位置

编译后的.so文件位于build文件夹的如图位置:

(4.1.27)JNI_第8张图片

把.so文件放到相应的目录即可,默认路径如图所示的jniLibs

(4.1.27)JNI_第9张图片

如果你不想把.so放在上面的默认路径,可以在buid.gradle中进行如下配置:

android {  
  // .. android settings ..
  sourceSets.main {
      jniLibs.srcDir 'src/main/myCppLibraries' // <-- 你的.so库的实际路径
      }
}

第七步、动态库加载和调用

调用和普通java函数一样,无非是需要提前加载下动态库

  1. 加载动态库
  2. 调用其native方法
public class JniTest {

    //加载so库
    static {
        System.loadLibrary( "MyJniTest" );
    }

    //使用JNI的关键字native
    //这个关键字决定我们那些方法能在我们的C文件中使用
    //只须声明,不必实现
    public native void yu();
    public native double sum(double x,double y);
}

import test.com.myjni.JniTest;

public class MainActivity extends AppCompatActivity {

    TextView tv;

    JniTest jniUtils=new JniTest();


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tv=(TextView)findViewById(R.id.tv);

        double a=1;
        double b=2;
        double sum=jniUtils.sum(a,b);

        tv.setText(String.valueOf(sum));
    }
}

三、加载动态库.so

  • 静态加载方法,将所有依赖库的so文件全部一股脑的放进armeabi文件夹即可
  • 动态加载的方法,将so文件放在assets文件夹中,当应用第一次启动的时候,必须将我们放在assets文件夹中的so文件拷贝到本地数据目录下

3.1 静态加载

Java在调用native(本地)方法之前,需要先加载动态库。如果在未加载动态之前就调用native方法,会抛出找不到动态链接库文件的异常。如下所示:

Exception in thread "main" java.lang.UnsatisfiedLinkError: com.study.jnilearn.HelloWorld.sayHello(Ljava/lang/String;)Ljava/lang/String;  
    at com.study.jnilearn.HelloWorld.sayHello(Native Method)  
    at com.study.jnilearn.HelloWorld.main(HelloWorld.java:9)  

加载动态库的两种方式:

  • System.loadLibrary(“HelloWorld”);
    • 只需要指定动态库的名字即可,不需要加lib前缀,也不要加.so、.dll和.jnilib后缀
  • System.load(“/Users/yangxin/Desktop/libHelloWorld.jnilib”);
    • 指定动态库的绝对路径名,需要加上前缀和后缀

Android支持的.so文件都应该以“lib”开头,AndroidStudio自动生成的.so也是自动加上这个名字的

一般在类的静态(static)代码块中加载动态库最合适,因为在创建类的实例时,类会被ClassLoader先加载到虚拟机,随后立马调用类的static静态代码块。这时再去调用native方法就万无一失了

AndroidStudio默认jnilibs路径:module/src/main/jnilibs/armeabi/...
- libgnustl_shared.so
- ibprotobuf.so
- ibcrypto.so
- ibssl.so
- ibnet_manager.so
- ibfilehash.so
- ibmoa_jni.so


public class MOA_JNI {

    static {
        System.out.println("load libs.....");
        System.loadLibrary("gnustl_shared");
        System.loadLibrary("protobuf");
        System.loadLibrary("crypto");
        System.loadLibrary("ssl");
        System.loadLibrary("net_manager");
        System.loadLibrary("filehash");
        System.loadLibrary("moa_jni");
    }


    public native int connectServer(int tunnel, int tunType, String ip, short port, boolean autoReconn);

    public native int remoteCall(int tunnel, short severType, int operType, short flag,
                                 byte[] byteArray, MoaResult result, int timeout);

    ...
}   

3.1.1 基于动态库名称的静态加载方式

System.loadLibrary("HelloWorld");

如果使用该方式,java会去java.library.path系统属性指定的目录下查找动态库文件,如果没有找到会抛出java.lang.UnsatisfiedLinkError异常

Exception in thread "main" java.lang.UnsatisfiedLinkError: no HelloWorld in java.library.path  
    at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1860)  
    at java.lang.Runtime.loadLibrary0(Runtime.java:845)  
    at java.lang.System.loadLibrary(System.java:1084)  
    at com.study.jnilearn.HelloWorld.(HelloWorld.java:13)  

从异常中可以看出来,他是在java.library.path中查找该名称对应的动态库

那么这个路径是什么呢?

可以通过调用System.getProperties(“java.library.path”)方法获取查找的目录列表,其中在mac下找libHelloWorld.jnilib文件,linux下找libHelloWorld.so文件,windows下找libHelloWorld.dll文件

下面是本机mac os x 系统下的查找目录:

String libraryDirs = System.getProperty("java.library.path");  
System.out.println(libraryDirs);  
// 输出结果如下:  
/Users/yhf/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:.  

有两种方式可以让java从java.library.path找到动态链接库文件:

  • 方式1:将动态链接库拷贝到java.library.path目录下
  • 方式2:给jvm添加“-Djava.library.path=动态链接库搜索目录”参数,指定系统属性java.library.path的值
java -Djava.library.path=/Users/yangxin/Desktop

Linux/Unix环境下可以通过设置LD_LIBRARY_PATH环境变量,指定库的搜索目录

3.2 动态加载

  • (4.1.27.11)Android动态加载so文件
  • (4.1.27.12)Android动态加载补充 加载SD卡中的SO库

四、查找动态库中指定对应函数

我们明白了调用native方法之前,首先要调用System.loadLibrary接口加载一个实现了native方法的动态库才能正常访问,否则就会抛出Java.lang.UnsatisfiedLinkError异常,找不到XX方法的提示。

现在我们想想,在Java中调用某个native方法时,JVM是通过什么方式,能正确的找到动态库中C/C++实现的那个native函数呢?

JVM查找native方法有两种方式:

  1. 按照JNI规范的命名规则
    我们严格按照java代码与native代码的转换方式,进行命名,jvm就能套路的找到java 对应的native方法
  2. 调用JNI提供的RegisterNatives函数,将本地函数注册到JVM中。

4.1 Native函数标准映射规则

public static native String sayHello(String name); 

JNIEXPORT jstring JNICALL Java_com_study_jnilearn_HelloWorld_sayHello  
  (JNIEnv *, jclass, jstring);  
  • JNIEXPORT 和 JNICALL的作用
    • 在Windows中编译dll动态库规定,如果动态库中的函数要被外部调用,需要在函数声明中添加__declspec(dllexport)标识,表示将该函数导出在外部可以调用。
    • 在Linux/Unix系统中,这两个宏可以省略不加
  • 函数返回值类型
    • java函数的返回值,依照一定的规则进行转变
  • Java_包名+类名_方法名
  • 函数参数
    • JNIEnv*
      定义任意native函数的第一个参数(包括调用JNI的RegisterNatives函数注册的函数),指向JVM函数表的指针
      函数表中的每一个入口指向一个JNI函数,每个函数用于访问JVM中特定的数据结构
    • jclass
      调用java中native方法的实例或Class对象,如果这个native方法是实例方法,则该参数是jobject,如果是静态方法,则是jclass
    • 其他
      java中的函数,依照对应关系的转变

4.2 本地RegisterNatives函数手动注册

背景:

  • 应用层级的Java类别透过VM而呼叫到本地函数。一般是依赖VM去寻找*.so里的本地函数。
  • 如果需要连续呼叫很多次,每次都需要寻找一遍,会多花许多时间。此时,组件开发者可以自行将本地函数向VM进行登记

在JNI_OnLoad的触发

  • 当VM载入libmedia_jni.so动态库时,就呼叫JNI_OnLoad()函数。
  • 接着,JNI_OnLoad()呼叫 native_methods_register() 函数
  • 此时,就呼叫到AndroidRuntime::registerNativeMethods()函数,向VM(即AndroidRuntime)登记gMethods[]表格所含的本地函数了

效果:

  1. 更有效率去找到函数。
  2. 可在执行期间进行抽换。由于gMethods[]是一个<名称,函数指针>对照表,在程序执行时,可多次呼叫registerNativeMethods()函数来更换本地函数之指针,而达到弹性抽换本地函数之目的
/*
 * JAVA方法与NDK函数的映射关系表
 */
static JNINativeMethod s_methods[] = {
    {"connectServer", "(Ljava/lang/String;S)I", (void*)connectServer},
    {"disconnectServer", "()I", (void*)disconnectServer},
    {"remoteCall", "(SI[BLcom/sangfor/moa/MoaResult;I)I", (void*)remoteCall},
    {"recvMessage", "(Lcom/sangfor/moa/MoaResult;)I", (void*)recvMessage},
    {"isConnected", "()Z", (void*)isConnected},
    {"setNdkLogLevel", "(I)I", (void*)setNdkLogLevel},
    {"setNdkLogFile", "(Ljava/lang/String;I)I", (void*)setNdkLogFile},

    {"getFileHash", "(Ljava/lang/String;)Ljava/lang/String;", (void*)getFileHash},
    {"getStreamHash", "([B)Ljava/lang/String;", (void*)getStreamHash},
};

static jboolean native_methods_register(JNIEnv* env, const char* className)
{
    jclass cls;
    cls = env->FindClass(className);//获取RdpConn类引用
    if (cls == NULL)
    {
        return false;
    }

    //获取JNINativeMethod的长度
    jint num = sizeof(s_methods) / sizeof(s_methods[0]);

    //注册java层的jni接口
    jint ret = env->RegisterNatives(cls, s_methods, num);
    if (ret != JNI_OK)
    {
        env->DeleteLocalRef(cls);
        return false;
    }

    env->DeleteLocalRef(cls);
    return true;
}


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

    //A.2.1 注册RdpConn类中的本地接口,以避免每次呼叫都触发 .so文件内的函数查询
    if (!native_methods_register(env, "com/sangfor/moa/MOA_JNI"))
    {
        return -1;
    }

}


public class MOA_JNI {


    /*  connectServer - 连接服务器    */ 
    public native int connectServer(String ip, short port);

    /*  disconnectServer - 与服务器断开连接  */ 
    public native int disconnectServer();

    /*  RemoteCall - 向服务端查询的接口   */ 
    public native int remoteCall(short severType, int operType, byte[] byteArray,
                                 MoaResult result, int timeout);

    /*  recvMessage - 获取消息(服务器推送消息或本地消息)
     *  若暂时没有任何消息,调用会被阻塞,直到有消息
     */ 
    public native int recvMessage(MoaResult result);

    /*  isConnected - 查看通道连接状态
     */ 
    public native boolean isConnected();

    /*  setNdkLogLevel - 设置底层NDK打日志的级别   */ 
    public native int setNdkLogLevel(int level);

    /*  setNdkLogFile - 设置底层NDK打文件日志
     *  @filePath: 日志文件路径 .  ""表示打印到console
     *  @sizeLimit:日志文件的大小限制(bytes)
     */ 
    public native int setNdkLogFile(String filePath, int sizeLimit);


    /*  getFileHash - 获取文件的哈希值(七牛文件服务使用)     
     *  @file - 文件全路径
     *  retrun - 计算出的哈希值(调用失败会返回 "null")
     */ 
    public native String getFileHash(String file);

    /*  getStreamHash - 获取字节流的哈希值(七牛文件服务使用)  
     *  @byteArray - 字节流
     *  retrun - 计算出的哈希值(调用失败会返回 "null")
     */ 
    public native String getStreamHash(byte[] byteArray);

}

五、JNI数据类型及与Java数据类型的映射关系

当我们在调用一个Java native方法的时候,方法中的参数是如何传递给C/C++本地函数中的呢?

Java方法中的参数与C/C++函数中的参数,它们之间是怎么转换的呢?

我们通过一个简单的示例来看下



class MyClass {}  

public class HelloWorld {  

    //前面8个为基本数据类型,后面4个全部为引用类型
    public static native void test(short s, int i, long l, float f, double d, char c,   
            boolean z, byte b, String str, Object obj, MyClass p, int[] arr);  

    public static void main(String[] args) {  
        String obj = "obj";  
        short s = 1;  
        long l = 20;  
        byte b = 127;  
        test(s, 1, l, 1.0f, 10.5, 'A', true, b, "中国", obj, new MyClass(), new int[] {});  
    }  

    static {  
        System.loadLibrary("HelloWorld");  
    }  
}   

//转化后
JNIEXPORT void JNICALL Java_test_com_MyJni_HelloWorld_test  
    (JNIEnv *env, jclass cls, jshort s, jint i, jlong l, jfloat f,  
     jdouble d, jchar c, jboolean z, jbyte b, jstring j_str, jobject jobj1, jobject job2, jintArray j_int_arr)  
{  
    printf("s=%hd, i=%d, l=%ld, f=%f, d=%lf, c=%c, z=%c, b=%d", s, i, l, f, d, c, z, b);  
    const char *c_str = NULL;  
    c_str = (*env)->GetStringUTFChars(env, j_str, NULL);  
    if (c_str == NULL)  
    {  
        return; // memory out  
    }  
    (*env)->ReleaseStringUTFChars(env, j_str, c_str);  
    printf("c_str: %s\n", (char*)c_str);  
}  

(4.1.27)JNI_第10张图片

从头文件函数的原型可以得知,test方法中形参的数据类型自动转换成了JNI中相应的数据类型,不难理解,在调用Java native方法将实参传递给C/C++函数的时候,会自动将java形参的数据类型自动转换成C/C++相应的数据类型,所以我们在写JNI程序的时候,必须要明白它们之间数据类型的对应关系

在Java语言中数据类型分为基本数据类型和引用类型,其中基本数据类型有8种:byte、char、short、int、long、float、double、boolean,除了基本数据类型外其它都是引用类型:Object、String、数组等。

  • 8种基本数据类型分别对应JNI数据类型中的jbyte、jchar、jshort、jint、jlong、jfloat、jdouble、jboolean
  • 所有的JNI引用类型全部是jobject类型
  • 为了使用方便和类型安全,JNI定义了一个引用类型集合,集合当中的所有类型都是jobject的子类,这些子类和Java中常用的引用类型相对应
    • jstring表示字符串
    • jclass表示class字节码对象
    • jthrowable表示异常
    • jarray表示数组
      jarray派生了8个子类,分别对应Java中的8种基本数据类型(jintArray、jshortArray、jlongArray等)

5.1 JVM函数表操作手柄JNIEnv

  • JNIEnv 作用 :
    • 调用 Java 函数 : JNIEnv 代表 Java 运行环境, 可以使用 JNIEnv 调用 Java 中的代码;
    • 操作 Java 对象 : Java 对象传入 JNI 层就是 Jobject 对象, 需要使用 JNIEnv 来操作这个 Java 对象

(4.1.27)JNI_第11张图片

  • JNIEnv*
    • 定义任意native函数的第一个参数(包括调用JNI的RegisterNatives函数注册的函数),指向JVM函数表的指针
    • 函数表中的每一个入口指向一个JNI函数,每个函数用于访问JVM中特定的数据结构
  • JNIEnv
    • 是一个线程相关的结构体, 该结构体代表了 Java 在本线程的运行环境
    • JNIEnv 是一个指针, 指向一个线程相关的结构, 线程相关结构指向 JNI 函数指针 数组, 这个数组中存放了大量的 JNI 函数指针, 这些指针指向了具体的 JNI 函数
  • JNIEnv 与 JavaVM :
    • JavaVM : JavaVM 是 Java虚拟机在 JNI 层的代表, JNI 全局只有一个;
    • JNIEnv : JavaVM 在线程中的代表, 每个线程都有一个, JNI 中可能有很多个 JNIEnv;

5.1.1 线程相关性

JNIEnv 是线程相关的, 即 在 每个线程中 都有一个 JNIEnv 指针, 每个JNIEnv 都是线程专有的, 其它线程不能使用本线程中的 JNIEnv, 线程 A 不能调用 线程 B 的 JNIEnv;

JNIEnv只在当前线程中有效。本地方法不能将JNIEnv从一个线程传递到另一个线程中。相同的 Java 线程中对本地方法多次调用时,传递给该本地方法的JNIEnv是相同的。但是,一个本地方法可被不同的 Java 线程所调用,因此可以接受不同的 JNIEnv

JNIEnv 不能跨线程 :
– 当前线程有效 : JNIEnv 只在当前线程有效, JNIEnv 不能在 线程之间进行传递, 在同一个线程中, 多次调用 JNI层方法, 传入的 JNIEnv 是相同的;
– 本地方法匹配多JNIEnv : 在 Java 层定义的本地方法, 可以在不同的线程调用, 因此 可以接受不同的 JNIEnv;

5.2 JNI基本类型

Java类型 本地类型(JNI) 描述
boolean(布尔型) jboolean 无符号8个比特
byte(字节型) jbyte 有符号8个比特
char(字符型) jchar 无符号16个比特
short(短整型) jshort 有符号16个比特
int(整型) jint 有符号32个比特
long(长整型) jlong 有符号64个比特
float(浮点型) jfloat 32个比特
double(双精度浮点型) jdouble 6 4个比特
void(空型) void N/A

这些基本数据类型都是可以在Native层直接使用的

5.3 JNI引用类型

  • 所有的JNI引用类型全部是jobject类型
  • 为了使用方便和类型安全,JNI定义了一个引用类型集合,集合当中的所有类型都是jobject的子类,这些子类和Java中常用的引用类型相对应
    • jstring表示字符串
    • jclass表示class字节码对象
    • jthrowable表示异常
    • jarray表示数组
      jarray派生了8个子类,分别对应Java中的8种基本数据类型(jintArray、jshortArray、jlongArray等)

(4.1.27)JNI_第12张图片

  1. 引用数据类型则不能直接使用,需要根据JNI函数进行相应的转换后,才能使用
  2. 多维数组(包括二维数组)都是引用类型,需要使用 jobjectArray 类型存取其值

5.3.1 jvalue类型

  • jvalue是一个unio(联合)类型,在C语中为了节约内存,会用联合类型变量来存储声明在联合体中的任意类型数据。
  • 在JNI中将基本数据类型与引用类型定义在一个联合类型中,表示用jvalue定义的变量,可以存储任意JNI类型的数据,后面会介绍jvalue在JNI编程当中的应用。
typedef union jvalue {  
    jboolean z;  
    jbyte    b;  
    jchar    c;  
    jshort   s;  
    jint     i;  
    jlong    j;  
    jfloat   f;  
    jdouble  d;  
    jobject  l;  
} jvalue;

5.4 JNI描述符

就是对是一种对函数返回值和参数的编码

5.4.1 类描述符

类描述符是类的完整名称(包名+类名),将原来的 . 分隔符换成 / 分隔符

例如:在java代码中的java.lang.String类的类描述符就是java/lang/String

在实践中,我发现可以直接用该类型的域描述符取代,也是可以成功的

jclass intArrCls = env->FindClass("java/lang/String")
jclass intArrCls = env->FindClass("Ljava/lang/String;")

5.4.2 域描述符

  • 基本类型
Java 语言
Z boolean
B byte
C char
S short
I int
J long
F float
D double
  • 引用类型则为 L_ 该类型类描述符_;
    • String类型的域描述符为 Ljava/lang/String;
  • 数组则为 [_其类型的域描述符
    • int [ ] 其描述符为[I
    • float [ ] 其描述符为[F
    • String [ ] 其描述符为[Ljava/lang/String;
  • 多维数组则是 n个[_该类型的域描述符 , N代表的是几维数组
    • int [ ][ ] 其描述符为[[I
    • float[ ][ ] 其描述符为[[F

5.4.3 方法描述符

将参数类型的域描述符按照申明顺序放入一对括号中后跟返回值类型的域描述符,规则如下:

  • (参数的域描述符的叠加)返回类型描述符。
  • 对于,没有返回值的,用V(表示void型)表示
Java层方法                     JNI函数签名

String test ( )                 Ljava/lang/String;

int f (int i, Object object)    (ILjava/lang/Object;)I

void set (byte[ ] bytes)        ([B)V
...         

5.5 注意

  • 1、JNI如果使用C++语言编写的话,所有引用类型派生自jobject,使用C++的继承结构特性,使用相应的类型。如下所示:
class _jobject {};  
class _jclass : public _jobject {};  
class _jstring : public _jobject {};  
class _jarray : public _jobject {};  
class _jbooleanArray : public _jarray {};  
class _jbyteArray : public _jarray {};  
...  
  • 2、JNI如果使用C语言编写的话,所有引用类型使用jobject,其它引用类型使用typedef重新定义,如:typedef jobject jstring

六、JNI操控Java数据的方式

读取和返回的时候需要借助jni定义的中间数据类型并使用 env:JNIEnv函数表指针 进行对jvm中数据的操作,但是中间的变换可以使用C\C++代码

我们知道了JNI将JAVA数据和native数据的数据类型进行了转换,那么jni是如何对java传递下来的数据进行操作的呢?

6.1 对基本类型的操作

基本类型很容易理解,就是对C/C++中的基本类型用typedef重新定义了一个新的名字,在JNI中可以直接访问

typedef unsigned char   jboolean;  
typedef unsigned short  jchar;  
typedef short           jshort;  
typedef float           jfloat;  
typedef double          jdouble;  
typedef int jint;  
#ifdef _LP64 /* 64-bit Solaris */  
typedef long jlong;  
#else  
typedef long long jlong;  
#endif  

typedef signed char jbyte; 

6.2 对Object类型的操作

JNI把Java中的所有对象当作一个C指针传递到本地方法中,这个指针指向JVM中的内部数据结构,而内部的数据结构在内存中的存储方式是不可见的,只能从JNIEnv指针指向的函数表中选择合适的JNI函数来操作JVM中的数据结构。

譬如访问java.lang.String对应的JNI类型jstring时,没有像访问基本数据类型一样直接使用,因为它在Java是一个引用类型,所以在本地代码中只能通过GetStringUTFChars这样的JNI函数来访问字符串的内容

6.2.1 字符串操作示例

package com.study.jnilearn;  

public class Sample {  

    public native static String sayHello(String text);  

    public static void main(String[] args) {  
        String text = sayHello("yangxin");  
        System.out.println("Java str: " + text);  
    }  

    static {  
        System.loadLibrary("Sample");  
    }  
}  


JNIEXPORT jstring JNICALL Java_com_study_jnilearn_Sample_sayHello  
  (JNIEnv *env, jclass cls, jstring j_str)  
{  
    const char *c_str = NULL;  
    char buff[128] = {0};  
    jboolean isCopy;    // 返回JNI_TRUE表示原字符串的拷贝,返回JNI_FALSE表示返回原字符串的指针  
    c_str = (*env)->GetStringUTFChars(env, j_str, &isCopy);  
    printf("isCopy:%d\n",isCopy);  
    if(c_str == NULL)  
    {  
        return NULL;  
    }  
    printf("C_str: %s \n", c_str);  
    sprintf(buff, "hello %s", c_str);  
    (*env)->ReleaseStringUTFChars(env, j_str, c_str);  
    return (*env)->NewStringUTF(env,buff);  
}  

(4.1.27)JNI_第13张图片

  • 访问字符串
    • sayHello函数接收一个jstring类型的参数text,但jstring类型是指向JVM内部的一个字符串,和C风格的字符串类型char*不同,所以在JNI中不能通把jstring当作普通C字符串一样来使用,必须使用合适的JNI函数来访问JVM内部的字符串数据结构
/**
* 读取JVM内部的字符串内容
* - Java默认使用Unicode编码,而C/C++默认使用UTF编码,所以在本地代码中操作字符串的时候,必须使用合适的JNI函数把jstring转换成C风格的字符串
* - GetStringUTFChars可以把一个jstring指针(指向JVM内部的Unicode字符序列)转换成一个UTF-8格式的C字符串
* env:JNIEnv函数表指针
* j_str:jstring类型(Java传递给本地代码的字符串指针)
* isCopy:取值JNI_TRUE和JNI_FALSE,如果值为JNI_TRUE,表示返回JVM内部源字符串的一份拷贝,并为新产生的字符串分配内存空间。如果值为JNI_FALSE,表示返回JVM内部源字符串的指针,意味着可以通过指针修改源字符串的内容,不推荐这么做,因为这样做就打破了Java字符串不能修改的规定。但我们在开发当中,并不关心这个值是多少,通常情况下这个参数填NULL即可
*/
GetStringUTFChars(env, j_str, &isCopy) 
  • 异常检查
    • 调用完GetStringUTFChars之后不要忘记安全检查,因为JVM需要为新诞生的字符串分配内存空间,当内存空间不够分配的时候,会导致调用失败,失败后GetStringUTFChars会返回NULL,并抛出一个OutOfMemoryError异常

JNI的异常和Java中的异常处理流程是不一样的,Java遇到异常如果没有捕获,程序会立即停止运行。而JNI遇到未决的异常不会改变程序的运行流程,也就是程序会继续往下走,这样后面针对这个字符串的所有操作都是非常危险的,因此,我们需要用return语句跳过后面的代码,并立即结束当前方法

  • 释放字符串
    • 在调用GetStringUTFChars函数从JVM内部获取一个字符串之后,JVM内部会分配一块新的内存,用于存储源字符串的拷贝,以便本地代码访问和修改
    • 即然有内存分配,用完之后马上释放是一个编程的好习惯。通过调用ReleaseStringUTFChars函数通知JVM这块内存已经不使用了,你可以清除了

这两个函数是配对使用的,用了GetXXX就必须调用ReleaseXXX,而且这两个函数的命名也有规律,除了前面的Get和Release之外,后面的都一样

  • 创建字符串
    • 通过调用NewStringUTF函数,会构建一个新的java.lang.String字符串对象。这个新创建的字符串会自动转换成Java支持的Unicode编码
    • 如果JVM不能为构造java.lang.String分配足够的内存,NewStringUTF会抛出一个OutOfMemoryError异常,并返回NULL

在这个例子中我们不必检查它的返回值,如果NewStringUTF创建java.lang.String失败,OutOfMemoryError这个异常会被在Sample.main方法中抛出。如果NewStringUTF创建java.lang.String成功,则返回一个JNI引用,这个引用指向新创建的java.lang.String对象

更多字符串内容,请查看(4.1.27.6)JNI/NDK开发指南(三)——从字符串处理了解JNI的函数机制

七、JNI函数

7.1 *.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)
  3. 初期值之设定(Initialization) 可以手动注册函数表
/**
* 系统接口,在库被加载时调用,在此接口中实现接口注册功能
*/
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved)
{
    JNIEnv* env = NULL;

    //初始化jni env环境  【A.1. 告诉VM此C组件使用那一个JNI版本。 预先校验是否支持】
    if (!callback_environment_init(vm, &env))
    {
        return -1;
    }

    //A.2.1 注册RdpConn类中的本地接口,以避免每次呼叫都触发 .so文件内的函数查询
    if (!native_methods_register(env, "com/sangfor/moa/MOA_JNI"))
    {
        return -1;
    }

    //A.2.2 初始化net_manager
    if (!theCallManager().init())
    {
        return -1;
    }
    //A.2.3 初始化 theSocketManager  : 初始化session + 初始化定时器(设置定时器定时检查网络通断情况)
    if (!theSocketManager().init())
    {
        return -1;
    }
    //A.2.4 开启线程,执行 theSocketManager().run()---->在一个无限循环中执行相关任务
    if (!theSocketManager().start())
    {
        return -1;
    }

    return JNI_VERSION_1_4;
}

/**
* 初始化虚拟机环境  【A.1. 告诉VM此C组件使用那一个JNI版本。 预先校验是否支持】
*@param [in] vm 虚拟机对象
*@param [out] env jni环境变量
*@return 成功:true, 失败:false
*/
static jboolean callback_environment_init(JavaVM* vm, JNIEnv **env)
{
    jboolean ret = (jboolean)vm->GetEnv((void**)env, JNI_VERSION_1_4);
    if (ret != JNI_OK)
    {
        return false;
    }

    return true;
}

7.2 返回值

-直接使用该JNI构造一个jstring对象返回

jstring str = env->newStringUTF("HelloJNI"); 
return str ; 
  • 返回数组
jobjectArray ret = 0;  
jsize len = 5;  
jstring str;  
string value("hello");  

ret = (jobjectArray)(env->NewObjectArray(len, env->FindClass("java/lang/String"), 0));  
for(int i = 0; i < len; i++)  
{  
    str = env->NewStringUTF(value..c_str());  
    env->SetObjectArrayElement(ret, i, str);  
}  
return ret; 
  • 返回自定义对象
jclass    m_cls   = env->FindClass("com/ldq/ScanResult");    

jmethodID m_mid   = env->GetMethodID(m_cls,"","()V");    

jfieldID  m_fid_1 = env->GetFieldID(m_cls,"ssid","Ljava/lang/String;");    
jfieldID  m_fid_2 = env->GetFieldID(m_cls,"mac","Ljava/lang/String;");    
jfieldID  m_fid_3 = env->GetFieldID(m_cls,"level","I");    

jobject   m_obj   = env->NewObject(m_cls,m_mid);    

env->SetObjectField(m_obj,m_fid_1,env->NewStringUTF("AP1"));    
env->SetObjectField(m_obj,m_fid_2,env->NewStringUTF("00-11-22-33-44-55"));    
env->SetIntField(m_obj,m_fid_3,-50);    
return m_obj;  
  • 返回对象集合
jclass list_cls = env->FindClass("Ljava/util/ArrayList;");//获得ArrayList类引用    

if(listcls == NULL)    
{    
    cout << "listcls is null \n" ;    
}    
jmethodID list_costruct = env->GetMethodID(list_cls , "","()V"); //获得得构造函数Id    

jobject list_obj = env->NewObject(list_cls , list_costruct); //创建一个Arraylist集合对象    
//或得Arraylist类中的 add()方法ID,其方法原型为: boolean add(Object object) ;    
jmethodID list_add  = env->GetMethodID(list_cls,"add","(Ljava/lang/Object;)Z");     

jclass stu_cls = env->FindClass("Lcom/feixun/jni/Student;");//获得Student类引用    
//获得该类型的构造函数  函数名为  返回类型必须为 void 即 V    
jmethodID stu_costruct = env->GetMethodID(stu_cls , "", "(ILjava/lang/String;)V");    

for(int i = 0 ; i < 3 ; i++)    
{    
    jstring str = env->NewStringUTF("Native");    
    //通过调用该对象的构造函数来new 一个 Student实例    
    jobject stu_obj = env->NewObject(stucls , stu_costruct , 10,str);  //构造一个对象    

    env->CallBooleanMethod(list_obj , list_add , stu_obj); //执行Arraylist类实例的add方法,添加一个stu对象    
}    

return list_obj ;   

7.3 操作Java层的类

//获得jfieldID 以及 该字段的初始值    
jfieldID  nameFieldId ;    

jclass cls = env->GetObjectClass(obj);  //获得Java层该对象实例的类引用,即HelloJNI类引用    

nameFieldId = env->GetFieldID(cls , "name" , "Ljava/lang/String;"); //获得属性句柄    

if(nameFieldId == NULL)    
{    
   cout << " 没有得到name 的句柄Id \n;" ;    
}    
jstring javaNameStr = (jstring)env->GetObjectField(obj ,nameFieldId);  // 获得该属性的值    
const char * c_javaName = env->GetStringUTFChars(javaNameStr , NULL);  //转换为 char *类型    
string str_name = c_javaName ;      
cout << "the name from java is " << str_name << endl ; //输出显示    
env->ReleaseStringUTFChars(javaNameStr , c_javaName);  //释放局部引用    

//构造一个jString对象    
char * c_ptr_name = "I come from Native" ;    

jstring cName = env->NewStringUTF(c_ptr_name); //构造一个jstring对象    

env->SetObjectField(obj , nameFieldId , cName); // 设置该字段的值  

7.4 回调Java层方法

jstring str = NULL;    

jclass clz = env->FindClass("cc/androidos/jni/JniTest");    
//获取clz的构造函数并生成一个对象    
jmethodID ctor = env->GetMethodID(clz, "", "()V");    
jobject obj = env->NewObject(clz, ctor);    

// 如果是数组类型,则在类型前加[,如整形数组int[] intArray,则对应类型为[I,整形数组String[] strArray对应为[Ljava/lang/String;    
jmethodID mid = env->GetMethodID(clz, "sayHelloFromJava", "(Ljava/lang/String;II[I)I");    
if (mid)    
{    
  LOGI("mid is get");    
  jstring str1 = env->NewStringUTF("I am Native");    
  jint index1 = 10;    
  jint index2 = 12;    
  //env->CallVoidMethod(obj, mid, str1, index1, index2);    

  // 数组类型转换 testIntArray能不能不申请内存空间    
  jintArray testIntArray = env->NewIntArray(10);    
  jint *test = new jint[10];    
  for(int i = 0; i < 10; ++i)    
  {    
      *(test+i) = i + 100;    
  }    
  env->SetIntArrayRegion(testIntArray, 0, 10, test);    


  jint javaIndex = env->CallIntMethod(obj, mid, str1, index1, index2, testIntArray);    
  LOGI("javaIndex = %d", javaIndex);    
  delete[] test;    
  test = NULL;    
}    
static void event_callback(int eventId,const char* description) {  //主进程回调可以,线程中回调失败。  
    if (gEventHandle == NULL)  
        return;  

    JNIEnv *env;  
    bool isAttached = false;  

    if (myVm->GetEnv((void**) &env, JNI_VERSION_1_2) < 0) { //获取当前的JNIEnv  
        if (myVm->AttachCurrentThread(&env, NULL) < 0)  
            return;  
        isAttached = true;  
    }  

    jclass cls = env->GetObjectClass(gEventHandle); //获取类对象  
    if (!cls) {  
        LOGE("EventHandler: failed to get class reference");  
        return;  
    }  

    jmethodID methodID = env->GetStaticMethodID(cls, "callbackStatic",  
        "(ILjava/lang/String;)V");  //静态方法或成员方法  
    if (methodID) {  
        jstring content = env->NewStringUTF(description);  
        env->CallVoidMethod(gEventHandle, methodID,eventId,  
            content);  
        env->ReleaseStringUTFChars(content,description);  
    } else {  
        LOGE("EventHandler: failed to get the callback method");  
    }  

    if (isAttached)  
        myVm->DetachCurrentThread();  
}  

线程中回调
把c/c++中所有线程的创建,由pthread_create函数替换为由Java层的创建线程的函数AndroidRuntime::createJavaThread。

static pthread_t create_thread_callback(const char* name, void (*start)(void *), void* arg)    
{    
    return (pthread_t)AndroidRuntime::createJavaThread(name, start, arg);    
}   


static void checkAndClearExceptionFromCallback(JNIEnv* env, const char* methodName) {  //异常检测和排除  
    if (env->ExceptionCheck()) {    
        LOGE("An exception was thrown by callback '%s'.", methodName);    
        LOGE_EX(env);    
        env->ExceptionClear();    
    }    
}    

static void receive_callback(unsigned char *buf, int len)  //回调  
{    
    int i;    
    JNIEnv* env = AndroidRuntime::getJNIEnv();    
    jcharArray array = env->NewCharArray(len);    
    jchar *pArray ;    

    if(array == NULL){    
        LOGE("receive_callback: NewCharArray error.");    
        return;     
    }    

    pArray = (jchar*)calloc(len, sizeof(jchar));    
    if(pArray == NULL){    
        LOGE("receive_callback: calloc error.");    
        return;     
    }    

    //copy buffer to jchar array    
    for(i = 0; i < len; i++)    
    {    
        *(pArray + i) = *(buf + i);    
    }    
    //copy buffer to jcharArray    
    env->SetCharArrayRegion(array,0,len,pArray);    
    //invoke java callback method    
    env->CallVoidMethod(mCallbacksObj, method_receive,array,len);    
    //release resource    
    env->DeleteLocalRef(array);    
    free(pArray);    
    pArray = NULL;    

    checkAndClearExceptionFromCallback(env, __FUNCTION__);    
}  


public void Receive(char buffer[],int length){  //java层函数  
        String msg = new String(buffer);    
        msg = "received from jni callback" + msg;    
        Log.d("Test", msg);    
    }  

7.5 传对象到JNI调用

jclass stu_cls = env->GetObjectClass(obj_stu); //或得Student类引用    

if(stu_cls == NULL)    
{    
  cout << "GetObjectClass failed \n" ;    
}    
//下面这些函数操作,我们都见过的。O(∩_∩)O~    
jfieldID ageFieldID = env->GetFieldID(stucls,"age","I"); //获得得Student类的属性id     
jfieldID nameFieldID = env->GetFieldID(stucls,"name","Ljava/lang/String;"); // 获得属性ID    

jint age = env->GetIntField(objstu , ageFieldID);  //获得属性值    
jstring name = (jstring)env->GetObjectField(objstu , nameFieldID);//获得属性值    

const char * c_name = env->GetStringUTFChars(name ,NULL);//转换成 char *    

string str_name = c_name ;     
env->ReleaseStringUTFChars(name,c_name); //释放引用    

cout << " at Native age is :" << age << " # name is " << str_name << endl ;   

7.6 与C++互转

  • jbytearray转c++byte数组
jbyte * arrayBody = env->GetByteArrayElements(data,0);     
jsize theArrayLengthJ = env->GetArrayLength(data);     
BYTE * starter = (BYTE *)arrayBody;  
  • jbyteArray 转 c++中的BYTE[]
jbyte * olddata = (jbyte*)env->GetByteArrayElements(strIn, 0);    
jsize  oldsize = env->GetArrayLength(strIn);    
BYTE* bytearr = (BYTE*)olddata;    
int len = (int)oldsize;  
  • C++中的BYTE[]转jbyteArray
jbyte *by = (jbyte*)pData;    
jbyteArray jarray = env->NewByteArray(nOutSize);    
env->SetByteArrayRegin(jarray, 0, nOutSize, by);    
  • jbyteArray 转 char *
char* data = (char*)env->GetByteArrayElements(strIn, 0);   
  • char* 转jstring
jstring WindowsTojstring(JNIEnv* env, char* str_tmp)    
{    
 jstring rtn=0;    
 int slen = (int)strlen(str_tmp);    
 unsigned short* buffer=0;    
 if(slen == 0)    
 {    
  rtn = env->NewStringUTF(str_tmp);    
 }    
 else    
 {    
  int length = MultiByteToWideChar(CP_ACP, 0, (LPCSTR)str_tmp, slen, NULL, 0);    
  buffer = (unsigned short*)malloc(length*2+1);    
  if(MultiByteToWideChar(CP_ACP, 0, (LPCSTR)str_tmp, slen, (LPWSTR)buffer, length) > 0)    
  {    
   rtn = env->NewString((jchar*)buffer, length);    
  }    
 }    
 if(buffer)    
 {    
  free(buffer);    
 }    
 return rtn;    
}    
  • char* jstring互转
JNIEXPORT jstring JNICALL Java_com_explorer_jni_SambaTreeNative_getDetailsBy    
  (JNIEnv *env, jobject jobj, jstring pc_server, jstring server_user, jstring server_passwd)    
{    
    const char *pc = env->GetStringUTFChars(pc_server, NULL);    
    const char *user = env->GetStringUTFChars(server_user, NULL);    
    const char *passwd = env->GetStringUTFChars(server_passwd, NULL);    
    const char *details = smbtree::getPara(pc, user, passwd);    
    jstring jDetails = env->NewStringUTF(details);    
    return jDetails;    
}  

7.7 C/C++访问Java实例方法和静态方法

  • JNI/NDK开发指南(六)——C/C++访问Java实例方法和静态方法

7.8 C/C++访问Java实例变量和静态变量

  • JNI/NDK开发指南(七)——C/C++访问Java实例变量和静态变量

7.9 调用构造方法和父类实例方法

  • JNI/NDK开发指南(八)——调用构造方法和父类实例方法

7.10 JNI局部引用、全局引用和弱全局引用

  • JNI/NDK开发指南(十)——JNI局部引用、全局引用和弱全局引用

7.11 JNI异常处理

  • JNI/NDK开发指南(十一)——JNI异常处理

7.12 JNI调用性能测试及优化

  • JNI/NDK开发指南(九)——JNI调用性能测试及优化

7.13 JNI操作函数一览

函数 Java数组类型 本地类型 说明
GetBooleanArrayElements jbooleanArray jboolean ReleaseBooleanArrayElements释放
GetByteArrayElements jbyteArray jbyte ReleaseByteArrayElements释放
GetCharArrayElements jcharArray jchar ReleaseShortArrayElements释放
GetShortArrayElements jshortArray jshort ReleaseBooleanArrayElements释放
GetIntArrayElements jintArray jint ReleaseIntArrayElements释放
GetLongArrayElements jlongArray jlong ReleaseLongArrayElements释放
GetFloatArrayElements jfloatArray jfloat ReleaseFloatArrayElements释放
GetDoubleArrayElements jdoubleArray jdouble ReleaseDoubleArrayElements释放
GetObjectArrayElement 自定义对象 object
SetObjectArrayElement 自定义对象 object
GetArrayLength 获取数组大小
NewArray 创建一个指定长度的原始数据类型的数组
GetPrimitiveArrayCritical 得到指向原始数据类型内容的指针,该方法可能使垃圾回收不能执行,该方法可能返回数组的拷贝,因此必须释放此资源。
ReleasePrimitiveArrayCritical 释放指向原始数据类型内容的指针,该方法可能使垃圾回收不能执行,该方法可能返回数组的拷贝,因此必须释放此资源。
NewStringUTF jstring类型的方法转换
GetStringUTFChars jstring类型的方法转换
DefineClass 从原始类数据的缓冲区中加载类
FindClass 该函数用于加载本地定义的类。它将搜索由CLASSPATH 环境变量为具有指定名称的类所指定的目录和 zip文件
GetObjectClass 通过对象获取这个类。该函数比较简单,唯一注意的是对象不能为NULL,否则获取的class肯定返回也为NULL
GetSuperclass 获取父类或者说超类 。 如果 clazz 代表类class而非类 object,则该函数返回由 clazz 所指定的类的超类。 如果 clazz指定类 object 或代表某个接口,则该函数返回NULL
IsAssignableFrom 确定 clazz1 的对象是否可安全地强制转换为clazz2
Throw 抛出 java.lang.Throwable 对象
ThrowNew 利用指定类的消息(由 message 指定)构造异常对象并抛出该异常
ExceptionOccurred 确定是否某个异常正被抛出。在平台相关代码调用 ExceptionClear() 或 Java 代码处理该异常前,异常将始终保持抛出状态
ExceptionDescribe 将异常及堆栈的回溯输出到系统错误报告信道(例如 stderr)。该例程可便利调试操作
ExceptionClear 清除当前抛出的任何异常。如果当前无异常,则此例程不产生任何效果
FatalError 抛出致命错误并且不希望虚拟机进行修复。该函数无返回值
NewGlobalRef 创建 obj 参数所引用对象的新全局引用。obj 参数既可以是全局引用,也可以是局部引用。全局引用通过调用DeleteGlobalRef() 来显式撤消。
DeleteGlobalRef 删除 globalRef 所指向的全局引用
DeleteLocalRef 删除 localRef所指向的局部引用
AllocObject 分配新 Java 对象而不调用该对象的任何构造函数。返回该对象的引用。clazz 参数务必不要引用数组类。
getObjectClass 返回对象的类
IsSameObject 测试两个引用是否引用同一 Java 对象
NewString 利用 Unicode 字符数组构造新的 java.lang.String 对象
GetStringLength 返回 Java 字符串的长度(Unicode 字符数)
GetStringChars 返回指向字符串的 Unicode 字符数组的指针。该指针在调用 ReleaseStringchars() 前一直有效
ReleaseStringChars 通知虚拟机平台相关代码无需再访问 chars。参数chars 是一个指针,可通过 GetStringChars() 从 string 获得
NewStringUTF 利用 UTF-8 字符数组构造新 java.lang.String 对象
GetStringUTFLength 以字节为单位返回字符串的 UTF-8 长度
GetStringUTFChars 返回指向字符串的 UTF-8 字符数组的指针。该数组在被ReleaseStringUTFChars() 释放前将一直有效
ReleaseStringUTFChars 通知虚拟机平台相关代码无需再访问 utf。utf 参数是一个指针,可利用 GetStringUTFChars() 获得
NewObjectArray 构造新的数组,它将保存类 elementClass 中的对象。所有元素初始值均设为 initialElement
SetArrayRegion 将基本类型数组的某一区域从缓冲区中复制回来的一组函数
GetFieldID 返回类的实例(非静态)域的属性 ID。该域由其名称及签名指定。访问器函数的GetField 及 SetField系列使用域 ID 检索对象域。GetFieldID() 不能用于获取数组的长度域。应使用GetArrayLength()。
GetField 该访问器例程系列返回对象的实例(非静态)域的值。要访问的域由通过调用GetFieldID() 而得到的域 ID 指定。
SetField 该访问器例程系列设置对象的实例(非静态)属性的值。要访问的属性由通过调用SetFieldID() 而得到的属性 ID指定。
GetStaticFieldID
GetStaticField
SetStaticField 同上,只不过是静态属性。
GetMethodID 返回类或接口实例(非静态)方法的方法 ID。方法可在某个 clazz 的超类中定义,也可从 clazz 继承。该方法由其名称和签名决定。 GetMethodID() 可使未初始化的类初始化。要获得构造函数的方法 ID,应将 作为方法名,同时将void (V) 作为返回类型。
CallVoidMethod
CallObjectMethod
CallBooleanMethod
CallByteMethod
CallCharMethod
CallShortMethod
CallIntMethod
CallLongMethod
CallFloatMethod
CallDoubleMethod
GetStaticMethodID 调用静态方法
CallMethod
RegisterNatives 向 clazz 参数指定的类注册本地方法。methods 参数将指定 JNINativeMethod 结构的数组,其中包含本地方法的名称、签名和函数指针。nMethods 参数将指定数组中的本地方法数。
UnregisterNatives 取消注册类的本地方法。类将返回到链接或注册了本地方法函数前的状态。该函数不应在常规平台相关代码中使用。相反,它可以为某些程序提供一种重新加载和重新链接本地库的途径。

7.14 注意

7.14.1 UTF-8编码

JNI使用改进的UTF-8字符串来表示不同的字符类型。Java使用UTF-16编码。UTF-8编码主要使用于C语言,因为它的编码用\u000表示为0xc0,而不是通常的0×00。非空ASCII字符改进后的字符串编码中可以用一个字节表示

7.14.2 错误

JNI不会检查NullPointerException、IllegalArgumentException这样的错误,原因是:导致性能下降

在绝大多数C的库函数中,很难避免错误发生。
JNI允许用户使用Java异常处理。大部分JNI方法会返回错误代码但本身并不会报出异常。因此,很有必要在代码本身进行处理,将异常抛给Java。在JNI内部,首先会检查调用函数返回的错误代码,之后会调用ExpectOccurred()返回一个错误对象

jthrowable ExceptionOccurred(JNIEnv *env);  
例如:一些操作数组的JNI函数不会报错,因此可以调用ArrayIndexOutofBoundsException或ArrayStoreExpection方法报告异常。 

参考文献

  • JNI/NDK开发指南

    • JNI/NDK开发指南(开山篇)
    • JNI/NDK开发指南(一)—— JNI开发流程及HelloWorld
    • JNI/NDK开发指南(二)——JVM查找java native方法的规则
    • JNI/NDK开发指南(三)——JNI数据类型及与Java数据类型的映射关系
    • JNI/NDK开发指南(四)——字符串处理
    • Android NDK开发Crash错误定位
    • JNI/NDK开发指南(五)——访问数组(基本类型数组与对象数组)
    • JNI/NDK开发指南(六)——C/C++访问Java实例方法和静态方法
    • JNI/NDK开发指南(七)——C/C++访问Java实例变量和静态变量
    • JNI/NDK开发指南(八)——调用构造方法和父类实例方法
    • JNI/NDK开发指南(九)——JNI调用性能测试及优化
    • JNI/NDK开发指南(十)——JNI局部引用、全局引用和弱全局引用
    • JNI/NDK开发指南(十一)——JNI异常处理
    • Android JNI局部引用表溢出:local reference table overflow (max=512)

    • (4.1.27.1)Android—简单的JNI实例

    • (4.1.27.2)创建简单的JniDemo和Jni中打印log信息
    • (4.1.27.3)使用AndroidStudio编译NDK的方法
    • (4.1.27.4)使用AndroidStudio编译NDK的错误解决方案
    • (4.1.27.5)Jni打包及引用aar
    • (4.1.27.6)JNI 实战全面解析
    • (4.1.27.7)Android动态加载so文件
    • (4.1.27.8)Android动态加载补充 加载SD卡中的SO库
    • (4.1.27.9)JNI/NDK开发指南(一)——JVM查找java native方法的规则
    • (4.1.27.10)JNI/NDK开发指南(二)——JNI数据类型及与Java数据类型的映射关系
    • (4.1.27.11)JNI/NDK开发指南(三)——从字符串处理了解JNI的函数机制
    • (4.1.27.12)JNI/NDK开发指南(四)——Android NDK开发Crash错误定位
    • (4.1.27.13)JNI/NDK开发指南(五)访问数组(基本类型数组与对象数组)
    • (4.1.27.14)JNI/NDK开发指南(六)——C/C++访问Java实例方法和静态方法
    • (4.1.27.15)JNI/NDK开发指南(七)——C/C++访问Java实例变量和静态变量
    • (4.1.27.16)JNI/NDK开发指南(八)——调用构造方法和父类实例方法
    • (4.1.27.17)JNI/NDK开发指南(九)——JNI局部引用、全局引用和弱全局引用
    • (4.1.27.18)JNI/NDK开发指南(十)——JNI异常处理
    • (4.1.27.19)JNI/NDK开发指南(十一)——JNI调用性能测试及优化
  • JNI与Android VM之间的关系

你可能感兴趣的:((4.1.27)JNI)