本文主要记录了自己在开发过程中对NDK入门知识的一个学习过程,Android NDK开发涉及到的知识比较多,其中C、C++语言就需要一定的功底。另外还有JNI以及不同CPU平台对编译动态so的支持等知识。
● NDK(Native Development Kit),是一个Android Native开发工具集,它的主要作用就是快速开发C、C++的动态库,并将编译生成的so打包进apk。其中Java与Native代码的交互需要使用到JNI。
● NDK使用C、C++语言开发的程序代码具有运行效率高,安全性好的特点。(编译型语言与解释型语言的区别)
● NDK开发的动态库可以移植到不同的平台
JNI标准成为Java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,但是它并不妨碍你使用其他编程语言,只要调用约定受支持就可以了。
静态注册: 静态注册方式是通过手动编写一组 JNI 函数来完成注册。您需要在本地代码中使用特定的命名规则将本地函数与 Java 方法进行映射,并在 Java 代码中声明本地方法。然后,在 Java 代码中加载本地库时,调用 System.loadLibrary() 方法来加载本地库并触发静态注册过程。这样可以将本地函数与 Java 方法关联起来,使得 Java 代码可以调用本地函数。缺点是当包名、类名、方法名发生修改时,Native层对应的JNI方法名也需要发生修改。
动态注册: 动态注册方式是通过使用 JNI 提供的函数在运行时进行注册。您需要在本地代码中编写一个 JNI_OnLoad() 函数,在该函数中调用 JNI 提供的函数来注册本地函数。然后,在 Java 代码中加载本地库时,JVM 会自动调用 JNI_OnLoad() 函数进行动态注册。
public class JNIDynamic {
public native String dynamicFun();
}
//指定类名
const char *jniDynamicClassName = "com/example/uiviewandrid/JNIDynamic";
//声明函数
jstring nativeDynamicFun(JNIEnv *env, jobject thiz);
//参数列表对应关系:param1:Java层方法名 param2:java层方法签名信息 param3:对应的native方法签名
static JNINativeMethod gMethods[] = {
{"dynamicFun","()Ljava/lang/String;",(jstring)(nativeDynamicFun)},
};
jstring nativeDynamicFun(JNIEnv *env, jobject thiz){
std::string hello = "Hello from C++ dynamic register";
return env->NewStringUTF(hello.c_str());
}
extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
// 开始动态注册,通过JavaVM获取JNIEnv(操作杆),跟Java层大量交互都是通过JNIEnv来实现。
JNIEnv *jniEnv = nullptr;
int result = vm->GetEnv(reinterpret_cast<void **>(&jniEnv), JNI_VERSION_1_6);
// result 等于0 成功,非0 失败
if (result != 0) {
return -1;
}
// 获取MainActivity class,通过包名 + 类名 获取
jclass jniDynamicClass = jniEnv->FindClass(jniDynamicClassName);
// 动态注册函数, RegisterNatives(MainActivity class, 动态函数的数组, 注册动态函数数量)
jniEnv->RegisterNatives(jniDynamicClass, gMethods,
sizeof(gMethods) / sizeof(JNINativeMethod));
__android_log_print(ANDROID_LOG_DEBUG, "TAG", "load success");
return JNI_VERSION_1_6;
}
Java类型 | JNI类型 | Native签名 |
---|---|---|
byte | jbyte | B |
char | jchar | C |
short | jshort | S |
int | jint | I |
float | jfloat | F |
long | jlong | J |
double | jdouble | D |
boolean | jboolean | Z |
Java类型 | JNI类型 | Native签名 |
---|---|---|
void | void | V |
String | jstring | Ljava/lang/String; |
object | jobject | L全限定类名; |
class | jclass | Ljava/lang/Class; |
Throwable | jthrowable | Ljava/lang/Throwable; |
Object[] | jobject | [L全限定类名; |
int[] | jintArray | [I |
long[] | jlongArray | [J |
… | … | … |
const char *jniUserInfoClassName = "com/example/uiviewandrid/UserInfo";
jobject nativeGetUserInfo(JNIEnv *env, jobject obj) {
//根据类名找到类
jclass jniUserInfoClass = env->FindClass(jniUserInfoClassName);
//寻找类的构造器方法
jmethodID constructor = env->GetMethodID(jniUserInfoClass, "<init>", "()V");
//调用构造器创建对象
jobject nativeUserInfo = env->NewObject(jniUserInfoClass, constructor);
//给对象属性赋值
jfieldID name = env->GetFieldID(jniUserInfoClass, "name", "Ljava/lang/String;");
jfieldID age = env->GetFieldID(jniUserInfoClass, "age", "I");
jstring nameValue = env->NewStringUTF("Name C++");
jint ageValue = 30;
env->SetObjectField(nativeUserInfo, name, nameValue);
env->SetIntField(nativeUserInfo, age, ageValue);
//返回对象
return nativeUserInfo;
}
在 Android 中,ABI(Application Binary Interface)指的是应用程序二进制接口,它定义了应用程序与底层操作系统和硬件交互的接口规范。Android ABI 分类是根据底层处理器架构和指令集进行的分类。下面是几种常见的 Android ABI 类型及其简介:
armeabi-v7a: 这是针对基于 ARMv7 架构的设备的 ABI。它支持 32 位 ARM 指令集,并提供对浮点运算和 SIMD(单指令多数据)指令的优化。这是大多数现代 ARM 设备所使用的 ABI。
arm64-v8a: 这是针对基于 ARMv8 架构的设备的 ABI。它同样支持 32 位 ARM 指令集,但还支持 64 位 ARM 指令集。arm64-v8a ABI 通常用于较新的 ARM 架构设备。
x86: 这是针对基于 x86 架构的设备的 ABI。x86 是一种常见的 PC 和服务器处理器架构。x86 ABI 适用于 Android 模拟器和一些基于 x86 的 Android 设备。
x86_64: 这是针对基于 x86-64 架构的设备的 ABI。它是 x86 架构的 64 位扩展,提供更高的性能和更大的内存寻址能力。x86_64 ABI 通常用于较新的 x86-64 架构设备。
mips: 这是针对基于 MIPS(Microprocessor without Interlocked Pipeline Stages)架构的设备的 ABI。MIPS 是一种低功耗、高性能的处理器架构,主要用于嵌入式系统和网络设备。
在构建 Android 应用时,通常需要为每个 ABI 构建相应的本机库,以便应用程序可以在不同的设备上运行。您可以通过在 Gradle 配置文件中指定目标 ABI 来进行配置,并生成适用于特定 ABI 的本机库。
请注意,随着 Android 设备的更新和演进,一些 ABI 可能会逐渐被淘汰或不再受支持。为了确保应用程序的兼容性和性能,建议根据目标用户群体的设备统计数据来选择支持的 ABI 类型。
借助于AndroidStudio这一强大的Android开发神器,让我们在进行混合开发时能够得心应手,Android SDK中目前已经集成了NDK开发的整套工具,使用CMake进行编译配置,这样能够简单快速的编译出动态或静态库。我们将借助AS实现NDK的HellWorld,首先就是环境配置,我们需要配置NDK的开发环境:
打开AS的SDK Manager,找到SDK Tools,我们勾选上NDK与CMake,默认最新版本,可以选择自己需要的版本进行下载,AS会自动为你下载解压到对应的路径,NDK和CMake都会被下载到Sdk路径下。
android {
......
defaultConfig {
......
externalNativeBuild {
cmake {
//提供给C++编译器的一个标志 可选 cppFlags '-std=c++11' 例如这里可以指定C++版本
cppFlags ''
}
}
ndk{
//指定需要生成哪些平台的so,一共四个: 'arm64-v8a','x86','armeabi-v7a','x86_64'
abiFilters 'arm64-v8a','x86'
}
}
externalNativeBuild {
//指定CMakeLists.text文件的路径以及版本
cmake {
path file('src/main/cpp/CMakeLists.txt')
version '3.22.1'
}
}
......
//指定ndk的版本
ndkVersion '21.4.7075529'
}
在cpp文件夹下创建你需要的C/C++文件,创建CMakeLists.text文件,CMakeLists.txt文件也可以放到别的文件夹下,同时需要在java文件夹下创建JNI接口函数对native函数进行声明。
编写JNI方法
package com.example.uiviewandrid
class JNILoader {
companion object {
init {
System.loadLibrary("UIViewAndroid")
}
}
/**
* 在 Kotlin 中,external 是一个关键字,用于声明一个外部函数或属性。它表示该函数或属性的实现是由外部提供的,而不是在当前代码中定义的。
* 使用 external 关键字可以在 Kotlin 中与其他语言进行交互,比如使用 Java 或 C/C++ 编写的原生代码。
* 在这种情况下,您可以使用 external 来声明 Kotlin 函数或属性,然后在底层的原生代码中实现其具体逻辑。
*/
external fun sayHello(): String
}
我们在定义完一个JNI方法之后,编译器会给出如上图所对应的提示,告诉我们创建对应JNI方法,我们点击进去他就会按照JNI的命名规则为我们创建对应的JNI函数。
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_uiviewandrid_JNILoader_sayHello(JNIEnv *env, jobject obj) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
# 指定最小版本
cmake_minimum_required(VERSION 3.18.1)
project(UIViewAndroid)
add_library( # Sets the name of the library. lib${name}.so
UIViewAndroid
# Sets the library as a shared library.指定静态还是动态
SHARED
# Provides a relative path to your source file(s).需要加载的cpp文件,相对路径
NativeLib.cpp)
find_library( # Sets the name of the path variable.寻找log这个库
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log)
target_link_libraries( # Specifies the target library.
UIViewAndroid
# Links the target library to the log library
# included in the NDK.
${log-lib})
7.到这里我们就对项目完成了编写,接下来找个地方调用我们的JNI函数,编译项目运行查看结果即可完成一次JNI调用了。
首先我们要根据目标机型打包对应ABI的so,根据设备类型选择对应的abiFilters,注意:使用AS编译后的so会自动打包到apk中,如果需要提供给第三方使用,可以到build/intermediates/cmake/debug/obj 目录中拷贝出来。第三方库一般直接放在main/jniLibs文件夹下,也有放在默认libs目录下的,但是必须在build.gradle中声明jni库的目录
sourceSets {
main {
jniLibs.srcDirs = ['jniLibs']
}
}
jin.h文件中声明了大量的JNI相关的方法,其中有三组方法是用来管理引用的。
jobject NewGlobalRef(jobject obj)
{ return functions->NewGlobalRef(this, obj); }
void DeleteGlobalRef(jobject globalRef)
{ functions->DeleteGlobalRef(this, globalRef); }
jobject NewLocalRef(jobject ref)
{ return functions->NewLocalRef(this, ref); }
void DeleteLocalRef(jobject localRef)
{ functions->DeleteLocalRef(this, localRef); }
jweak NewWeakGlobalRef(jobject obj)
{ return functions->NewWeakGlobalRef(this, obj); }
void DeleteWeakGlobalRef(jweak obj)
{ functions->DeleteWeakGlobalRef(this, obj); }