Android JNI与NDK编程

作者: 一个有故事的程序员 | 来源:发表于2019-05-30 17:39 被阅读22次

    导语

    Java JNI本意为Java Native Interface(java本地接口), 是为方便java调用C或者C++等本地代码所封装的一层接口. 由于Java的跨平台性导致本地交互能力的不好, 一些和操作系统相关的特性Java无法完成, 于是Java提供了JNI专门用于和本地代码交互。

    主要内容

    • JNI的开发流程
    • NDK的开发流程
    • JNI的数据类型和类型签名
    • JNI调用Java方法的流程

    具体内容

    NDK是Android所提供的一个工具合集, 通过NDK可以在Android中更加方便地通过JNI来访问本地代码. NDK还提供了交叉编译工具, 开发人员只需要简单的修改mk文件就可以生成特定的CPU平台的动态库. 好处如下:

    • 代码的保护。由于apk的java层代码很容易被反编译,而C/C++库反编译难度较大。
    • 可以方便地使用C/C++开源库。
    • 便于移植,用C/C++写的库可以方便在其他平台上再次使用
    • 提供程序在某些特定情形下的执行效率,但是并不能明显提升Android程序的性能。

    JNI的开发流程

    在Java中声明natvie方法

    创建一个类

    生命了两个native方法:get和set(String)。这是需要在JNI实现的方法。JniTest头部有一个加载动态库的过程, 加载so库名称填入的虽然是jni-test, 但是so库全名称应该是libjni-test.so,这是加载so库的规范。

    编辑Java源文件得到class文件, 然后通过javah命令导出JNI头文件

    在包的的根路径, 进行命令操作

    javac com/szysky/note/androiddevseek_14/JNITest.java
    javah com.szysky.note.androiddevseek_14.JNITest
    

    执行之后会在, 操作的路径下生成一个com_szysky_note_androiddevseek_14_JNITest.h头文件, 这个就是第二步生成的东西:

    • 函数名:格式遵循:Java包名类名方法名包名之间的.分割全部替换成分割。
    • 参数: jstring是代表String类型参数. 具体的类型关系后面会说明。
      • JNIEnv *: 表示一个指向JNI环境的指针, 可以通过它来访问JNI提供的方法。
      • jobject: 表示java对象中的this。
      • JNIEXPORT和JNICALL: 这是JNI种所定义的宏, 可以在jni.h这个头文件查到。
    #ifdef __cplusplus
    extern "C" {
    #endif
    

    而这个宏定义是必须的, 作用是指定extern”C”内部的函数采用C语言的命名风格来编译. 如果设定那么当JNI采用C++来实现时, 由于C/C++编译过程对函数的命名风格不同, 这将导致JNI在链接时无法根据函数名找到具体的函数, 那么JNI调用肯定会失效。

    用C/C++实现natvie方法

    JNI方法是指的Java中声明的native方法, 这里可以选择c++和c来实现. 过程都是类似的. 只有少量的区别, 这里两种都实现一下。

    在工程的主目录创建一个子目录, 名称任意, 然后将之前通过javah命令生成的.h头文件复制到创建的目录下, 接着创建test.cpp和test.c两个文件,实现如下:

    #include "com_szysky_note_androiddevseek_14_JNITest.h"
    #include <stdio.h>
    JNIEXPORT jstring JNICALL Java_com_szysky_note_androiddevseek_114_JNITest_get(JNIEnv *env, jobject thiz){
        printf("执行在c++文件中 get方法\n");
        return env->NewStringUTF("Hello from JNI .");
    }
    JNIEXPORT void JNICALL Java_com_szysky_note_androiddevseek_114_JNITest_get(JNIEnv *env, jobject thiz, jstring string){
        printf("执行在c++文件中 set方法\n");
        char* str = (char*) env->GetStringUTFChars(string, NULL);
        printf("\n, str");
        env->ReleaseStringUTFChars(string, str);
    }
    
    #include "com_szysky_note_androiddevseek_14_JNITest.h"
    #include <stdio.h>
    JNIEXPORT jstring JNICALL Java_com_szysky_note_androiddevseek_114_JNITest_get(JNIEnv *env, jobject thiz){
        printf("执行在c文件中 get方法\n");
        return (*env)->NewStringUTF("Hello from JNI .");
    JNIEXPORT void JNICALL Java_com_szysky_note_androiddevseek_114_JNITest_get(JNIEnv *env, jobject thiz, jstring string){
        printf("执行在c文件中 set方法\n");
        char* str = (char*) (*env)->GetStringUTFChars(env, string, NULL);
        printf("%s\n, str");
        (*env)->ReleaseStringUTFChars(env, string, str);
    }}
    

    其实C\C++在实现上很相似, 但是对于env的操作方式有所不同。

    C++: env->ReleaseStringUTFChars(string, str);
    C:  (*env)->ReleaseStringUTFChars(env, string, str); 
    
    编译so库并在java中调用

    so库的编译这里采用gcc. 命令cd到放置刚才生成c/c++的目录下。
    使用如下命令:

    gcc -shared -I /user/lib/jvm/java-7-openjdk-amd64/include -fPIC test.cpp -o libjni-test.so
    gcc -shared -I /user/lib/jvm/java-7-openjdk-amd64/include -fPIC test.c -o libjni-test.so
    

    /user/lib/jvm/java-7-openjdk-amd64是本地jdk的安装路径,libjni-test.so是生产的so库的名字。Java中通过:System.loadLibrary("jni-test")加载,其中lib和.so不需要指出。

    切换到主目录,通过Java指令执行Java程序:java -Djava.library.path=jni com.ryg.JniTest。其中-Djava.library.path=jni指明了so库的路径。

    NDK的开发流程

    下载并配置NDK

    下载好NDK开发包,并且配置好NDK的全局变量。

    创建一个Android项目,并声明所需的native方法
    public static native String getStringFromC();
    
    实现Android项目中所声明的native方法
    1. 生成C/C++的头文件
      打开控制台,用cd命令切换到当前项目当前目录,
      使用javah命令生成头文件。
    javah -classpath bin\classes;C:\MOX\AndroidSDK\platforms\android-23\android.jar -d jni cn.hudp.hellondk.MainActivity
    

    说明:bin\classes 为项目的class文件的相对路径 ; C:\MOX\AndroidSDK\platforms\android-23\android.jar 为android.jar的全路径,因为我们的的Activity使用到了Android SDK,所以生成头文件时需要他; -d jni就是生成的头文件输出到项目的jni文件夹下; 最后跟的cn.hudp.hellondk.MainActivity是native方法所在的类的包名和类名。

    1. 编写修改对应的android.mk文件( mk文件是NDK开发所用到的配置文件)
    # Copyright (C) 2009 The Android Open Source Project
     # #
     Licensed under the Apache License, Version 2.0 (the "License");
     # you may not use this file except in compliance with the License.
     # You may obtain a copy of the License at
     # #
     http://www.apache.org/licenses/LICENSE-2.0
     # #
     Unless required by applicable law or agreed to in writing, software
     # distributed under the License is distributed on an "AS IS" BASIS,
     # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     # See the License for the specific language governing permissions and
     # limitations under the License.
     # L
     OCAL_PATH := $(call my-dir)
     include $(CLEAR_VARS)
     
     ## 对应Java部分 System.loadLibrary(String libName) 的libname
     LOCAL_MODULE := hello
     
     ## 对应c/c++的实现文件名
     LOCAL_SRC_FILES := hello.c
     include $(BUILD_SHARED_LIBRARY)
    
    1. 编写Application.mk,来指定需生成的平台对应的动态库,这里是全平台支持,也可以特殊指定。目前常见的架构平台有armeabi、x86和mips。其中移动设备主要是armeabi,因此大部分apk中只包含armeabi的so库。
    APP_ABI := all
    
    切换到jni目录的父目录,然后通过ndk-build命令编译产生so库

    ndk-build 命令会默认指定jni目录为本地源码的目录。
    将编译好的so库放到Android项目中的 app/src/main/jniLbis 目录下,或者通过如下app的gradle设置新的存放so库的目录:

    android{
        ……
        sourceSets.main{
            jniLibs.srcDir 'src/main/jni_libs'
        }
    }
    

    还可以通过 defaultConfig 区域添加NDK选项:

    android{
        ……
        defaultConfig{
            ……
            ndk{
                moduleName "jni-test"
            }
        }
    }
    

    还可以在 productFlavors 设置动态打包不同平台CPU对应的so库进apk( 缩小APK体积):

    
    gradle
    android{
        ……
        productFlavors{
            arm{
                ndk{
                    adiFilter "armeabi"
                }
            } 
            x86{
                ndk{
                    adiFilter "x86"
                }
            }
        }
    }
    

    在Android中调用:

    public class MainActivity extends Activity {
        public static native String getStringFromC();
        static{//在静态代码块中调用所需要的so文件,参数对应.so文件所对应的LOCAL_MODULE;
            System.loadLibrary("hello");
        }
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            //在需要的地方调用native方法
            Toast.makeText(getApplicationContext(), get(), Toast.LENGTH_LONG).show();
        }
    }
    

    更多参考

    JNI的数据类型和类型签名

    JNI的数据类型包含两种:基本类型和引用类型。
    基本类型主要有jboolean,jchar,jint等,和Java中的数据类型对应如下:

    JNI类型 Java类型 描述
    jboolean boolean 无符号8位整型
    jbyte byte 无符号8位整型
    jchar char 无符号16位整型
    jshort short 有符号16位整型
    jint int 32位整型
    jlong long 64位整型
    jfloat float 32位浮点型
    jdouble double 64位浮点型
    void void 无类型

    JNI中的引用类型主要有类, 对象和数组. 他们和Java中的引用类型的对应关系如下:

    JNI类型 Java类型 描述
    jobject Object Object类型
    jclass Class Class类型
    jstring String String类型
    jobjeckArray Object[] 对象数组
    jbooleanArray boolean[] boolean数组
    jbyteArray byte[] byte数组
    jcharArray char[] char数组
    jshortArray short[] short数组
    jintArray int[] int数组
    jlongArray long[] long数组
    jfloatArray float[] float数组
    jdoubleArray double[] double数组
    jthrowable Throwable Throwable

    JNI的类型签名标识了一个特定的Java类型,这个类型既可以是类也可以是方法,也可以是数据类型。

    类的签名比较简单,它采用“L+包名+类型+;”的形式,只需要将其中的.替换为/即可. 例如java.lang.String, 它的签名为Ljava/lang/String;,末尾的;也是一部分。

    基本数据类型的签名采用一系列大写字母来表示,如下:

    JNI类型 Java类型 描述 JNI类型 Java类型 描述
    boolean Z byte B char C
    short S int I long J
    float F double D void V

    基本数据类型的签名基本都是单词的首字母, 但是boolean除外因为B已经被byte占用, 而long的表示也被Java类签名占用. 所以不同.

    而对象和数组, 对象的签名就是对象所属的类签名, 数组的签名[+类型签名例如byte数组. 首先类型为byte,所以签名为B然后因为是数组那么最终形成的签名就是[B.例如如下各种对应:

    char[]      [C
    float[]     [F
    double[]    [D
    long[]      [J
    String[]    [Ljava/lang/String;
    Object[]    [Ljava/lang/Object;
    

    如果是多维数组那么就根据数组的维度多少来决定[的多少, 例如int[][]那么就是[[I

    • 方法的签名为(参数类型签名)+返回值类型签名。
      方法boolean fun(int a, double b, int[] c). 参数类型的签名是连在一起, 那么按照方法的签名规则就是(ID[I)Z
    • 方法:void fun(int a, String s, int[] c), 那么签名就是(ILjava/lang/String;[I)V
    • 方法:int fun(), 对应签名()I
    • 方法:int fun(float f), 对应签名(F)I

    JNI调用Java方法的流程

    JNI调用java方法的流程是先通过类名找到类, 然后在根据方法名找到方法的id, 最后就可以调用这个方法了. 如果是调用Java的非静态方法, 那么需要构造出类的对象后才可以调用它。

    演示一下调用静态的方法:

    1. 首先在java中声明要被调用的静态方法。这里触发的时机是一个按钮的点击,自行添加。
    static{
             System.loadLibrary("jni-test");
         }
     /**
      * 定义一个静态方法 , 提供给JNI调用
      */
     public static void methodCalledByJni(String fromJni){
         Log.e("susu", "我是从JNI被调用的消息,  JNI返回的值是:"+fromJni );
     }
     // 定义调用本地方法, 好让本地方法回调java中的方法
     public native void callJNIConvertJavaMethod();
     @Override
     public void onClick(View view) {
         switch (view.getId()){
             case R.id.btn_jni2java:
                 // 调用JNI的方法
                 callJNIConvertJavaMethod();
                 break;
         }
     }
    
    1. 在JNI的test.cpp中添加一个c的函数用来处理调用java的逻辑,并提供一个方法供java代码调起来触发。一共两个方法。
    // 定义调用java中的方法的函数
     void callJavaMethod( JNIEnv *env, jobject thiz){
         // 先找到要调用的类
         jclass clazz = env -> FindClass("com/szysky/note/androiddevseek_14/MainActivity");
         if (clazz == NULL){
             printf("找不到要调用方法的所属类");
             return;
         }
         // 获取java方法id
         // 参数二是调用的方法名,  参数三是方法的签名
         jmethodID id = env -> GetStaticMethodID(clazz, "methodCalledByJni", "(Ljava/lang/String;)V");
         if (id == NULL){
             printf("找不到要调用方法");
             return;
         }
         jstring msg = env->NewStringUTF("我是在c中生成的字符串");
         // 开始调用java中的静态方法
         env -> CallStaticVoidMethod(clazz, id, msg);
     }
     void Java_com_szysky_note_androiddevseek_114_MainActivity_callJNIConvertJavaMethod(JNIEnv *env, jobject thiz){
         printf("调用c代码成功, 马上回调java中的代码");
         callJavaMethod(env, thiz);
     }
    

    稍微说明一下, 程序首先根据类名com/szysky/note/androiddevseek_14/MainActivity找到类, 然后在根据方法名methodCalledByJni找到方法, 并传入方法对应签名(Ljava/lang/String;), 最后通过JNIEnv对象的CallStaticVoidMethod()方法来完成最终调用。

    最后只要在Java_com_szysky_note_androiddevseek_114_MainActivity_callJNIConvertJavaMethod方法中调用callJavaMethod方法即可.

    流程就是–> 按钮触发了点击的onClikc –> 然后Java中会调用JNI的callJNIConvertJavaMethod() –> JNI的callJNIConvertJavaMethod()方法内部会调用具体实现回调Java中的方法callJavaMethod() –> 方法最终通过CallStaticVoidMethod()调用了Java中的methodCalledByJni()来接收一个参数并打印一个log。

    生成so库的文件保存在git中的app/src/main/backup目录下一个两个版本代码, 第一个就是第二小节中的NDK开发代码, 第二个就是第四小节的代码就是目前的. 而so库是最新的, 包含了所有的JNI代码生成的库文件。

    JNI调用Java的过程和Java中方法的定义有很大关联, 针对不同类型的java方法, JNIEnv提供了不同的接口去调用, 更为细节的部分要去开发中或者去网站去了解更多.

    更多内容戳这里(整理好的各种文集)

    相关文章

      网友评论

        本文标题:Android JNI与NDK编程

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