转载

Android进阶3:Android的NDK开发-JNI基础

API实现了Java和其他语言的通信(主要是C&C++)。从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI标准至少要保证本地代码能工作在任何Java虚拟机环境。

Android NDK官方原文档: developer.android.google.cn/ndk/

官方的Android体系架构图

Android进阶3:Android的NDK开发-JNI基础

可以看到Android上层的Application和ApplicationFramework都是使用Java编写,底层包括系统和使用众多的Libraries都是C/C++编写的,所以上层Java要调用底层的C/C++函数库必须通过Java的JNI来实现

1.1 JNI 与 NDK 区别

  • JNI:JNI是一套编程接口,用来实现Java代码与本地的C/C++代码进行交互;
  • NDK: NDK是Google开发的一套开发和编译工具集,可以生成动态链接库,主要用于Android的JNI开发

1.2 JNI 作用

  • 扩展:JNI扩展了JVM能力,驱动开发,例如开发一个wifi驱动,可以将手机设置为无限路由;
  • 高效: 本地代码效率高,游戏渲染,音频视频处理等方面使用JNI调用本地代码,C语言可以灵活操作内存;
  • 复用: 在文件压缩算法 7zip开源代码库,机器视觉,OpenCV开放算法库等方面可以复用C平台上的代码,不必在开发一套完整的Java体系,避免重复发明轮子;
  • 特殊: 产品的核心技术一般也采用JNI开发,不易破解;

1.3 JNI在Android中作用: JNI可以调用本地代码库(即C/C++代码),并通过 Dalvik 虚拟机与应用层和应用框架层进行交互,Android中JNI代码主要位于应用层和应用框架层;

应用层: 该层是由JNI开发,主要使用标准JNI编程模型; 应用框架层: 使用的是Android中自定义的一套JNI编程模型,该自定义的JNI编程模型弥补了标准JNI编程模型的不足;

2. 两种基本的JNI开发流程

Android Studio版本:3.0.1 NDK下载和CMake下载

在Android Studio2.2以后,AS开始支持使用Cmake编译JNI的C++代码,使用LLDB调试程序。在此之前编译JNI代码使用ndk-build编译工具。

在Android Studio 3.0.1中配置jni需要在SDK Tools中下载支持JNI开发的配置,如下图 在SDK Manager->SDK tool中下载下列四项:

Android进阶3:Android的NDK开发-JNI基础

在File->Project Structure中添加下载好的Ndk路径,一般下载好了NDK后,下面有个"Select Default"的按钮:

Android进阶3:Android的NDK开发-JNI基础

在local.properties的文件中加入ndk的路径:

Android进阶3:Android的NDK开发-JNI基础

2.1 新建项目开发JNI

接下来在 Android studio3.0 中正式开发JNI ,Android studio已经支持创建C/C++的开发,并使用 CMake的模式构建NDK开发

  • 创建一个支持C/C++的Android项目
Android进阶3:Android的NDK开发-JNI基础

创建过程一直Next下去,直到最后一步

Android进阶3:Android的NDK开发-JNI基础

这里需要特殊说明:

C++ Standard:
Exceptions Support:
Runtime Type Information Support:

点击Finish等待项目创建完成。

  • 支持C/C++项目的整体结构
Android进阶3:Android的NDK开发-JNI基础

和以前唯一不同的就是多出了cpp目录以及External Build Files两个目录,那么这两个都有什么用呢?

cpp 目录
External Build Files 目录

native-lib.cpp 文件内容:

#include <jni.h>
#include <string>

extern "C"
JNIEXPORT jstring

JNICALL
Java_com_jni_demo_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

复制代码

简单可以看到,首先定义hello变量,之后return回该字符

MainActivity.java 文件的内容

public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    //应用启动时加载
    static {
        System.loadLibrary("native-lib");
    }

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

        // Example of a call to a native method
        TextView tv = (TextView) findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
     //native方法
    public native String stringFromJNI();
}

复制代码

Android Studio2.2起开始使用Cmake编译C++代码,当然也兼容之前的ndk-build的方式。

配置Cmake 查看官网文档: developer.android.com/studio/proj…

CMakeLists.txt ,简单的翻译下

# 有关使用CMake在Android Studio的更多信息,请阅读文档:https://d.android.com/studio/projects/add-native-code.html

# 设置CMake的最低版本构建本机所需库
cmake_minimum_required(VERSION 3.4.1)

# 创建并命名库,将其设置为静态的
# 或共享,并提供其源代码的相对路径。
# 你可以定义多个library库,并使用CMake来构建。
# Gradle会自动将包共享库关联到你的apk程序。

add_library( # 设置库的名称
             native-lib
             # 将库设置为共享库。
             SHARED
             # 为源文件提供一个相对路径。
             src/main/cpp/native-lib.cpp )
# 搜索指定预先构建的库和存储路径变量。因为CMake包括系统库搜索路径中默认情况下,只需要指定想添加公共NDK库的名称,在CMake验证库之前存在完成构建
find_library( # 设置path变量的名称
              log-lib
              # 在CMake定位前指定的NDK库名称
              log )
# 指定库CMake应该链接到目标库中,可以链接多个库,比如定义库,构建脚本,预先构建的第三方库或者系统库
target_link_libraries( # 指定目标库
                       native-lib
                       # 目标库到日志库的链接 包含在NDK
                       ${log-lib} )

复制代码

build.gradle 文件

Android进阶3:Android的NDK开发-JNI基础

可以看出,AS帮我们配置cmake时自动帮我们添加了,上述两块代码。但是在我们自己配置cmake工具时,需要自己手动填写,拷贝。

我们Build编译一下,在编译输出文件夹 可以看到:

Android进阶3:Android的NDK开发-JNI基础

编译到运行示例 APP 的流程过程:

  1. Gradle 调用外部构建脚本,也就是 CMakeLists.txt;
  2. CMake 会根据构建脚本的指令去编译一个 C++ 源文件 ,也就是 native-lib.cpp,并将编译后的产物扔进 共享对象库 中,并将其命名为 libnative-lib.so ,然后 Gradle 将其打包到 APK 中;
  3. 在运行期间,APP 的 MainActivity 会调用 System.loadLibrary() 方法,加载 native library。而这个库的原生函数,stringFromJNI(),就可以为 APP 所用了;
  4. MainActivity.onCreate() 方法会调用 stringFromJNI(),然后返回 “Hello from C++”,并更新 TextView 的显示; 注意:Instant Run 并不兼容使用了 native code 的项目。Android Studio 会自动禁止 Instant Run 功能。

.so库在apk里面:

Android进阶3:Android的NDK开发-JNI基础

在原项目基础上增加自己的代码

//在MainActivity.java中增加一个native方法
public native String getHelloJni();
复制代码

你会发现 getHelloJni( ) 方法是红色的。不要急,按住Alt+Enter回车后,系统会自动为你在之前.cpp文件中创建一个getHelloJni( )的C++代码,是不是很智能……

#include <jni.h>
#include <string>

extern "C"
JNIEXPORT jstring

JNICALL
Java_com_jni_demo_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_jni_demo_MainActivity_getHelloJni(JNIEnv *env, jobject instance) {

    // TODO


    return env->NewStringUTF(returnValue);
}

复制代码

在原项目基础上修改库的名字

Android进阶3:Android的NDK开发-JNI基础
Android进阶3:Android的NDK开发-JNI基础
Android进阶3:Android的NDK开发-JNI基础

2.2 在现有Project上开发JNI

  • 第一步: 在java文件夹中新建JniTest.java,把需要的native方法先定义好。
package com.baidu.jni.demo;

/**
 * <p/>
 * 功能 :
 * <p/>
 * <p>
 * <p>Copyright baidu.com 2018 All right reserved</p>
 *
 * @author tuke 时间 2018/6/1
 * @email tuke@baidu.com
 * <p>
 * 最后修改人 无
 */
public class JniTest {

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

    public  native String getHello(String string, int[] ints);

    public native String getSayBaibai(int a ,float b,boolean c);

}

复制代码

然后build 项目,在build文件夹找到是否生成JniTest.class字节码文件. 只有生成了.class字节码文件才能下一步

Android进阶3:Android的NDK开发-JNI基础
  • 第二步:需要使用javah 命令生成.h头文件。

    • 1,手动命令生成 在AS下的Terminal 进入当前工程目录:
cd app/build/intermediates/classes/debug
复制代码

然后通过命令行:

javah -jni com.baidu.jni.demo.JniTest
复制代码

JDK 10以后移除了javah命令, JDK10、JDK11、JDK12新特性 ,使用javac -h . xxxx.java 代替

其中com.baidu.jni.demo.是包名,JniTest是java代码。 然后会在当前目录下生成: com_baidu_jni_demo_JniTest.h 头文件 然后在src->main 新建jni文件夹,新建xxx.cpp文件,并且把刚才生成的com_baidu_jni_demo_JniTest.h头文件,拷贝到jni文件夹,编写jnitest.c如下:

//
// Created by Tu,Ke on 2018/6/1.
//

#include "com_baidu_jni_demo_JniTest.h"
//extern "C"
JNIEXPORT jstring JNICALL
Java_com_baidu_jni_demo_JniTest_getHello
        (JNIEnv * env, jclass, jstring, jintArray) {
    return env->NewStringUTF("helloworld");
}
//extern "C"
JNIEXPORT jstring JNICALL
Java_com_baidu_jni_demo_JniTest_getSayBaibai(JNIEnv *env, jobject instance, jint a, jfloat b,
                                             jboolean c) {

    // TODO
    return env->NewStringUTF("helloworld");
}
复制代码
  • 2,工具生成javah命令 在Android Studio 的Preference 中的External Tool中新建工具:
Android进阶3:Android的NDK开发-JNI基础

在"Program"是JDK的javah命令的路径,我的Mac结尾没有.exe,有的资料是javah.exe可能是Windows这样配置。

Android进阶3:Android的NDK开发-JNI基础

在JniTest.java文件右键,选择External Tool ->javah -jni 然后会自动在src->main下新建一个jni文件夹,并且自动生成com_baidu_jni_demo_JniTest.h头文件,和第一种方法一模一样。

接着新建C文件,include进来就OK。

在JniTest.java文件中新建一个native方法,开始是红色的,使用上面的External Tool->javah -jni 生成之后如下图:

Android进阶3:Android的NDK开发-JNI基础

此时.h头文件已经被更新:

Android进阶3:Android的NDK开发-JNI基础

然后在C文件里继续实现就好。

  • 第三步:还是使用Cmake编译C++代码,和上面不同的是 手动添加CmakeLists.txt文件,并修改build.gradle文件

添加CmakeLists.txt文件,在app的目录下:

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.
             sayhello

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             src/main/jni/hello.cpp )

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
                       sayhello

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )
复制代码

build.gradle添加,下面两项:

Android进阶3:Android的NDK开发-JNI基础

这样整个JNI过程就完成了,下面就是靠自己学习编写JNI代码实现so逻辑了。

3.JNI数据类型

  • JNI数据类型映射 由头文件代码可以看到,jni.h有很多类型预编译的定义,并且区分了 C 和 C++的不同环境。
#ifdef HAVE_INTTYPES_H
# include <inttypes.h>      /* C99 */
typedef uint8_t         jboolean;       /* unsigned 8 bits */
typedef int8_t          jbyte;          /* signed 8 bits */
typedef uint16_t        jchar;          /* unsigned 16 bits */
typedef int16_t         jshort;         /* signed 16 bits */
typedef int32_t         jint;           /* signed 32 bits */
typedef int64_t         jlong;          /* signed 64 bits */
typedef float           jfloat;         /* 32-bit IEEE 754 */
typedef double          jdouble;        /* 64-bit IEEE 754 */
#else
typedef unsigned char   jboolean;       /* unsigned 8 bits */
typedef signed char     jbyte;          /* signed 8 bits */
typedef unsigned short  jchar;          /* unsigned 16 bits */
typedef short           jshort;         /* signed 16 bits */
typedef int             jint;           /* signed 32 bits */
typedef long long       jlong;          /* signed 64 bits */
typedef float           jfloat;         /* 32-bit IEEE 754 */
typedef double          jdouble;        /* 64-bit IEEE 754 */
#endif

/* "cardinal indices and sizes" */
typedef jint            jsize;

#ifdef __cplusplus
/*
 * Reference types, in C++
 */
class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};
class _jbooleanArray : public _jarray {};
//……

typedef _jobject*       jobject;
typedef _jclass*        jclass;
typedef _jstring*       jstring;
typedef _jarray*        jarray;
typedef _jobjectArray*  jobjectArray;
typedef _jbooleanArray* jbooleanArray;
//……

#else /* not __cplusplus */

/*
 * Reference types, in C.
 */
typedef void*           jobject;
typedef jobject         jclass;
typedef jobject         jstring;
typedef jobject         jarray;
typedef jarray          jobjectArray;
typedef jarray          jbooleanArray;
//……

#endif

复制代码

当是C++环境时, jobject, jclass, jstring, jarray 等都是继承自_jobject类,而在 C 语言环境是,则它的本质都是空类型指针 typedef void* jobject ;

  • 基本数据类型

下图是Java基本数据类型和本地类型的映射关系,这些基本数据类型都是可以直接在 Native 层直接使用的:

Android进阶3:Android的NDK开发-JNI基础
  • 引用数据类型 另外,还有引用数据类型和本地类型的映射关系:
Android进阶3:Android的NDK开发-JNI基础

需要注意的是,

1)引用类型不能直接在 Native 层使用,需要根据 JNI 函数进行类型的转化后,才能使用; 2)多维数组(含二维数组)都是引用类型,需要使用 jobjectArray 类型存取其值;

  • 方法和变量 ID

同样不能直接在 Native 层使用。当 Native 层需要调用 Java 的某个方法时,需要通过 JNI 函数获取它的 ID,根据 ID 调用 JNI 函数获取该方法;变量的获取也是类似。ID 的结构体如下:

struct _jfieldID;                       /* opaque structure */
typedef struct _jfieldID* jfieldID;     /* field IDs */

struct _jmethodID;                      /* opaque structure */
typedef struct _jmethodID* jmethodID;   /* method IDs */

复制代码

4.JNI 描述符

  • 域描述符
    • 基本类型描述符下面是基本的数据类型的描述符,除了 boolean 和 long 类型分别是 Z 和 J 外,其他的描述符对应的都是Java类型名的大写首字母。另外,void 的描述符为 V

      Android进阶3:Android的NDK开发-JNI基础
    • 引用类型描述符一般引用类型描述符的规则如下,注意不要丢掉“;”: L + 类描述符 + ; 如,String 类型的域描述符为: Ljava/lang/String; 数组的域描述符特殊一点,如下,其中有多少级数组就有多少个“[”,数组的类型为类时,则有分号,为基本类型时没有分号 [ + 其类型的域描述符

例如:

int[]    描述符为 [I
double[] 描述符为 [D
String[] 描述符为 [Ljava/lang/String;
Object[] 描述符为 [Ljava/lang/Object;
int[][]  描述符为 [[I
double[][] 描述符为 [[D

复制代码

对应在 jni.h 获取 Java 的字段的 native 函数如下, name为 Java 的字段名字,sig 为域描述符

//C
jfieldID    (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);
jobject     (*GetObjectField)(JNIEnv*, jobject, jfieldID);
//C++
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
    { return functions->GetFieldID(this, clazz, name, sig); }
jobject GetObjectField(jobject obj, jfieldID fieldID)
    { return functions->GetObjectField(this, obj, fieldID); }

复制代码
  • 类描述符

类描述符是类的完整名称:包名+类名,java 中包名用 . 分割,jni 中改为用 / 分割 如,Java 中 java.lang.String 类的描述符为 java/lang/String native 层获取 Java 的类对象,需要通过 FindClass() 函数获取 , jni.h 的函数定义如下:

//C
jclass  (*FindClass)(JNIEnv*, const char*);
//C++
jclass FindClass(const char* name)
    { return functions->FindClass(this, name); }

复制代码

name 就是类的引用类型描述符,如 Java 对象 cn.cfanr.jni.JniTest,对应字符串为Lcn/cfanr/jni/JniTest; 如下:

jclass jclazz = env->FindClass("Lcn/cfanr/jni/JniTest;");

复制代码
  • 方法描述符

方法描述符需要将 所有参数类型的域描述符按照声明顺序放入括号 ,然后再加上 返回值类型的域描述符 ,其中没有参数时,不需要括号,如下规则:

(参数……)返回类型
复制代码

例如:

Java 层方法   ——>  JNI 函数签名
String getString()  ——>  Ljava/lang/String;
int sum(int a, int b)  ——>  (II)I
void main(String[] args) ——> ([Ljava/lang/String;)V

复制代码

另外, 对应在 jni.h 获取 Java 方法的 native 函数 如下,其中 jclass 是获取到的类对象,name 是 Java 对应的方法名字,sig 就是上面说的方法描述符: 所有参数就是 类的对象,函数名,方法描述符(其实就包含参数列表和返回值类型了)

//C
jmethodID   (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
//C++
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
    { return functions->GetMethodID(this, clazz, name, sig); }


复制代码

不过在实际编程中,如果使用 javah 工具来生成对应的 native 代码,就不需要手动编写对应的类型转换了。

5.JNIEnv 分析

JNIEnv 是 jni.h 文件最重要的部分,它的本质是指向函数表指针的指针(JavaVM也是),函数表里面定义了很多 JNI 函数,同时它也是区分 C 和 C++环境的(由上面介绍描述符时也可以看到),在 C 语言环境中,JNIEnv 是strut JNINativeInterface*的指针别名。

struct _JNIEnv;
struct _JavaVM;
typedef const struct JNINativeInterface* C_JNIEnv;

#if defined(__cplusplus)  
typedef _JNIEnv JNIEnv;   //C++中的 JNIEnv 类型
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;  //C语言的 JNIEnv 类型
typedef const struct JNIInvokeInterface* JavaVM;
#endif


复制代码
  • JNIEnv的特点

    • JNIEnv 是一个指针,指向一组 JNI 函数,通过这些函数可以实现 Java 层和 JNI 层的交互,就是说通过 JNIEnv 调用 JNI 函数可以访问 Java 虚拟机,操作 Java 对象;
    • 所有本地函数都会接收 JNIEnv 作为第一个参数;(不过 C++ 的JNI 函数已经对 JNIEnv 参数进行了封装,不用写在函数参数上)
    • 用作线程局部存储,不能在线程间共享一个 JNIEnv 变量,也就是说 JNIEnv 只在创建它的线程有效,不能跨线程传递;相同的 Java 线程调用本地方法,所使用的 JNIEnv 是相同的,一个 native 方法不能被不同的 Java 线程调用;
  • C++的 JNIEnv

typedef _JNIEnv JNIEnv; 可知,C++的 JNIEnv 是 _JNIEnv 结构体,而 _JNIEnv 结构体定义了 JNINativeInterface 的结构体指针,内部定义的函数实际上是调用 JNINativeInterface 的函数,所以C++的 env 是一级指针,调用时不需要加 env 作为函数的参数,例如: env->NewStringUTF(env, "hello")

struct _JNIEnv {
    /* do not rename this; it does not seem to be entirely opaque */
    const struct JNINativeInterface* functions;
#if defined(__cplusplus)
    jint GetVersion()
    { return functions->GetVersion(this); }

    jclass DefineClass(const char *name, jobject loader, const jbyte* buf, jsize bufLen)
    { return functions->DefineClass(this, name, loader, buf, bufLen); }

    jclass FindClass(const char* name)
    { return functions->FindClass(this, name); }

    jmethodID FromReflectedMethod(jobject method)
    { return functions->FromReflectedMethod(this, method); }

    jfieldID FromReflectedField(jobject field)
    { return functions->FromReflectedField(this, field); }

    jobject ToReflectedMethod(jclass cls, jmethodID methodID, jboolean isStatic)
    { return functions->ToReflectedMethod(this, cls, methodID, isStatic); }

    jclass GetSuperclass(jclass clazz)
    { return functions->GetSuperclass(this, clazz); }
    //……
}

复制代码

6.JNI 的两种注册方式

Java 的 native 方法是如何链接 C/C++中的函数的呢?可以通过静态和动态的方式注册JNI。

  • 静态注册 :原理:根据函数名建立 Java 方法和 JNI 函数的一一对应关系。流程如下:
    • 先编写 Java 的 native 方法;
    • 然后用 javah 工具生成对应的头文件,执行命令 javah packagename.classname可以生成由包名加类名命名的 jni 层头文件,或执行命名javah -o custom.h packagename.classname,其中 custom.h 为自定义的文件名;
    • 实现 JNI 里面的函数,再在Java中通过System.loadLibrary加载 so 库即可;

静态注册的方式有两个重要的关键词 JNIEXPORTJNICALL ,这两个关键词是 宏定义 ,主要是注明该函数式 JNI 函数, 当虚拟机加载 so 库时,如果发现函数含有这两个宏定义时,就会链接到对应的 Java 层的 native 方法

由前面生成头文件的方法,重新创建一个cn.cfanr.test_jni.Jni_Test.java的类

public class Jni_Test {
    private static native int swap();

    private static native void swap(int a, int b);

    private static native void swap(String a, String b);

    private native void swap(int[] arr, int a, int b);

    private static native void swap_0(int a, int b);
}

复制代码

用 javah 工具生成以下头文件:

#include <jni.h>
/* Header for class cn_cfanr_test_jni_Jni_Test */

#ifndef _Included_cn_cfanr_test_jni_Jni_Test
#define _Included_cn_cfanr_test_jni_Jni_Test
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     cn_cfanr_test_jni_Jni_Test
 * Method:    swap
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_cn_cfanr_test_1jni_Jni_1Test_swap__
  (JNIEnv *, jclass);    // 凡是重载的方法,方法后面都会多一个下划线

/*
 * Class:     cn_cfanr_test_jni_Jni_Test
 * Method:    swap
 * Signature: (II)V
 */
JNIEXPORT void JNICALL Java_cn_cfanr_test_1jni_Jni_1Test_swap__II
  (JNIEnv *, jclass, jint, jint);

/*
 * Class:     cn_cfanr_test_jni_Jni_Test
 * Method:    swap
 * Signature: (Ljava/lang/String;Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_cn_cfanr_test_1jni_Jni_1Test_swap__Ljava_lang_String_2Ljava_lang_String_2
  (JNIEnv *, jclass, jstring, jstring);

/*
 * Class:     cn_cfanr_test_jni_Jni_Test
 * Method:    swap
 * Signature: ([III)V
 */
JNIEXPORT void JNICALL Java_cn_cfanr_test_1jni_Jni_1Test_swap___3III
  (JNIEnv *, jobject, jintArray, jint, jint);  // 非 static 的为 jobject

/*
 * Class:     cn_cfanr_test_jni_Jni_Test
 * Method:    swap_0
 * Signature: (II)V
 */
JNIEXPORT void JNICALL Java_cn_cfanr_test_1jni_Jni_1Test_swap_10   
  (JNIEnv *, jclass, jint, jint);   // 不知道为什么后面没有 II

#ifdef __cplusplus
}
#endif
#endif


复制代码

可以看出 JNI 的调用函数的定义是按照一定规则命名的: JNIEXPORT 返回值 JNICALL Java_全路径类名_方法名_参数签名(JNIEnv* , jclass, 其它参数); 其中 Java_ 是为了标识该函数来源于Java。 经检验(不一定正确), 如果是重载的方法,则有“参数签名”,否则没有 ;另外如果使用的是 C++,在函数前面加上 extern “C”(表示按照 C 的方式编译),函数命名后面就不需要加上“参数签名”。

另外还需要注意几点特殊规则:

  1. java的包名或类名或方法名中含下划线 _ ,在c++里要用 _1 连接;
  2. 重载的本地方法命名要用双下划线 __ 连接;
  3. 参数签名 的斜杠 “/” 改为下划线 _ 连接,分号 ; 改为 _2 连接,左方括号 [ 改为 _3 连接;
  4. 对于 Java 的 native 方法,static 和非 static 方法的区别在于第二个参数, static 的为 jclass非 static 的 为 jobject ;JNI 函数中是没有修饰符的。

优点:实现比较简单,可以通过 javah 工具将 Java代码的 native 方法直接转化为对应的native层代码的函数; 缺点:

  1. javah 生成的 native 层函数名特别长,可读性很差;
  2. 后期修改文件名、类名或函数名时,头文件的函数将失效,需要重新生成或手动改,比较麻烦;
  3. 程序运行效率低,首次调用 native 函数时,需要根据函数名在 JNI 层搜索对应的本地函数,建立对应关系,有点耗时;
  • 动态注册原理: 直接告诉 native 方法其在JNI 中对应函数的指针 。通过使用 JNINativeMethod 结构来保存 Java native 方法和 JNI 函数关联关系 ,步骤:

  • 先编写 Java 的 native 方法;

  • 编写 JNI 函数的实现( 函数名可以随便命名 );

  • 利用结构体 JNINativeMethod 保存Java native方法和 JNI函数的对应关系

  • 利用 registerNatives(JNIEnv* env) 注册类的所有本地方法;

  • JNI_OnLoad 方法中调用注册方法;

  • 在Java中通过System.loadLibrary加载完JNI动态库之后, 会调用JNI_OnLoad函数,完成动态注册

jni.h中的

//JNINativeMethod结构体
typedef struct {
    const char* name;       //Java中native方法的名字
    const char* signature;  //Java中native方法的描述符
    void*       fnPtr;      //对应JNI函数的指针
} JNINativeMethod;

/**
 * @param clazz java类名,通过 FindClass 获取
 * @param methods JNINativeMethod 结构体指针
 * @param nMethods 方法个数
 */
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)

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

复制代码

参考链接:

www.jianshu.com/p/ac00d5999… zhixinliu.com/2015/07/01/…

原文  https://juejin.im/post/5da3e4775188254c4e6a984d
正文到此结束
Loading...