关于android:Android-NDK开发入门

4次阅读

共计 21360 个字符,预计需要花费 54 分钟才能阅读完成。

JNI 简介

JNI(Java Native Interface 英文缩写),译为 Java 本地接口。是 Java 泛滥开发技术中的一门技术,意在利用本地代码,为 Java 程序提供更高效、更灵便的拓展。只管 Java 一贯以其良好的跨平台性而著称,但真正的跨平台非 C /C++ 莫属,因为以后世上 90% 的零碎都是基于 C /C++ 编写的。同时,Java 的跨平台是以就义效率换来对多种平台的兼容性,因此 JNI 就是这种跨平台的支流实现形式之一。

总之,JNI 是一门技术,是 Java 与 C /C++ 沟通的一门技术。首先,来回顾下 Android 的零碎架构图。

咱们来简略介绍下每一层的作用。

Linux 层

Linux 内核

因为 Android 零碎是根底 Linux 内核构建的,所以 Linux 是 Android 零碎的根底。事实上,Android 的硬件驱动、过程治理、内存治理、网络管理都是在这一层。

硬件形象层

硬件形象层(Hardware Abstraction Layer 缩写),硬件形象层次要为下层提供规范显示界面,并向更高级别的 Java API 框架提供显示设施硬件性能。HAL 蕴含多个库模块,其中每个模块都为特定类型的硬件组件实现一个界面,例如相机或蓝牙模块。当框架 API 要求拜访设施硬件时,Android 零碎将为该硬件组件加载对应的库模块。

零碎运行库和运行环境层

Android Runtime

Android 5.0(API 21)之前,应用的是 Dalvik 虚拟机,之后被 ART 所取代。ART 是 Android 操作系统的运行环境,通过运行虚拟机来执行 dex 文件。其中,dex 文件是专为安卓设计的的字节码格局,Android 打包和运行的就是 dex 文件,而 Android toolchain(一种编译工具)能够将 Java 代码编译为 dex 字节码格局,转化过程如下图。

如上所示,Jack 就是一种编译工具链,能够将 Java 源代码编译为 DEX 字节码,使其可在 Android 平台上运行。

原生 C /C++ 库

很多外围 Android 零碎组件和服务都是应用 C 和 C++ 编写的,为了不便开发者调用这些原生库性能,Android 的 Framework 提供了调用相应的 API。例如,您能够通过 Android 框架的 Java OpenGL API 拜访 OpenGL ES,以反对在利用中绘制和操作 2D 和 3D 图形。

应用程序框架层

Android 平台最罕用的组件和服务都在这一层,是每个 Android 开发者必须相熟和把握的一层,是利用开发的根底。

Application 层

Android 零碎 App,如电子邮件、短信、日历、互联网浏览和联系人等零碎利用。咱们能够像调用 Java API Framework 层一样间接调用零碎的 App。

接下来咱们看一下如何编写 Android JNI,以及须要的流程。

NDK

NDK 是什么

NDK(Native Development Kit 缩写)一种基于原生程序接口的软件开发工具包,能够让您在 Android 利用中利用 C 和 C++ 代码的工具。通过此工具开发的程序间接在本地运行,而不是虚拟机。

在 Android 中,NDK 是一系列工具的汇合,次要用于扩大 Android SDK。NDK 提供了一系列的工具能够帮忙开发者疾速的开发 C 或 C ++ 的动静库,并能主动将 so 和 Java 利用一起打包成 apk。同时,NDK 还集成了穿插编译器,并提供了相应的 mk 文件隔离 CPU、平台、ABI 等差别,开发人员只须要简略批改 mk 文件(指出“哪些文件须要编译”、“编译个性要求”等),就能够创立出 so 文件。

NDK 配置

创立 NDK 工程之前,请先保障本地曾经搭建好了 NDK 的相干环境。顺次抉择【Preferences…】->【Android SDK】下载配置 NDK,如下所示。

而后,新建一个 Native C++ 工程,如下所示。

而后勾选【Include C++ support】选项,点击【下一步】,达到【Customize C++ Support】设置页,如下所示。

而后,点击【Finish】按钮即可。

NDK 我的项目目录

关上新建的 NDK 工程,目录如下图所示。

咱们接下来看一下,Android 的 NDK 工程和一般的 Android 利用工程有哪些不一样的中央。首先,咱们来看下 build.gradle 配置。

apply plugin: 'com.android.application'

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.2"

    defaultConfig {
        applicationId "com.xzh.ndk"
        minSdkVersion 16
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        externalNativeBuild {
            cmake {cppFlags ""}
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
            version "3.10.2"
        }
    }
}

dependencies {// 省略援用的第三方库}

能够看到,相比一般的 Android 利用,build.gradle 配置中多了两个 externalNativeBuild 配置项。其中,defaultConfig 外面的的 externalNativeBuild 次要是用于配置 Cmake 的命令参数,而内部的
externalNativeBuild 的次要是定义了 CMake 的构建脚本 CMakeLists.txt 的门路。

而后,咱们来看一下 CMakeLists.txt 文件,CMakeLists.txt 是 CMake 的构建脚本,作用相当于 ndk-build 中的 Android.mk,代码如下。

# 设置 Cmake 最小版本
cmake_minimum_required(VERSION 3.4.1)

# 编译 library
add_library( # 设置 library 名称
             native-lib

             # 设置 library 模式
             # SHARED 模式会编译 so 文件,STATIC 模式不会编译
             SHARED

             # 设置原生代码门路
             src/main/cpp/native-lib.cpp )

# 定位 library
find_library( # library 名称
              log-lib

              # 将 library 门路存储为一个变量,能够在其余中央用这个变量援用 NDK 库
              # 在这里设置变量名称
              log )

# 关联 library
target_link_libraries( # 关联的 library
                       native-lib

                       # 关联 native-lib 和 log-lib
                       ${log-lib} )

对于 CMake 的更多常识,能够查看 CMake 官网手册。

官网示例

默认创立 Android NDK 工程时,Android 提供了一个简略的 JNI 交互示例,返回一个字符串给 Java 层,办法名的格局为:Java_包名_类名_办法名。首先,咱们看一下 native-lib.cpp 的代码。

#include <jni.h>
#include <string>

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

而后,咱们在看一下 Android 的 MainActivity.java 的代码。

package com.xzh.ndk;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView tv = findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
    }

    public native String stringFromJNI();}

初识 Android JNI

1,JNI 开发流程

  1. 编写 java 类, 申明了 native 办法;
  2. 编写 native 代码;
  3. 将 native 代码编译成 so 文件;
  4. 在 java 类中引入 so 库, 调用 native 办法;

2,native 办法命名

extern "C"
JNIEXPORT void JNICALL
Java_com_xfhy_jnifirst_MainActivity_callJavaMethod(JNIEnv *env, jobject thiz) {}

函数命名规定: Java_类全门路_办法名,波及的参数的含意如下:

  • JNIEnv* 是定义任意 native 函数的第一个参数,示意指向 JNI 环境的指针,能够通过它来拜访 JNI 提供的接口办法。
  • jobject 示意 Java 对象中的 this,如果是静态方法则示意 jclass。
  • JNIEXPORT 和 JNICALL: 它们是 JNI 中所定义的宏, 能够在 jni.h 这个头文件中查找到。

3,JNI 数据类型与 Java 数据类型的对应关系

首先,咱们在 Java 代码里编写一个 native 办法申明,而后应用【alt+enter】快捷键让 AS 帮忙咱们创立一个 native 办法,如下所示。

public static native void ginsengTest(short s, int i, long l, float f, double d, char c,
                                   boolean z, byte b, String str, Object obj, MyClass p, int[] arr);


// 对应的 Native 代码
Java_com_xfhy_jnifirst_MainActivity_ginsengTest(JNIEnv *env, jclass clazz, jshort s, jint i, jlong l, jfloat f, jdouble d, jchar c,
                                                jboolean z, jbyte b, jstring str, jobject obj, jobject p, jintArray arr) {

}

上面,咱们整顿下 Java 和 JNI 的类型对照表,如下所示。

Java 类型 Native 类型 有无合乎 字长
boolean jboolean 无符号 8 字节
byte jbyte 有符号 8 字节
char jchar 无符号 16 字节
short jshort 有符号 16 字节
int jint 有符号 32 字节
long jlong 有符号 64 字节
float jfloat 有符号 32 字节
double jdouble 有符号 64 字节

对应的援用类型如下表所示。

| Java 类型 | Native 类型 |
|–|–|
| java.lang.Class | jclass |
|java.lang.Throwable | jthrowable |
|java.lang.String | jstring |
|jjava.lang.Object[] | jobjectArray |
|Byte[]| jbyteArray |
|Char[] | jcharArray |
|Short[] | jshortArray |
|int[] | jintArray |
|long[] | jlongArray |
|float[] | jfloatArray |
|double[] | jdoubleArray |

3.1 根本数据类型

Native 的根本数据类型其实就是将 C /C++ 中的根本类型用 typedef 从新定义了一个新的名字,在 JNI 中能够间接拜访,如下所示。

typedef uint8_t  jboolean; /* unsigned 8 bits */
typedef int8_t   jbyte;    /* signed 8 bits */
typedef uint16_t jchar;    /* unsigned 16 bits */
typedef int16_t  jshort;   /* signed 16 bits */
typedef int32_t  jint;     /* signed 32 bits */
typedef int64_t  jlong;    /* signed 64 bits */
typedef float    jfloat;   /* 32-bit IEEE 754 */
typedef double   jdouble;  /* 64-bit IEEE 754 */

3.2 援用数据类型

如果应用 C ++ 语言编写,则所有援用派生自 jobject 根类,如下所示。

class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jthrowable : public _jobject {};

JNI 应用 C 语言时,所有援用类型都应用 jobject。

4,JNI 的字符串解决

4.1 native 操作 JVM

JNI 会把 Java 中所有对象当做一个 C 指针传递到本地办法中, 这个指针指向 JVM 外部数据结构, 而外部的数据结构在内存中的存储形式是不可见的. 只能从 JNIEnv 指针指向的函数表中抉择适合的 JNI 函数来操作 JVM 中的数据结构。

比方 native 拜访 java.lang.String 对应的 JNI 类型 jstring 时,不能像拜访根本数据类型那样应用,因为它是一个 Java 的援用类型,所以在本地代码中只能通过相似 GetStringUTFChars 这样的 JNI 函数来拜访字符串的内容。

4.2 字符串操作的示例


// 调用
String result = operateString("待操作的字符串");
Log.d("xfhy", result);

// 定义
public native String operateString(String str);

而后在 C 中进行实现,代码如下。

extern "C"
JNIEXPORT jstring JNICALL
Java_com_xfhy_jnifirst_MainActivity_operateString(JNIEnv *env, jobject thiz, jstring str) {
    // 从 java 的内存中把字符串拷贝进去  在 native 应用
    const char *strFromJava = (char *) env->GetStringUTFChars(str, NULL);
    if (strFromJava == NULL) {
        // 必须空查看
        return NULL;
    }

    // 将 strFromJava 拷贝到 buff 中, 待会儿好拿去生成字符串
    char buff[128] = {0};
    strcpy(buff, strFromJava);
    strcat(buff, "在字符串前面加点货色");

    // 开释资源
    env->ReleaseStringUTFChars(str, strFromJava);

    // 主动转为 Unicode
    return env->NewStringUTF(buff);
}
4.2.1 native 中获取 JVM 字符串

在下面的代码中,operateString 函数接管一个 jstring 类型的参数 str,jstring 是指向 JVM 外部的一个字符串,不能间接应用。首先,须要将 jstring 转为 C 格调的字符串类型 char* 后能力应用,这里必须应用适合的 JNI 函数来拜访 JVM 外部的字符串数据结构。

GetStringUTFChars(jstring string, jboolean* isCopy)对应的参数的含意如下:

  • string : jstring,Java 传递给 native 代码的字符串指针。
  • isCopy : 个别状况下传 NULL,取值能够是 JNI_TRUE 和 JNI_FALSE,如果是 JNI_TRUE 则会返回 JVM 外部源字符串的一份拷贝,并为新产生的字符串分配内存空间。如果是 JNI_FALSE 则返回 JVM 外部源字符串的指针,意味着能够在 native 层批改源字符串,然而不举荐批改,因为 Java 字符串的准则是不能批改的。

Java 中默认是应用 Unicode 编码,C/C++ 默认应用 UTF 编码,所以在 native 层与 java 层进行字符串交换的时候须要进行编码转换。GetStringUTFChars 就刚好能够把 jstring 指针 (指向 JVM 外部的 Unicode 字符序列) 的字符串转换成一个 UTF- 8 格局的 C 字符串。

4.2.2 异样解决

在应用 GetStringUTFChars 的时候, 返回的值可能为 NULL,这时须要解决一下, 否则持续往下面走的话,应用这个字符串的时候会呈现问题. 因为调用这个办法时,是拷贝,JVM 为新生成的字符串分配内存空间,当内存空间不够调配的时候就会导致调用失败。调用失败就会返回 NULL,并抛出 OutOfMemoryError。JNI 遇到未决的异样不会改变程序的运行流程, 还是会持续往下走。

4.2.3 开释字符串资源

native 不像 Java,咱们须要手动开释申请的内存空间。GetStringUTFChars 调用时会新申请一块空间用来装拷贝进去的字符串,这个字符串用来不便 native 代码拜访和批改之类的。既然有内存调配,那么就必须手动开释,开释办法是 ReleaseStringUTFChars。能够看到和 GetStringUTFChars 是一一对应配对的。

4.2.4 构建字符串

应用 NewStringUTF 函数能够构建出一个 jstring,须要传入一个 char * 类型的 C 字符串。它会构建一个新的 java.lang.String 字符串对象,并且会主动转换成 Unicode 编码。如果 JVM 不能为结构 java.lang.String 调配足够的内存,则会抛出一个 OutOfMemoryError 异样并返回 NULL。

4.2.5 其余字符串操作函数
  1. GetStringChars 和 ReleaseStringChars:这对函数和 Get/ReleaseStringUTFChars 函数性能相似,用于获取和开释的字符串是以 Unicode 格局编码的。
  2. GetStringLength:获取 Unicode 字符串 (jstring) 的长度。UTF- 8 编码的字符串是以 0 结尾,而 Unicode 的不是,所以这里须要独自辨别开。
  3. 「GetStringUTFLength」: 获取 UTF- 8 编码字符串的长度, 就是获取 C /C++ 默认编码字符串的长度. 还能够应用规范 C 函数「strlen」来获取其长度。
  4. strcat: 拼接字符串,规范 C 函数。如strcat(buff, "xfhy"); 将 xfhy 增加到 buff 的开端。
  5. GetStringCritical 和 ReleaseStringCritical: 为了减少间接传回指向 Java 字符串的指针的可能性(而不是拷贝). 在这 2 个函数之间的区域, 是相对不能调用其余 JNI 函数或者让线程阻塞的 native 函数. 否则 JVM 可能死锁. 如果有一个字符串的内容特地大, 比方 1M, 且只须要读取外面的内容打印进去, 此时比拟适宜用该对函数, 可间接返回源字符串的指针。
  6. GetStringRegion 和 GetStringUTFRegion: 获取 Unicode 和 UTF- 8 字符串中指定范畴的内容 (如: 只须要 1 - 3 索引处的字符串), 这对函数会将源字符串复制到一个事后调配的缓冲区(本人定义的 char 数组) 内。

通常,GetStringUTFRegion 会进行越界查看,越界会抛 StringIndexOutOfBoundsException 异样。GetStringUTFRegion 其实和 GetStringUTFChars 有点类似,然而 GetStringUTFRegion 外部不会分配内存,不会抛出内存溢出异样。因为其外部没有分配内存,所以也没有相似 Release 这样的函数来开释资源。

4.2.6 小结
  • Java 字符串转 C /C++ 字符串: 应用 GetStringUTFChars 函数, 必须调用 ReleaseStringUTFChars 开释内存。
  • 创立 Java 层须要的 Unicode 字符串, 应用 NewStringUTF 函数。
  • 获取 C /C++ 字符串长度,应用 GetStringUTFLength 或者 strlen 函数。
  • 对于小字符串,GetStringRegion 和 GetStringUTFRegion 这 2 个函数是最佳抉择, 因为缓冲区数组能够被编译器提取调配,不会产生内存溢出的异样。当只须要解决字符串的局部数据时,也还是不错。它们提供了开始索引和子字符串长度值,复制的耗费也是十分小
  • 获取 Unicode 字符串和长度,应用 GetStringChars 和 GetStringLength 函数。

数组操作

5.1 根本类型数组

根本类型数组就是 JNI 中的根本数据类型组成的数组,能够间接拜访。例如,上面是 int 数组求和的例子,代码如下。

//MainActivity.java
public native int sumArray(int[] array);
extern "C"
JNIEXPORT jint JNICALL
Java_com_xfhy_jnifirst_MainActivity_sumArray(JNIEnv *env, jobject thiz, jintArray array) {
    // 数组求和
    int result = 0;

    // 形式 1  举荐应用
    jint arr_len = env->GetArrayLength(array);
    // 动静申请数组
    jint *c_array = (jint *) malloc(arr_len * sizeof(jint));
    // 初始化数组元素内容为 0
    memset(c_array, 0, sizeof(jint) * arr_len);
    // 将 java 数组的 [0-arr_len) 地位的元素拷贝到 c_array 数组中
    env->GetIntArrayRegion(array, 0, arr_len, c_array);
    for (int i = 0; i < arr_len; ++i) {result += c_array[i];
    }
    // 动静申请的内存 必须开释
    free(c_array);

    return result;
}

C 层拿到 jintArray 之后首先须要获取它的长度,而后动静申请一个数组(因为 Java 层传递过去的数组长度是不定的,所以这里须要动静申请 C 层数组), 这个数组的元素是 jint 类型的。malloc 是一个常常应用的拿来申请一块间断内存的函数,申请之后的内存是须要手动调用 free 开释的。而后就是调用 GetIntArrayRegion 函数将 Java 层数组拷贝到 C 层数组中并进行求和。

接下来,咱们来看另一种求和形式,代码如下。

extern "C"
JNIEXPORT jint JNICALL
Java_com_xfhy_jnifirst_MainActivity_sumArray(JNIEnv *env, jobject thiz, jintArray array) {
    // 数组求和
    int result = 0;

    // 形式 2  
    // 此种形式比拟危险,GetIntArrayElements 会间接获取数组元素指针, 是能够间接对该数组元素进行批改的.
    jint *c_arr = env->GetIntArrayElements(array, NULL);
    if (c_arr == NULL) {return 0;}
    c_arr[0] = 15;
    jint len = env->GetArrayLength(array);
    for (int i = 0; i < len; ++i) {//result += *(c_arr + i); 写成这种模式, 或者上面一行那种都行
        result += c_arr[i];
    }
    // 有 Get, 个别就有 Release
    env->ReleaseIntArrayElements(array, c_arr, 0);

    return result;
}

在下面的代码中,咱们间接通过 GetIntArrayElements 函数拿到原数组元素指针,间接操作就能够拿到元素求和。看起来要简略很多,然而这种形式我集体感觉是有点危险,毕竟这种能够在 C 层间接进行源数组批改不是很保险的。GetIntArrayElements 的第二个参数个别传 NULL,传递 JNI_TRUE 是返回长期缓冲区数组指针(即拷贝一个正本),传递 JNI_FALSE 则是返回原始数组指针。

5.2 对象数组

对象数组中的元素是一个类的实例或其余数组的援用,不能间接拜访 Java 传递给 JNI 层的数组。操作对象数组稍显简单,上面举一个例子:在 native 层创立一个二维数组,且赋值并返回给 Java 层应用。

public native int[][] init2DArray(int size);

// 交给 native 层创立 ->Java 打印输出
int[][] init2DArray = init2DArray(3);
for (int i = 0; i < 3; i++) {for (int i1 = 0; i1 < 3; i1++) {Log.d("xfhy", "init2DArray[" + i + "][" + i1 + "]" + "=" + init2DArray[i][i1]);
    }
}
extern "C"
JNIEXPORT jobjectArray JNICALL
Java_com_xzh_jnifirst_MainActivity_init2DArray(JNIEnv *env, jobject thiz, jint size) {
    // 创立一个 size*size 大小的二维数组

    //jobjectArray 是用来装对象数组的   Java 数组就是一个对象 int[]
    jclass classIntArray = env->FindClass("[I");
    if (classIntArray == NULL) {return NULL;}
    // 创立一个数组对象, 元素为 classIntArray
    jobjectArray result = env->NewObjectArray(size, classIntArray, NULL);
    if (result == NULL) {return NULL;}
    for (int i = 0; i < size; ++i) {jint buff[100];
        // 创立第二维的数组 是第一维数组的一个元素
        jintArray intArr = env->NewIntArray(size);
        if (intArr == NULL) {return NULL;}
        for (int j = 0; j < size; ++j) {
            // 这里轻易设置一个值
            buff[j] = 666;
        }
        // 给一个 jintArray 设置数据
        env->SetIntArrayRegion(intArr, 0, size, buff);
        // 给一个 jobjectArray 设置数据 第 i 索引, 数据位 intArr
        env->SetObjectArrayElement(result, i, intArr);
        // 及时移除援用
        env->DeleteLocalRef(intArr);
    }

    return result;
}

接下来,咱们来剖析下代码。

  1. 首先,是利用 FindClass 函数找到 java 层 int[]对象的 class,这个 class 是须要传入 NewObjectArray 创建对象数组的。调用 NewObjectArray 函数之后,即可创立一个对象数组,大小是 size,元素类型是后面获取到的 class。
  2. 进入 for 循环构建 size 个 int 数组,构建 int 数组须要应用 NewIntArray 函数。能够看到我构建了一个长期的 buff 数组,而后大小是轻易设置的, 这里是为了示例,其实能够用 malloc 动静申请空间,省得申请 100 个空间,可能太大或者太小了。整 buff 数组次要是拿来给生成进去的 jintArray 赋值的,因为 jintArray 是 Java 的数据结构,咱 native 不能间接操作,得调用 SetIntArrayRegion 函数,将 buff 数组的值复制到 jintArray 数组中。
  3. 而后调用 SetObjectArrayElement 函数设置 jobjectArray 数组中某个索引处的数据, 这里将生成的 jintArray 设置进去。
  4. 最初须要将 for 外面生成的 jintArray 及时移除援用。创立的 jintArray 是一个 JNI 部分援用,如果部分援用太多的话,会造成 JNI 援用表溢出。

6,Native 调 Java 办法

相熟 JVM 的都应该晓得,在 JVM 中运行一个 Java 程序时,会先将运行时须要用到的所有相干 class 文件加载到 JVM 中,并按需加载, 进步性能和节约内存。当咱们调用一个类的静态方法之前,JVM 会先判断该类是否曾经加载,如果没有被 ClassLoader 加载到 JVM 中,会去 classpath 门路下查找该类。找到了则加载该类,没有找到则报 ClassNotFoundException 异样。

6.1 Native 调用 Java 静态方法

首先,咱们编写一个 MyJNIClass.java 类,代码如下。

public class MyJNIClass {

    public int age = 30;

    public int getAge() {return age;}

    public void setAge(int age) {this.age = age;}

    public static String getDes(String text) {if (text == null) {text = "";}
        return "传入的字符串长度是 :" + text.length() + "内容是 :" + text;}

}

而后,在 native 中调用 getDes()办法,为了简单一点,这个 getDes()办法不仅有入参,还有返参,如下所示。

extern "C"
JNIEXPORT void JNICALL
Java_com_xzh_allinone_jni_CallMethodActivity_callJavaStaticMethod(JNIEnv *env, jobject thiz) {
    // 调用某个类的 static 办法
    //1. 从 classpath 门路下搜寻 MyJNIClass 这个类, 并返回该类的 Class 对象
    jclass clazz = env->FindClass("com/xzh/jni/jni/MyJNIClass");
    //2. 从 clazz 类中查找 getDes 办法 失去这个静态方法的办法 id
    jmethodID mid_get_des = env->GetStaticMethodID(clazz, "getDes", "(Ljava/lang/String;)Ljava/lang/String;");
    //3. 构建入参, 调用 static 办法, 获取返回值
    jstring str_arg = env->NewStringUTF("我是 xzh");
    jstring result = (jstring) env->CallStaticObjectMethod(clazz, mid_get_des, str_arg);
    const char *result_str = env->GetStringUTFChars(result, NULL);
    LOGI("获取到 Java 层返回的数据 : %s", result_str);

    //4. 移除部分援用
    env->DeleteLocalRef(clazz);
    env->DeleteLocalRef(str_arg);
    env->DeleteLocalRef(result);
}

能够发现,Native 调用 Java 静态方法还是比较简单的,次要会经验以下几个步骤。

  1. 首先,调用 FindClass 函数传入 Class 描述符(Java 类的全类名,这里在 AS 中输出 MyJNIClass 时会有提醒补全,间接 enter 即可补全),找到该类并失去 jclass 类型。
  2. 而后,通过 GetStaticMethodID 找到该办法的 id,传入办法签名,失去 jmethodID 类型的援用。
  3. 构建入参,而后调用 CallStaticObjectMethod 去调用 Java 类外面的静态方法,而后传入参数,返回的间接就是 Java 层返回的数据。其实,这里的 CallStaticObjectMethod 是调用的援用类型的静态方法,与之类似的还有:CallStaticVoidMethod(无返参),CallStaticIntMethod(返参是 Int),CallStaticFloatMethod 等。
  4. 移除部分援用。

6.2 Native 调用 Java 实例办法

接下来,咱们来看一下在 Native 层创立 Java 实例并调用该实例的办法,大抵上是和下面调用静态方法差不多的。首先,咱们批改下 cpp 文件的代码,如下所示。

extern "C"
JNIEXPORT void JNICALL
Java_com_xzh_allinone_jni_CallMethodActivity_createAndCallJavaInstanceMethod(JNIEnv *env, jobject thiz) {jclass clazz = env->FindClass("com/xzh/allinone/jni/MyJNIClass");
    // 获取构造方法的办法 id
    jmethodID mid_construct = env->GetMethodID(clazz, "<init>", "()V");
    // 获取 getAge 办法的办法 id
    jmethodID mid_get_age = env->GetMethodID(clazz, "getAge", "()I");
    jmethodID mid_set_age = env->GetMethodID(clazz, "setAge", "(I)V");
    jobject jobj = env->NewObject(clazz, mid_construct);

    // 调用办法 setAge
    env->CallVoidMethod(jobj, mid_set_age, 20);
    // 再调用办法 getAge 获取返回值 打印输出
    jint age = env->CallIntMethod(jobj, mid_get_age);
    LOGI("获取到 age = %d", age);

    // 但凡应用是 jobject 的子类, 都须要移除援用
    env->DeleteLocalRef(clazz);
    env->DeleteLocalRef(jobj);
}

如上所示,Native 调用 Java 实例办法的步骤如下:

  1. Native 调用 Java 实例办法。
  2. 获取构造方法的 id,获取须要调用办法的 id。其中获取构造方法时,办法名称固定写法就是<init>,而后前面是办法签名。
  3. 应用 NewObject()函数构建一个 Java 对象。
  4. 调用 Java 对象的 setAge 和 getAge 办法, 获取返回值, 打印后果。
  5. 删除援用。

NDK 谬误定位

因为 NDK 大部分的逻辑是在 C /C++ 实现的,当 NDK 产生谬误某种致命的谬误的时候导致 APP 闪退。对于这类谬误问题是十分不好排查的,比方内存地址拜访谬误、应用野指针、内存泄露、堆栈溢出等 native 谬误都会导致 APP 解体。

尽管这些 NDK 谬误不好排查,然而咱们在 NDK 谬误产生后也不是毫无办法可言。具体来说,当拿到 Logcat 输入的堆栈日志,再联合 addr2line 和 ndk-stack 两款调试工具,就能够很够准确地定位到相应产生谬误的代码行数,进而迅速找到问题。

首先,咱们关上 ndk 目录下下的 sdk/ndk/21.0.6113669/toolchains/ 目录,能够看到 NDK 穿插编译器工具链的目录构造如下所示。

而后,咱们再看一下 ndk 的文件目录,如下所示。

其中,ndk-stack 放在 $NDK_HOME 目录下,与 ndk-build 同级目录。addr2line 在 ndk 的穿插编译器工具链目录下。同时,NDK 针对不同的 CPU 架构实现了多套工具,在应用 addr2line 工具时,须要依据以后手机 cpu 架构来抉择。比方,我的手机是 aarch64 的,那么须要应用 aarch64-linux-android-4.9 目录下的工具。Android NDK 提供了查看手机的 CPU 信息的命令,如下所示。

adb shell cat /proc/cpuinfo

在正式介绍两款调试工具之前,咱们能够先写好解体的 native 代码不便咱们查看成果。首先,咱们修复 native-lib.cpp 外面的代码,如下所示。

void willCrash() {
    JNIEnv *env = NULL;
    int version = env->GetVersion();}

extern "C"
JNIEXPORT void JNICALL
Java_com_xzh_allinone_jni_CallMethodActivity_nativeCrashTest(JNIEnv *env, jobject thiz) {LOGI("解体前");
    willCrash();
    // 前面的代码是执行不到的, 因为解体了
    LOGI("解体后");
    printf("oooo");
}

下面的这段代码是很显著的空指针异样,运行后谬误日志如下。

2020-10-07 17:05:25.230 12340-12340/? A/DEBUG: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
2020-10-07 17:05:25.230 12340-12340/? A/DEBUG: Build fingerprint: 'Xiaomi/dipper/dipper:10/QKQ1.190828.002/V11.0.8.0.QEACNXM:user/release-keys'
2020-10-07 17:05:25.230 12340-12340/? A/DEBUG: Revision: '0'
2020-10-07 17:05:25.230 12340-12340/? A/DEBUG: ABI: 'arm64'
2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: Timestamp: 2020-06-07 17:05:25+0800
2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: pid: 11527, tid: 11527, name: m.xfhy.allinone  >>> com.xfhy.allinone <<<
2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: uid: 10319
2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: Cause: null pointer dereference
2020-10-07 17:05:25.237 12340-12340/? A/DEBUG:     x0  0000000000000000  x1  0000007fd29ffd40  x2  0000000000000005  x3  0000000000000003
2020-10-07 17:05:25.237 12340-12340/? A/DEBUG:     x4  0000000000000000  x5  8080800000000000  x6  fefeff6fb0ce1f1f  x7  7f7f7f7fffff7f7f
2020-10-07 17:05:25.237 12340-12340/? A/DEBUG:     x8  0000000000000000  x9  a95a4ec0adb574df  x10 0000007fd29ffee0  x11 000000000000000a
2020-10-07 17:05:25.237 12340-12340/? A/DEBUG:     x12 0000000000000018  x13 ffffffffffffffff  x14 0000000000000004  x15 ffffffffffffffff
2020-10-07 17:05:25.237 12340-12340/? A/DEBUG:     x16 0000006fc6476c50  x17 0000006fc64513cc  x18 00000070b21f6000  x19 000000702d069c00
2020-10-07 17:05:25.237 12340-12340/? A/DEBUG:     x20 0000000000000000  x21 000000702d069c00  x22 0000007fd2a00720  x23 0000006fc6ceb127
2020-10-07 17:05:25.237 12340-12340/? A/DEBUG:     x24 0000000000000004  x25 00000070b1cf2020  x26 000000702d069cb0  x27 0000000000000001
2020-10-07 17:05:25.237 12340-12340/? A/DEBUG:     x28 0000007fd2a004b0  x29 0000007fd2a00420
2020-10-07 17:05:25.237 12340-12340/? A/DEBUG:     sp  0000007fd2a00410  lr  0000006fc64513bc  pc  0000006fc64513e0
2020-10-07 17:05:25.788 12340-12340/? A/DEBUG: backtrace:
2020-10-07 17:05:25.788 12340-12340/? A/DEBUG:       #00 pc 00000000000113e0  /data/app/com.xfhy.allinone-4VScOmUWz8wLqqwBWZCP2w==/lib/arm64/libnative-lib.so (_JNIEnv::GetVersion()+20) (BuildId: b1130c28a8b45feda869397e55c5b6d754410c8d)
2020-10-07 17:05:25.788 12340-12340/? A/DEBUG:       #01 pc 00000000000113b8  /data/app/com.xfhy.allinone-4VScOmUWz8wLqqwBWZCP2w==/lib/arm64/libnative-lib.so (willCrash()+24) (BuildId: b1130c28a8b45feda869397e55c5b6d754410c8d)
2020-10-07 17:05:25.788 12340-12340/? A/DEBUG:       #02 pc 0000000000011450  /data/app/com.xfhy.allinone-4VScOmUWz8wLqqwBWZCP2w==/lib/arm64/libnative-lib.so (Java_com_xfhy_allinone_jni_CallMethodActivity_nativeCrashTest+84) (BuildId: b1130c28a8b45feda869397e55c5b6d754410c8d)
2020-10-07 17:05:25.788 12340-12340/? A/DEBUG:       #03 pc 000000000013f350  /apex/com.android.runtime/lib64/libart.so (art_quick_generic_jni_trampoline+144) (BuildId: 2bc2e11d57f839316bf2a42bbfdf943a)
2020-10-07 17:05:25.788 12340-12340/? A/DEBUG:       #04 pc 0000000000136334  /apex/com.android.runtime/lib64/libart.so (art_quick_invoke_stub+548) (BuildId: 2bc2e11d57f839316bf2a42bbfdf943a)

首先,找到要害信息Cause: null pointer dereference,然而咱们不晓得产生在具体哪里,所以接下来咱们须要借助 addr2line 和 ndk-stack 两款工具来帮助咱们进行剖析。

7.1 addr2line

当初,咱们应用工具 addr2line 来定位地位。首先,执行如下命令。

/Users/xzh/development/sdk/ndk/21.0.6113669/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line -e /Users/xzh/development/AllInOne/app/libnative-lib.so 00000000000113e0 00000000000113b8

作者:潇风寒月
链接:https://juejin.im/post/6844904190586650632
起源:掘金
著作权归作者所有。商业转载请分割作者取得受权,非商业转载请注明出处。

其中 - e 是指定 so 文件的地位,而后开端的 00000000000113e0 和 00000000000113b8 是出错地位的汇编指令地址。

/Users/xzh/development/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/include/jni.h:497
/Users/xzh/development/AllInOne/app/src/main/cpp/native-lib.cpp:260

能够看到,是 native-lib.cpp 的 260 行出的问题,咱们只须要找到这个地位而后修复这个文件即可。

7.2 ndk-stack

除此之外,还有一种更简略的形式,间接输出命令。

adb logcat | ndk-stack -sym /Users/xzh/development/AllInOne/app/build/intermediates/cmake/debug/obj/arm64-v8a

开端是 so 文件的地位,执行完命令后就能够在手机上产生 native 谬误,而后就能在这个 so 文件中定位到这个谬误点。

********** Crash dump: **********
Build fingerprint: 'Xiaomi/dipper/dipper:10/QKQ1.190828.002/V11.0.8.0.QEACNXM:user/release-keys'
#00 0x00000000000113e0 /data/app/com.xfhy.allinone-oVu0tjta-aW9LYa08eoK1Q==/lib/arm64/libnative-lib.so (_JNIEnv::GetVersion()+20) (BuildId: b1130c28a8b45feda869397e55c5b6d754410c8d)
                                                                                                        _JNIEnv::GetVersion()
                                                                                                        /Users/xzh/development/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/include/jni.h:497:14
#01 0x00000000000113b8 /data/app/com.xfhy.allinone-oVu0tjta-aW9LYa08eoK1Q==/lib/arm64/libnative-lib.so (willCrash()+24) (BuildId: b1130c28a8b45feda869397e55c5b6d754410c8d)
                                                                                                        willCrash()
                                                                                                        /Users/xzh/development/AllInOne/app/src/main/cpp/native-lib.cpp:260:24
#02 0x0000000000011450 /data/app/com.xfhy.allinone-oVu0tjta-aW9LYa08eoK1Q==/lib/arm64/libnative-lib.so (Java_com_xzh_allinone_jni_CallMethodActivity_nativeCrashTest+84) (BuildId: b1130c28a8b45feda869397e55c5b6d754410c8d)
                                                                                                        Java_com_xzh_allinone_jni_CallMethodActivity_nativeCrashTest
                                                                                                        /Users/xzh/development/AllInOne/app/src/main/cpp/native-lib.cpp:267:5

能够看到,下面的日志明确指出了是 willCrash()办法出的错,它的代码行数是 260 行。

8,JNI 援用

家喻户晓,Java 在新创建对象的时候,不须要思考 JVM 是怎么申请内存的,也不须要在应用完之后去开释内存。而 C ++ 不同,须要咱们手动申请和开释内存(new->delete,malloc->free)。在应用 JNI 时,因为本地代码不能间接通过援用操作 JVM 外部的数据结构,要进行这些操作必须调用相应的 JNI 接口间接操作 JVM 外部的数据内容。咱们不须要关怀 JVM 中对象的是如何存储的,只须要学习 JNI 中的三种不同援用即可。

8.1 JNI 部分援用

通常,本地函数中通过 NewLocalRef 或调用 FindClass、NewObject、GetObjectClass、NewCharArray 等创立的援用,就是部分援用。部分援用具备如下一些特色:

  • 会阻止 GC 回收所援用的对象
  • 不能跨线程应用
  • 不在本地函数中跨函数应用
  • 开释: 函数返回后部分援用所援用的对象会被 JVM 主动开释,也能够调用 DeleteLocalRef 开释。

通常是在函数中创立并应用的就是部分援用,部分援用在函数返回之后会主动开释。那么咱们为啥还须要去手动调用 DeleteLocalRef 进行开释呢?

比方,开了一个 for 循环,外面一直地创立部分援用,那么这时就必须得应用 DeleteLocalRef 手动开释内存。不然部分援用会越来越多,最终导致解体(在 Android 低版本上部分援用表的最大数量有限度,是 512 个,超过则会解体)。

还有一种状况,本地办法返回一个援用到 Java 层之后,如果 Java 层没有对返回的部分援用应用的话,部分援用就会被 JVM 主动开释。

8.2 JNI 全局援用

全局援用是基于部分援用创立的,应用 NewGlobalRef 办法创立。全局援用具备如下一些个性:

  • 会阻止 GC 回收所援用的对象
  • 能够跨办法、跨线程应用
  • JVM 不会主动开释, 需调用 DeleteGlobalRef 手动开释

8.3 JNI 弱全局援用

弱全局援用是基于部分援用或者全局援用创立的,应用 NewWeakGlobalRef 办法创立。弱全局援用具备如下一些个性:

  • 不会阻止 GC 回收所援用的对象
  • 能够跨办法、跨线程应用
  • 援用不会主动开释,只有在 JVM 内存不足时才会进行回收而被开释.,还有就是能够调用 DeleteWeakGlobalRef 手动开释。

参考:
Android Developers NDK 指南 C++ 库反对
JNI/NDK 开发指南
Android 内存泄露之 jni local reference table overflow

正文完
 0