如果你是一名 C/C++ 开发人员,正在尝试将 C/C++ 的代码往安卓上迁移,那么这篇文章对你有很大的帮助
如果你是一名 Android 开发人员,正在尝试将外部 so 嵌入到你的 app 中,那么这篇文章对你有很大的帮助
本人属于前一种情况,由于工作的需求,需要把 C/C++ 的 so 库集成至 Android 中进行开发。本人对 Android 开发了解不多,更多是在站在 C/C++ 开发人鱼的角度来描述问题。而对于职业的 Android 开发人员,你们可以从这篇文章中,更加详细地了解 native 侧的细节。
这篇文章中,我将通过一个示例,一步一步地展示如何将 C/C++ 代码嵌入以 so 的形式嵌入至 android 中。
所有代码已上传至 GitHub,大家可以对照着看,比较容易理解Android_Studio_Import_so_Demo
为了让事情变得更简单,我们假设有一个 Adder
类(C++写的),我们要把它的功能嵌入到 Android 中,以下是它的源码:
adder.h
class Adder
{
public:
static int add(int a, int b);
};
adder.cpp
#include "adder.h"
int Adder::add(int a, int b)
{
return a + b;
}
想要在 Android 上运行 C/C++ 代码,首先我们要将 C/C++ 代码进行交叉编译。所谓交叉编译就是在一个平台上生成另一个平台的可执行的代码。
例如我们在 Macos 上编译出可以在 Android 运行代码。交叉编译需要用到交叉编译器,这里引用维基百科一段对于交叉编译器的介绍:
交叉编译器(英语:Cross compiler)是指一个在某个系统平台下可以产生另一个系统平台的可执行文件的编译器。交叉编译器在目标系统平台(开发出来的应用程序序所运行的平台)难以或不容易编译时非常有用。
交叉编译器的存在对于从一个开发主机为多个平台编译代码是非常有必要的。直接在平台上编译有时行不通,例如在一个嵌入式系统的单片机 ,因为它们没有操作系统,所以直接编译行不通。
交叉编译器和源代码至源代码编译器不同,交叉编译器用于二进制代码的跨平台软件开发,而源到源编译器是将某种编程语言的程序源代码作为输入,生成以另一种编程语言构成的等效源代码的编译器,但两者都是编程工具。
Android 提供了原生开发套件(NDK, Native Development Kit) 工具。NDK 中就包含了 Android 的交叉编译器。
那么如何利用 NDK 进行交叉编译呢?这里推荐使用 cmake,只需要简单的几个步骤就能完成。下面请跟随我的脚本。
在我们电脑上安装 NDK。这很简单,到 NDK下载页面 下载解压,放到你认为合适的位置就可以了。笔者用的 NDK 21,放在电脑 ~/NDK/
目录中
Android Studio 编译原生库默认的构建工具是 CMake,想要进行 Native 开发,CMake是绕不过去的。
我们的 CMakeLists.txt 非常简单,add_library
生成 so,install
命令将需要用的 so 和 头文件放置到合适的位置。
cmake_minimum_required(VERSION 3.10)
project(native_proj)
include(GNUInstallDirs)
include_directories(include)
add_library(adder SHARED src/adder.cpp)
# set lib subdir
if(ANDROID)
set(LIB_SUBDIR ${ANDROID_ABI})
else()
set(LIB_SUBDIR ${CMAKE_SYSTEM_NAME})
endif()
# so installation
install(TARGETS adder
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/${LIB_SUBDIR})
# Headers installation
install(
DIRECTORY ${CMAKE_SOURCE_DIR}/include/
DESTINATION include
FILES_MATCHING PATTERN "*.h"
)
可以简单测试下编译是否正常:
mkdir build
cd build
cmake .. -DCMAKE_INSTALL_PREFIX=../dist
cmake --build .
一切顺利的话,在 dist 目录就会出现编译产物了,目录结构大致如下( 那个 Darwin 就是 mac 操作系统的名字)
├── dist
│ ├── include
│ │ └── adder.h
│ └── lib
│ └── Darwin
│ └── libadder.dylib
下一步就是交叉编译了。CMake 支持通过指定 CMAKE_TOOLCHAIN_FILE
来改变编译环境,非常好用的特性。NDK 中提供了一个 toolchain.cmake 文件来帮助我们切换环境。详细的 cmake 语法就不展开了,直接上脚本。
prompt() {
echo "
Options:
-a [arm64-v8a|armeabi-v7a]: android abi
example:
$0 -a arm64-v8a
"
}
if (($#==0)); then
prompt
exit 0
fi
ANDROID_ABI=arm64-v8a
while getopts "a:" arg #选项后面的冒号表示该选项需要参数
do
case $arg in
a)
if [ "$OPTARG" == "arm64-v8a" ]; then
ANDROID_ABI=$OPTARG
elif [ "$OPTARG" == "armeabi-v7a" ]; then
ANDROID_ABI=$OPTARG
else
echo "bad argument for android abi"
exit 1
fi
;;
?) #当有不认识的选项的时候arg为?
echo "unkonw argument"
exit 1
;;
esac
done
echo "ANDROID_ABI: $ANDROID_ABI"
build_dir=build_state_android_$ANDROID_ABI
mkdir -p $build_dir
cd $build_dir || exit 1
ANDROID_NDK_HOME=~/NDK/android-ndk-r21/
cmake .. \
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=$ANDROID_ABI \
-DANDROID_STL=c++_shared \
-DCMAKE_INSTALL_PREFIX=../dist \
-DCMAKE_BUILD_TYPE=Release \
cmake --build . --target install -j8
ANDROID_NDK_HOME
指的是 ndk 安装的目录,这里根据你们自己情况进行修改。
ANDROID_ABI
有 arm64-v8a
和 armeabi-v7a
两种,分别对应 64 位和 32 位。简单测试下,运行./android_build.sh -a arm64-v8a
,在 dist 目录下出现编译产物:
├── dist
│ ├── include
│ │ └── adder.h
│ └── lib
│ └── arm64-v8a
│ └── libadder.so
以上三个步骤就完成了对 C/C++ 代码的交叉编译,其编译产物,一些头文件和so文件,将被嵌入至 Android 中。
新建一个 Android 项目来简单演示如何将外部 so 引入。
接下来的内容涉及到了 JNI 开发,相关推荐资料包括:
我们定义一个 native 方法 add
,通过add
计算两个数的和
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d(TAG, "onCreate: " + add(10, 10));
}
private native int add(int a, int b);
}
将交叉编译头文件 copy 到 main/cpp/include
中,将 so copy 到 main/lib
中。目录结构大致是这样的:
├── main
│ ├── CMakeLists.txt
│ ├── cpp
│ │ ├── include
│ │ │ └── adder.h
│ │ └── jni_src.cpp
│ ├── java
│ │ └── com
│ │ └── bytedance
│ │ └── importso
│ │ └── MainActivity.java
│ ├── lib
│ │ ├── arm64-v8a
│ │ │ └── libadder.so
│ │ └── armeabi-v7a
│ │ └── libadder.so
在 jni_src.cpp
中我们调用 Adder
这个类来完成功能
#include "adder.h"
#include
extern "C"
JNIEXPORT jint JNICALL Java_com_bytedance_importso_MainActivity_add(
JNIEnv* env, jobject obj, jint a, jint b)
{
return Adder::add(a, b);
}
添加 CMakeLists.txt 在 main 文件夹下(其实位置随意),编写内容为:
cmake_minimum_required(VERSION 3.10)
project(import_so_jni_project)
include_directories(cpp/include)
add_library(import_so_jni SHARED cpp/jni_src.cpp)
find_library(ADDER_LIB
NAMES adder
PATHS ${PROJECT_SOURCE_DIR}/lib/${ANDROID_ABI}
NO_CMAKE_FIND_ROOT_PATH)
target_link_libraries(import_so_jni
PRIVATE ${ADDER_LIB})
上面的 cmake 中,我们通过 include_directories(cpp/include)
引入头文件,find_library
来引入需要的 so 文件,其中NO_CMAKE_FIND_ROOT_PATH
很重要,记得一定要加上,否则在 Android 环境下没法找到外部的 so
修改 build.gradle 文件有两个目的:
我们先解决第一个 cmake 编译的问题,在 build.gradle 中添加如下:
android {
compileSdkVersion 29
buildToolsVersion "29.0.3"
defaultConfig {
...
// 0
ndk{
abiFilters "armeabi-v7a", "arm64-v8a"
}
// 1
externalNativeBuild{
cmake {
version "3.10.2"
arguments "-DANDROID_STL=c++_shared"
}
}
}
...
// 2
externalNativeBuild{
cmake{
path "src/main/CMakeLists.txt"
}
}
}
在 0 处添加 abiFilters
指明只需要 “armeabi-v7a”, “arm64-v8a”; 在 1 处添加 cmake 需要的参数;在 2 处指明 CMakeLists.txt 的路径
接着,我们添加 sourceSets
,其中 jniLibs.srcDirs
指明外部 so 的位置,这样在打包的时候,会将里面的资源一起打包在 App 中。如果做这一步,app在启动的时候会出现 crash,日志出现 dlopen failed: library "libadder.so" not found
这样的错误
android {
...
sourceSets{
main{
jniLibs.srcDirs = ['src/main/lib']
}
}
}
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
// load jni so
static {
System.loadLibrary("import_so_jni");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d(TAG, "onCreate: " + add(10, 10));
}
private native int add(int a, int b);
}
至此,所有工作已经完毕,让我们启动 app,就可以在 logcat 结果了,大功告成
D/MainActivity: onCreate: 20
这篇博客主要描述如何将 so 导入 AS 中,通过一个具体的例子,说明了从交叉编译到AS集成so的具体步骤。
所有代码我已经上传至 GitHub,大家可以参考参考 Android_Studio_Import_so_Demo