如何使用android的ndk编译器 编译c++的库
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何使用android的ndk编译器 编译c++的库相关的知识,希望对你有一定的参考价值。
1. 概述 首先回顾一下 android NDK 开发中,Android.mk 和 Application.mk 各自的职责。 Android.mk,负责配置如下内容: (1) 模块名(LOCAL_MODULE) (2) 需要编译的源文件(LOCAL_SRC_FILES) (3) 依赖的第三方库(LOCAL_STATIC_LIBRARIES,LOCAL_SHARED_LIBRARIES) (4) 编译/链接选项(LOCAL_LDLIBS、LOCAL_CFLAGS) Application.mk,负责配置如下内容: (1) 目标平台的ABI类型(默认值:armeabi)(APP_ABI) (2) Toolchains(默认值:GCC 4.8) (3) C++标准库类型(默认值:system)(APP_STL) (4) release/debug模式(默认值:release) 由此我们可以看到,本文所涉及的编译选项在Android.mk和Application.mk中均有出现,下面我们将一个个详细介绍。 2. APP_ABI ABI全称是:Application binary interface,即:应用程序二进制接口,它定义了一套规则,允许编译好的二进制目标代码在所有兼容该ABI的操作系统和硬件平台中无需改动就能运行。(具体的定义请参考 百度百科 或者 维基百科 ) 由上述定义可以判断,ABI定义了规则,而具体的实现则是由编译器、CPU、操作系统共同来完成的。不同的CPU芯片(如:ARM、Intel x86、MIPS)支持不同的ABI架构,常见的ABI类型包括:armeabi,armeabi-v7a,x86,x86_64,mips,mips64,arm64-v8a等。 这就是为什么我们编译出来的可以运行于Windows的二进制程序不能运行于Mac OS/Linux/Android平台了,因为CPU芯片和操作系统均不相同,支持的ABI类型也不一样,因此无法识别对方的二进制程序。 而我们所说的“交叉编译”的核心原理也跟这些密切相关,交叉编译,就是使用交叉编译工具,在一个平台上编译生成另一个平台上的二进制可执行程序,为什么可以做到?因为交叉编译工具实现了另一个平台所定义的ABI规则。我们在Windows/Linux平台使用Android NDK交叉编译工具来编译出Android平台的库也是这个道理。 这里给出最新 Android NDK 所支持的ABI类型及区别: 那么,如何指定ABI类型呢?在 Application.mk 文件中添加一行即可: APP_ABI := armeabi-v7a //只编译armeabi-v7a版本APP_ABI := armeabi armeabi-v7a //同时编译armeabi,armeabi-v7a版本APP_ABI := all //编译所有版本 3. LOCAL_LDLIBS Android NDK 除了提供了Bionic libc库,还提供了一些其他的库,可以在 Android.mk 文件中通过如下方式添加依赖: LOCAL_LDLIBS := -lfoo 其中,如下几个库在 Android NDK 编译时就默认链接了,不需要额外添加在 LOCAL_LDLIBS 中: (1) Bionic libc库 (2) pthread库(-lpthread) (3) math(-lmath) (4) C++ support library (-lstdc++) 下面我列了一个表,给出了可以添加到“LOCAL_LDLIBS”中的不同版本的Android NDK所支持的库: 下面是我总结的一些常用的CFLAGS编译选项: (1)通用的编译选项 -O2 编译优化选项,一般选择O2,兼顾了优化程度与目标大小 -Wall 打开所有编译过程中的Warning -fPIC 编译位置无关的代码,一般用于编译动态库 -shared 编译动态库 -fopenmp 打开多核并行计算, -Idir 配置头文件搜索路径,如果有多个-I选项,则路径的搜索先后顺序是从左到右的,即在前面的路径会被选搜索 -nostdinc 该选项指示不要标准路径下的搜索头文件,而只搜索-I选项指定的路径和当前路径。 --sysroot=dir 用dir作为头文件和库文件的逻辑根目录,例如,正常情况下,如果编译器在/usr/include搜索头文件,在/usr/lib下搜索库文件,它将用dir/usr/include和dir/usr/lib替代原来的相应路径。 -llibrary 查找名为library的库进行链接 -Ldir 增加-l选项指定的库文件的搜索路径,即编译器会到dir路径下搜索-l指定的库文件。 -nostdlib 该选项指示链接的时候不要使用标准路径下的库文件 (2) ARM平台相关的编译选项 -marm -mthumb 二选一,指定编译thumb指令集还是arm指令集 -march=name 指定特定的ARM架构,常用的包括:-march=armv6, -march=armv7-a -mfpu=name 给出目标平台的浮点运算处理器类型,常用的包括:-mfpu=neon,-mfpu=vfpv3-d16 -mfloat-abi=name 给出目标平台的浮点预算ABI,支持的参数包括:“soft”, “softfp” and “hard” 参考技术A 64位是x86-64还是arm64,确定之后去Google Android Developer下载ndk交叉编译器本回答被提问者采纳Android 编译C++
Android 编译C++项目
前言
在开发过程中,有一些底层库,算法、加解密之类的功能,不是用Java写的,而是C或者C++,而我们需要在Android工程中调用C/C++的函数达到理想的要求,那么这个时候你就需要知道怎么使用它们。
正文
在之前我其实就遇到过这个问题,一顿操作之后可以掉用了,但是忘记记录了,导致我再次遇到这样的问题时,人傻了,就是那种似曾相似又解决不了的感觉,痛定思痛之下,我决定记录一下,好记性不如烂笔头。
而编译C和C++项目只有两种情况,一种是已知的情况,另一种是未知的情况。分别说明一下,就是有一天老板告诉我要做一个项目,里面会用到一些C/C++的底层库,NDK等内容,你去了解一下,这属于已知情况,那么你在创建项目的时候就可以做好。但是不部分都是未知的情况,还是有一天老板告诉你之前的某个项目需要添加新的功能,软硬件相结合,硬件给你提供了C/C++的代码,让你在项目中使用,这属于未知情况。
相对来说已知比未知要好,兵法有云:运筹帷幄之中,决胜千里之外, 所以两种情况我都会说明一下怎么处理,对你来说也许有用,也许没有用,交给缘分吧。
一、基本知识
在写代码之前我们需要先知道要做的是什么?一些名词是否了解里面的含义,例如JNI是什么?NDK是什么?Java怎么调用C/C++?不知道没有关系,当场百度一下就知道了,有一个概念是很重要的,你就不会像无头苍蝇一样。
① 要做什么?
我们最终的目的是通过Java能够调用C/C++的函数,获取返回值显示在Activity中,这是我们所需要的结果。
② JNI是什么?
JNI
是Java Native Interface
的缩写,通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植。从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,但是它并不妨碍你使用其他编程语言,只要调用约定受支持就可以了。使用java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样做是可以接受的,甚至是必须的。例如,使用一些旧的库,与硬件、操作系统进行交互,或者为了提高程序的性能。JNI标准至少要保证本地代码能工作在任何Java 虚拟机环境。(PS:本段来自百度百科)
我们总结一下:通过JNI,Java和C/C++的代码能互相调用。
③ NDK是什么?
NDK(Native Development Kit缩写)一种基于原生程序接口的软件开发工具包,可以让您在 Android 应用中利用 C 和 C++ 代码的工具。通过此工具开发的程序直接在本地运行,而不是虚拟机。在Android中,NDK是一系列工具的集合,主要用于扩展Android SDK。NDK提供了一系列的工具可以帮助开发者快速的开发C或C++的动态库,并能自动将so和Java应用一起打包成apk。同时,NDK还集成了交叉编译器,并提供了相应的mk文件隔离CPU、平台、ABI等差异,开发人员只需要简单修改mk文件(指出“哪些文件需要编译”、“编译特性要求”等),就可以创建出so文件。(PS:本段来自知乎)
我们总结一下:通过NDK,可以创建so文件,可以将so文件和Java代码一起打包成apk。
二、配置NDK
如果你是新安装的Android Studio,那么它里面默认是没有NDK配置,File → Settings… → Android SDK 。
切换到SDK Tools,然后找一下NDK和CMake两个工具,我这里是已经下载了,这里Apply是不可用的,你如果没有下载配置就勾选上,点击Apply会有弹窗提示你要下载那些内容。如果你想下载指定版本,就勾选上Show Package Details ,就能看到其他的工具版本了,例如:
三、创建新工程
下面我们创建支持 C/C++ 的新项目,这里我们在创建工程的时候选择Native C++,如下图所示:
点击Next,输入工程名,这里有这样一句话:Creates a new project with an Empty Activity configured to use JNI,意思是创建一个配置为使用jni的空活动的新项目。
点击Next,然后选择C++的版本,你可以使用默认,也可以用其他的版本。
这里我们就使用默认的,点击Finish完成工程创建。
创建工程出现问题了,这里的错误意思是在Android Studio中使用SDK管理器安装缺少的组件cmake 3.18.1。也就是说我们虽然安装了Cmake,但是安装的版本不符合这个NDK的编译要求。
勾选上这个需要的版本,点击Apply,然后出现弹窗提示,点击OK,之后就是下载了。
下载完点击Finish,在回到SDK管理窗口点击Apply,最后看到工程窗口,并没有自动去编译。
你可以点击这个图标或者Try Again,再编译一次。
① 工程目录说明
出现这样的字样就代表编译成功了,也意味着我们的项目创建成功了,我们来看看工程目录。
- cpp
这里面就是关于C++的一些配置,我们可以在这里面写C/C++的代码。 - includes
它里面就是项目所使用ndk的版本,可能会多个ndk版本对应一个cmake版本。 - CMakeLists.txt
这是一个配置文件,你可以理解为build.gradle文件,主要作用就是配置C++。 - native-lib.cpp
这是一个C++文件,里面就是C++代码了,我们需要详细的了解这个文件。 - app下的build.gradle
在这个gradle也配置Native,在里面可以找到这样一段代码:
externalNativeBuild
cmake
path file('src/main/cpp/CMakeLists.txt')
version '3.18.1'
就是配置一下C++的Build文件,下面我们先运行一下看看。
运行没有问题,下面我们需要分析一下,打开MainActivity。
public class MainActivity extends AppCompatActivity
// Used to load the 'studynative' library on application startup.
static
System.loadLibrary("studynative");
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Example of a call to a native method
TextView tv = binding.sampleText;
tv.setText(stringFromJNI());
/**
* A native method that is implemented by the 'studynative' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
这里的代码有几个关键点,第一个是加载库,System.loadLibrary("studynative");
,这里的studynative你可以在CMakeList.txt中看到,然后我们写了一个stringFromJNI()
方法,用于调用C++的代码,得到一个String的返回值,然后设置在TextView上,MainActivity基本的内容分析完成了,下面我们需要分析一下这个stringFromJNI函数是怎么调用C++的,看一下native-lib.cpp文件。
② 分析cpp文件
首先我们看一下这个cpp文件的完整代码,如下所示:
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_llw_studynative_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */)
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
#include
,是包含头文件命令,这两个就是头文件,这两句代码就是说在这个cpp文件的这个位置插入这两个头文件的代码。
extern "C"
,用于避免编绎器按照C++的方式去编绎C函数。
JNIEXPORT
,用来表示该函数是否可导出(即:方法的可见性,有一些类似于public修饰符)。
JNICALL
,用来表示函数的调用规范。
你按住Ctrl键,点击一下JNIEXPORT
或JNICALL
,会跳转到jni.h
文件中,如下图所示:
你会看到JNIEXPORT
或JNICALL
有一个#define
。
#define
,在C语言中,可以用 #define
定义一个标识符来表示一个常量。其特点是:定义的标识符不占内存,只是一个临时的符号,预编译后这个符号就不存在了。那么对于
#define JNIEXPORT __attribute__ ((visibility ("default")))
#define JNICALL
这里的JNIEXPORT和JNICALL表示宏定义,宏可这样理解:
① 宏 JNIEXPORT 代表的就是右侧的表达式: __attribute__ ((visibility ("default")))
② JNIEXPORT 是右侧表达式的别名,宏可表达的内容很多,如:一个具体的数值、一个规则、一段逻辑代码等;
然后就是visibility表示是否可见,default表示外部可见,类似于public,可以被外部调用。如果改成hidden :表示隐藏,类似于private修饰符,只能被内部调用。
jstring
,这是一个数据类型,是 Java中String数据类型在 JNI 中的代表,宏JNICALL 右边是空的,说明只是个空定义,空定义是可以去掉的,我们试一下去掉再运行,如下图所示:
Java_com_llw_studynative_MainActivity_stringFromJNI
,这是一个函数名,有没有似曾相识的感觉,好像和我们在MainActivity看到的stringFromJNI()
函数相似,但是呢,名字没有这么长啊,而实际上是同一个函数,你可以按住Ctrl键,点击Java_com_llw_studynative_MainActivity_stringFromJNI
跳转到MainActivity中的stringFromJNI()
,也可以点击stringFromJNI()
跳转到native-lib.cpp中的Java_com_llw_studynative_MainActivity_stringFromJNI
。
那么回到之前的问题,为什么函数名字变长了,这跟JNI native函数的注册方式有关,JNI Native函数有两种注册方式:① 静态注册:按照JNI接口规范的命名规则注册;② 动态注册:在.cpp的JNI_OnLoad方法里注册;
JNI接口规范的命名规则:Java_<PackageName>_<ClassName>_<MethodName>
,例如Java_com_llw_studynative_MainActivity_stringFromJNI,下面我们来体验一下这种注册方式,在MainActivity中增加这样一行代码:
public native String testFromJNI();
代码位置如下图所示:
这个函数我们还有在cpp文件中创建中创建,所以爆红很正常,鼠标点击这个函数,Alt + Enter,会出现一个提示框。
第一项的意思是创建名为testFromJNI的JNI函数,回车一下就会快速在native-lib.cpp中创建了。
可以看到这个函数自动生成了,是不是很方便呢?然后我们在这个函数体里面写几行代码,如下图所示:
然后修改一下MainActivity中的onCreate()方法中的代码,调用testFromJNI函数。
再运行一下:
代码可行,说明我们刚才写的函数没有问题,下面我们继续分析函数中的参数。
JNIEnv
,代表了Java环境,通过JNIEnv*就可以对Java端的代码进行操作,如:① 创建Java对象;② 调用Java对象的方法;③ 获取Java对象的属性等;
jobject
,代表了定义native函数的Java类 或 Java类的实例:如果native函数是static,则代表类Class对象;如果native函数非static,则代表类的实例对象。我们可以通过jobject访问定义该native方法的成员方法、成员变量等。
③ JNI数据类型
前面说到jstring表示Java中的String类型,那么其他的数据类型在JNI中怎么表示呢,进入jni.h
,找到最上方的位置,我们可以看到一些数据类型的定义。
这里可以看到常见的变量,int、boolean、float、double、byte、char等,这里是JNI所定义的,他们之间有一个映射关系,参考下表:
JNI | Java | C/C++ |
---|---|---|
jint / jsize | int | int |
jshort | short | short |
jlong | long | long / long long (__int64) |
jbyte | jbyte | signed char |
jboolean | boolean | unsigned char |
jchar | char | unsigned short |
jfloat | float | float |
jdouble | double | double |
jobject | Object | _jobject* |
jstring | String | string |
那么新项目中怎么样使用C++就说完了,下面我们说明一下在现有的项目中怎么增加C++的使用。
四、现有工程使用C++
这里我们将项目结构改成Project
然后右键StudyNative,点击New → Module ,出现弹窗。
点击Next,这里就是默认选中Empty Activity的。
点击Next。
点击Finish。
工程创建完成,这个功能结构你应该很熟悉了,那么怎么添加C++进去呢?
① 创建C++文件
右键点击old工程的 main 目录,然后依次选择 New > Directory,会出现一个小窗口。
输入cpp然后回车。
右键点击 cpp 目录,然后依次选择 New > C/C++ Source File,出现弹窗,输入C++文件名。
点击OK。
② 创建CMake
右键点击old目录,然后依次选择 New > File。输入“CMakeLists.txt”作为文件名,然后回车。
这个文件的名字不能乱改的,如果你不习惯的话可以把这个文件再移动到cpp文件夹下。
然后我们配置一下old的build.gradle文件,添加如下代码:
externalNativeBuild
cmake
path file('src/main/cpp/CMakeLists.txt')
version '3.18.1'
这里的路径很重要,如果你的CMakeLists.txt不在cpp下面,你就要改成对应的路径,Sync Now。
③ 使用C++
下面我们在MainActivity中加载使用C++,首先需要这样的代码:
static
System.loadLibrary("old");
这是加载C++的库,这个名字就是CMakeList.txt中的project()中的名字。
下面我们添加一个native函数。
public native String oldString();
这里要注意一点就是你要生成的JNI函数是在那个cpp文件中,这里有两个,我们需要在old下的old-lib.cpp中生成,选中回车,在old-lib.cpp中生成如下代码:
#include <jni.h>
#include <string>
extern "C"
JNIEXPORT jstring JNICALL
Java_com_llw_old_MainActivity_oldString(JNIEnv *env, jobject thiz)
std::string result = "This old project used C++";
return env->NewStringUTF(result.c_str());
然后我们调用运行一下,就在onCreate()打印一下看看。
OK,这样我们在老项目中就可以使用C++了。
这里可以自己切换使用那个工程,都能够正常运行的。
五、源码
欢迎 Star 和 Fork
源码地址:StudyNative
以上是关于如何使用android的ndk编译器 编译c++的库的主要内容,如果未能解决你的问题,请参考以下文章
在Android环境下编译调用c++出现以下错误,大神们这是啥原因呀??我已经配置NDK了。