API实现了Java和其他语言的通信(主要是C&C++)。从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI标准至少要保证本地代码能工作在任何Java虚拟机环境。
Android NDK官方原文档: developer.android.google.cn/ndk/
可以看到Android上层的Application和ApplicationFramework都是使用Java编写,底层包括系统和使用众多的Libraries都是C/C++编写的,所以上层Java要调用底层的C/C++函数库必须通过Java的JNI来实现
1.1 JNI 与 NDK 区别
1.2 JNI 作用
1.3 JNI在Android中作用: JNI可以调用本地代码库(即C/C++代码),并通过 Dalvik 虚拟机与应用层和应用框架层进行交互,Android中JNI代码主要位于应用层和应用框架层;
应用层: 该层是由JNI开发,主要使用标准JNI编程模型; 应用框架层: 使用的是Android中自定义的一套JNI编程模型,该自定义的JNI编程模型弥补了标准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中下载下列四项:
在File->Project Structure中添加下载好的Ndk路径,一般下载好了NDK后,下面有个"Select Default"的按钮:
在local.properties的文件中加入ndk的路径:
接下来在 Android studio3.0 中正式开发JNI ,Android studio已经支持创建C/C++的开发,并使用 CMake的模式构建NDK开发 。
创建过程一直Next下去,直到最后一步
这里需要特殊说明:
C++ Standard: Exceptions Support: Runtime Type Information Support:
点击Finish等待项目创建完成。
和以前唯一不同的就是多出了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(); } 复制代码
配置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
文件
可以看出,AS帮我们配置cmake时自动帮我们添加了,上述两块代码。但是在我们自己配置cmake工具时,需要自己手动填写,拷贝。
我们Build编译一下,在编译输出文件夹 可以看到:
。
libnative-lib.so
,然后 Gradle 将其打包到 APK 中; 注意:Instant Run 并不兼容使用了 native code 的项目。Android Studio 会自动禁止 Instant Run 功能。
.so库在apk里面:
//在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); } 复制代码
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字节码文件才能下一步
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"); } 复制代码
在"Program"是JDK的javah命令的路径,我的Mac结尾没有.exe,有的资料是javah.exe可能是Windows这样配置。
在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 生成之后如下图:
此时.h头文件已经被更新:
然后在C文件里继续实现就好。
添加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添加,下面两项:
这样整个JNI过程就完成了,下面就是靠自己学习编写JNI代码实现so逻辑了。
#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 层直接使用的:
需要注意的是,
1)引用类型不能直接在 Native 层使用,需要根据 JNI 函数进行类型的转化后,才能使用; 2)多维数组(含二维数组)都是引用类型,需要使用 jobjectArray 类型存取其值;
同样不能直接在 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 */ 复制代码
基本类型描述符下面是基本的数据类型的描述符,除了 boolean 和 long 类型分别是 Z 和 J 外,其他的描述符对应的都是Java类型名的大写首字母。另外,void 的描述符为 V
引用类型描述符一般引用类型描述符的规则如下,注意不要丢掉“;”: 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 代码,就不需要手动编写对应的类型转换了。
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 复制代码
由 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); } //…… } 复制代码
Java 的 native 方法是如何链接 C/C++中的函数的呢?可以通过静态和动态的方式注册JNI。
静态注册的方式有两个重要的关键词 JNIEXPORT
和 JNICALL
,这两个关键词是 宏定义
,主要是注明该函数式 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 的方式编译),函数命名后面就不需要加上“参数签名”。
另外还需要注意几点特殊规则:
_
,在c++里要用 _1
连接; __
连接; 参数签名
的斜杠 “/”
改为下划线 _
连接,分号 ;
改为 _2
连接,左方括号 [
改为 _3
连接; static 的为 jclass
, 非 static 的 为 jobject
;JNI 函数中是没有修饰符的。 优点:实现比较简单,可以通过 javah 工具将 Java代码的 native 方法直接转化为对应的native层代码的函数; 缺点:
动态注册原理: 直接告诉 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/…