骑麦兜看落日

[Android]JNI

字数统计: 2.1k阅读时长: 9 min
2018/09/18 Share

NDK概述

NDK和SO

NDK全称Native Development Kit,是Google在Android开发中提供的一套用于快速创建native工程的一个工具,使用这个工具可以很方便的编写和调试JNI的代码。

因为C语言不跨平台,在Windows系统下使用NDK编译在Linux下能执行的函数库——SO文件(Shared Objects),其实质就是一堆c、c++的头文件和实现文件打包成一个库

目前Android系统目前支持以下七种不同的CPU架构,每一种对应着各自的应用程序二进制接口ABI:定义了二进制文件(尤其是.so文件)如何运行在相应的系统平台上,从使用的指令集,内存对齐到可用的系统函数库

1
2
3
4
5
6
7
ARMv5——armeabi
ARMv7 ——armeabi-v7a
ARMv8——arm64- v8a
x86——x86
MIPS ——mips
MIPS64——mips64
x86_64——x86_64

JNI

JNI 全称Java Native Inteface,即Java本地接口,是Java中定义的一种用于连接Java和C/C++接口的一种实现方式

Java语言装载到虚拟机中,不能和硬件交互,不能驱动开发


AndroidStudio编译.so库

安装JNI的开发环境

  • NDK

    ​ NDK是一个工具集,允许你的App使用一些底层语言代码

  • CMake

    ​ 跨平台的编译工具,可以用简单的语句来描述所有平台的编译过程,能够输出各种各样的makefile或者project文件,能测试编译器所支持的C++特性

  • LLDB

    ​ 支持断点调试C/C++源码

创建支持c/c++的project

创建project界面选择include C++ support

配置cmakelists.txt文件

  • cmake的最低版本要求
1
cmake_minimum_required(VERSION 3.4.1)
  • 使用 add_library() 的CMake指令构建脚本添加源文件或库,为了确保CMake可以在编译时定位标头文件,需要将 include_directories() 命令添加到CMake构建脚本中并指定标头的路径:

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    add_library( # Sets the name of the library.
    my-native-lib #library的名称

    # Sets the library as a shared library.
    SHARED

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

    # Specifies a path to native header files.
    include_directories(src/main/cpp/include/)
  • 使用find_library添加NDK库

    预构建的 NDK 库已经存在于 Android 平台上,因此无需再构建或将其封装到APK中

    由于 NDK 库已经是 CMake 搜索路径的一部分,不需要在本地 NDK 安装中指定库的位置,只需要向 CMake 提供库的名称,并将其关联原生库

  • 1
    2
    3
    4
    5
    6
    find_library( # Sets the name of the path variable.
    log-lib #例如:添加ndk中log-lib库

    # Specifies the name of the NDK library that
    # you want CMake to locate.
    log )
  • 为了确保创建的原生库可以使用log 库中的函数,需要使用CMake构建脚本中的 target_link_libraries()命令关联库

    1
    2
    3
    4
    5
    6
    target_link_libraries( # Specifies the target library.
    my-native-lib

    # Links the target library to the log library
    # included in the NDK.
    ${log-lib} )
  • 指示CMake构建android_native_app_glue.c,可以将`NativeActivity声明周期时间和触摸输入置于静态库中并将静态库关联到库中

    1
    2
    3
    4
    5
    6
    add_library( app-glue
    STATIC
    ${ANDROID_NDK}/sources/android/native_app_glue/android_native_app_glue.c )

    # You need to link static libraries against your shared native library.
    target_link_libraries( my-native-lib app-glue ${log-lib} )
  • cmakelists.txt文件内容

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    # For more information about using CMake with Android Studio, read the
    # documentation: https://d.android.com/studio/projects/add-native-code.html

    # 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.
    my-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
    src/main/cpp/hello.cpp )

    # Specifies a path to native header files.
    include_directories(src/main/cpp/include/)

    # 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.
    my-native-lib

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

添加其他预构建库

  • 使用IMPORTED标志告知CMake将库导入项目中

  • 1
    2
    3
    add_library( imported-lib
    SHARED
    IMPORTED )
  • 使用set_target_properties()命令指定库的路径

    某些库为特定的 CPU 架构(或应用二进制接口 (ABI))提供了单独的软件包,并将其组织到单独的目录中

    要向CMake构建脚本中添加库的多个 ABI 版本,而不必为库的每个版本编写多个命令,可以使用ANDROID_ABI路径变量,此变量使用NDK支持的一组默认ABI,或者手动配置Gradle而让其使用的一组经过筛选的ABI

    1
    2
    3
    4
    5
    6
    7
    8
    9
    add_library(...)
    set_target_properties( # Specifies the target library.
    imported-lib

    # Specifies the parameter you want to define.
    PROPERTIES IMPORTED_LOCATION

    # Provides the path to the library you want to import.
    imported-lib/src/${ANDROID_ABI}/libimported-lib.so )
  • 为了确保CMake可以在编译时定位标头文件,需要使用include_directories()命令,并包含标头文件的路径

  • 1
    include_directories( imported-lib/include/ )

module中gradle文件

  • 默认配置中的externalNativeBuild的配置

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    defaultConfig {
    ...
    externalNativeBuild {
    cmake {
    cppFlags "" //配置c++的版本库,其中""表示使用默认的,如 cppFlags "-std=c++14" 为c++14版本
    abiFilters "armeabi", "armeabi-v7a", "x86","x86_64","mips","mips64" // 输出指定abi体系结构下的so库
    }
    }
    }
  • 配置所引用的cmakelists.txt文件

  • 1
    2
    3
    4
    5
    externalNativeBuild {
    cmake {
    path "CMakeLists.txt"
    }
    }
  • 完整的gradle文件内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    apply plugin: 'com.android.application'

    android {
    ...
    defaultConfig {
    ...
    externalNativeBuild {
    cmake {
    cppFlags "" //配置c++的版本库,其中""表示使用默认的,如 cppFlags "-std=c++14" 为c++14版本
    abiFilters "armeabi", "armeabi-v7a", "x86","x86_64" // 输出指定abi体系结构下的so库
    }
    }
    }
    ...
    //配置引用的CMakeLists.txt文件
    externalNativeBuild {
    cmake {
    path "CMakeLists.txt"
    }
    }
    }

    dependencies {
    ...
    }

加载编写的原生库

1
2
3
4
5
static {
System.loadLibrary("my-native-lib"); //通过静态块,加载原生库
}

public native String stringFromJNI(); //定义一个native 方法
1
2
3
4
5
6
7
8
9
10
11
12
#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring

JNICALL
Java_com_youbale_cmakedemo_Mylib_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}

JNI基本语法

JNI简要流程

C/C++ code -> 本地方法接口类 -> 普通java业务类

JNI接口函数与指针

平台相关代码通过调用JNI函数访问Java虚拟机功能的,JNI函数可通过接口指针获得

接口指针是指针的指针,指向一个指针数组,而指针数组中的每个元素又指向一个接口函数

JNI接口指针只在当前线程中有效,本地方法不能将接口指针从一个线程传递到另一个线程中,但一个本地方法可被不同的Java线程所调用

1
2
3
4
5
6
7
8

+--------+ +--------+ +---------+
JNI接口指针 -> | 指针 | -> | 指针 | -> | 接口函数 |
+--------+ +--------+ +---------+
|每个线程的| | 指针 | -> | 接口函数 |
|JNI | +--------+ +---------+
|数据结构 | | 指针 | -> | 接口函数 |
+---------+ +--------+ +---------+

加载和链接本地方法

对本地方法的加载通过System.loadLibrary方法实现,其参数为库名,没有后缀

1
2
3
4
5
6
7
package pkg;
class cls {
native double f(int i, String s);
stack {
System.loadLibrary("pkg_Cls");
}
}

解析本地方法名

本地方法名由以下几部分串接而成

1
2
3
4
5
前缀 Java_
managled全限定的类名
下划线(_)分隔符
managled方法名
对于重载的本地方法,加上两个下划线(_),后跟managled参数签名

JNI的变量类型与Java类型对比表

Java中变量的类型 JNI对应的类型名 C/C++变量类型
boolean jboolean unsigned char
float jfloat float
double jdouble double
byte jbyte signed char
char jchar unsigned short
short jshort short
int jint/jsize int
long jlong long long
Object jobject void *
String jstring void *

本地方法的参数

1
2
3
4
package pkg;
class Cls {
native double f(int i, String s);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
jdouble Java_pkg_Cls_f_ILjava_lang_String_2 (
JNIEnv *ebc, /* 接口指针 */
jobject obj, /* "this" */
jint i, /* 第一个参数 */
jstring s) /* 第二个参数 */
{
/* 取得 Java 字符串的 C 版本 */
const char *str = (*env)->GetStringUTFChars(s, 0);
/* 处理该字符串 */
...
/* 至此完成对 str 的处理 */
(*env)->ReleaseStringUTFChars(s, str);
return ...
}

JNI接口指针是本地方法的第一个参数,其类型是JNIEnv

第二个参数随本地方法是静态还是非静态而有所不同:非静态本地方法的第二个参数是对象的引用,静态本地方法的第二个参数是对其java类的引用

其余参数都用于通常java方法的参数

本地方法调用利用返回值将结果传回调用程序中


参考资料

CATALOG
  1. 1. NDK概述
    1. 1.1. NDK和SO
    2. 1.2. JNI
  2. 2. AndroidStudio编译.so库
    1. 2.1. 安装JNI的开发环境
    2. 2.2. 创建支持c/c++的project
    3. 2.3. 配置cmakelists.txt文件
    4. 2.4. 添加其他预构建库
    5. 2.5. module中gradle文件
    6. 2.6. 加载编写的原生库
  3. 3. JNI基本语法
    1. 3.1. JNI简要流程
    2. 3.2. JNI接口函数与指针
    3. 3.3. 加载和链接本地方法
    4. 3.4. 解析本地方法名
    5. 3.5. JNI的变量类型与Java类型对比表
    6. 3.6. 本地方法的参数
    7. 3.7. 参考资料