Android NDK开发入门
Posted wx5aae83353cec4
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android NDK开发入门相关的知识,希望对你有一定的参考价值。
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开发流程
- 编写java类,声明了native方法;
- 编写native代码;
- 将native代码编译成so文件;
- 在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 其他字符串操作函数
- GetStringChars和ReleaseStringChars:这对函数和Get/ReleaseStringUTFChars函数功能类似,用于获取和释放的字符串是以Unicode格式编码的。
- GetStringLength:获取Unicode字符串(jstring)的长度。 UTF-8编码的字符串是以\\0结尾,而Unicode的不是,所以这里需要单独区分开。
- 「GetStringUTFLength」: 获取UTF-8编码字符串的长度,就是获取C/C++默认编码字符串的长度.还可以使用标准C函数「strlen」来获取其长度。
- strcat: 拼接字符串,标准C函数。如
strcat(buff, "xfhy");
将xfhy添加到buff的末尾。 - GetStringCritical和ReleaseStringCritical: 为了增加直接传回指向Java字符串的指针的可能性(而不是拷贝).在这2个函数之间的区域,是绝对不能调用其他JNI函数或者让线程阻塞的native函数.否则JVM可能死锁. 如果有一个字符串的内容特别大,比如1M,且只需要读取里面的内容打印出来,此时比较适合用该对函数,可直接返回源字符串的指针。
- 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;
接下来,我们来分析下代码。
- 首先,是利用FindClass函数找到java层int[]对象的class,这个class是需要传入NewObjectArray创建对象数组的。调用NewObjectArray函数之后,即可创建一个对象数组,大小是size,元素类型是前面获取到的class。
- 进入for循环构建size个int数组,构建int数组需要使用NewIntArray函数。可以看到我构建了一个临时的buff数组,然后大小是随便设置的,这里是为了示例,其实可以用malloc动态申请空间,免得申请100个空间,可能太大或者太小了。整buff数组主要是拿来给生成出来的jintArray赋值的,因为jintArray是Java的数据结构,咱native不能直接操作,得调用SetIntArrayRegion函数,将buff数组的值复制到jintArray数组中。
- 然后调用SetObjectArrayElement函数设置jobjectArray数组中某个索引处的数据,这里将生成的jintArray设置进去。
- 最后需要将for里面生成的jintArray及时移除引用。创建的jintArray是一个JNI局部引用,如果局部引用太多的话,会造成JNI引用表溢出。
6,Native调Java方法
熟悉JVM的都应该知道,在
以上是关于Android NDK开发入门的主要内容,如果未能解决你的问题,请参考以下文章