Android NDK开发入门

JNI 简介

JNI (Java Native Interface英文缩写),译为Java本地接口。是Java众多开发技术中的一门技术,意在利用本地代码,为Java程序提供更高效、更灵活的拓展。尽管Java一向以其良好的跨平台性而著称,但真正的跨平台非C/C++莫属,由于当前世上90%的系统都是基于C/C++编写的。同时,Java的跨平台是以牺牲效率换来对多种平台的兼容性,于是JNI就是这种跨平台的主流实现方式之一。javascript

总之,JNI是一门技术,是Java 与C/C++ 沟通的一门技术。首先,来回顾下Android的系统架构图。
在这里插入图片描述
咱们来简单介绍下每一层的做用。html

Linux层

Linux 内核

因为Android 系统是基础Linux 内核构建的,因此Linux是Android系统的基础。事实上,Android 的硬件驱动、进程管理、内存管理、网络管理都是在这一层。java

硬件抽象层

硬件抽象层(Hardware Abstraction Layer缩写),硬件抽象层主要为上层提供标准显示界面,并向更高级别的 Java API 框架提供显示设备硬件功能。HAL 包含多个库模块,其中每一个模块都为特定类型的硬件组件实现一个界面,例如相机或蓝牙模块。当框架 API 要求访问设备硬件时,Android 系统将为该硬件组件加载对应的库模块。linux

系统运行库和运行环境层

Android Runtime

Android 5.0(API 21)以前,使用的是Dalvik虚拟机,以后被ART所取代。ART是Android操做系统的运行环境,经过运行虚拟机来执行dex文件。其中,dex文件是专为安卓设计的的字节码格式,Android打包和运行的就是dex文件,而Android toolchain(一种编译工具)能够将Java代码编译为dex字节码格式,转化过程以下图。
在这里插入图片描述
如上所示,Jack就是一种编译工具链,能够将Java 源代码编译为 DEX 字节码,使其可在 Android 平台上运行。android

原生C/C++ 库

不少核心 Android 系统组件和服务都是使用C 和 C++ 编写的,为了方便开发者调用这些原生库功能,Android的Framework提供了调用相应的API。例如,您能够经过 Android 框架的 Java OpenGL API 访问 OpenGL ES,以支持在应用中绘制和操做 2D 和 3D 图形。shell

应用程序框架层

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

相关文章
相关标签/搜索