Android JNI学习

本篇总结Android JNI如何通过两种不同的构建方式与C/C++进行交互,话不多少先上两张效果图,注意:本项目是基于Android Studio3.0开发的。


Android JNI学习_第1张图片
图1.png

Android JNI学习_第2张图片
图2.png

以上就是与C/C++语言交互的内容部分分,涵盖了开发基本上的需求,让我进入正题

两种不同的构建方式

以下介绍一种基于CMake的方式来构建,一种是基于比较传统的mk文件构建方式,虽然基于CMake的方式更加的只能,但是也是基于传统的方式来的,这种方式只能适用于Android Studio 2.2以上的版本。无论基于什么构建,首先,你必须要下载Android NDK开发包。点我下载

基于CMake方式

基于CMake方式构建的项目,在写C++和C代码的时候可以与写Java代码一样的有提示,而且还可以直接的debug。
打开一个项目,从菜单栏中选择 Tools > Android > SDK Manager。
点击 SDK Tools 选项卡。
勾选 LLDB,CMake 和 NDK。如图:


Android JNI学习_第3张图片
setting.png
  • The Android Native Development Kit (NDK): 让你能在 Android 上面使用 C 和 C++ 代码的工具集。
  • CMake: 外部构建工具。如果你准备只使用 ndk-build 的话,可以不使用它。
  • LLDB: Android Studio 上面调试本地代码的工具。
创建支持C/C++的项目

点击新建项目,勾选以下选项以支持C/C++


Android JNI学习_第4张图片
图3.png
  • C++ Standard:点击下拉框,可以选择标准 C++,或者选择默认 CMake 设置的 Toolchain Default 选项。
  • Exceptions Support:如果你想使用有关 C++ 异常处理的支持,就勾选它。勾选之后,Android Studio 会在 module 层的 build.gradle 文件中的 cppFlags 中添加 -fexcetions 标志。
  • Runtime Type Information Support:如果你想支持 RTTI,那么就勾选它。勾选之后,Android Studio 会在 module 层的 build.gradle 文件中的 cppFlags 中添加 -frtti 标志。


    Android JNI学习_第5张图片
    图4.png

    最后点击Finish完成工程的创建。


    Android JNI学习_第6张图片
    图5.png

    与之前普通项目就多了这三个文件。
  • .externalNativeBuild:这个文件夹与下面的CMakeLists.txt文件是对应的,他主要由CMakeLists.txt文件来构建so文件库的文件夹。
  • cpp:这个是存放C/C++源代码的文件夹。
  • CMakeLists.txt:这个文件主要是构建整个C++/C的脚本文件。里面需要编写构建的代码,有点类似Android.mk文件。
    到这里,基于CMake的构建就完成了,点击运行可以直接看到来自C++的字符。
    在这里,主要讲将CMakeLists.txt的用法,如何用了构建:
    先看如下代码:
cmake_minimum_required(VERSION 3.4.1)

include_directories(src/main/cpp/include/)

add_library( # Sets the name of the library.
             native-lib

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             src/main/cpp/native-lib.cpp )

为了让 CMake 将源代码(native source code)编译成 native library。需要在编译文件中添加 cmake_minimum_required() 和 add_library() 命令。
看下图:

Android JNI学习_第7张图片
图6.png

include_directories(src/main/cpp/include/)这行代码的意思就是当你使用 add_library(),将一个源文件(source file)或库添加到你的 CMake 构建脚本,同步你的项目,然后你会发现 Android studio 将关联的头文件也显示了处理。然而,为了让 CMake 在编译时期能定位到你的头文件,你需要在 CMake 构建脚本中添加 include_directories() 命令,并指定头文件路径。

target_link_libraries( # Specifies the target library.
                       native-lib

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

这段代码就是如果你的native-lib代码中有引用别的头文件,在这里就需要把头文件链接到这里,这个与add_library的顺序是一致的。
最后在Java代码中load的so库是与上面native-lib是一致的。

  static {
        System.loadLibrary("native-lib");
    }

至此,一个完整的jni项目就集成完毕,至于如何引用第三方C/C++的库到CMake中,后期会介绍将librtmp,FFmpeg集成到Android中来讲解。

传统的构建JNI项目的方式

这个方式比较古老了,相信从eclipse转过来的童鞋都知道,这里我就不详细讲述这种方式的集成,这里推荐大家看这位大神的一篇博客:看不到我,看不到我
我这里就详细总结一下Android.mk与Application.mk文件的编写。

Android.mk

Android.mk是Android提供的一种makefile文件,用来指定诸如编译生成so库名、引用的头文件目录、需要编译的.c/.cpp文件和.a静态库文件

最基本的格式应该是这样:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

LOCAL_xxx       := xxx  
LOCAL_MODULE := NdkTools
LOCAL_SRC_FILES := NdkTools.cpp
LOCAL_xxx       := xxx  

include $(BUILD_SHARED_LIBRARY)
  • LOCAL_PATH:变量制定了该.mk的路径,$(call my-dir)调用NDK内部的函数获得当前.mk文件的路径,每个Android.mk文件必须以定义LOCAL_PATH为开始。它用于在开发tree中查找源文件。宏my-dir 则由Build System提供。返回包含Android.mk的目录路径。
  • include $(CLEAR_VARS):清空了除了LOCAL_PATH之外的所有LOCAL_xxx变量的值,CLEAR_VARS 变量由Build System提供。并指向一个指定的GNU Makefile,由它负责清理很多LOCAL_xxx.
    例如:LOCAL_MODULE, LOCAL_SRC_FILES, LOCAL_STATIC_LIBRARIES等等。但不清理LOCAL_PATH.
    这个清理动作是必须的,因为所有的编译控制文件由同一个GNU Make解析和执行,其变量是全局的。所以清理后才能避免相互影响。
  • 中间就是对于模块参数的设置,主要包括:模块名字、模块源文件、模块类型、编译好的模块存放位置、以及编译的平台等
  • include $(BUILD_xxx_xxx): 执行NDK的默认脚本,它会收集include $(CLEAR_VARS)脚本后所有定义的LOCAL_xxx变量,然后根据它们来生成模块。BUILD_SHARED_LIBRARY:是Build System提供的一个变量,指向一个GNU Makefile Script。
    它负责收集自从上次调用 include $(CLEAR_VARS) 后的所有LOCAL_XXX信息。并决定编译为什么。
    BUILD_STATIC_LIBRARY :编译为静态库。
    BUILD_SHARED_LIBRARY :编译为动态库
    BUILD_EXECUTABLE :编译为Native C可执行程序
    BUILD_PREBUILT :该模块已经预先编译

下面详细说说中间比较常见的东西吧:

  • LOCAL_MODULE := NdkTools:LOCAL_MODULE模块必须定义,以表示Android.mk中的每一个模块。名字必须唯一且不包含空格。Build System会自动添加适当的前缀和后缀。例如,NdkTools,要产生动态库,则生成libNdkTools.so. 但请注意:如果模块名被定为:libNdkTools,则生成libfoo.so. 不再加前缀
  • LOCAL_MODULE_PATH :=$(TARGET_ROOT_OUT) 指定最后生成的模块的目标地址,TARGET_ROOT_OUT:根文件系统,路径为out/target/product/generic/root
    TARGET_OUT:system文件系统,路径为out/target/product/generic/system
    TARGET_OUT_DATA:data文件系统,路径为out/target/product/generic/data
    除了上面的这些,NDK还提供了很多其他的TARGET_XXX_XXX变量,用于将生成的模块拷贝到输出目录的不同路径,默认是TARGET_OUT
  • LOCAL_SRC_FILES := NdkTools.cpp:LOCAL_SRC_FILES变量必须包含将要打包如模块的C/C++ 源码。不必列出头文件,build System 会自动帮我们找出依赖文件。缺省的C++源码的扩展名为.cpp. 也可以修改,通过LOCAL_CPP_EXTENSION

可能以后还会经常用到一些以“LOCAL_”开头的编译变量,这些变量是啥意思,可以参考这篇文章:看不到我,看不到我

使用C代码通过JNI与Java代码沟通

好了,前面铺垫完了,接下才是编写代码中经常需要用到的地方

  • 传递Boolean类型
JNIEXPORT jboolean

JNICALL Java_com_dramascript_ndktest_NdkTools_getBoolean
        (JNIEnv *evn, jobject obj, jboolean b) {
    unsigned char bo = b;
    bo = 1;
    return (jboolean)bo;
}
  • 传递byte类型
JNIEXPORT jbyte

JNICALL Java_com_dramascript_ndktest_NdkTools_getByte
        (JNIEnv *evn, jobject obj, jbyte b) {
    int i = b;
    i = 15;
    return (jbyte)
    i;
}
  • 传递char类型
JNIEXPORT jchar

JNICALL Java_com_dramascript_ndktest_NdkTools_getChar
        (JNIEnv *evn, jobject obj, jchar c) {
    char ch = c;
    ch = 'S';
    return (jchar)
    ch;
}
  • 传递short类型
JNIEXPORT jshort

JNICALL Java_com_dramascript_ndktest_NdkTools_getShort
        (JNIEnv *evn, jobject obj, jshort s) {
    short st = s;
    st = 100;
    return (jshort)
    st;
}
  • 传递int类型
JNIEXPORT jint

JNICALL Java_com_dramascript_ndktest_NdkTools_getInt
        (JNIEnv *evn, jobject obj, jint i) {
    int a = i;
    a = 1024;
    return (jint)
    a;
}
  • 传递long类型
JNIEXPORT jlong

JNICALL Java_com_dramascript_ndktest_NdkTools_getLong
        (JNIEnv *evn, jobject obj, jlong l) {
    long lg = l;
    lg = 1024L;
    return (jlong)
    lg;
}
  • 传递float类型
JNIEXPORT jfloat

JNICALL Java_com_dramascript_ndktest_NdkTools_getFloat
        (JNIEnv *evn, jobject obj, jfloat f) {
    float ft = f;
    ft = 23;
    return (jfloat)
    ft;
}
  • 传递double类型
JNIEXPORT jdouble

JNICALL Java_com_dramascript_ndktest_NdkTools_getDouble
        (JNIEnv *evn, jobject obj, jdouble d) {
    double dl = d;
    dl = 2048.12;
    return (jdouble)
    dl;
}
  • 传递string类型
JNIEXPORT jstring

JNICALL Java_com_dramascript_ndktest_NdkTools_getString
        (JNIEnv *env, jobject obj, jstring s) {

    char *st = (char *) env->GetStringUTFChars(s, 0);
    char *str = "我很好!";
    jstring rtn;
    rtn = env->NewStringUTF(str);
    return rtn;
}
  • 传递数组类型
JNIEXPORT jbyteArray

JNICALL Java_com_dramascript_ndktest_NdkTools_getByteArray
        (JNIEnv *evn, jobject obj, jbyteArray ba) {

    //获得byte数组
    jbyte * bytes = evn->GetByteArrayElements(ba, 0);
    int chars_len = evn->GetArrayLength(ba);
    //返回新的byte数组
    jbyteArray arr = evn->NewByteArray(6);
    jbyte * by = evn->GetByteArrayElements(arr, 0);
    char ch[10] = "abcd";
    for (int i = 0; i < 4; ++i) {
        by[i] = ch[i];
    }
    evn->SetByteArrayRegion(arr, 0, 6, by);
    return arr;

}
  • 传递对象类型
JNIEXPORT jobject

JNICALL Java_com_dramascript_ndktest_NdkTools_getObject
        (JNIEnv *env, jobject obj, jobject paramIn) {

    //获取
    jclass paramInClass = env->GetObjectClass(paramIn);
    if (paramInClass == NULL) {
        return NULL;
    }
    if (env->IsInstanceOf(obj, paramInClass))//判断jobject是否是某个jclass类型。
    {
        jboolean iscopy;
        jfieldID intId = env->GetFieldID(paramInClass, "age", "I");
        jint num = (int) env->GetIntField(paramIn, intId);

        jfieldID strId = env->GetFieldID(paramInClass, "name", "Ljava/lang/String;");
        jstring str = (jstring)(env)->GetObjectField(paramIn, strId);
        const char *locstr = env->GetStringUTFChars(str, &iscopy);

        env->ReleaseStringUTFChars(str, locstr);
    }

    //返回
    jclass cls = env->FindClass("com/dramascript/ndktest/User");
    jmethodID id = env->GetMethodID(cls, "", "()V");

    jobject paramOut = env->NewObjectA(cls, id, 0);

    jfieldID intId = env->GetFieldID(cls, "age", "I");
    env->SetIntField(paramOut, intId, 23);

    jfieldID strId = env->GetFieldID(cls, "name", "Ljava/lang/String;");
    env->SetObjectField(paramOut, strId, (jstring)(env)->NewStringUTF("林俊杰"));

    return paramOut;
}

  • 传递数组对象类型
JNIEXPORT jobjectArray

JNICALL Java_com_dramascript_ndktest_NdkTools_getObjectArray
        (JNIEnv *env, jobject _obj, jobjectArray objarr) {

    //获取
    //JNI提供了两个函数来访问对象数组,GetObjectArrayElement返回数组中指定位置的元素,
    // SetObjectArrayElement修改数组中指定位置的元素。与基本类型不同的是,我们不能一次得到数据
    // 中的所有对象元素或者一次复制多个对象元素到缓冲区。
    jobject objects = env->GetObjectArrayElement(objarr, 0);//返回第0个对象
    jsize length = (env)->GetArrayLength(objarr);

    //返回

    //申明一个object数组
    jobjectArray args = 0;
    //获取object所属类,一般为ava/lang/Object就可以了
    jclass objClass = (env)->FindClass("java/lang/Object");
    //新建object数组
    args = (env)->NewObjectArray(5, objClass, 0);


    //给每一个实例的变量付值,并且将实例作为一个object,添加到objcet数组中
    for (int i = 0; i < 5; i++) {
        jclass cls = env->FindClass("com/dramascript/ndktest/User");
        jmethodID id = env->GetMethodID(cls, "", "()V");

        jobject paramOut = env->NewObjectA(cls, id, 0);

        jfieldID intId = env->GetFieldID(cls, "age", "I");
        env->SetIntField(paramOut, intId, 23);

        jfieldID strId = env->GetFieldID(cls, "name", "Ljava/lang/String;");
        env->SetObjectField(paramOut, strId, (jstring)(env)->NewStringUTF("书戏"));

        //添加到objcet数组中
        (env)->SetObjectArrayElement(args, i, paramOut);
    }
    return args;
}

JNIEXPORT jobject

JNICALL Java_com_dramascript_ndktest_NdkTools_getObjectList
        (JNIEnv *env, jobject obj) {

    jclass list_cls = env->FindClass("Ljava/util/ArrayList;");//获得ArrayList类引用
    jmethodID list_costruct = env->GetMethodID(list_cls, "", "()V"); //获得得构造函数Id
    jobject list_obj = env->NewObject(list_cls, list_costruct); //创建一个Arraylist集合对象
    //或得Arraylist类中的 add()方法ID,其方法原型为: boolean add(Object object) ;
    jmethodID list_add = env->GetMethodID(list_cls, "add", "(Ljava/lang/Object;)Z");

    jclass cls = env->FindClass("com/dramascript/ndktest/User");
    jmethodID id = env->GetMethodID(cls, "", "()V");

    for (int i = 0; i < 3; i++) {
        jobject paramOut = env->NewObject(cls, id);
        env->CallBooleanMethod(list_obj, list_add, paramOut); //执行Arraylist类实例的add方法,添加一个stu对象
    }
    return list_obj;
}
  • C层代码调用Java代码的某个方法
JNIEXPORT void JNICALL Java_com_dramascript_ndktest_NdkTools_callSetData
(JNIEnv *env, jobject obj){

    jclass clz = env->FindClass("com/dramascript/ndktest/NdkTools");
    jmethodID mid = env->GetMethodID(clz, "setData", "(Ljava/lang/String;I)V");
    env->CallVoidMethod(obj, mid, env->NewStringUTF("haha in C ."),20);
    

}

注意:在C++中调用JNI头文件的方法和在C代码中调用的方法传的参数是不一样的,如下:

//在C++可以少env这个参数
 jfieldID intId = env->GetFieldID(paramInClass, "age", "I");

//在C里面是不能缺少的
 jfieldID intId = (*env)->GetFieldID(*env,paramInClass, "age", "I");

至此,结束,代码在:Demo代码,如果觉得不错,给点打赏呗。

你可能感兴趣的:(Android JNI学习)