Android NDK 与 JNI
为什么要有这两个?
对于Android的嵌入式软件开发、调用到硬件、与操作系统交互或者执行一些比较重要的行为来说,一般是通过与C或C++结合进行,Java调用本地C/C++通常的做法就是将编译好的C/C++动态库.so放到我们的Android APK中,然后加载该库进行调用即可,但是如果我们需要在我们的项目中编写自己的本地代码怎么办呢?这就需要用到两个东西:NDK和JNI
1、NDK
What?
NDK的全称呼是:Native Development Kit,它是一个本地开发工具集,能让我们在Android Studio上使用C和C++代码,并能生成.so库与应用一起打包成APK,也就是一个方便我们Native开发用的一套工具。
2、JNI
上面有了NDK,我就可以构建出.so库共Java程序调用,但是如何编写出能够供java程序调用的C、C++代码还需要JNI提供
What?
JNI全称Java Native Interface,即 Java本地接口,提供一套规范增强了 Java 与 本地代码交互的能力, 使得Java 与 本地其他类型语言(如C、C++)交互,是Java和C、C++联系的桥梁
使用NDK与JNI的优势
- 使用C、C++代码编写的程序执行效率更高
- 使用C、C++代码编写的程序更难进行反编译得到源代码,从而提高安全性
- 使用C、C++代码编写的程序代码不仅可用于android中,还可以嵌入到其他平台进行使用
- NDK提供了交叉编译器,可生成特定的CPU平台动态库
- NDK还可以方便的使用其他语言开发的开源库
另一个工具CMake
使用NDK构建代码的主要方法有以下三种:
- 基于Make的ndk-build。
- CMake的。
- 用于与其他构建系统集成或与configure基于项目的项目一起使用的独立工具链。
使用外部构建工具原因:
当我们编写本地C、C++代码后,需要使用NDK一个一个编译生成目标动态库,期间需要定义一系列规则,如制定编译文件,指定交叉编译的平台等。这样导致了整个过程十分繁琐,因此就有了make自动化编译工具,它依据一个makefile规则文件进行批处理,相当与一个脚本。
而对于一个大工程,编写makefile实在是件复杂的事,于是人们设计了一个工具CMake,它能够自动生成makefile和其它文件,从而帮助程序员减轻负担,我们需要关注的就是CmakeList.txt的编写工作。
开始–Hello World
本系列使用的是CMake进行外部构建
因此需要Android Studio2.2本版本以上(2.2版本以后开始支持CMake编译)
安装工具
安装NDK开发所需的工具,直接打开android studio中的SDK Manager勾选SDK Tools下的NDK和CMake进行安装即可。
创建项目
新建一个支持Native的Android工程,咔咔下一步就好了。
完成后整个app项目的结构如下图:
接下来看一下与普通项目不一样的地方
1. 首先看到build.gradle的内容:新增了两个externalNativeBuild
android {
compileSdkVersion 28
buildToolsVersion '28.0.3'
defaultConfig {
applicationId "com.example.myapplication"
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
//指定C++版本,这里用了个默认
cppFlags ""
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
//指定CMakeLists.txt所在的位置
path "src/main/cpp/CMakeLists.txt"
//指定CMake版本号
version "3.10.2"
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
更多的常用CMake属性:
buildStagingDirectory:配置native构建后文件的存放路径
cmake {
buildStagingDirectory "./outputs/cmake"
}
2. CMakeLists.txt内容如下:
# 指定 cmake 的最小版本
cmake_minimum_required(VERSION 3.10.2)
# 设置项目名称
project("myapplication")
# 创建库,设置编译类型
add_library( # 设置库的名称
native-lib
# 设置编译类型为一个动态库.so,static表示静态库.a,不指定表示为一般可执行文件
SHARED
# c++代码的所在位置,相对路径
native-lib.cpp )
# 查找到指定的预编译库,并将它的路径存储在变量中
find_library( # 设置路径变量的名称.
log-lib
# 指定NDK库的名称
log )
# 设置 target 需要链接的库进行链接
# 在 Windows 下,系统会根据链接库目录,搜索xxx.lib 文件,
# Linux 下会搜索 xxx.so 或者 xxx.a 文件,如果都存在会优先链接动态库(so 后缀)。
target_link_libraries( native-lib
${log-lib} )
3.native-lib.cpp 是AS自动给我们生成的c++示例代码,内容如下:
#include <jni.h>
#include <string>
//与c代码兼容
extern "C"
JNIEXPORT jstring JNICALL
//JNIEnv 结构体指针
//env二级指针
//代表Java运行环境,可调用Java中的代码
Java_com_example_myapplication_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* 相当于java中的this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
每个native函数,都至少有两个参数(JNIEnv*, jobject)
- 当native方法为静态方法时:
jclass 代表native方法所属类的class对象(这里就是MainActivity.class) - 当native方法为非静态方法时:
jobject 代表native方法所属的对象
数据类型对比
Java基本数据类型与JNI数据类型的映射关系如下表
Java类型 | JNI类型 |
---|---|
boolean | jboolean |
boolean | jboolean |
byte | jbyte |
char | jchar |
short | jshort |
int | jint |
long | jlong |
float | jfloat |
double | jdouble |
void | void |
引用类型映射关系如下表
Java类型 | JNI类型 |
---|---|
String | jstring |
object | jobject |
byte[] | jByteArray |
object[](String[]) | jobjectArray |
4. MainActivity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 调用本地方法
findViewById<TextView>(R.id.sample_text).text = stringFromJNI()
}
//声明Native方法
// MainActivity中新增一个native方法,将会在加载的native-lib下查找
external fun stringFromJNI(): String
companion object {
//加载native-lib动态库
init {
System.loadLibrary("native-lib")
}
}
}
5. CPU架构
运行项目后将会在以下路径生成.so动态库:
这里可以看到一共有四个文件夹:
arm64-v8a、armeabi-v7a、x86、x86_64
这就是我们通常所说的ABIs,Android 设备的CPU类型,不同的CPU支持的指令集是不一样的。同时由于C/C++语言本身不具备跨平台的能力,所以必须针对不同的cpu类型编译出不同的.so库,android设备在加载.so库时将会在对应的文件夹下查找。所以为了减少apk大小同时保证很好的运行,通常在编译时需要为不同设备编译出对应的.so库即可,而不是进行全部编译适配。
以下是对应关系:
- armeabiv-v7a: 第7代及以上的 ARM 处理器。2011年15月以后的生产的大部分Android设备都使用它.
- arm64-v8a: 第8代、64位ARM处理器,很少设备,三星 Galaxy S6是其中之一。
- armeabi: 第5代、第6代的ARM处理器,早期的手机用的比较多。
- x86: 平板、模拟器用得比较多。
- x86_64: 64位的平板。
6. 最终结果: