一 相關(guān)配置
Android Studio創(chuàng)建新項目時選擇最后一項Native C++,這時候生成的項目配置就是默認(rèn)的CMake開發(fā)的配置,如下圖所示:
但是如果原有項目不支持NDK開發(fā),要添加native代碼,需要以下配置流程:
-
創(chuàng)建native代碼文件,在
main目錄下創(chuàng)建cpp目錄,右鍵點擊cpp目錄,然后依次選擇 New > C/C++ Source File,文件名取為demo,文件后綴為cpp,點擊OK,這時候就會生成一個demo.cpp的原生代碼文件。目前這個文件為空,后面我們會往里面添加C++代碼。 -
創(chuàng)建 CMake 構(gòu)建腳本。右鍵點擊
cpp目錄,然后依次選擇 New > File,輸入CMakeLists.txt作為文件名,然后點擊 OK。我們需要編輯這個txt腳本:cmake_minimum_required:
cmake_minimum_required(VERSION 3.22.1),表示cmake的最小版本號,按照習(xí)慣,腳本的第一行一般都是配置這個屬性。project("demo")。配置項目的名稱,我們寫成demo即可,這行配置可以沒有。add_library(<name> [STATIC | SHARED | MODULE] [EXCLUDE_FROM_ALL] [<source>...]),三個參數(shù),分別表示庫的 名字,庫的類型,庫的路徑。我們配置如下:add_library(demo SHARED demo.cpp),其中第三個參數(shù)就是我們在第一步中創(chuàng)建的C++文件。這樣配置的結(jié)果,cmake就會在/app/build/intermediates/cmake/debug/obj/目錄下生成對應(yīng)的libdemo.so。-
這個命令以找到 NDK 庫并將其路徑存儲為一個變量??梢允褂么俗兞吭跇?gòu)建腳本的其他部分引用 NDK 庫。以下配置會找到 Android 專有的日志支持庫,并將其路徑存儲在
log-lib中:find_library( # Defines the name of the path variable that stores the # location of the NDK library. log-lib # Specifies the name of the NDK library that # CMake needs to locate. log ) -
為了讓原生庫能夠調(diào)用
log庫中的函數(shù),需要使用 CMake 構(gòu)建腳本中的target_link_libraries()命令來關(guān)聯(lián)這些庫:# Links your native library against one or more other native libraries. target_link_libraries( # Specifies the target library. demo # Links the log library to the target library. ${log-lib} )
最終的CMakeLists.txt文件如下:
cmake_minimum_required(VERSION 3.22.1)
# Declares and names the project.
project("demo")
# 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.
demo
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
demo.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.
demo
# Links the target library to the log library
# included in the NDK.
${log-lib})
-
指定ndk路徑。我們可以在項目根目錄的
local.properties文件中指定ndk的路徑,比如這樣配置:ndk.dir=/Users/zhouzhihui/Library/Android/sdk/ndk/25.2.9519653在新的Android Studio中這樣配置會報一個警告:Please delete ndk.dir from local.properties and set android.ndkVersion to [25.2.9519653] in all native modules in the project,那么我們就用新的配置方法,先刪除這一行配置,然后在
app/build.gradle文件中添加ndkVersion的配置,如下代碼:android { ndkVersion "25.2.9519653" compileSdk 33 ...... -
將 Gradle 關(guān)聯(lián)到原生庫。將
externalNativeBuild塊添加到模塊級build.gradle文件中,并使用cmake或ndkBuild塊對其進(jìn)行配置,這個配置和ndkVersion的配置是同一級別的:externalNativeBuild { cmake { path file('src/main/cpp/CMakeLists.txt') version '3.22.1' } }當(dāng)然還可以指定其它參數(shù),具體可以參考官網(wǎng)說明:關(guān)聯(lián)Gradle。
-
指定abi。如果不指定abi,gradle 默認(rèn)只生成
arm64-v8a構(gòu)架的so文件(我的小米手機是這樣,應(yīng)該是生成與設(shè)備相對應(yīng)的so,歡迎指正),我們可以如下指定abi:ndk { abiFilters "arm64-v8a", "armeabi-v7a", "x86", "x86_64" }這樣配置,就能生成四個架構(gòu)下的so文件。
經(jīng)過上面四個步驟,我們ndk的原生代碼開發(fā)配置基本完成了,然后點擊Run app按鈕,運行項目,最終apk中的lib目錄生成了四個so,如圖:

二 編寫C++代碼
由于native方法是在java層調(diào)用,因此對于一個so,我們就專門創(chuàng)建一個對應(yīng)的java類。我們前面步驟新建了demo.cpp,那么我們就創(chuàng)建個對應(yīng)的Demo.java類。我們在主package包目錄下創(chuàng)建一個新目錄demo,在該目錄下創(chuàng)建一個java類:Demo.java,要使用native代碼,首先將so加載到虛擬機中,如下代碼:
static {
System.loadLibrary("demo");
}
然后在Demo.java中創(chuàng)建我們的第一個native方法,而且是一個static方法:
public static native String getStr();
這時候會報錯Cannot resolve corresponding JNI function Java_com_example_nativecpp_demo_Demo_getStr.,因為getStr方法還沒在demo.cpp中實現(xiàn),然后我們將鼠標(biāo)移到飆紅的代碼處,同時按住鍵盤的alt+enter鍵,就會出現(xiàn)修復(fù)的建議,如圖所示:

然后點擊enter鍵,在demo.cpp中就會出現(xiàn)以下代碼:
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_nativecpp_demo_Demo_getStr(JNIEnv *env, jclass clazz) {
// TODO: implement getStr()
}
這個自動生成的c++代碼函數(shù)就是getStr的native實現(xiàn),我們修改下,最終實現(xiàn)如下:
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_nativecpp_demo_Demo_getStr(JNIEnv *env, jclass /* this */) {
std::string hello = "stringFromJNI";
return env->NewStringUTF(hello.c_str());
}
按照上面的步驟,我們繼續(xù)添加兩個非static的native方法:
public native int add(int a, int b);
public native String concat(String a, String b);
對應(yīng)的native實現(xiàn)如下:
extern "C" JNIEXPORT jint JNICALL
Java_com_example_nativecpp_demo_Demo_add(JNIEnv *env, jobject, jint a, jint b) {
string sum = std::to_string(a + b);
std::stringstream ss;
ss << a << "+" << b << "==" << sum;
__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "%s", ss.str().c_str());
return a + b;
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_nativecpp_demo_Demo_concat(JNIEnv *env, jobject thiz, jstring a, jstring b) {
//1、直接使用GetStringUTFChars方法將傳遞過來的jstring轉(zhuǎn)為char*
char *c1 = (char *) (env->GetStringUTFChars(a, JNI_FALSE));
char *c2 = (char *) (env->GetStringUTFChars(b, JNI_FALSE));
//2、再使用本地函數(shù)strcat 拼接兩個char*對象,然后NewStringUTF轉(zhuǎn)為jstring返回去
char *res = strcat(c1, c2);
return env->NewStringUTF(res);
}
不難發(fā)現(xiàn),demo.cpp的方法名字很長,而且遵循一定的約束:
- 定義:通過 JNIEXPORT 和 JNICALL 兩個宏定義聲明,在虛擬機加載 so 時發(fā)現(xiàn)上面兩個宏定義的函數(shù)時就會鏈接到對應(yīng)的 native 方法
- 規(guī)則:Java + 包名 + 類名 + 方法名, 其中使用下劃線將每部分隔開,包名也使用下劃線隔開,如果名稱中本來就包含下劃線,將使用下劃線加數(shù)字替換。
明顯有以下缺點:
1 必須遵循注冊規(guī)則
2 名字過長
3 運行時去找效率不高
下面我們介紹一下更加簡潔而且更加高效的native方法的注冊方式:動態(tài)注冊。
三 動態(tài)注冊
我們在Demo.java中加上第四個native方法:
public native int[] sortArray(int[] arr);
為了動態(tài)注冊該方法,首先,在demo.cpp中我們實現(xiàn)sortArray方法的功能,但不靜態(tài)注冊,如下:
void sort(int &a, int &b) {
a = a + b;
b = a - b;
a = a - b;
}
jintArray sortArray(JNIEnv *env, jobject thiz, jintArray arr) {
jsize len = env->GetArrayLength(arr);
// jint *body = env->GetIntArrayElements(arr, 0);
jint *out = env->GetIntArrayElements(arr, NULL);
for (int i = 0; i < len; i++) {
for (int j = 0; j < len - i - 1; j++) {
if (out[j] > out[j + 1]) {
sort(out[j], out[j + 1]);
// jsize temp = out[j + 1];
// out[j + 1] = out[j];
// out[j] = temp;
}
}
}
jintArray arrSorted = env->NewIntArray(len);
env->SetIntArrayRegion(arrSorted, 0, len, out);
env->ReleaseIntArrayElements(arr, out, 0);
env->DeleteLocalRef(arr);
__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "%s", "new arr");
for (int i = 0; i < len; i++) {
__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "%d", out[i]);
}
return arrSorted;
}
該方法就是接收一個java的整數(shù)數(shù)組,然后使用冒泡從小到大排序,返回一個新的排過序的新的數(shù)組。這個方法不滿足前面討論過的靜態(tài)注冊的兩條約束條件,因此如果在java層調(diào)用該方法,就會報錯:
java.lang.UnsatisfiedLinkError: No implementation found for int[] com.example.nativecpp.demo.Demo.sortArray(int[]) (tried Java_com_example_nativecpp_demo_Demo_sortArray and Java_com_example_nativecpp_demo_Demo_sortArray___3I),如圖:

還記得我們在Demo.java中加載so的代碼static { System.loadLibrary("demo"); }嗎?這個加載會觸發(fā)demo.cpp里的JNI_OnLoad方法,通常我們在 JNI_OnLoad 方法中完成動態(tài)注冊,直接上代碼吧:
jint RegisterNatives(JNIEnv *env) {
jclass clazz = env->FindClass("com/example/nativecpp/demo/Demo");
if (clazz == NULL) {
__android_log_print(ANDROID_LOG_ERROR, LOG_TAG,"con't find class: com/example/nativecpp/demo/Demo");
return JNI_ERR;
}
JNINativeMethod method[] = {
{"sortArray", "([I)[I", (void *) sortArray}
};
return env->RegisterNatives(clazz, method,sizeof(method) / sizeof(method[0]));
}
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "enter jni_onload");
JNIEnv *env = NULL;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
jint result = RegisterNatives(env);
__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "register natives result=%d", result);
return JNI_VERSION_1_6;
}
動態(tài)注冊的邏輯在RegisterNatives方法中,主要分三步:
- 通過反射找到方法所在的java類:
jclass clazz = env->FindClass("com/example/nativecpp/demo/Demo"); - 定義要注冊的方法:
JNINativeMethod method[] = { {"sortArray", "([I)[I", (void *) sortArray} };,可以看到變量method是一個數(shù)組,數(shù)組的每個元素都對應(yīng)一個要動態(tài)注冊的方法,由于我們只有一個方法要動態(tài)注冊,因此method數(shù)組只有一個元素。元素的第一個參數(shù)是java層定義的函數(shù)名,第三個參數(shù)是native層定義的函數(shù)名,兩個函數(shù)名沒有必然關(guān)系,可以不一樣。要注意的是元素的第二個參數(shù)也就是函數(shù)簽名(如果不知道函數(shù)簽名也可以使用快捷鍵alt+enter自動修復(fù)),具體的函數(shù)簽名可以參考:Android深入理解JNI(二)類型轉(zhuǎn)換、方法簽名和JNIEnv - 進(jìn)行注冊:
env->RegisterNatives(clazz, method,sizeof(method) / sizeof(method[0]));
四 最終的源代碼
所有的代碼如下:
Demo.java:
public class Demo {
// Used to load the 'nativecpp' library on application startup.
static {
Log.i("zzh", "load 1");
System.loadLibrary("demo");
Log.i("zzh", "load 2");
}
/**
* A native method that is implemented by the 'nativecpp' native library,
* which is packaged with this application.
*/
public static native String getStr();
public native int add(int a, int b);
public native String concat(String a, String b);
// 動態(tài)注冊
public native int[] sortArray(int[] arr);
}
demo.cpp
#include <jni.h>
#include <string>
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <sstream>
#include <android/log.h>
using namespace std;
#define LOG_TAG "zzh-cmake"
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_nativecpp_demo_Demo_getStr(JNIEnv *env, jclass /* this */) {
std::string hello = "stringFromJNI";
return env->NewStringUTF(hello.c_str());
}
extern "C" JNIEXPORT jint JNICALL
Java_com_example_nativecpp_demo_Demo_add(JNIEnv *env, jobject, jint a, jint b) {
string sum = std::to_string(a + b);
std::stringstream ss;
ss << a << "+" << b << "==" << sum;
__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "%s", ss.str().c_str());
return a + b;
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_nativecpp_demo_Demo_concat(JNIEnv *env, jobject thiz, jstring a, jstring b) {
//1、直接使用GetStringUTFChars方法將傳遞過來的jstring轉(zhuǎn)為char*
char *c1 = (char *) (env->GetStringUTFChars(a, JNI_FALSE));
char *c2 = (char *) (env->GetStringUTFChars(b, JNI_FALSE));
//2、再使用本地函數(shù)strcat 拼接兩個char*對象,然后NewStringUTF轉(zhuǎn)為jstring返回去
char *res = strcat(c1, c2);
return env->NewStringUTF(res);
}
void sort(int &a, int &b) {
a = a + b;
b = a - b;
a = a - b;
}
jintArray sortArray(JNIEnv *env, jobject thiz, jintArray arr) {
jsize len = env->GetArrayLength(arr);
// jint *body = env->GetIntArrayElements(arr, 0);
jint *out = env->GetIntArrayElements(arr, NULL);
for (int i = 0; i < len; i++) {
for (int j = 0; j < len - i - 1; j++) {
if (out[j] > out[j + 1]) {
sort(out[j], out[j + 1]);
// jsize temp = out[j + 1];
// out[j + 1] = out[j];
// out[j] = temp;
}
}
}
jintArray arrSorted = env->NewIntArray(len);
env->SetIntArrayRegion(arrSorted, 0, len, out);
env->ReleaseIntArrayElements(arr, out, 0);
env->DeleteLocalRef(arr);
__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "%s", "new arr");
for (int i = 0; i < len; i++) {
__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "%d", out[i]);
}
return arrSorted;
}
jint RegisterNatives(JNIEnv *env) {
jclass clazz = env->FindClass("com/example/nativecpp/demo/Demo");
if (clazz == NULL) {
__android_log_print(ANDROID_LOG_ERROR, LOG_TAG,"con't find class: com/example/nativecpp/demo/Demo");
return JNI_ERR;
}
JNINativeMethod method[] = {
{"sortArray", "([I)[I", (void *) sortArray}
};
return env->RegisterNatives(clazz, method,sizeof(method) / sizeof(method[0]));
}
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "enter jni_onload");
JNIEnv *env = NULL;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
jint result = RegisterNatives(env);
__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "register natives result=%d", result);
return JNI_VERSION_1_6;
}
MainActivity.java
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity:zzh";
Demo mDemo;
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mDemo = new Demo();
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Example of a call to a native method
TextView tv = binding.sampleText;
tv.setText(Demo.getStr());
int[] arr = new int[]{10, 3, 2, 1, 9, 11, 8, 4};
binding.sampleText.setOnClickListener(v -> {
Log.i(TAG, "demo add 1+2=" + mDemo.add(1, 2));
Log.i(TAG, "demo concat a concat b=" + mDemo.concat("a", "b"));
int[] newArr = mDemo.sortArray(arr);
Log.i(TAG, "demo after sort array=" + Arrays.toString(arr));
Log.i(TAG, "demo after sort array2=" + Arrays.toString(newArr));
});
}
}
運行的日志:
2023-02-27 22:26:27.368 14524-14524 zzh com.example.nativecpp I load 1
2023-02-27 22:26:27.369 14524-14524 zzh-cmake com.example.nativecpp D enter jni_onload
2023-02-27 22:26:27.369 14524-14524 zzh-cmake com.example.nativecpp D register natives result=0
2023-02-27 22:26:27.369 14524-14524 zzh com.example.nativecpp I load 2
2023-02-27 22:26:31.715 14524-14524 zzh-cmake com.example.nativecpp D 1+2==3
2023-02-27 22:26:31.715 14524-14524 MainActivity:zzh com.example.nativecpp I demo add 1+2=3
2023-02-27 22:26:31.715 14524-14524 MainActivity:zzh com.example.nativecpp I demo concat a concat b=ab
2023-02-27 22:26:31.715 14524-14524 zzh-cmake com.example.nativecpp D new arr
2023-02-27 22:26:31.715 14524-14524 zzh-cmake com.example.nativecpp D 1
2023-02-27 22:26:31.715 14524-14524 zzh-cmake com.example.nativecpp D 2
2023-02-27 22:26:31.715 14524-14524 zzh-cmake com.example.nativecpp D 3
2023-02-27 22:26:31.715 14524-14524 zzh-cmake com.example.nativecpp D 4
2023-02-27 22:26:31.715 14524-14524 zzh-cmake com.example.nativecpp D 8
2023-02-27 22:26:31.715 14524-14524 zzh-cmake com.example.nativecpp D 9
2023-02-27 22:26:31.715 14524-14524 zzh-cmake com.example.nativecpp D 10
2023-02-27 22:26:31.715 14524-14524 zzh-cmake com.example.nativecpp D 11
2023-02-27 22:26:31.715 14524-14524 MainActivity:zzh com.example.nativecpp I demo after sort array=[1, 2, 3, 4, 8, 9, 10, 11]
2023-02-27 22:26:31.715 14524-14524 MainActivity:zzh com.example.nativecpp I demo after sort array2=[1, 2, 3, 4, 8, 9, 10, 11]
從日志中可以看出,native方法sortArray把java層傳入的int[]也給修改了。
參考:
ndk使用入門
JNI 靜態(tài)注冊和動態(tài)注冊
Android深入理解JNI(二)類型轉(zhuǎn)換、方法簽名和JNIEnv
