记录一次Android NDK与JNI使用基础

准备工作

  • 1.开发环境


    developEnvironment.png
  • 2.AndroidStudio版本


    ASVersion.png
  • 3.在SDKManager中下载NDK工具


    ndk.png

静态注册Native函数

  1. 创建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生成,如下图
    make_project.png

使用Terminal生成的.class文件在同级目录下,使用Build->Make project生成的在build目录下,如下图

使用javac生成.png

使用Build生成.png

5.在Terminalcd到相应目录下,执行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头文件如下图

头文件.png

.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
JNIEXPORTJNICALL:他们是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
如下图:

目录.png

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" 添加头文件
  • 实现两个方法方法
  1. 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之后)

  1. 加载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.运行项目


running.png

动态注册Native函数

  • 不必忍受冗长的函数名,自由命名函数名,在JNI_OnLoad方法里进行注册。

1.修改生成后的.h头文件名为jni-test.h,如下

修改头文件名.png

修改.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/StringMenuLandroid/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.JniTestclass文件路径

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
这篇文章写得非常详细,感谢作者的帮助

推荐阅读更多精彩内容