• 欢迎访问搞代码网站,推荐使用最新版火狐浏览器和Chrome浏览器访问本网站!
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏搞代码吧

Android-NDK开发入门

android 搞代码 3年前 (2022-03-02) 19次浏览 已收录 0个评论
文章目录[隐藏]

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配置。

<code class="javascript">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,代码如下。

<code class="javascript"># 设置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的代码。

<code class="javascript">#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 的代码。

<code class="javascript">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办法命名

<code class="javascript">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办法,如下所示。

<code class="javascript">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中能够间接拜访,如下所示。

<code class="javascript">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根类,如下所示。

<code class="javascript">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 字符串操作的示例

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

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

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

<code class="javascript">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数组求和的例子,代码如下。

<code class="javascript">//MainActivity.java
public native int sumArray(int[] array);
<code class="javascript">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层数组中并进行求和。

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

<code class="javascript">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层应用。

<code class="javascript">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]);
    }
}
<code class="javascript">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类,代码如下。

<code class="javascript">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()办法不仅有入参,还有返参,如下所示。

<code class="javascript">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文件的代码,如下所示。

<code class="javascript">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信息的命令,如下所示。

<code class="javascript">adb shell cat /proc/cpuinfo

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

<code class="javascript">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");
}

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

<code class="javascript">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来定位地位。首先,执行如下命令。

<code class="javascript">/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是出错地位的汇编指令地址。

<code class="javascript">/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

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

<code class="javascript">adb logcat | ndk-stack -sym /Users/xzh/development/AllInOne/app/build/intermediates/cmake/debug/obj/arm64-v8a

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

<code class="javascript">********** 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


搞代码网(gaodaima.com)提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发送到邮箱[email protected],我们会在看到邮件的第一时间内为您处理,或直接联系QQ:872152909。本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:Android-NDK开发入门

喜欢 (0)
[搞代码]
分享 (0)
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址