Android NDK ~ 基礎入門指南

1. NDK相關術語

1.1 什麼是 NDK

NDK 全稱是 Native Development Kit,它是一套用於本地代碼開發工具集,讓開發者能夠在 Android 應用中使用 C/C++ 代碼,並提供衆多平臺庫。主要用於以下幾種常見:

  • 在平臺之間移植其應用。
  • 進一步提升設備性能,以降低延遲,或運行計算密集型應用,如遊戲或物理模擬。
  • 重複使用您自己或其他開發者的 C/C++ 庫。

NDK開發工具集將 C/C++ 代碼編譯成 .so(share object 共享庫)文件,然後打包進 APK 文件

1.2 什麼是 ABIs

不同的 Android 設備使用不同的 CPU,不同的 CPU 支持不同的指令集。CPU 與指令集的每種組合都有專屬的應用二進制接口,即 ABI(Android Binary Interace)。ABI 可以非常精確地定義應用的機器代碼在運行時如何與系統交互。您必須爲應用要使用的每個 CPU 架構指定 ABI。(來自Android官方)

所以 NDK 需要爲不同的 ABI 生成對應的 .so 文件,以便我們的本地代碼能夠正常運行在不同的 Android 設備上,由於 Android 設備絕大部分都是 ARM 架構的,所以一般只需要提供這一個 ABI 的 .so 文件即可。下面是 NDK 支持的 ABI:

NDK支持的ABI有

通過命令查看 APK 支持的 ABI:

aapt dump badging xxx.apk | grep abi

//查看支付寶APK支持的 ABI
$ aapt dump badging alipay.apk | grep abi
native-code: 'armeabi'

也可以將 APK 拖到 AndroidStudio 查看 lib 文件夾:

alipay支持的ABI

在gradle中配置特定的ABI打進APK

上面我們查看到 支付寶APK 只支持 ABI 爲 armeabi 的設備,armeabi 文件夾下的 so 文件有 36.9M。如果爲每個 ABI 都提供相應的 .so 庫,那麼 APK包的體積 就會變得非常龐大,可以在 gradle 中指定兼容的 ABI 哪怕程序中還存在其他 ABI 對應的 .so 文件,也不會將其打包進 APK:

defaultConfig {
    ndk {
        // 指定ndk需要兼容的ABI
        // x86,armeabi-v7a等ABI的so不會打包進APK 
        abiFilters 'armeabi'
    }
}

微信APK 只支持 ABI 爲 armeabi-v7a 的設備,armeabi-v7a 是支持 armeabi,市面上的主流的 Android 設備基本上都是 arm 架構的,所以支持這一個 ABI 即可。

通過命令查看 Android系統 支持的 ABI:

getprop | grep abilist

例如查看 華爲Mate20 支持的 ABI

華爲Mate20

也可以通過 API 來獲取系統支持的 ABI

// 同樣查看 華爲Mate20 支持的ABI

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    String[] supportedAIs = Build.SUPPORTED_ABIS;
}

// 輸出結果
arm64-v8a
armeabi-v7a
armeabi

1.3 JNI 和 NDK 的區別

JNI 是 Java Native Interface 的縮寫,它允許運行在 JVM 虛擬上的 Java 代碼和 C/C++ 或彙編進行交互

JNI 定義了 Java 代碼和 Native 代碼的交互細節,本文後面會介紹 JNI 一些常用的規範

Android 主要是 Java 編寫的,當然 Kotlin 也可以編寫 Android 程序,Kotlin 最終編譯後也是 Java 字節碼,所以他們都是基於 JVM 的語言。

所以 JNI 和 NDK 的區別是:JNI 是定義 Java 代碼和 Native 代碼的交互規範,NDK 是一個工具集,主要用於編譯 Native 代碼,生成 ABI 對應的 .so 文件,並將其打包進 APK 文件中

2. 第一個NDK程序

AndroidStudio3.5 版本使得開發 NDK 更加方面了,我們需要在 AndroidStudio 中先安裝一下組件:

  • NDK

    這套工具使開發中能在 Android 應用中使用 C/C++ 代碼

  • CMake

    一款開源的,跨平臺的,用於編譯或測試的工具,可與 Gradle 搭配使用來編譯原生庫。如果在使用 ndk-build 編譯,則不需要此組件。

  • LLDB

    用於在 AndroidStudio 中調試 Native 代碼

AndroidStudio NDK

安裝完畢之後,在 AndroidStudio 中新建一個 Native C++ 程序:

第一步

第二步

第三步

運行結果如下圖所示:

第一個NDK程序

1.4 第一個 NDK 程序分析

下面我們來分析下這個 NDK 程序,先來看下 MainActivity 界面:

public class MainActivity extends AppCompatActivity {

    // 加載 'native-lib' library 
    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView tv = findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
    }
    
    // 通過 native 關鍵字,映射 JNI 方法
    public native String stringFromJNI();
}

下面來看下 native-lib.cpp 中的 JNI 方法:

// jstring 就是 stringFromJNI 方法的返回類型
extern "C" JNIEXPORT jstring JNICALL
Java_com_chiclaim_androidnative_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    
    // NewStringUTF() 返回一個 jstring
    return env->NewStringUTF(hello.c_str());
}

上面的 JNI 方法非常長:Java_com_chiclaim_androidnative_MainActivity_stringFromJNI

它的命名規則爲:Java_類的全路徑_方法名

因爲 MainActivity 中的 native 方法名爲 stringFromJNI 所以它對應的 JNI 方法名爲:

Java_com_chiclaim_androidnative_MainActivity_stringFromJNI

這個也不用去記,例如我們在 MainActivity 中創建一個新的 native 方法,可以通過 alt + Enter 快捷鍵幫我們自動生成對應的 JNI 方法。

這樣就將 Java 方法和 Native 方法關聯起來了,通過類的全路徑來定位對應的方法在哪裏。

我們發現上面的例子中 JNI 中的方法返回的是一個 jstring,而 Java 中對應的方法返回的是 String,也就是說 Java 中的 String,在 JNI 中使用 jstring 對應,關於 Java 和 JNI 數據類型對應關係,待會再介紹。

native-lib.cpp 文件所在的目錄中還有一個名爲 CMakeLists.txt 的文件,它是 CMake 腳本。例如程序中的 native-lib.cpp,程序怎麼知道這就是一個需要編譯的庫呢?所以需要一個腳本來控制 CMake 的編譯。我們來看下這個文件:

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.
        native-lib

        # Sets the library as a shared library.
        SHARED

        # Provides a relative path to your source file(s).
        native-lib.cpp)

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
        log-lib

        # Specifies the name of the NDK library that
        # you want CMake to locate.
        log)

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
        native-lib

        # Links the target library to the log library
        # included in the NDK.
        ${log-lib})

上面的註釋非常詳盡,每個參數都有備註,由於是英文的,所以我在這裏在做一下解釋:

  • cmake_minimum_required

    配置 cmake 兼容的最低版本

  • add_library

    創建一個共享或者靜態庫,可以定義多個庫,cmake 將會替我們編譯這些庫
    Gradle 會自動將這些共享庫打包到 APK 中

  • find_library

    尋找預編譯好的庫,將它的路徑使用一個變量保存
    因爲 Android 系統中已經存在了一些已經編譯好的共享庫了,我們自己開發庫,可能需要使用這些已經存在的庫

  • target_link_libraries

    find_library 是用來定位預編譯好的庫,定位到了,還需要將其鏈接到目標庫,這樣目標庫才能使用這個庫

可能 find_librarytarget_link_libraries,不太好理解,我們仍然以上面的程序爲例

// 這段代碼意思就是找到 `log` 庫,將 `log` 庫的路徑保存在 `log-lib` 變量裏
find_library(
        log-lib
        log)

// 將 log 這個庫鏈接到我們寫的 native-lib
target_link_libraries(
        native-lib
        ${log-lib})

log 庫就是 Android 的日誌庫

經過這兩個步驟我們就可以在 native-lib 這個 Native 代碼中使用 Android 的日誌庫:

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

// 添加日誌頭文件
#include <android/log.h>

#define TAG "AndroidNativeDemo"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__)

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

運行後,控制檯成功輸出了這行日誌。日誌格式和在 Android 中輸出的一樣,因爲 Android 中的 Log,底層也是調用了 native 代碼。

我們可以嘗試將 target_link_libraries 註釋,將 log 庫鏈接到目標庫這個步驟註釋掉,程序則無法編譯了。提示我們:

undefined reference to '__android_log_print'

最後,我們再來看下 build.gradle 文件:

android {

    // 省略其他配置...

    externalNativeBuild {
        cmake {
            // 指定 CMakeLists 路徑
            path "src/main/cpp/CMakeLists.txt"
            // 設置 cmake 版本
            version "3.10.2"
        }
    }
}

externalNativeBuild 主要用來指定 cmake 的版本以及設置 cmake 腳本文件

同理,我們也可以按照上面分析的步驟,在一個已有的 Android 項目中添加 NDK 支持

3. JNI 數據類型與描述符

JNI 定義了 Java 代碼和 Native 代碼的交互規範,既然是兩者之間的交互,那麼 Java 中的一些概念在 JNI 中也有相應的概念與之對應,比如 數據類型和描述符。

我們知道 Java 有許多的數據類型,JNI 相應的數據類型與之對應,Java 有兩種數據類型,一個是基本數據類型,另一個引用類型。

基本數據類型對應表:

Java Type Native Type
boolean jboolean
byte jbyte
char jchar
short jshort
int jint
long jlong
float jfloat
double jdouble

引用類型對應表:

Java Type Native Type
Object jobject
Class jclass
String jstring
Object[] jobjectArray
boolean[] jbooleanArray
byte[] jbyteArray
char[] jcharArray
short[] jshortArray
int[] jintArray
long[] jlongArray
float[] jfloatArray
double[] jdoubleArray
void void

介紹完了數據類型,下面來看下 描述符,這個也是 NDK 開發中經常用到的

這裏的描述符簡單來說就是 Java 代碼編譯成 class字節碼 對應的描述符

例如下面代碼一段 Java 代碼:

public class Person {
    private String name;

    public String getName(){
        return name;
    }
}

代碼很簡單,一個 name 屬性和一個 getName 方法,他們分別對應的 class字節碼 爲:

public String name;
// 該屬性對應的部分字節碼:
  public java.lang.String name;
    descriptor: Ljava/lang/String;
    flags: ACC_PUBLIC


public String getName(){
    return name;
}
// 該方法對應的部分字節碼:
  public java.lang.String getName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC

描述符 就是字節碼裏 descriptor ,所以 name 屬性的描述符爲 Ljava/lang/StringgetName() 方法的描述符爲 ()Ljava/lang/String,所以描述符忘記了,可以使用 javap 命令查看下。

描述符主要分爲下面幾種:

  • 類的描述符

    類描述符就是一個類的完整路徑,將包名中的 . 換成 /

    例子 描述符
    java.lang.Object java/lang/Object
    java.lang.String java/lang/String
  • 基本類型的描述符

    Java Type 描述符
    boolean Z
    byte B
    char C
    short S
    int I
    long J
    float F
    double D
  • 引用類型的描述符

    引用類型的描述符語法:L+類描述符+;,數組類型的描述符語有點不同,
    一維數組由一個[開頭,二維數組由[[開頭,以此類推。後面加上數組元素的描述符

    例子 描述符
    java.lang.Object Ljava/lang/Object;
    java.lang.Object[] [Ljava/lang/Object;
    int[] [I
    long[][] [[J
  • 方法的描述符

    方法的描述符語法:(parameterDescriptor)returnTypeDescriptor
    如果方法沒有返回值,void 使用 V 表示

    例子 描述符
    int add(int a,int b) (II)I
    void operate() ()V
    boolean equals(Object obj) (Ljava/lang/Object;)Z

4. 小結

至此,我們詳細介紹了 NDK 開發中經常遇見的術語,如 ABI、JNI、NDK。還介紹瞭如何使用 AndroidStudio 開發一個 NDK 程序,然後分析了這個 NDK程序的代碼以及相關配置文件,詳細介紹了每個配置的含義是什麼。最後介紹了 JNI 的規範中的數據類型和描述符,下一接我們開始介紹常用的 JNI 函數。

5. Reference


如果你覺得本文幫助到你,給我個關注和讚唄!

與此同時,我編寫了一份: 超詳細的 Android 程序員所需要的技術棧思維導圖

如果有需要可以移步到我的 GitHub -> AndroidAll,裏面包含了最全的目錄和對應知識點鏈接,幫你掃除 Android 知識點盲區。 由於篇幅原因只展示了 Android 思維導圖:超詳細的Android技術棧

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章