一、概述
没什么好讲的了,Android学习成长过程必经之路就是了解Framework层的源码及原理,在跟踪流程过程中,难免遇到很多natvie函数,这个时候学习native能帮助我们更轻松的读懂这方便的代码。
这篇文章也会从最基础的东西开始讲起,Android的native开发基础是什么,那就是JNI和NDK的概念。当然还会涉及到C++语言的一些基础知识。
二、JNI & NDK
JNI(java native Interface)顾名思义,就是java本地接口。Java虚拟机提供的一种能力,能够提供接口帮助开发者调用到本地代码库(native lib)。
如下图:Java虚拟机运行时数据区
构建一个JNI项目
构建一个JNI项目差点让这篇文章未半而中道崩殂,建议没有过经验的同学第一次直接创建一个C++项目,和原本项目比较差异,再在已有项目上添加JNI功能。需要的环境搭建这里就不多讲了。
这里就直接讲如何在已有的项目上添加JNI,首先是模块的build.gradle文件,再android节点中添加cmake的文件索引和依赖版本,我目前使用的AS是| 2024.1.2 Beta 1,如果你也是使用这个版本,那么添加如下,仅此而已。
android {
...
externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt')
version '3.22.1'
}
}
}
接着在我们的项目中写一个JNI的调用,这里以官方例子为例。Kotlin和Java有所不同的是,Kotlin当中修饰native方法的关键字是external,而静态块的库加载方式则是在companion object的init中完成的。
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("nativelib");
}
private Button button;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = findViewById(R.id.tv_button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
button.setText(stringFromJNI());
}
});
}
public native String stringFromJNI();
}
在build.gradle中我们声明的CMakeLists.txt的目录是在模块的*src/main/cpp/*下,所以创建这样一个cpp文件夹,并创建CMakeLists.txt文件。关于定义不同库的作用,之后再讲,这里使用的是动态库。
cmake_minimum_required(VERSION 3.22.1)
project("nativelib")
# SHARED:共享库(动态库) 共享库在运行时被动态加载。
# STATIC:静态库 静态库在链接时会被复制到目标可执行文件中。
# MODULE:模块库 模块库在运行时被动态加载,但不用于链接其他目标。
add_library(${CMAKE_PROJECT_NAME} SHARED native-lib.cpp)
target_link_libraries(${CMAKE_PROJECT_NAME} android log)
- 第一个是cmake的最小要求版本
- 第二个是项目名称,可以理解为生成的so文件名,也就是给到system.loadlibrary的文件名
- 第三个是添加cpp文件,${CMAKE_PROJECT_NAME}就是指代上面的nativelib
- 第四个是目标链接库,这里链接了android 、log两个库,都是由NDK提供的
Tip:cmake文件是txt文件,所以在编写的过程不会有错误提示,刚开始在已有项目构建的过程就是这个文件编写出错,导致一直提示错误,而且不容易被发现。
最后创建需要的native-lib.cpp文件,并实现JNI的接口方法,也就是我们定义的native方法。
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_nativelib_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
这段代码就算没有学过native开发应该也能猜出,把定义的hello字符串返回。这样我们Clean Project一下项目,再重新Rebuild Project一下就构建完成了。切换项目目录结构到Android结构下,看到有cpp文件目录并且生成了includes的文件夹,就能说明构建成功。
run一下这个项目,会出来一个带有按钮的界面,点击按钮,按钮的文字就会更新成Hello from C++。
刚刚讲了一下Java最基础的JNI方式来调用本地方法,接着说一下NDK(Native Development Kit)本地开发工具,是Android提供的一套工具和库,用于在Android应用中使用C和C++代码。事实上natvie开发范围很大,而NDK可以理解为native的一个子集。
在上面CMake文件中有提到过链接NDK的库,那如何知道NDK有提供哪些库呢,可以在本地NDK安装目录中查看包含的库。
$ANDROID_SDK/ndk/<ndk-version>/platforms/android-<api-level>/arch-<arch>/usr/lib
在CMake或ndk-build脚本中,你可以使用find_library命令查找NDK库。例如:
find_library(log-lib log)
find_library(android-lib android)
这些命令会查找并链接NDK中的log和android库。
这是一个完整的例子
cmake_minimum_required(VERSION 3.4.1)
# 添加本地库
add_library(native-lib SHARED native-lib.cpp)
# 查找NDK库
find_library(log-lib log)
find_library(android-lib android)
# 链接NDK库
target_link_libraries(native-lib ${log-lib} ${android-lib})
最后做个总结,JNI是Java提供的一种接口来调用本地方法的一种方式,而NDK则是帮助我们在Android中使用C/C++代码开发,目的是提高应用的安全性、性能和效率。
三、指针
在学习C++语言中,指针这个概念特别难懂,还好Java没有指针,不然刚开始学习的成本就太高了。我会从我是一个新手的角度来讲,在了解指针过程中,我面临的问题以及需要搞懂的问题。
在C语言中,指针是需要分配内存空间的,由于c代码不会自动释放内存,所以在使用指针之后,不再需要使用都需要手动释放内存空间。了解指针基础,应该都知道指针指的是对象的内存地址。
int var = 10;
int *ptr = &var;
printf("Value of var: %d\n", var); // 输出:10
printf("Address of var: %p\n", &var); // 输出:var的地址
printf("Value of ptr: %p\n", ptr); // 输出:ptr存储的地址(即var的地址)
printf("Value pointed by ptr: %d\n", *ptr); // 输出:10
第一行是声明了一个变量var,并且给它赋值10。第二行,左边是声明了一个指向int类型的指针,右边是将var变量的地址赋值给指针。&符号就是用来获取对象地址的符号。倘若写成 int *ptr = var,那么它的意思就是讲var的值作为地址赋值给指针,但是由于10不是一个标准的地址格式,因此会报错。
可以看到只有使用*ptr,表示内存地址保存的数据,而直接使用ptr打印的就是地址信息。这里还有一点很重要,int *ptr = &var分配了两块内存空间,一块用来存储数据10,另一块内存空间来存储var的地址。
这里再介绍一下指针类型,指向不同类型的数据,包括基本数据类型、数组、结构体、函数等。
char *charPtr;
float *floatPtr;
double *doublePtr;
int arr[5] = {1, 2, 3, 4, 5};
int *arrPtr = arr; // 等价于 int *arrPtr = &arr[0];
因为数组名本身就是一个指向数组第一个元素的指针,所以不需要使用&符号来索引数组的地址。
struct Person {
char name[50];
int age;
};
struct Person person;
struct Person *personPtr = &person;
指向结构体,当需要使用age的时候可以通过 *(personPtr).age方式获取age,简化写法personPtr->age。当然也可以直接操作person结构体变量来获取age,即person.age。请记住,这里是创建了一个person结构体变量,然后指针指向这个结构体变量的地址,所以操作这个结构体变量和通过操作指针指向的地址的内容,都是指的同一个东西。
由于person变量没有初始化值,这个时候打印结构体,string会默认为空字符串,基本数据类型,没有显式初始化可能会输出一个随机值。
int var = 10;
int* ptr = &var;
int** ptr2 = &ptr; // 指向指针的指针
void func() {
std::cout << "Hello, World!" << std::endl;
}
void (*funcPtr)() = &func; // 定义一个指向函数的指针
funcPtr(); // 调用函数
int* ptr = new int; // 动态分配一个 int 类型的内存
*ptr = 10;
delete ptr; // 释放内存
最后指针的三个高级用法,指向指针的指针,通过*ptr2获取到的是ptr指针的地址,因此再通过/*号就可以获取到var内存地址的数据。
函数指针这里了解一下使用即可,同样需要定义函数的指针类型,这个类型和函数的返回值有关,而指针后面的括号,则是函数的参数。
动态分配,通过new的方式自动分配无数据内容的一块内存,并将内存地址赋值给指针。
总结一下,指针在定义的时候*表示接收的内存地址,使用指针的时候*表示地址的内容,使用&对象的时候,表示对象的地址。