核心概念補充
- 編譯→連結→交叉編譯:源碼
.c → 目標檔 .o → 可執行檔。交叉編譯在宿主機(如 x86 Linux)產出目標機(如 ARM Android)二進制,需工具鏈模擬目標環境。
- NDK 工具鏈:提供
arm-linux-androideabi-gcc 等編譯器、libc、libm、liblog 等庫。
標準 NDK 開發流程(6 步驟)
- 建立 Android 專案。
- Java/Kotlin 宣告
native 方法。
- 建立
jni/ 目錄,撰寫 C/C++ 實作(函數命名規則:Java_包名_類名_方法名)。
- 編寫
Android.mk(或 CMakeLists.txt)。
- 執行
ndk-build 生成 .so 動態庫。
- Java 端
System.loadLibrary 載入並呼叫。
完整 Hello World 範例
Java 端
public class DemoActivity extends Activity {
static { System.loadLibrary("Hello"); }
public native String helloFromJNI();
public void onClick(View v) {
Toast.makeText(this, helloFromJNI(), Toast.LENGTH_SHORT).show();
}
}
C 端 (Hello.c)
#include <stdio.h>
#include <jni.h>
jstring Java_cn_itcast_ndk_DemoActivity_helloFromC(JNIEnv *env, jobject obj) {
return (*env)->NewStringUTF(env, "hello from c");
}
Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := Hello
LOCAL_SRC_FILES := Hello.c
include $(BUILD_SHARED_LIBRARY)
進階:javah 產生頭檔
# 1. 編譯 Java
javac -d bin/classes src/com/.../DemoActivity.java
# 2. 產生 .h
cd bin/classes
javah com.example.DemoActivity
# 產生 com_example_DemoActivity.h,複製到 jni/ 內含引用
常見錯誤與解決方案(含 Logcat 除錯)
| 錯誤訊息 | 原因 | 解決 |
Android NDK: Your APP_BUILD_SCRIPT points to an unknown file | Android.mk 路徑錯誤或遺失 | 確認 jni/Android.mk 存在,ndk-build 在專案根目錄執行 |
jni/Android.mk:4: *** missing separator | Makefile 語法錯誤(縮排必須用 Tab) | 檢查每行命令前為 Tab,非空格 |
make: *** [obj/.../Hello.o] Error 1 | C 語法錯誤 | 查看第一個 error 訊息,修正對應行 |
UnsatisfiedLinkError: Library Hello not found | .so 未生成或名稱不符 | 確認 LOCAL_MODULE 名稱,loadLibrary 不含 "lib" 前綴與 ".so" 後綴 |
UnsatisfiedLinkError: hello_from_c | native 方法找不到對應 C 函數 | 檢查函數簽名是否與 javah 產生的 .h 一致 |
Build fingerprint: ... signal 11 (SIGSEGV) | C 代碼邏輯錯(空指標、越界、類型不符) | 用 __android_log_print 定位崩潰行,或用 ndk-gdb 除錯 |
Logcat 除錯技巧
Android.mk 加入 log 库
LOCAL_LDLIBS += -llog
C 代碼定義巨集
#include <android/log.h>
#define LOG_TAG "MyTag"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
// 使用
LOGD("x=%d", x); LOGI("init done");
中文亂碼處理
- NDK r6+:源碼儲存為 UTF-8,編譯鏈預設 UTF-8,Java 端直接
NewStringUTF 即可。
- NDK r4 舊版:編碼為 ISO-8859-1,Java 端需
new String(str.getBytes("ISO-8859-1"), "UTF-8") 轉碼。
Android.mk 關鍵變數詳解
| 變數 | 說明 |
LOCAL_PATH | $(call my-dir) 取得當前目錄 |
include $(CLEAR_VARS) | 重置所有 LOCAL_* 變數(除 LOCAL_PATH) |
LOCAL_MODULE | 模組名,生成 lib<name>.so |
LOCAL_SRC_FILES | 源檔列表,相對路徑 |
LOCAL_LDLIBS | 連結庫,如 -llog -lz |
include $(BUILD_SHARED_LIBRARY) | 產出動態庫 .so |
include $(BUILD_STATIC_LIBRARY) | 產出靜態庫 .a |
靜態庫 vs 動態庫選擇
- 動態庫:體積小、載入時解析符號、支援插件架構。
- 靜態庫:編譯期鏈入、無執行期相依、適合發布獨立模組給第三方。
資料傳遞:Java ↔ C 完整範例
Java 宣告
public class DataProvider {
public native int add(int x, int y);
public native String sayHelloInC(String s);
public native int[] intMethod(int[] arr);
}
C 實作重點
jint 直接對應 int,無需轉換。
jstring → char*:需呼叫 GetStringUTFChars 或自行 GetByteArrayElements 轉碼。
jintArray:GetIntArrayElements 取得指標,修改後自動反映回 Java 陣列(釋放時模式 0 為複製回)。
C 反向呼叫 Java
- 取得類引用:
jclass cls = (*env)->FindClass(env, "pkg/Class");
- 取得方法 ID:
jmethodID mid = (*env)->GetMethodID(env, cls, "methodName", "(Ljava/lang/String;)V");(簽名用 javap -s 取得)。
- 建立實例(非靜態):
jobject obj = (*env)->AllocObject(env, cls);
- 呼叫:
(*env)->CallVoidMethod(env, obj, mid, (*env)->NewStringUTF(env, "from c"));
靜態方法呼叫
用 GetStaticMethodID + CallStaticVoidMethod,無需 AllocObject,直接傳 cls。