2025年05月24日 21:17

Android JNI Demo

作者 admin, 2020年05月06日 15:11

« 上一篇 - 下一篇 »

admin

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中进行编译。

程序代码 [选择]
//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时,则分别对静态动态进行实例化,其中静态的实现代码如下:

程序代码 [选择]
//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目录运行如下命令:
程序代码 [选择]
javac com/example/jnidemo/NdkUtils/NdkUtilsStatic.java此时生NdkUtilsBase.class以及NdkUtilsStatic.class文件,这时候可以利用javah生成对应的JNI头文件:
程序代码 [选择]
javah com.example.jnidemo.NdkUtils.NdkUtilsStatic
通过以上命令生成的头文件名为com_example_jnidemo_NdkUtils_NdkUtilsStatic.h,其取名方式与包名和文件名相关,其内容如下:
程序代码 [选择]
/* 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。

既然已经生成了头文件,接下来只需要实现其定义:
程序代码 [选择]
#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:
程序代码 [选择]
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:

程序代码 [选择]
#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。
程序代码 [选择]
#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调用系统库时,会报错:
程序代码 [选择]
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方法,此时注册函数。
程序代码 [选择]
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;
}
程序代码 [选择]
#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)
最终示例图:


4. JNI Android Studio demo
Android Studio编译JNI库也十分方便,右键点击src->New->Folder->JNI Folder即可新建jni目录,将之前的C++文件移植过来,并再新建一个Native C++项目,拷贝其CMakeLits.txt文件至app目录下,修改如下:
程序代码 [选择]
# 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)
程序代码 [选择]
#build.gradle
dependencies {
    implementation 'com.jakewharton:butterknife:8.8.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
}
由此MainActivity就变成:
程序代码 [选择]
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;
        }
    }
}