准备工作
-
1.开发环境
-
2.AndroidStudio版本
-
3.在SDKManager中下载NDK工具
静态注册Native函数
- 创建Android项目
2.创建native方法的工具类JniTest
,代码如下
package com.xc.jnitest.exercise;
public class JniTest {
public native String get();
public native void set(String str);
}
3.修改MainActivity以及layout文件
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textview1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
MainActivity
public class MainActivity extends AppCompatActivity {
TextView mText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mText = findViewById(R.id.textview1);
JniTest jniTest = new JniTest();
mText.setText(jniTest.get());
}
}
4.生成.class
文件
- 可以在Terminal中使用 “
javac com/xc/jnitest/exercise/JniTest.java
”命令生成.class
文件 - 在AndroidStudio中可以直接
Build
->Make Project
生成,如下图
使用Terminal
生成的.class
文件在同级目录下,使用Build
->Make project
生成的在build
目录下,如下图
5.在Terminal
中cd
到相应目录下,执行javah -jni
命令生成.h
头文件
- 在
java
目录下执行javah -jni com.xc.jnitest.exercise.JniTest
命令 - 在
build/intermediates/javac/debug/compileDebugJavaWithJavac/classes
目录下执行javah -jni com.xc.jnitest.exercise.JniTest
命令
生成对应的.h
头文件如下图
.h
文件内容如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_xc_jnitest_exercise_JniTest */
#ifndef _Included_com_xc_jnitest_exercise_JniTest
#define _Included_com_xc_jnitest_exercise_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_xc_jnitest_exercise_JniTest
* Method: get
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_xc_jnitest_exercise_JniTest_get
(JNIEnv *, jobject);
/*
* Class: com_xc_jnitest_exercise_JniTest
* Method: set
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_com_xc_jnitest_exercise_JniTest_set
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
JNIEnv*
:表示一个纸箱JNI
环境的指针,可以通过它来访问JNI
提供的接口方法;
jobject
:表示Java对象中的this
JNIEXPORT
和JNICALL
:他们是JNI
中所定义的宏,可以在jni.h
这个头文件中查找到。
jstring
: 是返回值类型
Java_com_xc_jnitest_exercise
是包名
JniTest
: 是类名
get
: 是方法名
- 上边只是让你更理解创建步骤,如果嫌麻烦,可以直接使用
javah -d ../jni com.xc.jnitest.exercise.JniTest
创建.h
头文件,当然这里就可以省略生成.class
文件的步骤了,其中
-d ../jni
是指定要生成的头文件到那个目录下
6.在main
目录下创建一个jni
文件夹,将刚才生成的.h
文件剪切过来。在jni目录下新建一个c++
文件。命名为jni-test.cpp
。(注:c
文件的后缀为.c
,c++
文件后缀为.cpp
)
如下图:
7.编写jni-test.cpp
文件
#include <jni.h>
#include <stdio.h>
#include "com_xc_jnitest_exercise_JniTest.h"
JNIEXPORT jstring JNICALL Java_com_xc_jnitest_exercise_JniTest_get
(JNIEnv *env, jobject obj ){
return env->NewStringUTF("Hello from JNI !");
}
/*
* Class: com_xc_jnitest_exercise_JniTest
* Method: set
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_com_xc_jnitest_exercise_JniTest_set
(JNIEnv *env, jobject obj, jstring string){
char* str = (char*) env->GetStringUTFChars(string,NULL);
env->ReleaseStringUTFChars(string,str);
}
-
#inclued "com_xc_jnitest_exercise_JniTest.h"
添加头文件 - 实现两个方法方法
- 在
jni
目录下添加Android.mk
文件
LOCAL_PATH := $(call my-dir) //
include $(CLEAR_VARS)
LOCAL_MODULE := jni-test
LOCAL_SRC_FILES := jni-test.cpp
include $(BUILD_SHARED_LIBRARY)
-
LOCAL_PATH := $(call my-dir)
:每个Android.mk文件必须以定义开始。它用于在开发tree中查找源文件。宏my-dir
则由Build System 提供。返回包含Android.mk目录路径。 -
include $(CLEAR_VARS)
:CLEAR_VARS
变量由Build System提供。并指向一个指定的GNU Makefile,由它负责清理很多LOCAL_xxx。例如LOCAL_MODULE,LOCAL_SRC_FILES,LOCAL_STATIC_LIBRARIES等等。但不是清理LOCAL_PATH。这个清理是必须的,因为所有的编译控制文件由同一个GNU Make解析和执行,其变量是全局的。所以清理后才能便面相互影响。 -
LOCAL_MODULE := jni-test
:LOCAL_MODULE模块必须定义,以表示Android.mk中的每一个模块。名字必须唯一且不包含空格。Build System 会自动添加适当的前缀和后缀。例如,demo,要生成动态库,则生成libdemo.so。但请注意:如果模块名字被定义为libabd,则生成libabc.so。不再添加前缀。 -
LOCAL_SRC_FILES := ndkdemotest.c
:这行代码表示将要打包的C/C++源码。不必列出头文件,build System 会自动帮我们找出依赖文件。缺省的C++ 源码的扩展名为.cpp。 -
include $(BUILD_SHARED_LIBRARY)
:BUILD_SHARED_LIBRARY
是Build System提供的一个变量,指向一个GUN Makefile Script。它负责收集自从上次调用include $(CLEAR_VARS)
后的所有LOCAL_xxxxinx。并决定编译什么类型-
BUILD_STATIC_LIBRARY
:编译为静态库 -
BUILD_SHARED_LIBRARY
:编译为动态库 -
BUILD_EXECUTABLE
:编译为Native C 可执行程序 -
BUILD_PREBUILT
:该模块已经预先编译
-
9.配置app module的build.gradle
文件
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.xc.jnitest"
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
ndk{
moduleName "jni-test"
abiFilters "armeabi-v7a", "x86"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
}
}
sourceSets.main {
jni.srcDirs = []
jniLibs.srcDirs = ['src/main/jniLibs']
}
}
配置后就可以生成.so
文件了(PS:项目Build之后)
- 加载
so
库
package com.xc.jnitest.exercise;
public class JniTest {
static{
System.loadLibrary("jni-test");
}
public static native String get();
public static native void set(String str);
}
11.运行项目
动态注册Native函数
- 不必忍受冗长的函数名,自由命名函数名,在
JNI_OnLoad
方法里进行注册。
1.修改生成后的.h
头文件名为jni-test.h
,如下
修改
.h
文件方法名
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_xc_jnitest_exercise_JniTest */
#ifndef _Included_com_xc_jnitest_exercise_JniTest
#define _Included_com_xc_jnitest_exercise_JniTest
#ifdef __cplusplus
extern "C" {
#endif
jstring get (JNIEnv *, jobject);
void set (JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
2.修改.cpp
文件如下
#include <jni.h>
#include <stdio.h>
#include "jni-test.h"
jstring get(JNIEnv *env, jobject obj) {
return env->NewStringUTF("Hello from JNI !");
}
void set(JNIEnv *env, jobject obj, jstring string) {
char *str = (char *) env->GetStringUTFChars(string, NULL);
env->ReleaseStringUTFChars(string, str);
}
3.添加参数映射函数
//参数映射表
static JNINativeMethod getMethods[] = {
{"get", "()Ljava/lang/String;", (void *) get},
{"set", "(Ljava/lang/String;)", (void *) set},
};
它的返回值是JNINativeMethod
类型:
JNI允许我们提供一个函数映射表,注册给Java虚拟机,这样JVM就可以用函数映射表来调用相应的函数。这样就可以不必通过函数名来查找需要调用的函数了。Java与JNI通过JNINativeMethod
的结构来建立联系,它被定义在jni.h中,其结构内容如下:
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
- 第一个变量
name
,代表的是Java中的函数名 - 第二个变量
signature
,代表的是Java中的参数和返回值 - 第三个变量
fnPtr
,代表的是的指向C函数的函数指针
第二个变量signature
定义如下:
(参数1类型标示;参数2类型标示;参数3类型标示...)返回值类型标示
当参数为引用类型的时候,参数类型的标示的根式为"L包名",其中包名的.(点)要换成"/",比如String
就是Ljava/lang/String
,Menu
为Landroid/view/Menu
,如果返回值是void
,对应的签名就是V
。
如果是基本类类型,其签名如下(除了boolean和long,其他都是首字母大写):
类型标示 | Java类型 |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
数组类型:
类型标示 | Java类型 |
---|---|
[签名 | 数组 |
[i | int[] |
[Ljava/lang/Object | String[] |
- 可以使用JDK的
javap -s com.xc.jnitest.exercise.JniTest
直接查看他的signature
,这里的com.xc.jnitest.exercise.JniTest
为class
文件路径
4.注册native
方法
//native类路径
static const char *className = "com/com/xc/jnitest/exercise/JniTest";
//注册native方法
static int registerNatives(JNIEnv *engv) {
jclass clazz;
clazz = engv->FindClass(className); //找到native类
if (clazz == NULL) {
return JNI_FALSE;
}
//int len = sizeof(methods) / sizeof(methods[0]);
if (engv->RegisterNatives(clazz, getMethods,
sizeof(getMethods) / sizeof(getMethods[0])) <
0) {
return JNI_FALSE;
}
return JNI_TRUE;
}
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
jint result = -1;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return result;
}
assert(env != NULL);
//为了方便管理我们将不同java类中的native方法分别注册
if (registerNatives(env) < 0) { //注册native方法
return result;
}
//如果还有别的native类,可继续在此进行注册
return JNI_VERSION_1_6;
}
jni-test.cpp
完整代码如下
#include <jni.h>
#include <stdio.h>
#include <assert.h>
#include "jni-test.h"
jstring get(JNIEnv *env, jobject obj) {
return env->NewStringUTF("Hello from JNI !");
}
void set(JNIEnv *env, jobject obj, jstring string) {
char *str = (char *) env->GetStringUTFChars(string, NULL);
env->ReleaseStringUTFChars(string, str);
}
//参数映射表
static JNINativeMethod getMethods[] = {
{"get", "()Ljava/lang/String;", (void *) get},
{"set", "(Ljava/lang/String;)V", (void *) set},
};
//native类路径
static const char *className = "com/xc/jnitest/exercise/JniTest";
//注册native方法
static int registerNatives(JNIEnv *engv) {
jclass clazz;
clazz = engv->FindClass(className); //找到native类
if (clazz == NULL) {
return JNI_FALSE;
}
//int len = sizeof(methods) / sizeof(methods[0]);
if (engv->RegisterNatives(clazz, getMethods,
sizeof(getMethods) / sizeof(getMethods[0])) <
0) {
return JNI_FALSE;
}
return JNI_TRUE;
}
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
jint result = -1;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return result;
}
assert(env != NULL);
//为了方便管理我们将不同java类中的native方法分别注册
if (registerNatives(env) < 0) { //注册native方法
return result;
}
//如果还有别的native类,可继续在此进行注册
return JNI_VERSION_1_6;
}
5.齐活,运行!
native代码反调用Java层代码
1.获取class
对象:
-
jclass FindClass(const char* clsName)
:
通过类的名称(类的全名,这时候包名不是用'"."点号而是用"/"来区分的)来获取jclass。比如:
jclass jcl_string=env->FindClass("java/lang/String");
-
jclass GetObjectClass(jobject obj)
通过对象实例来获取jclass,相当于Java中的getClass()函数 -
jclass getSuperClass(jclass obj)
通过jclass可以获取其父类的jclass对象
2.获取属性方法
在Native本地代码中访问Java层的代码,一个常用的常见的场景就是获取Java类的属性和方法。所以为了在C/C++获取Java层的属性和方法,JNI在jni.h头文件中定义了jfieldID和jmethodID这两种类型来分别代表Java端的属性和方法。在访问或者设置Java某个属性的时候,首先就要现在本地代码中取得代表该Java类的属性的jfieldID,然后才能在本地代码中进行Java属性的操作,同样,在需要调用Java类的某个方法时,也是需要取得代表该方法的jmethodID才能进行Java方法操作。
-
GetFieldID/GetMethodID
获取某个属性/某个方法 -
GetStaticFieldID/GetStaticMethodID
获取某个静态属性/静态方法
方法实现如下:
jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz,const char *name, const char *sig);
3.构造对象
常用的JNI中创建对象的方法如下:
jobject NewObject(jclass clazz, jmethodID methodID, ...)
比如有我们知道Java类中可能有多个构造函数,当我们要指定调用某个构造函数的时候,会调用下面这个方法
jmethodID mid = (*env)->GetMethodID(env, cls, "<init>", "()V");
obj = (*env)->NewObject(env, cls, mid);
即把指定的构造函数传入进去即可。
现在我们来看下他上面的两个主要参数
- clazz:是需要创建的Java对象的Class对象
- methodID:是传递一个方法ID
简化代码如下:
jobject NewObjectA(JNIEnv *env, jclass clazz,
jmethodID methodID, jvalue *args);
这里多了一个参数,即jvalue *args,这里是args代表的是对应构造函数的所有参数的,我们可以应将传递给构造函数的所有参数放在jvalues类型的数组args中,该数组紧跟着放在methodID参数的后面。NewObject()收到数组中的这些参数后,将把它们传给编程任索要调用的Java方法。
如果参数不是数组怎么处理:
jobject NewObjectV(JNIEnv *env, jclass clazz,
jmethodID methodID, va_list args);
这个方法和上面不同在于,这里将构造函数的所有参数放到在va_list类型的参数args中,该参数紧跟着放在methodID参数的后面。
总结
1.静态注册native
函数
- 第1步:在Java中先声明
native
方法 - 第2步:编译Java源文件
javac
得到.class
文件 - 第3步:通过
javah -jni
命令导出JNI的.h
头文件,添加.c
或.cpp
文件,实现函数方法 - 第4步:使用Java需要交互的本地代码,实现在Java中声明的Native方法
- 第5步:添加
Android.mk
文件,修改build.gradle
文件,将本地代码编译成动态库 - 第6步:通过Java命令执行Java程序,最终实现Java调用本地代码。
2.动态注册native
函数, 在静态注册的基础上:
- 修改简化
.h
文件的方法名与方法 - 修改简化
.cpp
文件的方法名 - 添加映射函数与注册函数
项目git地址:https://github.com/x-fp/JniTest
参考文章:https://www.jianshu.com/p/87ce6f565d37
这篇文章写得非常详细,感谢作者的帮助