上一篇打包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.cpp
LOCAL_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目录下生成libs
和obj
这两个文件夹,在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: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
tvContent = findViewById(R.id.tv_content)
tvContent.text = nGetStringFromSo()
}
external fun nGetStringFromSo(): String
companion 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( sotest
SHARED
IMPORTED )
# 给sotest这个库设置so文件链接的位置
set_target_properties( sotest
PROPERTIES 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-lib
sotest
# 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 29
buildToolsVersion "29.0.1"
defaultConfig {
applicationId "com.example.nativesotest"
minSdkVersion 24
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// 设置ndk编译的cpu架构
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles 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。