【29】Android之学习native开发(一)

一、概述

没什么好讲的了,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,如果你也是使用这个版本,那么添加如下,仅此而已。

groovy 复制代码
	android {
		...
		externalNativeBuild {
        	cmake {
            	path file('src/main/cpp/CMakeLists.txt')
            	version '3.22.1'
        	}
    	}
	}

接着在我们的项目中写一个JNI的调用,这里以官方例子为例。Kotlin和Java有所不同的是,Kotlin当中修饰native方法的关键字是external,而静态块的库加载方式则是在companion object的init中完成的。

java 复制代码
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文件。关于定义不同库的作用,之后再讲,这里使用的是动态库。

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方法。

cpp 复制代码
#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库。

这是一个完整的例子

txt 复制代码
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代码不会自动释放内存,所以在使用指针之后,不再需要使用都需要手动释放内存空间。了解指针基础,应该都知道指针指的是对象的内存地址。

cpp 复制代码
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的地址。

这里再介绍一下指针类型,指向不同类型的数据,包括基本数据类型、数组、结构体、函数等。

cpp 复制代码
char *charPtr;
float *floatPtr;
double *doublePtr;

int arr[5] = {1, 2, 3, 4, 5};
int *arrPtr = arr;  // 等价于 int *arrPtr = &arr[0];

因为数组名本身就是一个指向数组第一个元素的指针,所以不需要使用&符号来索引数组的地址。

cpp 复制代码
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会默认为空字符串,基本数据类型,没有显式初始化可能会输出一个随机值。

cpp 复制代码
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的方式自动分配无数据内容的一块内存,并将内存地址赋值给指针。

总结一下,指针在定义的时候*表示接收的内存地址,使用指针的时候*表示地址的内容,使用&对象的时候,表示对象的地址。

相关推荐
诸神黄昏EX31 分钟前
Android 分区相关介绍
android
大白要努力!1 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee2 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood2 小时前
Perfetto学习大全
android·性能优化·perfetto
浩宇软件开发2 小时前
Android开发,使用TabLayout+ViewPager2实现校园健康安全宣传
android studio·android开发
Dnelic-5 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记
Eastsea.Chen7 小时前
MTK Android12 user版本MtkLogger
android·framework
长亭外的少年14 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
建群新人小猿17 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神18 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri