一、环境配置
1、NDK简介
Android NDK(Native Development Kit),即Android本地开发工具包,是Google为了允许开发者使用C/C++编写本地代码并在Android设备上运行而提供的一份工具和支持库。NDK提供了一系列系统头文件、函数库、调试器等工具,同时也提供了支持C++代码的STL等库文件。使用NDK可以加速CPU密集型任务和让现有C/C++代码可以在Android系统上运行。
2、安装NDK
首先,要在Android Studio中下载NDK,打开新建工程中的Gradle Scripts->build.gradle(Project:为你的项目名),将“classpath 'com.android.tools.build:gradle:x.x.x'”中的“x.x.x”修改成你的AS版本所需的gradle版本,然后再在app->build.gradle文件中添加以下内容:
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
...
//指向 CMakeLists.txt 文件所在的外部编译目录的绝对路径
path "src/main/cpp/CMakeLists.txt"
}
}
}
buildTypes {
...
externalNativeBuild {
cmake {
...
//指向 CMakeLists.txt 文件所在的外部编译目录的绝对路径
path "src/main/cpp/CMakeLists.txt"
}
}
}
externalNativeBuild {
// 定义ndk-build编译脚本的库
ndkBuild {
//指向Android.mk文件所在的绝对路径
path "src/main/jni/Android.mk"
}
}
}
这里以使用CMake为编译系统为例,在app->build.gradle文件中添加以下内容:
android {
...
defaultConfig {
...
// 指定支持abi
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
}
}
externalNativeBuild {
cmake {
//指向 CMakeLists.txt 文件所在的外部编译目录的绝对路径
path file('src/main/cpp/CMakeLists.txt')
}
}
}
3、配置CMakeLists.txt
CMake是一个跨平台的编译系统,Android NDK支持使用CMake_build原生代码。打开Android项目中的app目录,在cpp文件夹下创建一个新的CMakeLists.txt文件。以下是最简单的示例CMakeLists.txt:
cmake_minimum_required(VERSION 3.4.1)
# 创建一个动态库
add_library(
hello # 库名称
SHARED # 库类型:SHARED表示动态库
hello.c) # 库源文件
# 导入log库
find_library(
log-lib # 定义log库名称
log) # 指定库名称在系统中的实际文件名称
# 将log库链接到hello库中
target_link_libraries(
hello # 目标库名称
${log-lib}) # 链接库名称
二、JNI开发
1、JNI简介
Java Native Interface (JNI) 是一个Java平台编程接口,允许Java代码和其他语言如C/C++程序进行交互。Java虚拟机提供一个标准的JNI接口,它可以被C/C++编译器所使用,可以将Java垃圾回收机制与C/C++语言灵活地融合在一起。
2、JNI基础
在Java中使用JNI调用本地代码,需要在Java中定义native函数,并生成头文件。以下是一个Java类中定义native函数的示例:
public class NdkJniUtils {
static {
System.loadLibrary("ndkJniLib");
}
public native String sayHello(String name);
}
使用以下命令生成头文件:
javah -d jni -classpath {Java类的.class文件路径} {Java全限定类名(包括包名)}
3、JNI实践
以调用hello.c函数为例,先编写.h文件:
#include <jni.h>
JNIEXPORT jstring JNICALL Java_com_example_ndkdemo_MainActivity_hello(JNIEnv *env, jobject instance){
return (*env)->NewStringUTF(env, (const char *)"Hello NDK!");
}
在.c文件中实现hello.c函数:
#include <stdio.h>
#include <jni.h>
JNIEXPORT jstring JNICALL Java_com_example_ndkdemo_MainActivity_hello(JNIEnv *env, jobject instance){
return (*env)->NewStringUTF(env, "Hello NDK!");
}
4、编译
使用以下命令编译:
gcc -shared -I$JAVA_HOME/include -I$JAVA_HOME/include/linux hello.c -o libhello.so
将生成的libhello.so库文件拷贝到app/src/main/jniLibs/armeabi-v7a/目录下,通过System.loadLibrary()方法加载即可。
三、混合编程
1、混合编程简介
将Java和C++混合编程的方式,称之为混合编程。混合编程可以让我们在Java层面上方便的使用C++的特性,也可以让我们利用Java的强大库来进行快速开发。
2、JNI编程注意点
在JNI编程中,有几个重要的规则需要遵循:
- 尽量不要执行耗时任务
- 尽量避免崩溃或错误
- 避免资源泄露
- 锁定Global Reference和Local Reference
3、示例代码
代码示例中将展示如何从Java层面调用C++库。在Java文件中声明native方法:
public static native int add(int a, int b);
在C++文件中实现add()函数,并将其包装在JNICaller类中:
#include <jni.h>
using namespace std;
class JNICaller
{
public:
static void init(JNIEnv *env);//初始化
static int add(int a,int b); //导出给Java层面的native方法
};
void JNICaller::init(JNIEnv *env)
{
jclass jniCaller_clazz=env->FindClass("com/example/ndkdemo/JNICaller");//获取Java层面的类的引用
jmethodID construct=jniEnv->GetMethodID(jniCaller_clazz,<init>,"()V");//获取Java层面的构造函数的引用
//注册native方法
JNINativeMethod methods[] =
{
{"add", "(II)I", (void *) JNICaller::add}
};
jniEnv->RegisterNatives(jniCaller_clazz, methods, sizeof(methods) / sizeof(methods[0]));
printf("init JNI caller success.\n");
}
int JNICaller::add(int a,int b)
{
return a+b;
}
在Java层面中调用add()方法:
JNICaller.init();//初始化JNICaller
int result = JNICaller.add(a, b);//在Java层面中调用JNICaller的add()方法
四、NDK调试
1、NDK调试简介
NDK调试可以让我们在本地开发环境中调试C++层代码,可以极大的提高开发效率。NDK调试可以帮助我们在开发过程中及时发现bug并进行修复。NDK调试分为两种类型:GDB和LLDB
2、NDK调试步骤
将以下代码添加到app->build.gradle文件中:
externalNativeBuild {
cmake {
cmake {
// -DCMAKE_LIBRARY_OUTPUT_DIRECTORY=libs
// 指定编译生成文件的目录
arguments "-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=${project.buildDir}/libs/${outputPath}"
// -DCMAKE_BUILD_TYPE=Debug
// 指定编译类型为Debug模式
arguments "-DCMAKE_BUILD_TYPE=Debug"
// -DANDROID_TOOLCHAIN=clang
// 指定编译器为Clang编译器
arguments "-DANDROID_TOOLCHAIN=clang"
// -DANDROID_STL=c++_static
// 指定STL库为c++_static
arguments "-DANDROID_STL=c++_static"
}
}
ndkBuild {
//同上
}
}
在Android Studio中打开“Edit Configurations”菜单,在“Debugger”选项卡中设置:
- Debugger Type为Auto
- Debug Type为Native
点击“Run”菜单中的“Debug 'app'”进行调试
五、NDK压缩
在开发完成之后,需要将so库打包成apk文件。程序中使用的Shared Library(.so文件)原则上都应当打包进APK里。这样的做法直接导致.apk文件的大小增加,不过现在主流的压缩工具如upx等均对.so文件有压缩能力。毫无疑问,对于 .so 文件而言,应该利用好这样的能力来缩减APK包大小。
1、UPX
UPX是一个免费的、便携式的、可扩展的、高性能的文件压缩器,它是一个通用性的可执行文件压缩器。UPX支持Linux、MacOS X和近年出现的Windows安装包压缩,UPX完全符合ELF, PE, MZ, COFF, NLM和OMF格式标准。在NDK中,我们使用UPX来对.so库进行压缩。
2、NDK压缩步骤
在app/build.gradle文件中添加以下配置:
splits {
abi {
enable true
//This property should be set only to true for publishing part of your app as APK.
reset()
include "armeabi-v7a", "x86"
universalApk true
}
}
apply from: "../../tools/JniLibsCompression.gradle"
为了使用UPX,需要拷贝tools目录中的JniLibsCompression.gradle,然后添加以下代码到app/build.gradle文件:
apply from: "../../tools/JniLibsCompression.gradle"
project.ext.upx_enabled = true
project.ext.upx_options = "best --lzma"
android {
...
}
最后执行gradlew clean assembleRelease命令打包APK文件,生成的APK文件中的so库已经被压缩。