一.介绍
1.什么是ndk技术?
在学习ndk技术前,我们需要先了解一下JNI(Java Native Interface)技术,JNI技术是一种实现Java代码和C/C++代码之间交互的技术,它提供了一组编程接口,使得Java程序可以调用C/C++代码并与其进行通信。通过JNI技术,开发者可以将C/C++代码嵌入Java项目中,并在Java代码中调用这些C/C++函数。那么,NDK技术和它有什么关系呢?NDK是一种用于开发Android应用程序的工具集,它允许开发者使用C/C++编写部分或全部的Android应用程序代码,以便提高性能和访问底层系统功能。这样看起来NDK技术和JNI技术是一回事,就是为了实现Java调用C/C++或C/C++调用Java。确实如此,用一句话概括它们之间的关系就是:开发者使用NDK技术在Android应用程序中编写C/C++代码,并将其编译成共享库(如.so文件),然后使用JNI技术在Java代码中加载并与这些C/C++代码进行交互。
2.为什么要学习ndk?
第一点的话就是提高性能了,这个显而易见,C/C++的性能肯定比Java高,如果有些功能用Java实现性能不行,就可以把这部分代码用C/C++实现。第二点的话就是C/C++语言可以直接访问底层系统功能和硬件资源,如摄像头和传感器等,这是Java做不到的。最后一点是保密性,Java代码是编译成字节码,而C/C++代码是直接编译成机器码,反编译的难度比Java大的多。所以,如果哪部分功能需要保密,也可以用C/C++来实现。
3.编写C/C++代码并编译出.so文件
我们要在Android项目中调用C/C++代码,首先要将写好的C/C++代码编译成.so共享库,下面我会以Android Studio 2021来详细讲解编译出.so文件的过程。
第一步:打开Android Studio,新建一个Native C++项目,如下图所示:
项目新建完成后是下面这个样子:
我们可以看到main目录下面有一个cpp目录,这里就是我们编写C++代码的地方,我们先来看一下自动生成的CmakeLists.txt文件,代码如下:
# 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.18.1) //cmake的最低版本是3.18.1
# Declares and names the project.
project("ndkstudy")
# 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.
ndkstudy //生成的库的名称
# Sets the library as a shared library.
SHARED //设置生成的库为共享库.so
# Provides a relative path to your source file(s).
native-lib.cpp //c++源文件的相对路径
)
# 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) //使用find_library来查找log库,并把找到的log库存储在变量log-lib中
# 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.
ndkstudy
# Links the target library to the log library
# included in the NDK.
${log-lib}) //将ndkstudy库和log库进行链接
然后,我们再来看一下自动生成的native-lib.cpp文件,代码如下:
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ndkstudy_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}//函数的功能就是返回一个"Hello from C++"字符串
我们发现这个函数名特别长,其实就是对应java目录下com.example.ndkstudy包下MainActivity类下的stringFromJNI()这个函数。了解了这些之后,我们只需要Make Project即可,如下图所示:
然后就可以看到所生成的.so文件了,如果没有的话,可以刷新一下项目
接下来,我们建一个新的项目,然后把上面所生成的不同CPU架构的.so文件复制到新项目的main/jniLibs目录,jniLibs目录需要自己新建。
然后在app/build.gradle文件下添加以下的代码:
android {
namespace 'com.example.ndkstudy'
compileSdk 32
defaultConfig {
applicationId "com.example.ndkstudy"
minSdk 21
targetSdk 32
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk {
// 设置支持的SO库架构(开发者可以根据需要,选择一个或多个平台的so)
abiFilters "armeabi", "armeabi-v7a", "arm64-v8a", "x86","x86_64"
}//新增代码
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
接下来,就可以调用so库中的C++函数了,这里也是特别容易出错的地方,我先贴出代码,然后再详细讲解。
public class MainActivity extends AppCompatActivity {
private TextView tv_display;
{
System.loadLibrary("ndkstudy");//第一步,加载动态库,放到静态代码块里就行
}
@SuppressLint("MissingInflatedId")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv_display=findViewById(R.id.tv_display);
tv_display.setText(stringFromJNI());//第三步,调用本地函数,实际上调用的函数是Java_com_example_ndkstudy_MainActivity_stringFromJNI();
一个特别需要注意的点是我们在加载动态库和作本地方法声明的时候,需要在com.example.ndkstudy包下,MainActivity类下进行操作,也就是要对应那个特别长的函数名。如果以上的步骤都没有错的话,就可以在手机屏幕上看到输出的"Hello from C++"字符串了。这只是jni的最基本用法,下面来讨论一下java类型与c类型的转换。
二.Java类型和C类型的转换
在JNI开发中,Java类型和C/C++类型之间需要转换,因为二者之间的数据类型存在差异,转换的桥梁正是JNI类型。下面给出它们之间的对应关系:
Java类型 | JNI类型 | C/C++类型 | 大小 |
---|---|---|---|
boolean | jboolean | uint8_t | 无符号8位整型 |
byte | jbyte | int8_t | 有符号8位整型 |
char | jchar | uint16_t | 无符号16位整型 |
int | jint | int32_t | 有符号32位整型 |
short | jshort | int16_t | 有符号16位整型 |
long | jlong | int64_t | 有符号64位整型 |
float | jfloat | float | 32位单精度浮点型 |
double | jdouble | double | 64位双精度浮点型 |
这个只是java的基本数据类型和c/c++类型的对应关系,下面给出java的引用类型和c/c++的对应关系:
Java的引用类型 | JNI的引用类型 |
---|---|
java.lang.Object | jobject |
java.lang.String | jstring |
java.lang.Class | jclass |
Object[] | jobjectArray |
boolean[] | jbooleanArray |
byte[] | jbyteArray |
char[] | jcharArray |
short[] | jshortArray |
int[] | jintArray |
long[] | jlongArray |
float[] | jfloatArray |
double[] | jdoubleArray |
java.lang.Throwable | jthrowable |
void | void |
那么,什么时候需要将java类型转成c类型,什么时候又该将c类型转成java类型呢?当我们所声明的本地函数有形参的时候,我们需要将java类型转成c类型,因为我们在实际调用这个函数的时候,调用的是对应的c函数,所以需要进行转换。当我们调用的本地函数有返回值的时候,需要将c类型转成java类型,因为在调用这个函数之后,返回值需要return到java代码中,所以需要进行转换。下面举个例子来说明一下:
比如,我们用native关键字声明了一个本地函数public native void javaToJni(byte a,boolean b,int c,short d,long e,char f,float g,double h);那么它实际对应的c函数是:
extern "C" JNIEXPORT void JNICALL
Java_com_example_ndkstudy_MainActivity_javaToJni(JNIEnv *env, jobject thiz, jbyte a, jboolean b,
jint c, jshort d, jlong e, jchar f, jfloat g,
jdouble h) {
//将jni类型转换成c类型
int8_t c_a=a;
uint8_t c_b=b;
int32_t c_c=c;
int16_t c_d=d;
int64_t c_e=e;
uint16_t c_f=f;
float c_g=g;
double c_h=h;
前面两个变量是固定的,env是指向jni环境的指针,thiz是这个函数所属的java对象的引用,后面的参数就是自己实际定义的参数。注意,这些jni变量需要转换成c类型才能进行后续的操作。
如果本地函数的声明是这个样子呢?public native String test(String str);其实这个函数对应的c函数是:
extern "C" JNIEXPORT jstring JNICALL //jstring表示返回值为jstring类型
Java_com_example_ndkstudy_MainActivity_test(JNIEnv *env, jobject thiz, jstring str) {
// TODO: implement test()
const char *p=env->GetStringUTFChars(str, nullptr);//将jstring类型转换成c中的const char *类型
return env->NewStringUTF(p);//将const char *类型转换成jstring类型
}//所以这个函数的功能就是返回传进来的字符串
如果返回值是其他的类型,也和这个类似。比如,如果要返回一个int16_t类型,则函数的返回值类型设为jshort即可。