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:
通過命令查看 APK 支持的 ABI:
aapt dump badging xxx.apk | grep abi
//查看支付寶APK支持的 ABI
$ aapt dump badging alipay.apk | grep abi
native-code: 'armeabi'
也可以將 APK 拖到 AndroidStudio 查看 lib 文件夾:
在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
:
也可以通過 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 中新建一個 Native C++ 程序:
運行結果如下圖所示:
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_library 和 target_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/String
,getName()
方法的描述符爲 ()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
- https://developer.android.com/ndk/guides
- https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html
- http://allenfeng.com/2016/11/06/what-you-should-know-about-android-abi-and-so/
如果你覺得本文幫助到你,給我個關注和讚唄!
與此同時,我編寫了一份: 超詳細的 Android 程序員所需要的技術棧思維導圖。
如果有需要可以移步到我的 GitHub -> AndroidAll,裏面包含了最全的目錄和對應知識點鏈接,幫你掃除 Android 知識點盲區。 由於篇幅原因只展示了 Android 思維導圖: