💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## C++(CPP)在Android开发中的应用 ## #### 使用纯Java开发App的缺点 ### - 在某些场合下,使用纯Java开发Android应用程序不完美,比如: - 有高性能算法,Java语言无法满足 - 有跨平台需求,希望将App移植到iOS(现在很多设计都是C语言和C++来写底层,Android来写界面) - 已有代码的重用 #### 为什么使用NDK #### 1. 代码的保护。由于apk 的java 层代码很容易被反编译,而C/C++库反编译难度较大。 2. 可以方便地使用现存的开源库。大部分现存的开源库都是用C/C++代码编写的。 3. 提高程序的执行效率。将要求高性能的应用逻辑使用C 开发,从而提高应用程序的执行效率。 4. 便于移植。用C/C++写得库可以方便在其他的嵌入式平台上再次使用。 ### 引入NDK #### - 早在Android 1.6(2009年)时,google就提供了NDK(native development kit),NDK包括了一套Android的交叉编译环境和开发库,利用它可以编写C/C++程序,并编译成Android环境下使用的动态库,Java代码通过Jni规范,调用C/C++写的动态库。 - NDK - NDK是一系列工具的集合。 - 它提供了一系列的工具,帮助开发者快速开发C(或C++)的动态库,并能自动将so和java应用一起打包成apk。这些工具对开发者的帮助是巨大的。它集成了交叉编译器,并提供了相应的mk文件隔离CPU、平台、ABI等差异,开发人员只需要简单修改mk文件(指出“哪些文件需要编译”、“编译特性要求”等),就可以创建出so。它可以自动地将so和Java应用一起打包,极大地减轻了开发人员的打包工作。 - NDK 提供了一份稳定、功能有限的API 头文件声 - Google 明确声明该API 是稳定的,在后续所有版本中都稳定支持当前发布的API。从该版本的NDK中看出,这些API 支持的功能非常有限,包含有:C 标准库(libc)、标准数学库(libm)、压缩库(libz)、Log 库(liblog)。 - JNI - JavaNative Interface (JNI)标准是java平台的一部分,JNI是Java语言提供的Java和C/C++相互沟通的机制,Java可以通过JNI调用本地的C/C++代码,本地的C/C++的代码也可以通过JNI调用java代码。JNI 是本地编程接口,Java和C/C++互相通过的接口。Java通过C/C++使用本地的代码的一个关键性原因在于C/C++代码的高效性。 - 总结 - NDK就是为我们生成了c/c++的动态链接库而已,JNI只不过是java和c沟通的工具,两者与Android没有半毛钱关系,只因为安卓是java程序开发然后jni又能与c沟通,所以使“Java+C”的开发方式终于转正。 - Android是JVM架设在Linux之上的架构。所以无论如何,在Linux OS层面,都应该可以跑C/C++程序。 - Android Native C就是使用C/C++程序直接跑到Linux OS层面上的程序。与其它平台类似,只需要交叉编译后。并得到Linux OS root权限,就可以直接跑起来了。 - 官方定义:Android NDK 是一套允许您使用原生代码语言(例如 C 和 C++)实现部分应用的工具集。在开发某些类型应用时,这有助于您重复使用以这些语言编写的代码库。 - 目前最新的Android Studio 2.2中,集成了C/C++开发环境,开发人员在使用C/C++更加简单了。(以前,写Android的jni库时,需要用到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) - ![](https://i.imgur.com/KZFxWSR.png) - (2) Toolchains(默认值:GCC 4.8) - (3) C++标准库类型(默认值:system)(APP_STL) - (4) release/debug模式(默认值:release) - [NDK中文官方开发技术文档 ](https://developer.android.google.cn/ndk/index.html) - NDK下载地址 - [NDK 下载 ](https://developer.android.google.cn/ndk/downloads/index.html) - [AndroidDevTools](http://www.androiddevtools.cn/)(随时更新官网最新版本) - [官方下载](https://developer.android.com/ndk/downloads/index.html)(需要翻墙,而且翻墙后网站的NDK版本都不是最新版本) - 网友找到的一些版本----[NDK各个版本链接](http://blog.csdn.net/shuzfan/article/details/52690554) - 也可以打开---AndroidDevTools网站的上各个大学的镜像网站,找到Android,点击repository。可以看到最新最全的各个版本(包括sdk、sdktools、platform、platform-tools、Android源码等等官网上所持有的所有的资源),如下图是打开的大连东软信息学院镜像服务器中的Android资源 - 注意:NDK的r10版本以上,就已经集成了Cygwin的UNIX虚拟机的功能,不需要安装Cygwin的UNIX环境 - ![](https://i.imgur.com/KDcSXSQ.png) - - 下载完成,解压到自己认为合适的目录,**注意:目录路径中不能有中文和空格** ###创建NDK项目(支持 C/C++ 的新项目)(project) - 创建项目过程中,遇到问题,建议参考官网文章----[向您的项目添加 C 和 C++ 代码](https://developer.android.com/studio/projects/add-native-code.html)(貌似需要翻墙,没法翻墙的可以查看[在 Android Studio 2.2 中愉快地使用 C/C++ ](http://blog.csdn.net/wl9739/article/details/52607010)),而且这篇文章提到以下几点 - Android Studio 用于构建原生库的默认工具是 CMake。由于很多现有项目都使用构建工具包编译其原生代码,Android Studio 还支持 [ndk-build](https://developer.android.com/ndk/guides/ndk-build.html)。如果您想要将现有的 ndk-build 库导入到您的 Android Studio 项目中,请参阅介绍如何配置 Gradle 以[关联到您的原生库](https://developer.android.com/studio/projects/add-native-code.html#link-gradle)的部分。不过,如果您在创建新的原生库,则应使用 CMake。 - 下载 NDK 和构建工具 要为您的应用编译和调试原生代码,您需要以下组件: - [Android 原生开发工具包 (NDK)](https://developer.android.com/ndk/index.html):这套工具集允许您为 Android 使用 C 和 C++ 代码,并提供众多平台库,让您可以管理原生 Activity 和访问物理设备组件,例如传感器和触摸输入。 - [CMake](https://cmake.org/):一款外部构建工具,可与 Gradle 搭配使用来构建原生库。如果您只计划使用 ndk-build,则不需要此组件。[使用CMake变量](https://developer.android.com/ndk/guides/cmake.html#variables) - [LLDB](http://lldb.llvm.org/):一种调试程序,Android Studio 使用它来[调试原生代码](https://developer.android.com/studio/debug/index.html) - 您可以使用 SDK 管理器安装这些组件: - 在打开的项目中,从菜单栏选择 Tools > Android > SDK Manager。 - 点击 SDK Tools 标签。 - 选中 LLDB、CMake 和 NDK 旁的复选框,如图所示。 - ![](https://i.imgur.com/eTN2W2Z.png) #### 创建NDK项目(支持 C/C++ 的新项目)(project)或者在一个已存在的项目中创建NDK module #### - 创建一个NDK项目 - 创建支持原生代码的项目与创建任何其他 Android Studio 项目类似,不过还需要额外几个步骤:把 Include C++ support的勾打上 ![](https://i.imgur.com/KLACA7L.png) - 一直next,直到出一个左边一大白色C++的方形框时,表示这是一个C++的项目,右边选择C++标准,可以选择默认的Toolchain Default,同时可以选择C++11,因为C++11有更多的新特性和功能。,一般选择C++11。 ![](https://i.imgur.com/24eT4pk.png) - 点击Finish后,进入工程目录,如图所示,除了java文件夹外多了一个cpp文件夹,cpp就是存放c和c++代码的文件夹,如下图所示 ![](https://i.imgur.com/oA2Zpsi.png) - 同时,下面为CMakeLists.txt和build.gradle中的内容 - CMakeLists.txt(注释部分已删除) - ![](https://i.imgur.com/h3a4aqk.png) - build.gradle中的内容 - ![](https://i.imgur.com/4lDDu2j.gif) - PS:如果在创建project时,没有勾选,就只是一个简单的AS的Android项目,这时如果想要创建C++项目,可以参考下面的在module中 - 如何在一个module中添加CPP组件,创建原生源文件 - 一般,我们的项目创建时并不是一个C++项目,而是一个Android普通的项目,如果需要用到NDK,JNI方面的,这时就需要添加CPP组件,原生源文件,之前都是用ndk-build命令来进行NDK开发,所以正如上面这篇文章(向您的项目添加 C 和 C++ 代码)所述---->由于很多现有项目都使用构建工具包编译其原生代码,Android Studio 还支持 ndk-build(已非主流,过时)。如果您想要将现有的 ndk-build 库导入到您的 Android Studio 项目中,请参阅介绍如何配置 Gradle 以关联到您的原生库的部分。不过,如果您在创建新的原生库,则应使用 CMake。但是还是推荐使用CMake,因为在Android2.2以后的版本,进行JNI开发,更方便 - 同样,我们需要认真阅读[《向您的项目添加 C 和 C++ 代码》](https://developer.android.com/studio/projects/add-native-code.html)的---->”向现有项目添加 C/C++ 代码“这一小章节,注意CMake构建的脚本 CMakeLists.txt中的内容和build.gradle中的注意细节,一定要认真阅读[《向您的项目添加 C 和 C++ 代码》](https://developer.android.com/studio/projects/add-native-code.html),或者可以参考上面创建C++项目的build.gradle和CMakeLists.txt中的内容。 #### 添加 NDK API #### - Android NDK 提供了一套实用的原生 API 和库。通过将 [NDK 库](https://developer.android.com/ndk/guides/stable_apis.html)包含到项目的 CMakeLists.txt 脚本文件中,您可以使用这些 API 中的任意一种。 - [NDK 库(Android NDK 原生 API)](https://developer.android.com/ndk/guides/stable_apis.html) - [概览](https://developer.android.com/ndk/guides/stable_apis.html#purpose) - [主要的原生 API 更新](https://developer.android.com/ndk/guides/stable_apis.html#mnu)([Android平台版本对应的API等级](https://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels)) - [Android API 级别 3](https://developer.android.com/ndk/guides/stable_apis.html#a3) - [Android API 级别 4](https://developer.android.com/ndk/guides/stable_apis.html#a4) - [Android API 级别 5](https://developer.android.com/ndk/guides/stable_apis.html#a5) - [Android API 级别 8](https://developer.android.com/ndk/guides/stable_apis.html#a8) - [Android API 级别 9](https://developer.android.com/ndk/guides/stable_apis.html#a9) - 从 API 级别 9 开始,您可以使用原生代码编写整个 Android 应用,无需使用任何 Java。注:在原生代码中编写您的应用本身并不能让您的应用在 VM 中运行。 此外,您的应用仍必须通过 JNI 访问 Android 平台的大部分功能 - [Android API 级别 14](https://developer.android.com/ndk/guides/stable_apis.html#a14) - [Android API 级别 18 ](https://developer.android.com/ndk/guides/stable_apis.html#a18) #### JNI规范 #### - 在进行NDK开发时,阅读官网的《[向您的项目添加 C 和 C++ 代码](https://developer.android.com/studio/projects/add-native-code.html)》涉及到的[Android 的 JNI 提示](https://developer.android.com/training/articles/perf-jni.html),而在[Android 的 JNI 提示](https://developer.android.com/training/articles/perf-jni.html)中又涉及到了[JNI规范](http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html),来了解JNI如何工作,以及JNI的特点。 - 早已有大神将官方的文档翻译(可以说是摘抄)到自己的博客了 - [《Java 本地接口规范》](http://blog.csdn.net/zhanghw0917/article/details/7000377) - [《Java 本地接口规范》- 简介 ](http://blog.csdn.net/snowdream86/article/details/6886954) - [已整理的目录篇](http://116.196.111.149:8080/jni.htm) #### APK分析仪 #### - 如果您想要验证 Gradle 是否已将原生库打包到 APK 中,可以使用[APK分析器 ](https://developer.android.com/studio/build/apk-analyzer.html) - 步骤 - 选择 Build > Analyze APK。 - 从 app/build/outputs/apk/ 目录中选择 APK 并点击 OK。 - 如下图所示,您会在 APK 分析器窗口的 lib/<ABI>/ 下看到 编译好的动态库`libnative-lib.so`。 - ![](https://i.imgur.com/G3HpvJz.png) - 提示:如果您想要试验使用原生代码的其他 Android应用,请点击 File>New>Import Sample 并从Ndk列表中选择示例项目。 #### 指定 ABI #### - APP_ABI - ABI全称是:Application binary interface,即:应用程序二进制接口,它定义了一套规则,允许编译好的二进制目标代码在所有兼容该ABI的操作系统和硬件平台中无需改动就能运行。(具体的定义请参考 [百度百科](https://baike.baidu.com/item/ABI/10912305#viewPageContent) 或者 维基百科 ) - 由上述定义可以判断,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平台的库也是这个道理。 - 支持的 ABI(每个 ABI 支持一个或多个指令集。下图提供每个 ABI 支持的指令集概览。) - ![](https://i.imgur.com/DzKrRs9.png) - 默认情况下,Gradle 会针对 [NDK 支持的 ABI](https://developer.android.com/ndk/guides/abis.html#sa) 将您的原生库构建到单独的 .so 文件中,并将其全部打包到您的 APK 中。如果您希望 Gradle 仅构建和打包原生库的特定 ABI 配置,您可以在模块级 build.gradle 文件中defaultConfig节点内使用 ndk.abiFilters 标志指定这些配置,如下所示: ![](https://i.imgur.com/9Ot21no.png) - 在大多数情况下,您只需要在 ndk {} 块中指定 abiFilters(如上所示),因为它会指示 Gradle 构建和打包原生库的这些版本。不过,如果您希望控制 Gradle 应当构建的配置,并独立于您希望其打包到 APK 中的配置,请在 defaultConfig.externalNativeBuild.cmake {} 块(或 defaultConfig.externalNativeBuild.ndkBuild {} 块中)配置另一个 abiFilters 标志。Gradle 会构建这些 ABI 配置,不过仅会打包您在 defaultConfig.ndk{} 块中指定的配置。 - 为了进一步降低 APK 的大小,请考虑[配置 ABI APK 拆分](https://developer.android.com/studio/build/configure-apk-splits.html#configure-abi-split),而不是创建一个包含原生库所有版本的大型 APK,Gradle 会为您想要支持的每个 ABI 创建单独的 APK,并且仅打包每个 ABI 需要的文件。如果您配置 ABI 拆分,但没有像上面的代码示例一样指定 abiFilters 标志,Gradle 会构建原生库的所有受支持 ABI 版本,不过仅会打包您在 ABI 拆分配置中指定的版本。为了避免构建您不想要的原生库版本,请为 abiFilters 标志和 ABI 拆分配置提供相同的 ABI 列表。 #### 从 ndkCompile 迁移 #### - 如果您使用已过时的ndkCompile ,你应该迁移---使用 CMake 或 ndk-build 生成。因为ndkCompile生成的中间文件Android.mk 给你,迁移到ndk-build 可能是一个简单的选择。 - 从ndkCompile迁移到ndk-build,进行如下操作: 1. 使用ndkCompile,至少一次,通过Build > Make Project, 来编译项目,这将为您生成Android.mk文件。 2. 通过 `project-root/module-root/build/intermediates/ndk/debug/Android.mk. `到找到的自动生成的Android.mk 文件。 3. 迁移Android.mk 文件到其他的目录,比如和module下的build.gradle同一等级的目录,这将确保 clean时,Gradle 并不会删除该脚本文件, 4. 打开Android.mk文件并编辑脚本中的任何路径,这样,他们是相对于当前脚本文件的位置。 5. [Link Gradle to the Android.mk file. ](https://developer.android.com/studio/projects/add-native-code.html#link-gradle) 6. 禁用ndkCompile 通过打开build.properties文件并删除以下行:`android.useDeprecatedNdk = true` 7. 通过单击工具栏中的Sync Project ![](https://i.imgur.com/dISdrfs.png),应用这些改变。 #### 需要注意的几点 #### - CMakeLists.txt脚本文件,`Android.mk`,`Application.mk`,这2个文件全是通过CMakeLists.txt来写;如果项目目录的CPP文件夹下有N个原生源文件,那么CMakeLists.txt中的add_library中就要对应有N个路径,类似于native-lib.cpp ,对应的add_library就对应的`src/main/cpp/native-lib.cpp`,如果CPP有test.cpp,则add_library对应有`src/main/cpp/test.cpp`;这就是编译的配置,而且相互之间空格隔开; - 这配置和module目录下的build.gradle的`android--->externalNativeBuild--->cmake--->path "CMakeLists.txt",`编译build时,通过这个路径path,找到CMakeLists.txt里面的构建命令,这是相互对应的。CMake是Unix自动化固件,跨平台 - find_library是链接静态库,如果有其他的.so库,就写在find_library节点内。 - 在使用Cmake插件之前,是在module目录下的build.gradle中配置NDK选项,以后就不需要在module目录下的build.gradle中NDK,因为这些操作,都在CMakeLists.txt中配置好了 #### 具体的创建工程 #### - JNI知识总结 - cpp中原生源文件(.c、.cpp)方法名的命名规则 - 这里的命名规则只适用于跟java 文件中的native 方法相对应C 语言函数,而C 语言中的其他函数命名只要符合C 语言规则就行。java 文件中的native 方法有N个,对应的源文件中就有N个函数,来实现native方法。 - C++调用Java #### Java和C++字符串转换 #### - JNI 基本类型和本地等效类型的对应表格如下: ![](https://i.imgur.com/oBMdtIY.png) - 引用类型,JNI 还包含了很对对应于不同Java 对象的引用类型,JNI 引用类型的组织层次如下图 ![](https://i.imgur.com/DfLShli.png) - Java中任何一种类型,在Jni中都有一种对应的类型,比如:String--->jstring,但是Java中String是双字节的,在C++中不是双字节的,这就涉及到一个字符串转换,编码的转换 - string转换为jstring ~~~ jstring c2j(JNIEnv* env, string cstr) { return env->NewStringUTF(cstr.c_str()); } ~~~ - jstring转换为string ~~~ string j2c(JNIEnv* env, jstring jstr) { string ret; jclass stringClass = env->FindClass("java/lang/String"); jmethodID getBytes = env->GetMethodID(stringClass, "getBytes", "(Ljava/lang/String;)[B"); // 把参数用到的字符串转化成java的字符 jstring arg = c2j(env, "utf-8"); jbyteArray jbytes = (jbyteArray)env->CallObjectMethod(jstr, getBytes, arg); // 从jbytes中,提取UTF8格式的内容 jsize byteLen = env->GetArrayLength(jbytes); jbyte* JBuffer = env->GetByteArrayElements(jbytes, JNI_FALSE); // 将内容拷贝到C++内存中 if(byteLen > 0) { char* buf = (char*)JBuffer; std::copy(buf, buf+byteLen, back_inserter(ret)); } // 释放 env->ReleaseByteArrayElements(jbytes, JBuffer, 0); return ret; } ~~~ - jstring转换成c语言的char* 类型 ![](https://i.imgur.com/5jKKxay.png) #### javah和javap - javah - 开发时,Java代码中native声明了本地方法,由于本地方法没有方法体,需要到cpp文件中完善对应的本地方法,但是cpp中原生源文件(.c、.cpp)对应本地方法的方法名的命名很麻烦,如果一个一个来写,可能会出错,这时,可以使用javah来生成一个头文件,该头文件中直接写好了Java代码的本地方法对应的cpp中方法命名,可以直接复制到cpp文件中,这样便捷高效。 - javah用于生成native接口定义,比如`javah com.wsc.wangsc.ndktest.Jni ` - ![](https://i.imgur.com/RgmTGee.png) - ![](https://i.imgur.com/OBXEJaw.png) - jni类中native方法和.h头文件中对应的方法如下图红色方框 - ![](https://i.imgur.com/o7n4Sex.png) - ![](https://i.imgur.com/oZyD6XB.png) - 这样可以直接复制.h头文件中方法命名到cpp文件中 - javap - javap作用 1. 反编译 2. 生成函数签名 - javap用于生成java函数的签名,比如 `javap -s Jni`