美文网首页
记录一次Android NDK与JNI使用基础

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

作者: BayRoc | 来源:发表于2019-06-14 23:07 被阅读0次

    准备工作

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

    相关文章

      网友评论

          本文标题:记录一次Android NDK与JNI使用基础

          本文链接:https://www.haomeiwen.com/subject/ktvmfctx.html