Android JNI Demo

Posted by Bill on February 22, 2019

1. JNI简析

JNI即Java Native Interface,当java需要调用Native接口时,需要用到JNI。需要JNI的原因有如下:

  1. C/C++有成熟的实现方式且性能要更高,为了不重复去实现相同的功能,可以通过Java直接去调用Native模块。
  2. Java程序运行在虚拟机上,但虚拟机是与平台相关的,为了让Java程序实现平台无关,可以将平台相关的实现利用JNI去实现差异化。
  3. Java程序相对C/C++而言,容易被反编译,所以可以将关键代码使用JNI方式去实。

2. JNI使用步骤

JNI使用步骤分为两步:

  1. 加载JNI库
  2. 声明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[
方法类型 (参数类型)返回值 类型