NDK Android: khái niệm, Native Development Kit và JNI cho C++

Tác giả: IT Sectr Đã đăng: 2026-02-09 Thời gian đọc: 10 phút

NDK (Native Development Kit) là bộ công cụ để viết một phần ứng dụng bằng C và C++ cho Android. Không giống như Android SDK thông thường, NDK biên dịch mã thành các thư viện gốc (.so) hoạt động trực tiếp với bộ xử lý mà không có lớp máy ảo. Theo Google NDK Guides, 2026, NDK được sử dụng cho tính toán hiệu suất cao, trò chơi, xử lý âm thanh và tái sử dụng các dự án C/C++ hiện có. JNI (Java Native Interface) kết nối mã gốc với Kotlin và Java.

Những điểm chính

  • NDK — bộ công cụ biên dịch mã C/C++ thành thư viện gốc cho Android.
  • JNI — giao diện tương tác giữa máy ảo Java/Kotlin và mã gốc.
  • CMake — hệ thống xây dựng chính cho các dự án NDK, có thể cấu hình qua CMakeLists.txt.
  • ABI — kiến trúc bộ xử lý mục tiêu (arm64-v8a, armeabi-v7a, x86_64) mà thư viện được xây dựng cho.
  • NDK không cần thiết cho hầu hết ứng dụng Android và chỉ được dùng trong các tác vụ yêu cầu hiệu suất cao.

NDK là gì

NDK (Native Development Kit) là một bộ công cụ để biên dịch chéo mã C và C++ thành các thư viện thực thi cho Android. NDK bao gồm trình biên dịch Clang, thư viện chuẩn libc++, các tệp tiêu đề API Android và tiện ích xây dựng. Nhà phát triển viết mã gốc bằng C hoặc C++, biên dịch nó thành các tệp .so (đối tượng chia sẻ) và kết nối với ứng dụng qua JNI.

NDK không thay thế Android SDK mà bổ sung cho nó. Toàn bộ giao diện người dùng, vòng đời và dịch vụ hệ thống vẫn nằm trong Kotlin hoặc Java. Mã gốc giải quyết các tác vụ cụ thể: tính toán toán học, mật mã, codec, vật lý trong trò chơi. Google khuyến nghị chỉ sử dụng NDK khi SDK không cung cấp hiệu suất hoặc quyền truy cập cần thiết vào khả năng phần cứng.

Lịch sử của NDK bắt đầu từ năm 2009 (NDK r1). Trong thời gian này, bộ công cụ đã phát triển từ một tập hợp kịch bản thử nghiệm thành một hệ thống trưởng thành với hỗ trợ CMake, gỡ lỗi LLDB và phân tích hiệu năng. Tính đến năm 2026, phiên bản hiện tại là NDK r27 với Clang 19, hỗ trợ đầy đủ C++20 và libc++ là thư viện chuẩn duy nhất.

Khả năng chính của NDK

NDK cung cấp cho nhà phát triển quyền truy cập vào các khả năng cấp thấp của Android thông qua các API gốc. Các trường hợp sử dụng chính bao gồm: làm việc với OpenGL ES và Vulkan cho đồ họa 3D, xử lý âm thanh qua Oboe, hoạt động mật mã qua BoringSSL và tối ưu hóa NEON cho ARM. Tất cả các API này đều có sẵn từ C/C++ và yêu cầu NDK để biên dịch.

Ngoài ra, NDK cho phép tái sử dụng các thư viện C/C++ hiện có mà không cần viết lại bằng Kotlin. Điều này đặc biệt phù hợp với các dự án có lịch sử lâu dài bằng C++ — công cụ trò chơi (Unity, Unreal Engine), thư viện thị giác máy tính (OpenCV) hoặc gói mật mã (OpenSSL). Trong những trường hợp này, NDK tiết kiệm nhiều năm phát triển.

Thành phầnMục đích
ClangTrình biên dịch C/C++ hỗ trợ C++20
libc++Thư viện chuẩn C++ (duy nhất trong NDK r27)
CMakeHệ thống xây dựng thư viện gốc
LLDBTrình gỡ lỗi mã gốc trong Android Studio
ndk-buildHệ thống xây dựng cũ (đã thay thế bằng CMake)

NDK so với SDK: khi nào cần mã gốc

Việc lựa chọn giữa NDK và SDK thuần túy phụ thuộc vào tác vụ. Android SDK cung cấp các API Java/Kotlin sẵn sàng cho 90% kịch bản — mạng, tệp, thông báo, máy ảnh. NDK được sử dụng khi các API này không cung cấp hiệu suất hoặc chức năng cần thiết. Ví dụ, xử lý âm thanh thời gian thực qua SDK có thể nhưng với độ trễ 50–200 ms, trong khi thư viện gốc qua Oboe giảm độ trễ xuống 5–15 ms.

Kích thước APK là một yếu tố khác. Thêm NDK làm tăng kích thước ứng dụng vì mỗi kiến trúc ABI yêu cầu một thư viện .so riêng. Tuy nhiên, sử dụng Android App Bundle (AAB) giảm nhẹ vấn đề: Google Play chỉ gửi cho người dùng thư viện phù hợp với kiến trúc của họ. Một dự án gốc điển hình thêm 1–10 MB vào kích thước cài đặt trên mỗi thiết bị.

So sánh SDK và NDK theo tiêu chí

Tiêu chíSDK (Kotlin/Java)NDK (C/C++)
Hiệu suấtTrung bình (biên dịch JIT/AOT)Cao (mã máy)
Độ trễ âm thanh50–200 ms5–15 ms qua Oboe
Đồ họa 3DQua API Kotlin Canvas/OpenGLVulkan/OpenGL trực tiếp
Độ phức tạp phát triểnThấpCao (quản lý bộ nhớ, JNI)
Tính di động mãChỉ AndroidLinux, Windows, macOS, iOS
Kích thước APKTối thiểu+1–10 MB mỗi thư viện

Kiến trúc NDK: công cụ và thư viện

NDK được cài đặt riêng biệt với Android SDK qua SDK Manager. Bên trong thư mục NDK có chuỗi công cụ (trình biên dịch), thư viện nền tảng, tệp tiêu đề và tiện ích. Chuỗi công cụ NDK bao gồm Clang để biên dịch chéo cho tất cả các ABI mục tiêu: arm64-v8a, armeabi-v7a, x86_64 và x86. Trình biên dịch tự động chọn kiến trúc cần thiết dựa trên cờ CMake hoặc ndk-build.

Các API Android cho mã gốc được cung cấp bởi các tệp tiêu đề trong thư mục sysroot/usr/include. Ở đó có các khai báo cho tất cả API có sẵn trong mã gốc: native_activity.h cho vòng đời Activity, input.h cho sự kiện đầu vào, sensor.h cho cảm biến. Bao gồm các tiêu đề này cho phép truy cập khả năng phần cứng mà không cần lớp JNI.

Cấu trúc thư mục NDK

text
ndk/
  toolchains/
    llvm/prebuilt/windows-x86_64/
      bin/        // Clang, ld, llvm-profdata
      sysroot/    // Tệp tiêu đề API Android
  platforms/
    android-26/  // libc, libm, libdl cho API 26
    android-34/
  sources/
    cxx-stl/     // Tiêu đề libc++
  build/
    cmake/       // Tệp chuỗi công cụ CMake

API gốc Android

Thông qua NDK, các nhóm API gốc sau đây có sẵn: Native App Glue (quản lý vòng đời Activity từ C), OpenGL ES 3.2 và Vulkan 1.3 (đồ họa), Oboe (âm thanh độ trễ thấp), Neural Networks API (học máy trên thiết bị). Mỗi API có một tệp tiêu đề và thư viện tĩnh/động như một phần của NDK.

Làm việc với API gốc không yêu cầu JNI — các hàm được gọi trực tiếp từ mã C/C++. Tuy nhiên, tương tác với giao diện người dùng Kotlin vẫn yêu cầu JNI. Kiến trúc lai này phổ biến trong các công cụ trò chơi: đồ họa và vật lý bằng C++ (qua Vulkan), menu và giao diện người dùng bằng Kotlin (qua Jetpack Compose).

JNI: cách Kotlin gọi C++

JNI (Java Native Interface) là cơ chế tiêu chuẩn để gọi mã gốc từ máy ảo Java/Kotlin. Nhà phát triển khai báo một hàm bên ngoài trong Kotlin với từ khóa external và tải thư viện .so qua System.loadLibrary. Về phía C++, hàm được khai báo bằng quy ước đặt tên JNI, mã hóa tên gói và lớp.

Khai báo hàm gốc trong Kotlin

kotlin
class NativeBridge {
    companion object {
        init {
            System.loadLibrary("native-lib")
        }
    }

    external fun stringFromJNI(): String
    external fun fibonacci(n: Int): Long
    external fun processBuffer(data: ByteArray): ByteArray
}

// Sử dụng trong mã
val bridge = NativeBridge()
println(bridge.stringFromJNI())  // "Hello from C++"

Triển khai hàm JNI trong C++

cpp
#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_app_NativeBridge_stringFromJNI(
    JNIEnv* env, jobject /* this */) {
    std::string hello = "Hello from C++ NDK";
    return env->NewStringUTF(hello.c_str());
}

extern "C" JNIEXPORT jlong JNICALL
Java_com_example_app_NativeBridge_fibonacci(
    JNIEnv* env, jobject /* this */, jint n) {
    if (n <= 1) return n;
    jlong a = 0, b = 1;
    for (int i = 2; i <= n; i++) {
        jlong temp = a + b;
        a = b;
        b = temp;
    }
    return b;
}

Thiết lập CMakeLists.txt cho NDK

CMake là hệ thống xây dựng chính cho NDK, đã thay thế ndk-build cũ. Tệp CMakeLists.txt mô tả các tệp nguồn, thư viện và cờ biên dịch. Android Studio tự động gọi CMake khi xây dựng dự án nếu externalNativeBuild được cấu hình trong build.gradle. CMake tạo các tệp make cho mỗi ABI mục tiêu và biên dịch mã gốc song song.

Ví dụ CMakeLists.txt cho thư viện gốc

cmake
cmake_minimum_required(VERSION 3.22.1)
project("nativelib")

# Bao gồm tệp tiêu đề
include_directories(src/main/cpp/include)

# Tạo thư viện gốc
add_library(
    native-lib
    SHARED
    src/main/cpp/native-lib.cpp
    src/main/cpp/math_utils.cpp
    src/main/cpp/audio_processor.cpp
)

# Liên kết thư viện hệ thống Android
target_link_libraries(
    native-lib
    android
    log
    OpenSLES
    # libc++ được liên kết tự động
)

# Cờ tối ưu hóa
target_compile_options(native-lib PRIVATE -O3 -Wall -Wextra)

Cấu hình build.gradle cho NDK

groovy
android {
    defaultConfig {
        ndk {
            // ABI mục tiêu cho xây dựng
            abiFilters "arm64-v8a", "armeabi-v7a", "x86_64"
        }
    }
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
            version "3.22.1"
        }
    }
    buildTypes {
        release {
            externalNativeBuild {
                cmake {
                    arguments "-DCMAKE_BUILD_TYPE=Release"
                }
            }
        }
    }
}

ABI và xây dựng đa nền tảng

ABI (Application Binary Interface) là định dạng mã máy xác định khả năng tương thích với bộ xử lý của thiết bị. Mỗi kiến trúc ARM hoặc x86 có ABI riêng. NDK biên dịch mã gốc riêng biệt cho mỗi ABI được chỉ định. Các ABI phổ biến nhất vào năm 2026: arm64-v8a (99% thiết bị hiện đại), armeabi-v7a (thiết bị cũ 32-bit), x86_64 (trình giả lập và Chromebook).

Chỉ định abiFilters trong build.gradle giới hạn việc xây dựng chỉ cho các kiến trúc cần thiết, giảm thời gian biên dịch. Đối với Google Play, nên bao gồm tất cả ABI mà thư viện hỗ trợ — điều này đảm bảo nó hoạt động trên tất cả thiết bị. Google Play Console cho phép cấu hình phân phối APK cụ thể theo ABI qua App Bundle.

Xác định ABI thiết bị trong mã gốc

cpp
#include <jni.h>
#include <android/api-level.h>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_app_NativeBridge_getABIInfo(
    JNIEnv* env, jobject /* this */) {

    #if defined(__arm__)
        #if defined(__ARM_ARCH_7A__)
            return env->NewStringUTF("armeabi-v7a");
        #endif
    #elif defined(__aarch64__)
        return env->NewStringUTF("arm64-v8a");
    #elif defined(__x86_64__)
        return env->NewStringUTF("x86_64");
    #elif defined(__i386__)
        return env->NewStringUTF("x86");
    #endif

    return env->NewStringUTF("unknown");
}
ABIKiến trúcSố bitThiết bị
arm64-v8aARMv8-A64-bitGần như tất cả điện thoại hiện đại
armeabi-v7aARMv7-A32-bitThiết bị cũ (trước 2020)
x86_64x86-6464-bitTrình giả lập, Chromebook
x86x86 IA-3232-bitTrình giả lập cũ

Ví dụ mã gốc C++

Hãy xem xét một ví dụ thực tế: thư viện toán học làm việc với số dấu phẩy động. Mã gốc C++ thực hiện tính toán hiệu quả hơn Kotlin nhờ truy cập trực tiếp vào các lệnh NEON của ARM và không có kiểm tra ranh giới mảng trong thời gian chạy. Ví dụ này minh họa mẫu sử dụng NDK điển hình — chuyển các tính toán nặng sang lớp gốc.

Thao tác toán học trong mã gốc

cpp
#include <jni.h>
#include <cmath>
#include <vector>

extern "C" JNIEXPORT jfloatArray JNICALL
Java_com_example_app_NativeBridge_normalizeArray(
    JNIEnv* env, jobject, jfloatArray input) {

    jsize len = env->GetArrayLength(input);
    jfloat* elements = env->GetFloatArrayElements(input, nullptr);

    // Tính giá trị trung bình và độ lệch chuẩn
    float sum = 0.0f, sumSq = 0.0f;
    for (jsize i = 0; i < len; i++) {
        sum += elements[i];
        sumSq += elements[i] * elements[i];
    }
    float mean = sum / len;
    float stddev = std::sqrt(sumSq / len - mean * mean);

    // Chuẩn hóa: (x - mean) / stddev
    jfloat* result = new jfloat[len];
    for (jsize i = 0; i < len; i++) {
        result[i] = (elements[i] - mean) / stddev;
    }

    env->ReleaseFloatArrayElements(input, elements, JNI_ABORT);
    jfloatArray output = env->NewFloatArray(len);
    env->SetFloatArrayRegion(output, 0, len, result);
    delete[] result;
    return output;
}

Ghi nhật ký từ mã gốc

Để gỡ lỗi mã gốc, hãy sử dụng macro __android_log_print từ thư viện android/log.h. Các thông báo xuất hiện trong Logcat cùng với nhật ký Java/Kotlin. Mức ghi nhật ký (ANDROID_LOG_DEBUG, ANDROID_LOG_ERROR) giúp lọc thông báo.

cpp
#include <android/log.h>
#define LOG_TAG "NativeLib"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)

extern "C" JNIEXPORT void JNICALL
Java_com_example_app_NativeBridge_processData(
    JNIEnv* env, jobject, jint count) {

    LOGD("Processing %d items", count);

    for (int i = 0; i < count; i++) {
        // Xử lý nặng
        LOGD("Item %d processed", i);
    }

    LOGD("Processing complete");
}

Câu hỏi thường gặp

Có bắt buộc phải sử dụng NDK cho phát triển Android không?

Không. Đối với hầu hết ứng dụng, SDK Kotlin là đủ. NDK cần thiết cho các tác vụ hiệu suất cao: xử lý âm thanh/video thời gian thực, đồ họa 3D, mật mã hoặc tái sử dụng dự án C/C++ hiện có.

NDK sử dụng trình biên dịch nào?

NDK sử dụng Clang từ chuỗi công cụ LLVM. Từ NDK r23, GCC đã bị loại bỏ hoàn toàn. Clang biên dịch chéo cho tất cả ABI Android: arm64-v8a, armeabi-v7a, x86_64, x86.

ABI là gì trong bối cảnh NDK?

ABI (Application Binary Interface) là định dạng mã máy xác định khả năng tương thích với bộ xử lý. NDK xây dựng thư viện .so riêng cho mỗi ABI. arm64-v8a là ABI chính cho thiết bị Android hiện đại.

Có thể gỡ lỗi mã C++ qua NDK không?

Có. Android Studio hỗ trợ LLDB — trình gỡ lỗi mã gốc. Bạn có thể đặt điểm dừng trong tệp C++, xem biến và ngăn xếp cuộc gọi. Yêu cầu NDK và plugin LLDB.

NDK ảnh hưởng đến kích thước APK như thế nào?

Mỗi thư viện gốc thêm 100 KB đến vài MB vào APK. Mỗi ABI yêu cầu một thư viện .so riêng. Android App Bundle chỉ gửi đến người dùng kiến trúc phù hợp, giảm kích thước cài đặt.

Tổng kết

  • NDK — bộ công cụ biên dịch chéo mã C/C++ thành thư viện gốc cho Android bằng trình biên dịch Clang.
  • JNI — giao diện kết nối Kotlin/Java với mã gốc qua từ khóa external và quy ước đặt tên hàm.
  • CMake — hệ thống xây dựng chính của NDK, cấu hình qua CMakeLists.txt và externalNativeBuild trong Gradle.
  • ABI xác định kiến trúc bộ xử lý — arm64-v8a, armeabi-v7a, x86_64. Mỗi ABI yêu cầu xây dựng thư viện riêng.
  • Chỉ sử dụng NDK cho các tác vụ yêu cầu hiệu suất cao: trò chơi, âm thanh, đồ họa, mật mã, học máy trên thiết bị.
  • Google Play hỗ trợ phân chia ABI qua App Bundle: người dùng chỉ nhận thư viện cho kiến trúc thiết bị của họ.
  • Thư viện gốc làm tăng đáng kể kích thước APK nhưng cung cấp hiệu suất tối đa và độ trễ thấp cho các hoạt động quan trọng.

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm