學習 Android(十四)NDK基礎
Android NDK 是一個工具集,可讓我們使用 C 和 C++ 等語言以原生代碼實現應用的各個部分。對于特定類型的應用,這可以幫助我們重復使用以這些語言編寫的代碼庫。
接下來,我們將按照以下步驟進行講解
- NDK 是什么,作用和原理
- Android Studio 中配置 NDK 與 CMake
- 創建簡單 Native 庫(C/C++),Java 調用 Native 方法
- 了解 JNI 基本概念,基本數據類型映射,Java 和 C++ 函數簽名
- 學習如何傳遞 Java 字符串、數組到 Native ,反之亦然
1. NDK 是什么?作用和原理
1.1 NDK 是什么?
原生開發套件 (NDK) 是一套工具,能夠讓我們在 Android 應用中使用 C 和 C++ 代碼,并提供眾多平臺庫,我們可使用這些平臺庫管理原生 activity 和訪問實體設備組件,例如傳感器和觸控輸入。NDK 可能不適合大多數 Android 編程初學者(例如作者我),初學者只需使用 Java 代碼和框架 API 開發應用。然而,我們需要實現以下一個或多個目標,那么 NDK 就能派上用場:
-
進一步提升設備性能,以降低延遲或運行游戲或物理模擬等計算密集型應用。
-
重復使用您自己或其他開發者的 C 或 C++ 庫。
我們可以在 Android Studio 2.2 或更高版本中使用 NDK 將 C 和 C++ 代碼編譯到原生庫中,然后使用 Android Studio 的集成構建系統 Gradle 將原生庫打包到 APK 中。Java 代碼隨后可以通過 Java 原生接口 (JNI) 框架調用原生庫中的函數。
Android Studio 編譯原生庫的默認構建工具是 CMake。由于很多現有項目都使用 ndk-build 構建工具包,因此 Android Studio 也支持 ndk-build。不過,如果要創建新的原生庫,則應使用 CMake。
1.2 NDK 的工作原理
NDK 的本質是通過 JNI(Java Native Interface)橋接 Java/Kotlin 和 C/C++ 本地代碼,從而實現跨語言通信與調用,并在 Android 系統中生成 .so
動態鏈接庫供運行時加載。
- 整體架構流程圖如下所示
Java/Kotlin 層|| 調用 native 方法v
JNI (Java Native Interface)|| 負責參數類型轉換、方法注冊v
C/C++ 層代碼(通過 NDK 編譯)|| 編譯為 .so 動態庫v
libnative-lib.so 被 Android 加載并運行
-
Java 層聲明 native 方法
我們首先要在 Java 或 Kotlin 中用
native
關鍵字聲明一個方法:public class NativeLib {static {System.loadLibrary("native-lib"); // 加載 C/C++ 編譯生成的 .so 文件}public native int add(int a, int b); // native 方法,C/C++ 實現 }
-
C/C++ 層實現(通用JNI)
我們需要在 C/C++ 中用 JNI 方式實現這個方法,簽名必須完全匹配
extern "C" JNIEXPORT jint JNICALL Java_com_example_NativeLib_add(JNIEnv *env, jobject thisz, jnit a, jint b) {return a + b; }
-
JNIEnv*
是 JNI 環境指針(用于訪問 Java) -
jobject
是 Java 傳進來的對象引用(即 this)
-
-
構建和變異位動態庫(
.so
文件)使用
CMakeLists.txt
或Android.mk
構建規則,把你的 C++ 文件編譯成.so
:-
輸出目錄:
app/build/intermediates/cmake/debug/obj/arm64-v8a/libnative-lib.so
-
會被打包進 APK,在運行時由
System.loadLibrary
加載
-
-
運行時調用流程
-
用戶點擊或代碼調用
NativeLib.add()
-
JVM 會通過 JNI 找到
.so
文件中注冊的Java_com_example_NativeLib_add()
方法 -
調用 C++ 實現,返回結果給 Java
-
2. Android Studio 中配置 NDK 與 CMake
2.1 在 Android Studio 中操作:
-
打開 Preferences(設置):
-
macOS:
Android Studio > Preferences
-
Windows/Linux:
File > Settings
-
-
導航到:
Appearance & Behavior > System Settings > Android SDK > SDK Tools
-
勾選并安裝:
-
NDK (Side by side)
-
CMake
-
LLDB(可選,調試 C++ 用)
-
2.2 配置 build.gradle
文件
以下以 App 模塊的 build.gradle
(Groovy 版) 為例說明配置方式:
-
defaultConfig
中添加:defaultConfig {...externalNativeBuild {cmake {cppFlags ""}}ndk {abiFilters 'armeabi-v7a', 'arm64-v8a' // 你可以根據需求精簡架構} }
-
配置
externalNativeBuild
android {...externalNativeBuild {cmake {path "src/main/cpp/CMakeLists.txt" // 指向你的 CMake 配置文件version "3.22.1" // 根據你安裝的版本寫}} }
2.3 創建 C/C++ 文件和 CMake 配置
app/└── src/└── main/├── cpp/│ ├── native-lib.cpp│ └── CMakeLists.txt└── java/
native-lib.cpp
#include <jni.h>extern "C"
JNIEXPORT jint JNICALL
Java_com_example_ndkdemo_NativeLib_add(JNIEnv *env, jobject obj, jint a, jint b) {return a + b;
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.10.2)project("ndkdemo")add_library( # 構建庫名native-libSHAREDnative-lib.cpp
)find_library( # 找到 log 庫log-liblog
)target_link_libraries( # 鏈接 log 庫native-lib${log-lib}
)
2.4 Java 層調用 native 方法
public class NativeLib {static {System.loadLibrary("native-lib"); // 加載 .so}public native int add(int a, int b); // 聲明 native 方法
}
2.5 構建與運行
-
點擊 Build → Rebuild Project
-
.so 文件將生成在:
app/build/intermediates/cmake/debug/obj/arm64-v8a/libnative-lib.so
-
如果你運行到 ARM64 模擬器或真機,程序會自動加載對應
.so
并調用你的 native 方法。
3. 創建簡單 Native 庫(C/C++),Java 調用 Native 方法
3.1 步驟一:項目結構準備
在 Android Studio 中新建一個空項目(Empty Activity),選擇 Java語言,API 21,然后按如下結構添加文件:
app/└── src/└── main/├── cpp/│ ├── native-lib.cpp C++ 實現文件│ └── CMakeLists.txt CMake 構建文件└── java/com/example/ndkdemo/└── NativeLib.java Java 調用封裝類
3.2 步驟二:配置 build.gradle
(app 模塊)
android {defaultConfig {...// 指定使用的 ABI 架構ndk {abiFilters 'armeabi-v7a', 'arm64-v8a'}// 配置 CMake 構建externalNativeBuild {cmake {cppFlags ""}}}// 指定 CMake 構建文件路徑externalNativeBuild {cmake {path "src/main/cpp/CMakeLists.txt"}}
}
3.3 步驟三:創建 CMake 構建文件(CMakeLists.txt)
在 app/src/main/cpp/CMakeLists.txt
中實現
cmake_minimum_required(VERSION 3.10.2)
project("ndkdemo") // 記得這是填對應的名稱add_library( # native 庫名native-libSHAREDnative-lib.cpp
)find_library( # 引用 Android 日志庫(可選)log-liblog
)target_link_libraries(native-lib${log-lib}
)
3.4 步驟四:實現 C++ 代碼(native-lib.cpp)
在 app/src/main/cpp/native-lib.cpp
中實現
#include <jni.h>// 使用 extern "C" 避免 C++ 方法名被改寫(mangling)
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_ndkdemo_NativeLib_add(JNIEnv *env, jobject thiz, jint a, jint b) {return a + b;
}
3.5 步驟五:Java 封裝 Native 調用
在 com/example/ndkdemo/NativeLib.java
中實現
package com.example.ndkdemo;public class NativeLib {static {System.loadLibrary("native-lib"); // 加載 native-lib.so 動態庫}// native 方法聲明,由 C++ 實現public native int add(int a, int b);
}
3.6 步驟六:在 Activity 中調用驗證
在 com/example/ndkdemo/MainActivity.java
中實現
public class MainActivity extends AppCompatActivity {private final NativeLib nativeLib = new NativeLib();private TextView textView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);textView = findViewById(R.id.tv_result);int result = nativeLib.add(3, 4); // 調用 native 方法textView.setText("3 + 4 = " + result);}
}
3.7 步驟七:構建運行
編譯運行結果如下所示
4. 了解 JNI 基本概念,基本數據類型映射,Java 和 C++ 函數簽名
接下來我們將針對 JNI 進行相關的學習和了解
4.1 基本數據類型
Java 類型 | JNI 類型 | 描述 |
---|---|---|
boolean | jboolean | 無符號 8 位(通常為 unsigned char ),值為 JNI_TRUE (1) 或 JNI_FALSE (0) |
byte | jbyte | 有符號 8 位 |
char | jchar | 無符號 16 位 |
short | jshort | 有符號 16 位 |
int | jint | 有符號 32 位 |
long | jlong | 有符號 64 位 |
float | jfloat | 32 位 IEEE 浮點數 |
double | jdouble | 64 位 IEEE 浮點數 |
void | void | 對應 void 類型 |
4.2 引用類型
Java 類型 | JNI 類型 | 說明 |
---|---|---|
java.lang.Object | jobject | 所有對象的基類 |
任意 Java 類 | jclass | Java 類的引用 |
java.lang.String | jstring | Java 字符串 |
T[] (Java 數組) | jarray | 所有數組的基類 |
原始類型數組 | jintArray 、jbyteArray 等 | 特定類型的數組 |
Java 對象數組 | jobjectArray | 包含對象引用的數組 |
異常 | jthrowable | 可被 throw 的對象 |
4.3 特殊輔助類型
JNI 類型 | 定義 | 用途 |
---|---|---|
jsize | typedef jint jsize; | 表示數組、字符串長度或大小等 |
jfieldID | 不透明指針類型 | 標識一個類的字段 |
jmethodID | 不透明指針類型 | 標識一個類的方法 |
4.4 本地方法接口類型
JNI 提供的所有函數都通過這兩個結構體訪問:
類型名 | 說明 |
---|---|
JNIEnv * | 每個線程獨有,包含 JNI 所有函數指針 |
JavaVM * | JVM 實例指針,用于跨線程附加線程等操作 |
4.5 布爾常量
為兼容 C 語言布爾類型,定義了:
#define JNI_TRUE 1
#define JNI_FALSE 0
4.6 原始類型數組
Java 類型 | JNI 類型 |
---|---|
boolean[] | jbooleanArray |
byte[] | jbyteArray |
char[] | jcharArray |
short[] | jshortArray |
int[] | jintArray |
long[] | jlongArray |
float[] | jfloatArray |
double[] | jdoubleArray |
4.7 對象數組
Java 類型 | JNI 類型 | 說明 |
---|---|---|
String[] | jobjectArray | 指向一組 jstring 對象的數組 |
Object[] | jobjectArray | 可存放任意引用類型對象 |
SomeClass[] | jobjectArray | 存放 SomeClass 對象的數組 |
5 Java 和 C++ 函數簽名
Java 和 C++ 函數簽名是函數唯一身份的定義方式,但兩者的表現形式和規則存在差異。
5.1 Java 的函數簽名
Java 中函數簽名包括:函數名 + 參數類型列表(不包括返回值)
public int add(int a, int b) { ... }
Java 中,下面兩個方法簽名相同,會報錯
public void test(int x) { }
public int tes(int x) { } // 編譯報錯:簽名沖突(返回值不算簽名)
Java 方法簽名示例(包括參數類型):
方法聲明 | 簽名(方法名 + 參數類型) |
---|---|
void foo(int x) | foo(I) |
void foo(String s) | foo(Ljava/lang/String;) |
void foo(int[] arr) | foo([I) |
void foo(int x, String s) | foo(ILjava/lang/String;) |
5.2 C++ 的函數簽名
C++ 中函數簽名包括:函數名 + 參數類型列表(返回值不計入簽名)
int add(int a, int b);
double add(int a, int b); // 編譯錯誤:重定義函數(簽名沖突)
但和 Java 不同的是,C++ 支持函數重載:C++ 的重載機制在編譯和鏈接層處理得很好,不需要額外區分。但 Java 的重載雖然語法上支持,但在調用 native 方法時,需要開發者顯式編碼函數簽名,這讓處理重載略顯麻煩。并不是說 Java 不支持重載,而是說 Java 的重載不天然適用于 native binding,需要額外工作。
void print(int x);
void print(double x);
函數簽名還包括是否為指針、引用、常量等修飾:
void func(int &x); // 引用
void func(const int x); // const 修飾不同參數,簽名不同
5.3 Java 和 C++ 在 JNI 中的函數簽名映射
JNI 中為了讓 Java 調用 C/C++ 函數,會將 Java 方法 簽名映射為 JNI 名字。
public class MyClass {public native void hello(String msg);
}
對應的 C 函數簽名為:
JNIEXPORT void JNICALL Java_MyClass_hello(JNIENV *env, jobject obj, jstring msg);
規則如下:
-
包名和類名中的
.
替換為_
-
方法名拼接在類名后
-
參數類型在 JNI 中通過
jint
、jstring
、jbooleanArray
等類型傳遞
5.4 常見 JNI 簽名編碼表
Java 類型 | JNI 類型編碼 |
---|---|
int | I |
boolean | Z |
byte | B |
char | C |
short | S |
long | J |
float | F |
double | D |
void | V |
Object | L類名; |
int[] | [I |
String | Ljava/lang/String; |
6. 學習如何傳遞 Java 字符串、數組到 Native ,反之亦然
6.1 Java 與 Native(C/C++)之間的數據傳遞總覽
類型 | Java -> Native | Native -> Java |
---|---|---|
String | jstring → const char* (使用 GetStringUTFChars ) | 創建 jstring (用 NewStringUTF ) |
基本類型數組 | jintArray , jbyteArray 等 → jint* (使用 GetXxxArrayElements 或 GetXxxArrayRegion ) | 創建數組并填充(用 NewXxxArray + SetXxxArrayRegion ) |
對象數組 | jobjectArray → 單個元素用 GetObjectArrayElement 訪問 | 創建 jobjectArray 并填充每一項 |
自定義對象 | 傳入 jobject ,通過 JNI API 訪問其字段或方法 | 構造 Java 對象并返回 |
6.2 Java 字符串與 Native 的相互轉換:
-
Java -> Native :獲取 C 字符串:
extern "C" JNIEXPORT void JNICALL Java_com_example_hello_NativeLib_print(JNIEnv* env, jobject thiz, jstring jStr) {const char* cStr = (*env).GetStringUTFChars(jStr, 0);printf("收到字符串: %s\n", cStr);(*env).ReleaseStringUTFChars(jStr, cStr); // 一定要釋放 }
-
Native -> Java :創建 Java 字符串:
extern "C" JNIEXPORT jstring JNICALL Java_com_example_hello_NativeLib_stringFromJNI(JNIEnv* env,jobject thiz /* this */) {jstring result = (*env).NewStringUTF("你好 MainActivity");return result; }
6.3 Java 數組與 Native 的相互轉換:
-
Java int[] -> Native
extern "C" JNIEXPORT void JNICALL Java_com_example_demo_NativeLib_sum(JNIEnv* env, jobject thiz, jintArray arr) {jsize len = (*env).GetArrayLength(arr);jint* elems = (*env).GetIntArrayElements(arr, NULL);int sum = 0;for (int i = 0; i < len; i++) {sum += elems[i];}printf("總和: %d\n", sum);(*env).ReleaseIntArrayElements(arr, elems, 0); // 0 表示更新 Java 數組 }
-
Native int[] -> Java int[]
extern "C" JNIEXPORT jintArray JNICALL Java_com_example_demo_NativeLib_getNumbers(JNIEnv *env, jobject) {jint nums[] = {1, 2, 3, 4, 5};jintArray arr = (*env).NewIntArray(5);(*env).SetIntArrayRegion(arr, 0, 5, nums);return arr; }
在 Native (C/C++) 中使用 printf()
打印日志時,它的輸出位置取決于哪個平臺運行,在 Android 中 printf()
輸出不會自動出現在 Logcat,我們通常看不到它的輸出。
為此我們需要使用 __android_log_print
在 native-lib.cpp
中添加
#include <android/log.h>#define LOG_TAG "NativeLog"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
將在 Native 中使用 print() 的方法替換成 LOGI() 或者 LOGE() 方法,我們就可以在 Logcat 查看日志了
jintArray arr = (*env).NewIntArray(5);
(*env).SetIntArrayRegion(arr, 0, 5, nums);
return arr;
}
在 Native (C/C++) 中使用 `printf()` 打印日志時,它的輸出位置取決于哪個平臺運行,**在 Android 中 `printf()` 輸出不會自動出現在 Logcat**,我們通常看不到它的輸出。為此我們需要使用 `__android_log_print`在 `native-lib.cpp` 中添加```cpp
#include <android/log.h>#define LOG_TAG "NativeLog"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
將在 Native 中使用 print() 的方法替換成 LOGI() 或者 LOGE() 方法,我們就可以在 Logcat 查看日志了