1. JNI简析
JNI即Java Native Interface,当java需要调用Native接口时,需要用到JNI。需要JNI的原因有如下:
- C/C++有成熟的实现方式且性能要更高,为了不重复去实现相同的功能,可以通过Java直接去调用Native模块。
- Java程序运行在虚拟机上,但虚拟机是与平台相关的,为了让Java程序实现平台无关,可以将平台相关的实现利用JNI去实现差异化。
- Java程序相对C/C++而言,容易被反编译,所以可以将关键代码使用JNI方式去实。
2. JNI使用步骤
JNI使用步骤分为两步:
- 加载JNI库
- 声明JNI函数
具体可以以一个例子来,初衷是通过JNI获取系统时间
3. JNI SDK demo
3.1 Apk实现
在写JNI层前,首先实现上层Java部分,App部分先由AndroidStudio中完成,实现后,将相关代码移到Android 7.0 SDK中进行编译。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
//MainActivity.java
/**
* Demo class
*
* @author Bill
* @date 2019/02/20
*/
public class MainActivity extends AppCompatActivity implements View.OnClickListener, CompoundButton.OnCheckedChangeListener {
private Button mGetTimeButton;
private TextView mTimeText;
private CheckBox mCheckBox;
private NdkUtilsBase mNdkUtils;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
initListeners();
mNdkUtils = new NdkUtilsStatic();
}
void initViews(){
mGetTimeButton = (Button)findViewById(R.id.getTime);
mTimeText = (TextView)findViewById(R.id.textView);
mCheckBox = (CheckBox)findViewById(R.id.jniMode);
}
void initListeners(){
mGetTimeButton.setOnClickListener(this);
mCheckBox.setOnCheckedChangeListener(this);
}
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.getTime:
String currentTime = mNdkUtils.getTime();
mTimeText.setText(currentTime);
break;
default:break;
}
}
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
switch (compoundButton.getId()){
case R.id.jniMode:
if(isChecked){
mNdkUtils = new NdkUtilsDynamic();
}
else{
mNdkUtils = new NdkUtilsStatic();
}
break;
}
}
}
Apk中只有三个控件,分别是Button,CheckBox, TextView。其中通过CheckBox去控制使用静态还是动态注册。点击按钮后调用相关的JNI接口。NdkUtilsBase为基类,切换CheckBox时,则分别对静态动态进行实例化,其中静态的实现代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
//NdkUtilsStatic.java
package com.example.jnidemo.NdkUtils;
public class NdkUtilsStatic extends NdkUtilsBase{
private static final String JnilibPath = "demo_jni_static";
static{
System.loadLibrary(JnilibPath);
}
//实现的JNI接口,用以获取时间
@Override
public native String getTime();
}
动态的实现方式与上面类同,不同的是加载的库名为"demo_jni_dynamic"
。
3.1 静态注册
使用静态注册,首先需要编译NdkUtilsStatic.java文件,可以进入到JniDemo/java
目录运行如下命令:
1
javac com/example/jnidemo/NdkUtils/NdkUtilsStatic.java
此时生NdkUtilsBase.class以及NdkUtilsStatic.class文件,这时候可以利用javah生成对应的JNI头文件:
1
javah com.example.jnidemo.NdkUtils.NdkUtilsStatic
通过以上命令生成的头文件名为com_example_jnidemo_NdkUtils_NdkUtilsStatic.h
,其取名方式与包名和文件名相关,其内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_jnidemo_NdkUtils_NdkUtilsStatic */
#ifndef _Included_com_example_jnidemo_NdkUtils_NdkUtilsStatic
#define _Included_com_example_jnidemo_NdkUtils_NdkUtilsStatic
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_jnidemo_NdkUtils_NdkUtilsStatic
* Method: getTime
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_jnidemo_NdkUtils_NdkUtilsStatic_getTime
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
观察可得,生成了对应于Java中的getTime方法Java_com_example_jnidemo_NdkUtils_NdkUtilsStatic_getTime
,其中JNIEnv是与线程相关,代表JNI环境的结构体,并且通过该指针,可以调用Java方法。而由于getTime是非static方法,由对象进行调用,所以第二个参数变成了jobject。假如为static,该类型应当为jclass。
既然已经生成了头文件,接下来只需要实现其定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <jni.h>
#include "JNIHelp.h"
#include "com_example_jnidemo_NdkUtils_NdkUtilsStatic.h"
#include <log/log.h>
#include <stdlib.h>
#define LOG_TAG "jnidemo"
#include "TimeUtils/TimeUtils.h"
/*
* Class: com_example_jnidemo_NdkUtils_NdkUtilsStatic
* Method: getTime
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_jnidemo_NdkUtils_NdkUtilsStatic_getTime
(JNIEnv *mEnv, jobject){
char *timebuf = (char *)malloc(sizeof(char) * 16);
getTime(timebuf);
ALOGD("JNI getTime is %s\n", timebuf);
jstring str = mEnv->NewStringUTF(timebuf);
return str;
}
至于getTime是C获取时间的经典实现,取自APUE:
1
2
3
4
5
6
7
8
9
10
void getTime(char * buf){
time_t t;
struct tm * tmp;
time(&t);
tmp = localtime(&t);
if(strftime(buf,16,"%T",tmp) == 0){
ALOGD("failed to gettime!\n");
}
ALOGD("getTime is %s\n", buf);
}
由于当前是在SDK中直接编译,需要编写Android.mk:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#JniDemo/Android.mk
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(call all-java-files-under, java)
LOCAL_PACKAGE_NAME := JniDemo
LOCAL_JNI_SHARED_LIBRARIES := \
libdemo_jni_static \
libdemo_jni_dynamic \
LOCAL_PROGUARD_ENABLED := disabled
LOCAL_CERTIFICATE := platform
LOCAL_STATIC_JAVA_LIBRARIES := \
android-support-v4 \
android-support-v7-appcompat \
android-support-constraint-layout \
android-support-constraint-layout-solver
appcompat_dir := frameworks/support/v7/appcompat/res
constraint_layout_dir := libs/constraint-layout/res
res_dir := res $(constraint_layout_dir)
LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dir)) \
$(appcompat_dir)
LOCAL_AAPT_FLAGS := --auto-add-overlay \
--extra-packages android.support.v7.appcompat \
--extra-packages android.support.constraint
include $(BUILD_PACKAGE)
include $(CLEAR_VARS)
LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES := \
android-support-constraint-layout:libs/constraint-layout/libs/android-support-constraint-layout.jar
include $(BUILD_MULTI_PREBUILT)
include $(CLEAR_VARS)
LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES := \
android-support-constraint-layout-solver:libs/constraint-layout-solver/android-support-constraint-layout-solver.jar
include $(BUILD_MULTI_PREBUILT)
include $(call all-makefiles-under,$(LOCAL_PATH))
AndroidStuido中默认使用了AppCompatActivity,ConstraintLayout等,因此在Android.mk中必须要加上依赖。在7.0的环境中找不到constraint-layout,因此在项目中加上libs目录,并单独编译成staic java库。此外还需注意必须加上JNI的依赖库libdemo_jni_static
以及libdemo_jni_dynamic
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#JniDemo/jni/Android.mk
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE:= libdemo_jni_static
LOCAL_SRC_FILES:= \
com_example_jnidemo_NdkUtils_NdkUtilsStatic.cpp \
LOCAL_SHARED_LIBRARIES := \
libutils libcutils liblog
LOCAL_LDLIBS := -llog
LOCAL_C_INCLUDES += \
$(JNI_H_INCLUDE) \
com_example_jnidemo_NdkUtils_NdkUtilsStatic.h \
TimeUtils/TimeUtils.h
include $(BUILD_SHARED_LIBRARY)
在调试过程中,假如将App以adb进行安装后,应用在加载jni库时会崩溃。原因是API版本在24以上(包括24)时,JNI调用系统库时,会报错:
1
2
3
4
5
6
java.lang.UnsatisfiedLinkError: dlopen failed: library "libdemo_jni_staic.so"
("/system/lib/libdemo_jni_static.so") needed or dlopened by
"/system/lib/libnativeloader.so" is not accessible for the namespace
"classloader-namespace"
at java.lang.Runtime.loadLibrary0(Runtime.java:977)
at java.lang.System.loadLibrary(System.java:1602)
可以在vendor/etc/public.libraries.txt中增加libdemo_jni.so
。具体可以参考如下网址:
Android 7.0行为变更
3.2 动态注册
动态注册需要实现JNI_OnLoad
方法,并定义g_methods
定义函数的映射关系。在加载jni库时,将会调用JNI_OnLoad
方法,此时注册函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
JNIEXPORT jstring com_example_jnidemo_NdkUtils_NdkUtilsDynamic_getTime(
JNIEnv *env, jobject){
char *timebuf = (char *)malloc(sizeof(char) * 16);
getTime(timebuf);
ALOGD("JNI static getTime is %s\n", timebuf);
jstring str = env->NewStringUTF(timebuf);
free(timebuf);
return str;
}
static const JNINativeMethod g_methods[] = {
{ "getTime",
"()Ljava/lang/String;",
(void *)com_example_jnidemo_NdkUtils_NdkUtilsDynamic_getTime
},
};
static int register_com_example_jnidemo_NdkUtils_NdkUtilsDynamic(JNIEnv* env)
{
jclass clazz;
clazz = env->FindClass("com/example/jnidemo/NdkUtils/NdkUtilsDynamic");
if (clazz == NULL)
return JNI_FALSE;
if (env->RegisterNatives(clazz, g_methods, NELEM(g_methods)) < 0)
return JNI_FALSE;
return JNI_TRUE;
}
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;
jint Ret = -1;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK)
goto bail;
if (!register_com_example_jnidemo_NdkUtils_NdkUtilsDynamic(env))
goto bail;
Ret = JNI_VERSION_1_4;
bail:
return Ret;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#JniDemo/jni/Android.mk
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE:= libdemo_jni_dynamic
LOCAL_SRC_FILES:= \
com_example_jnidemo_NdkUtils_NdkUtilsDynamic.cpp\
LOCAL_SHARED_LIBRARIES := \
libutils libcutils liblog
LOCAL_LDLIBS := -llog
LOCAL_C_INCLUDES += \
$(JNI_H_INCLUDE) \
TimeUtils/TimeUtils.h
include $(BUILD_SHARED_LIBRARY)
最终示例图:
代码可以参考:Jni Demo
4. JNI Android Studio demo
Android Studio编译JNI库也十分方便,右键点击src->New->Folder->JNI Folder即可新建jni目录,将之前的C++文件移植过来,并再新建一个Native C++项目,拷贝其CMakeLits.txt文件至app目录下,修改如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# 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.
demo_jni_static
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/jni/com_example_jnidemo_NdkUtils_NdkUtilsStatic.cpp
)
add_library( # Sets the name of the library.
demo_jni_dynamic
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/jni/com_example_jnidemo_NdkUtils_NdkUtilsDynamic.cpp
src/main/jni/TimeUtils/TimeUtils.h
)
# 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.
demo_jni_dynamic
# Links the target library to the log library
# included in the NDK.
android
log
${log-lib})
target_link_libraries( # Specifies the target library.
demo_jni_static
# Links the target library to the log library
# included in the NDK.
android
log
${log-lib})
其中add_library
表明创建对应的JNI库,这里需要两个库,因此需要分开声明。find_library
为找prebuilt的库,target_link_libraries
为需要链接的库,由于引用了__android_log_buf_print
方法,因此需要加载liblog库,否则将提示undefined reference.
另外在app目录下的build.gradle中引用butterknife库,就可以自动生成findViewById了(最新的butterKnife10.1.0在调用buttefKnife.bind时程序会崩溃,因此使用网上很多人推荐的8.8.1)
1
2
3
4
5
#build.gradle
dependencies {
implementation 'com.jakewharton:butterknife:8.8.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
}
由此MainActivity就变成:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class MainActivity extends AppCompatActivity {
@BindView(R.id.textView)
TextView textView;
@BindView(R.id.getTime)
Button getTime;
@BindView(R.id.jniMode)
CheckBox jniMode;
private NdkUtilsBase mNdkUtils;
private final String TAG = "JniDemo";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mNdkUtils = new NdkUtilsStatic();
//需要在setContentView后调用
ButterKnife.bind(this);
}
@OnClick({R.id.getTime, R.id.jniMode})
public void onViewClicked(View view) {
switch (view.getId()) {
case R.id.getTime:
String currentTime = mNdkUtils.getTime();
textView.setText(currentTime);
break;
case R.id.jniMode:
CheckBox tmp = (CheckBox)view;
if(tmp.isChecked()){
Log.d(TAG,"use Dynamic mode!");
mNdkUtils = new NdkUtilsDynamic();
}
else{
Log.d(TAG,"use Static mode!");
mNdkUtils = new NdkUtilsStatic();
}
break;
}
}
}
具体代码可以参考Jni Demo(Android Studio)
5. JNI数据附录
5.1 JNI基础数据类型
Java类型 | JNI类型 | C/C++类型 | 大小 |
---|---|---|---|
Boolean | jboolean | unsigned char | 无符号8位 |
Byte | jbyte | char | 有符号8位 |
Char | jchar | unsigned short | 无符号16位 |
Short | jshort | short | 有符号16位 |
Integer | jint | int | 有符号32位 |
Long | jlong | long long | 有符号64位 |
Float | jfloat | float | 32位浮点值 |
Double | jdouble | double | 64位双精度浮点值 |
5.2 JNI引用数据类型
Java类型 | C/C++类型 |
---|---|
java.lang.Class | jclass |
java.lang.Throwable | jthrowable |
java.lang.String | jstring |
java.lang.Object | jobject |
java.util.Objects | jobjects |
java.lang.Object[] | jobjectArray |
Boolean[] | jbooleanArray |
Byte[] | jbyteArray |
Char[] | jcharArray |
Short[] | jshortArray |
int[] | jintArray |
long[] | jlongArray |
float[] | jfloatArray |
double[] | jdoubleArray |
通用数组 | jarray |
5.3 Java签名映射表
Java类型 | 签名 |
---|---|
Boolean | Z |
Byte | B |
Char | C |
Short | S |
Integer | I |
Long | J |
Float | F |
Double | D |
Void | V |
任何Java类的全名 | L任何Java类的全名; |
type[] | type[ |
方法类型 | (参数类型)返回值 类型 |