API实现了Java和其余语言的通讯(主要是C&C++)。从Java1.1开始,JNI标准成为java平台的一部分,它容许Java代码和其余语言写的代码进行交互。JNI标准至少要保证本地代码能工做在任何Java虚拟机环境。html
Android NDK官方原文档:developer.android.google.cn/ndk/java
官方的Android体系架构图android
能够看到Android上层的Application和ApplicationFramework都是使用Java编写,底层包括系统和使用众多的Libraries都是C/C++编写的,因此上层Java要调用底层的C/C++函数库必须经过Java的JNI来实现c++
1.1 JNI 与 NDK 区别git
1.2 JNI 做用github
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中下载下列四项:
接下来在Android studio3.0中正式开发JNI ,Android studio已经支持建立C/C++的开发,并使用CMake的模式构建NDK开发。
C++ Standard:
使用下拉列表选择您但愿使用哪一种 C++ 标准。选择 Toolchain Default 会使用默认的 CMake 设置;Exceptions Support:
若是您但愿启用对 C++ 异常处理的支持,请选中此复选框。若是启用此复选框,Android Studio 会将 -fexceptions 标志添加到模块级 build.gradle 文件的 cppFlags 中,Gradle 会将其传递到 CMake;Runtime Type Information Support:
若是您但愿支持 RTTI,请选中此复选框。若是启用此复选框,Android Studio 会将 -frtti 标志添加到模块级 build.gradle 文件的 cppFlags 中,Gradle 会将其传递到 CMake。点击Finish等待项目建立完成。
和之前惟一不一样的就是多出了cpp目录以及External Build Files两个目录,那么这两个都有什么用呢?
cpp 目录
存放全部 native code 的地方,包括源码,头文件,预编译项目等。对于新项目,Android Studio 建立了一个 C++ 模板文件:native-lib.cpp,而且将该文件放到了你的 app 模块的 src/main/cpp/ 目录下。这份模板代码提供了一个简答的 C++ 函数:stringFromJNI(),该函数返回一个字符串:”Hello from C++”External Build Files 目录
存放 CMake 或 ndk-build 构建脚本的地方。有点相似于 build.gradle 文件告诉 Gradle 如何编译你的APP 同样,CMake 和 ndk-build 也须要一个脚原本告知如何编译你的 native library。对于一个新的项目,Android Studio 建立了一个 CMake脚本:CMakeLists.txt,而且将其放到了你的 module 的根目录下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
文件
能够看出,AS帮咱们配置cmake时自动帮咱们添加了,上述两块代码。可是在咱们本身配置cmake工具时,须要本身手动填写,拷贝。
咱们Build编译一下,在编译输出文件夹 能够看到:
编译到运行示例 APP 的流程过程:
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字节码文件才能下一步
第二步:须要使用javah 命令生成.h头文件。
cd app/build/intermediates/classes/debug
复制代码
而后经过命令行:
javah -jni com.baidu.jni.demo.JniTest
复制代码
JDK 10之后移除了javah命令,JDK十、JDK十一、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
复制代码
JNIEnv的特色
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); }
//……
}
复制代码
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);
复制代码
参考连接: