文章目录

  • 一、编写so库代码
  • 二、安装Android NDK
  • 三、编译so库
    • 3.1 编辑Android.mk
    • 3.2 编辑Application.mk
    • 3.3 编译
  • 四、集成到Android工程中

上一篇打包so库及jar包的博客我讲了如何将自己的代码打包成so库,并且配合jar包供他人调用。但那种方式仅适合对方从java层调用,如果算法是比较核心的,而又为了效率必须从native来调用,那种方式就不合适了。本篇讲如何打包我们自己的核心代码供他人在native调用,如果对方愿意,也可以自己封装然后从java来调用,灵活性更高。并且在调试的时候更加方便。这种方式是更接近纯C/C++工程的集成方式。

一、编写so库代码

第一步来编写so库的代码,等会儿将这个代码打包成so库供Android工程调用。这个代码比较简单,同样只返回一个字符串。为了步骤清楚,我们新建一个文件夹NDKSo,然后在里面新建一个so文件夹来盛放我们的so代码部分,so文件夹之外,我们存放测试代码。整体目录如下:

NDKSo
├── main.cpp
└── so├── SoStringUtil.cpp└── SoStringUtil.h

main.cpp是我们用来调用代码测试so库代码是否正常工作的。
首先是头文件

#ifndef _SO_STRING_UTIL_H_
#define _SO_STRING_UTIL_H_#include<iostream>
#include<stdlib.h>using namespace std;string getStringFromSoLibrary();#endif

然后是cpp文件:

#include "SoStringUtil.h"string getStringFromSoLibrary()
{return "Hello from shared library!";
}

然后是main.cpp

#include <iostream>
#include <stdlib.h>
#include "so/SoStringUtil.h"using namespace std;int main()
{cout << getStringFromSoLibrary() << endl;getchar();return 0;
}

如何测试呢?你可以用各种IDE什么的,这里推荐用VSCode,不过VSCode需要配置一番才可以运行调试C/C++代码。简单起见,我就不演示如何用VSCode调试了,直接用命令行编译输出。
新建一个在NDKSo的终端,我这里是macOS所以使用clang,Windows和Linux都建议使用GCC。
输入编译命令:

clang++ -g *.cpp so/*.cpp -o main.o

如何使用编译器命令不详细展开。
注意的一点是你使用的是C还是C++工程,如果是C++可以使用g++或者clang++,如果是C可以使用gcc或clang。这里推荐你用C++版本的编译命令,因为如果用C的编译命令而你使用了某些C++,编译会出问题。
编译成功后,就会出现main.o文件,它是个可执行文件。
如果你的报错了,那在这个阶段你就需要使用IDE来对代码进行调试了,所以这就是这种方式的优势所在。它的调试不依赖于Android工程,能够让你更专注于算法的实现。
运行这个main.o,就会出现我们期待的输出:

zus-MacBook-Air:NDKSo zu$ ./main.o
Hello from shared library!

OK,至此我们就完成了so库代码。

二、安装Android NDK

虽然完成了代码,但是如果要在手机上运行,就不能使用GCC/clang来编译so,必须使用NDK。如果你已经安装了NDK(开发Android的都会有吧),并且把NDK添加到环境变量里,就可以跳过这步。
首先无论通过什么方式,SDK manager或者人肉也好,把NDK下载下来。如果是用SDK manager,它是放在<AndroidSDK目录>/ndk这里的,可能会多一层文件夹用版本号命名,我们的目的是把ndk-build这个可执行文件添加到环境变量里。
我这边是有版本号的文件夹,完整的目录是~/AndroidSDK/ndk/20.0.5594570/~代表的是用户目录,在这个文件夹里就是ndk内容。
在windows下,把上面那个路径添加到Path下,重新启动cmd即可。
在Mac下,要编辑~/.bash_profile文件,在ubuntu下,要编辑~/.bashrc文件。这里我以Mac举例。
输入sudo vim ~/.bash_profile,输入密码后会使用vim打开~/.bash_profile文件,如果你从未编辑过这个文件,那它应该不存在,会自动新建一个。打开后,按i进入插入模式,输入

export NDK_HOME=/Users/zu/AndroidSDK/ndk/20.0.5594570/
export PATH=$PATH:$NDK_HOME

然后输入:wq!保存,在终端中输入source ~/.bash_profile更新后即可使用ndk-build,这时不会再提示找不到命令了。而是NDK提示你的其他错误,无论如何,ndk-build命令现在可用了。
当然vim还是比较难用,如果是ubuntu一般会有gedit这个编辑器,把vim换成gedit,就能以更自然的方式去编辑了。mac可以先安装VSCode,然后把VSCode添加到环境变量里(这个搜索一下,很简单),把vim换成code就可以使用VSCode打开了。

三、编译so库

到此可以编译so库了。依据NDK官方文档,目前有三种方式可以编译C/C++项目:Android.mk和Application.mk、makefile、gradle。但是如果仅使用NDK手动编译,就必须选择第一种方式,因此这一步我们需要首先编辑Android.mk和Application.mk文件。

3.1 编辑Android.mk

这个文件的详细信息可参阅NDK官方文档-Android.mk。简要地说,这个文件相当于对工程的配置,比如要编译的源码文件、编译的模块名称等。
新建一个Android.mk文件到so目录下,内容如下:

LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional# 需要编译出的so的模块名
LOCAL_MODULE:= libndktest# All of the source files that we will compile.
LOCAL_SRC_FILES:= \SoStringUtil.cppLOCAL_C_INCLUDES += \$(LOCAL_PATH)/SoStringUtil.h \include $(BUILD_SHARED_LIBRARY)

3.2 编辑Application.mk

这个文件的详细信息可参考NDK官方文档-Application.mk。它指定了编译的一些参数以及模块配置。
同样位置在so目录下,内容如下:

APP_ABI := all
APP_OPTIM := release
APP_STL := c++_static
APP_CPPFLAGS := -frtti -fexceptions
APP_MODULES := libndktest
APP_BUILD_SCRIPT := Android.mk

要注意的是APP_BUILD_SCRIPT := Android.mk这一句,它指定了我们Android.mk的位置,Application.mk和Android.mk都在同级目录下,可以直接这样写。如果你的目录有差别,注意改这一句,规则和Linux下文件路径规则是一致的。
最后的目录是这样的

NDKSo
├── main.cpp
├── main.o
└── so├── Android.mk├── Application.mk├── SoStringUtil.cpp└── SoStringUtil.h

3.3 编译

接下来,把终端切换到so目录下。由于NDK有一套默认的Application.mk和路径,因此如果要它适应我们自己的目录结构,就要自己设置我们的工程目录并且为它指明Applicatiom.mk,当如果没有设置,直接输ndk-build,它会提示你。

它提示找不到工程目录,需要定义一个NDK_PROJECT_PATH变量。
输入下面的命令来临时添加这个变量,目录位置就是so目录,由于我现在终端位置就在so目录里,因此直接用./即可。

export NDK_PROJECT_PATH=./

再输入ndk-build,它会提示找不到Android.mk文件,实际上NDK有一个自己的Application.mk文件,但是它并没有指向我们自己的Android.mk文件。

输入以下命令来为它指明我们的Application.mk文件。

ndk-build NDK_APPLICATION_MK=./Application.mk

注意的是,这个命令同时也会开始进行编译。终端里一堆输出。

如果没有错的话,会在so目录下生成libsobj这两个文件夹,在libs目录下就有我们需要的so库,由于我在Application.mk文件中ABI指定为all,因此现在最常用的arm和x86的32位、64位库都会被编译出来。

四、集成到Android工程中

首先,我们要新建一个支持C/C++的Android工程。如何建立这样一个工程,可参见我的上一篇博客Android NDK开发:打包so库及jar包供他人使用中关于为Android工程添加C++支持的部分。

我这里的工程名为NaiveSoTest。然后cpp部分仅有一个名为native-lib.cpp的文件。

接下来,按照国际惯例,把生成的so库放到app下的jniLibs目录里。

接下来就可以完善cpp部分的代码了。要在cpp中使用动态链接库,有两种方法,一种是dlsym方式来动态寻找并链接so库,灵活性非常高,甚至可以通过替换so库的方式来热修复或热更新核心代码,但是难度更高。第二种就是在编译的时候链接库,这里使用第二种方式。

首先,要想使用一个库,必须先知道它提供了哪些接口。这里有两种方式,第一就是我们把so库的头文件复制到Android工程的cpp文件夹中,这种是最方便的,不过这个方式要求你在CMakeLists文件中设置好包含文件夹include_directories。第二种方式就是我们在任何一个文件中单次声明so里的方法,但是不用实现它,编译的时候编译器会去库中查找它的实现。

我在native-lib.cpp中的代码如下:

#include <jni.h>
#include <stdlib.h>
#include <iostream>
#include "SoStringUtil.h"using namespace std;//如果你不想用引入头文件的方法,可以把导入头文件的include语句注释掉,然后将下面这句取消注释。
//string getStringFromSoLibrary();extern "C"
JNIEXPORT jstring
JNICALL
Java_com_example_nativesotest_MainActivity_nGetStringFromSo(JNIEnv *env, jobject instance)
{string result = getStringFromSoLibrary();return env->NewStringUTF(result.c_str());
}

在MainActivity中这样调用(Kotlin代码):

class MainActivity : AppCompatActivity() {private lateinit var tvContent: TextViewoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)tvContent = findViewById(R.id.tv_content)tvContent.text = nGetStringFromSo()}external fun nGetStringFromSo(): Stringcompanion object{init {System.loadLibrary("native-lib")}}
}

然后修改CMakeLists.txt来设置链接。

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html# Sets the minimum version of CMake required to build the native library.cmake_minimum_required(VERSION 3.4.1)include_directories(src/main/cpp/)
file(GLOB CPP_FILES "src/main/cpp/*.cpp")# 添加so库存放位置
set(distribution_DIR ${CMAKE_SOURCE_DIR}/../../../../jniLibs)# 添加一个库,它链接我们的so文件
add_library( sotestSHAREDIMPORTED )# 给sotest这个库设置so文件链接的位置
set_target_properties( sotestPROPERTIES IMPORTED_LOCATION../../../../jniLibs/${ANDROID_ABI}/libndktest.so )# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.add_library( # Sets the name of the library.native-lib# Sets the library as a shared library.SHARED# Provides a relative path to your source file(s).src/main/cpp/native-lib.cpp )# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.find_library( # Sets the name of the path variable.log-lib# Specifies the name of the NDK library that# you want CMake to locate.log )# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.target_link_libraries( # Specifies the target library.native-libsotest# Links the target library to the log library# included in the NDK.${log-lib})

主要的点我都用中文注释写清楚了。需要注意的是,这个CMakeLists和上一篇文章中有些许不一样,首先就是注释掉了导出so库的语句。另外上一篇只是单纯地导出so库,因此并没有寻找log库以及链接等一系列操作。

然后是app的build.gradle文件,这个和上一篇文章中的也是有差别的。上一篇中,在sdk里我们只是编译so库而不涉及链接,因此只设置了NDK的编译选项。在app里我们只是导入so库并链接,但是并没有设置NDK的编译选项。在这篇文章中,我们的工程里既要导入外来的so库,这就需要设置jniLibs的位置,同时我们自己也要编译出so库,所以你也需要设置NDK的编译参数。下面放一个完整的gradle。

apply plugin: 'com.android.application'apply plugin: 'kotlin-android'apply plugin: 'kotlin-android-extensions'android {compileSdkVersion 29buildToolsVersion "29.0.1"defaultConfig {applicationId "com.example.nativesotest"minSdkVersion 24targetSdkVersion 29versionCode 1versionName "1.0"testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"// 设置ndk编译的cpu架构ndk {abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'}}buildTypes {release {minifyEnabled falseproguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'}}//设置CMakeLists文件的位置externalNativeBuild {cmake {path "CMakeLists.txt"}}//我们将外部so库放在jniLibs文件夹下,因此要将它设置为jniLibs使工程在打包的时候能将它包含进去,否则app运行时会报无法找到so库的错误。sourceSets {main {jniLibs.srcDirs = ['jniLibs']}}}dependencies {implementation fileTree(dir: 'libs', include: ['*.jar'])implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"implementation 'androidx.appcompat:appcompat:1.0.2'implementation 'androidx.core:core-ktx:1.0.2'implementation 'androidx.constraintlayout:constraintlayout:1.1.3'testImplementation 'junit:junit:4.12'androidTestImplementation 'androidx.test:runner:1.2.0'androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

其中的原理想一下其实也很简单。除了我们自己编译的外部so库,工程中自己的cpp代码也是会编译成一个so库的,因此需要设置ABI和CMakeLists的位置等,编译的同时将它和外部so库进行链接,这部分是在CMakeLists中设置的。然后在运行时,natibe-lib.so就会去链接外部so库,因此需要设置jniLibs来保证外部so库也被打包进去,否则届时就会报错找不到so库。
然后运行看一下结果:

运行无误。

然后可以用adb进入应用的安装目录看一下是否有两个so库。不过不知为何我的虚拟机没法root了,在windows上是OK的,所以暂时看不了。

源码可以看我的github。

Android NDK开发: 通过C/C++调用第三方so库相关推荐

  1. 【Android NDK 开发】NDK 交叉编译 ( Ubuntu 中交叉编译动态库 | Android Studio 中配置使用第三方动态库 )

    文章目录 I . 动态库 与 静态库 II . 编译动态库 III. Android Studio 使用第三方动态库 IV . Android Studio 关键代码 V . 博客资源 I . 动态库 ...

  2. 【Android NDK 开发】Android Studio 使用 CMake 导入动态库 ( 构建脚本路径配置 | 指定动态库查找路径 | 链接动态库 )

    文章目录 I . CMake 引入动态库与静态库区别 II . Android Studio 中 CMake 引入动态库流程 III . 指定动态库查找路径 IV . 链接函数库 V . 完整代码示例 ...

  3. 【Android NDK 开发】Android Studio 使用 CMake 导入静态库 ( CMake 简介 | 构建脚本路径配置 | 引入静态库 | 指定静态库路径 | 链接动态库 )

    文章目录 I . CMake 简介 II . Android Studio 中 CMake 引入静态库流程 III . 指定 CMake 最小版本号 IV . 导入函数库 ( 静态库 / 动态库 ) ...

  4. 【Android NDK 开发】Android.mk 配置静态库 ( Android Studio 配置静态库 | 配置动态库与静态库区别 | 动态库与静态库打包对比 )

    文章目录 I . Android Studio 中使用 Android.mk 配置静态库 总结 II . 第三方动态库来源 III . 配置 Android.mk 构建脚本路径 IV . 预编译 第三 ...

  5. 【Android NDK 开发】Android.mk 配置动态库 ( Android Studio 配置动态库 | 动态库加载版本限制 | 本章仅做参考推荐使用 CMake 配置动态库 )

    文章目录 I . Android Studio 中使用 Android.mk 配置动态库 总结 II . 第三方动态库来源 III . 配置 Android.mk 构建脚本路径 IV . 预编译 第三 ...

  6. Android如何调用第三方SO库

    问题描述:Android如何调用第三方SO库: 已知条件: SO库为Android版本连接库(*.so文件),并提供了详细的接口说明: 已了解解决方案: 1.将SO文件直接放到libs/armeabi ...

  7. Android NDK开发之旅(2):一篇文章搞定Android Studio中使用CMake进行NDK/JNI开发

    Android NDK开发之旅(2):一篇文章搞定android Studio中使用CMake进行NDK/JNI开发 (码字不易,转载请声明出处:http://blog.csdn.NET/andrex ...

  8. 【Android NDK 开发】JNI 方法解析 ( JNIEnv *env 参数 )

    文章目录 一. JNI 方法解析 二. JNIEnv *env 参数解析 三. C 语言 环境中 JNIEnv *env 参数解析 四. C ++ 环境中 JNIEnv *env 参数解析 总结 : ...

  9. Android NDK开发method GetStringUTFChars’could not be resolved

    Android NDK开发method GetStringUTFChars'could not be resolved 图1 最近用到android的ndk,但在eclipse中提示method Ge ...

最新文章

  1. Mysql InnoDB B+树索引和哈希索引的区别? MongoDB 为什么使用B-树?
  2. 关于事件相关电位SSVEP应用于视频游戏的研究
  3. wxPython多线程界面卡死或在不同平台崩溃问题
  4. 【知识星球】猫猫狗狗与深度学习那些事儿
  5. 【springboot】spring-boot-devtools 热部署 导致 mvn spring-boot:run 出现异常
  6. 使用Docker Compose 搭建lnmp
  7. EconomicIndoor集成测试
  8. Ajax Get请求获取后台返回的数据
  9. Linux内存管理slub分配器
  10. jwt token 附加用户信息_JWT的正确使用方法,API开发为什么使用JWT
  11. 使用react定义组件的两种方式
  12. 餐厅点餐系统源码(带电脑端和手机端)
  13. 详解二叉树的递归遍历与非递归遍历——(二)
  14. 1.投骰子的随机游戏
  15. Either re-interrupt this method or rethrow the “InterruptedException“ that can be caught here.
  16. 加利福尼亚大学圣地亚哥分校计算机科学专业,美国加州大学伯克利分校计算机专业排名一览...
  17. 计算机实验员技能大赛,“迎接挑战”计算机技能大赛圆满结束!
  18. 神经翻译笔记4扩展c. 2017-2019年间RNN和RNN语言模型的新进展
  19. MTK如何修改usb驱动能力
  20. 安装冰点还原后无法更改系统时间怎么办

热门文章

  1. Mysql支持中文全文检索的插件mysqlcft-应用中的问题
  2. Asp.net性能优化-提高ASP.Net应用程序性能的十大方法
  3. SSLOJ 1336.膜拜神牛
  4. linux内核的冷热页分配器
  5. 5、SQL Server数据库、T-SQL
  6. 汇编--查找第一个非0字符的五种方法
  7. vs2015调试时不显示vector内容的解决方法
  8. MySQL------报错Access denied for user ‘root‘@‘localhost‘ (using password:NO)解决方法
  9. 【干货】企业如何进行数字化转型及如何称为数据驱动型企业?
  10. 16篇最新推荐系统论文送你(文末附打包下载链接)