Introduction
如果你是一名 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;
}
1. 交叉編譯
想要在 Android 上運行 C/C++ 代碼,首先我們要將 C/C++ 代碼進行交叉編譯。所謂交叉編譯就是在一個平臺上生成另一個平臺的可執行的代碼。
例如我們在 Macos 上編譯出可以在 Android 運行代碼。交叉編譯需要用到交叉編譯器,這裏引用維基百科一段對於交叉編譯器的介紹:
交叉編譯器(英語:Cross compiler)是指一個在某個系統平臺下可以產生另一個系統平臺的可執行文件的編譯器。交叉編譯器在目標系統平臺(開發出來的應用程序序所運行的平臺)難以或不容易編譯時非常有用。
交叉編譯器的存在對於從一個開發主機爲多個平臺編譯代碼是非常有必要的。直接在平臺上編譯有時行不通,例如在一個嵌入式系統的單片機 ,因爲它們沒有操作系統,所以直接編譯行不通。
交叉編譯器和源代碼至源代碼編譯器不同,交叉編譯器用於二進制代碼的跨平臺軟件開發,而源到源編譯器是將某種編程語言的程序源代碼作爲輸入,生成以另一種編程語言構成的等效源代碼的編譯器,但兩者都是編程工具。
Android 提供了原生開發套件(NDK, Native Development Kit) 工具。NDK 中就包含了 Android 的交叉編譯器。
那麼如何利用 NDK 進行交叉編譯呢?這裏推薦使用 cmake,只需要簡單的幾個步驟就能完成。下面請跟隨我的腳本。
Step 0 安裝 NDK
在我們電腦上安裝 NDK。這很簡單,到 NDK下載頁面 下載解壓,放到你認爲合適的位置就可以了。筆者用的 NDK 21,放在電腦 ~/NDK/
目錄中
Step 1 編寫 CMakeLists.txt
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
Step 2 Android 編譯腳本
下一步就是交叉編譯了。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 Studio 中引入外部 so 文件
新建一個 Android 項目來簡單演示如何將外部 so 引入。
接下來的內容涉及到了 JNI 開發,相關推薦資料包括:
Step 0 定義 Native 方法
我們定義一個 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);
}
Step 1 將交叉編譯的產物 copy 到合適位置
將交叉編譯頭文件 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
Step 2 編寫 JNI
在 jni_src.cpp
中我們調用 Adder
這個類來完成功能
#include "adder.h"
#include <jni.h>
extern "C"
JNIEXPORT jint JNICALL Java_com_bytedance_importso_MainActivity_add(
JNIEnv* env, jobject obj, jint a, jint b)
{
return Adder::add(a, b);
}
Step 3 編寫 CMakeLists.txt
添加 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
Step 4 修改 build.gradle
修改 build.gradle 文件有兩個目的:
- 讓 AS 去編譯 cmake
- 讓 AS 打包 App 時,把我們的 libadder.so 給打包進去
我們先解決第一個 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']
}
}
}
至此,所有工作已經完畢,讓我們啓動 app,就可以在 logcat 結果了,大功告成
D/MainActivity: onCreate: 20
總結
這篇博客主要描述如何將 so 導入 AS 中,通過一個具體的例子,說明了從交叉編譯到AS集成so的具體步驟。
所有代碼我已經上傳至 GitHub,大家可以參考參考 Android_Studio_Import_so_Demo