一.介紹 1.什麼是ndk技術? 在學習ndk技術前,我們需要先瞭解一下JNI(Java Native Interface)技術,JNI技術是一種實現Java代碼和C/C++代碼之間交互的技術,它提供了一組編程介面,使得Java程式可以調用C/C++代碼並與其進行通信。通過JNI技術,開發者可以將C ...
一.介紹
1.什麼是ndk技術?
在學習ndk技術前,我們需要先瞭解一下JNI(Java Native Interface)技術,JNI技術是一種實現Java代碼和C/C++代碼之間交互的技術,它提供了一組編程介面,使得Java程式可以調用C/C++代碼並與其進行通信。通過JNI技術,開發者可以將C/C++代碼嵌入Java項目中,併在Java代碼中調用這些C/C++函數。那麼,NDK技術和它有什麼關係呢?NDK是一種用於開發Android應用程式的工具集,它允許開發者使用C/C++編寫部分或全部的Android應用程式代碼,以便提高性能和訪問底層系統功能。這樣看起來NDK技術和JNI技術是一回事,就是為了實現Java調用C/C++或C/C++調用Java。確實如此,用一句話概括它們之間的關係就是:開發者使用NDK技術在Android應用程式中編寫C/C++代碼,並將其編譯成共用庫(如.so文件),然後使用JNI技術在Java代碼中載入並與這些C/C++代碼進行交互。
2.為什麼要學習ndk?
第一點的話就是提高性能了,這個顯而易見,C/C++的性能肯定比Java高,如果有些功能用Java實現性能不行,就可以把這部分代碼用C/C++實現。第二點的話就是C/C++語言可以直接訪問底層系統功能和硬體資源,如攝像頭和感測器等,這是Java做不到的。最後一點是保密性,Java代碼是編譯成位元組碼,而C/C++代碼是直接編譯成機器碼,反編譯的難度比Java大的多。所以,如果哪部分功能需要保密,也可以用C/C++來實現。
3.編寫C/C++代碼並編譯出.so文件
我們要在Android項目中調用C/C++代碼,首先要將寫好的C/C++代碼編譯成.so共用庫,下麵我會以Android Studio 2021來詳細講解編譯出.so文件的過程。
第一步:打開Android Studio,新建一個Native C++項目,如下圖所示:
項目新建完成後是下麵這個樣子:
我們可以看到main目錄下麵有一個cpp目錄,這裡就是我們編寫C++代碼的地方,我們先來看一下自動生成的CmakeLists.txt文件,代碼如下:
# For more information about using CMake with Android Studio, read the # documentation: https://d.android.com/studio/projects/add-native-code.html # Sets the minimum version of CMake required to build the native library. cmake_minimum_required(VERSION 3.18.1) //cmake的最低版本是3.18.1 # Declares and names the project. project("ndkstudy") # 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. ndkstudy //生成的庫的名稱 # Sets the library as a shared library. SHARED //設置生成的庫為共用庫.so # Provides a relative path to your source file(s). native-lib.cpp //c++源文件的相對路徑
) # 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) //使用find_library來查找log庫,並把找到的log庫存儲在變數log-lib中 # 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. ndkstudy # Links the target library to the log library # included in the NDK. ${log-lib}) //將ndkstudy庫和log庫進行鏈接
然後,我們再來看一下自動生成的native-lib.cpp文件,代碼如下:
#include <jni.h> #include <string> extern "C" JNIEXPORT jstring JNICALL Java_com_example_ndkstudy_MainActivity_stringFromJNI( JNIEnv* env, jobject /* this */) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); }//函數的功能就是返回一個"Hello from C++"字元串
我們發現這個函數名特別長,其實就是對應java目錄下com.example.ndkstudy包下MainActivity類下的stringFromJNI()這個函數。瞭解了這些之後,我們只需要Make Project即可,如下圖所示:
然後就可以看到所生成的.so文件了,如果沒有的話,可以刷新一下項目
接下來,我們建一個新的項目,然後把上面所生成的不同CPU架構的.so文件複製到新項目的main/jniLibs目錄,jniLibs目錄需要自己新建。
然後在app/build.gradle文件下添加以下的代碼:
android { namespace 'com.example.ndkstudy' compileSdk 32 defaultConfig { applicationId "com.example.ndkstudy" minSdk 21 targetSdk 32 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" ndk { // 設置支持的SO庫架構(開發者可以根據需要,選擇一個或多個平臺的so) abiFilters "armeabi", "armeabi-v7a", "arm64-v8a", "x86","x86_64" }//新增代碼 } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'com.google.android.material:material:1.5.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.3' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' }
接下來,就可以調用so庫中的C++函數了,這裡也是特別容易出錯的地方,我先貼出代碼,然後再詳細講解。
public class MainActivity extends AppCompatActivity { private TextView tv_display; { System.loadLibrary("ndkstudy");//第一步,載入動態庫,放到靜態代碼塊里就行 } @SuppressLint("MissingInflatedId") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tv_display=findViewById(R.id.tv_display); tv_display.setText(stringFromJNI());//第三步,調用本地函數,實際上調用的函數是Java_com_example_ndkstudy_MainActivity_stringFromJNI();
} public native String stringFromJNI();//第二步,本地方法聲明,也就是說這個方法由c++實現,這裡作個聲明 }
一個特別需要註意的點是我們在載入動態庫和作本地方法聲明的時候,需要在com.example.ndkstudy包下,MainActivity類下進行操作,也就是要對應那個特別長的函數名。如果以上的步驟都沒有錯的話,就可以在手機屏幕上看到輸出的"Hello from C++"字元串了。這隻是jni的最基本用法,下麵來討論一下java類型與c類型的轉換。
二.Java類型和C類型的轉換
在JNI開發中,Java類型和C/C++類型之間需要轉換,因為二者之間的數據類型存在差異,轉換的橋梁正是JNI類型。下麵給出它們之間的對應關係:
Java類型 | JNI類型 | C/C++類型 | 大小 |
boolean | jboolean | uint8_t | 無符號8位整型 |
byte | jbyte | int8_t | 有符號8位整型 |
char | jchar | uint16_t | 無符號16位整型 |
int | jint | int32_t | 有符號32位整型 |
short | jshort | int16_t | 有符號16位整型 |
long | jlong | int64_t | 有符號64位整型 |
float | jfloat | float | 32位單精度浮點型 |
double | jdouble | double | 64位雙精度浮點型 |
這個只是java的基本數據類型和c/c++類型的對應關係,下麵給出java的引用類型和c/c++的對應關係:
Java的引用類型 | JNI的引用類型 |
java.lang.Object | jobject |
java.lang.String | jstring |
java.lang.Class | jclass |
Object[] | jobjectArray |
boolean[] | jbooleanArray |
byte[] | jbyteArray |
char[] | jcharArray |
short[] | jshortArray |
int[] | jintArray |
long[] | jlongArray |
float[] | jfloatArray |
double[] | jdoubleArray |
java.lang.Throwable | jthrowable |
void | void |
那麼,什麼時候需要將java類型轉成c類型,什麼時候又該將c類型轉成java類型呢?當我們所聲明的本地函數有形參的時候,我們需要將java類型轉成c類型,因為我們在實際調用這個函數的時候,調用的是對應的c函數,所以需要進行轉換。當我們調用的本地函數有返回值的時候,需要將c類型轉成java類型,因為在調用這個函數之後,返回值需要return到java代碼中,所以需要進行轉換。下麵舉個例子來說明一下:
比如,我們用native關鍵字聲明瞭一個本地函數public native void javaToJni(byte a,boolean b,int c,short d,long e,char f,float g,double h);那麼它實際對應的c函數是:
extern "C" JNIEXPORT void JNICALL Java_com_example_ndkstudy_MainActivity_javaToJni(JNIEnv *env, jobject thiz, jbyte a, jboolean b, jint c, jshort d, jlong e, jchar f, jfloat g, jdouble h) { //將jni類型轉換成c類型 int8_t c_a=a; uint8_t c_b=b; int32_t c_c=c; int16_t c_d=d; int64_t c_e=e; uint16_t c_f=f; float c_g=g; double c_h=h;
//後續代碼
}
前面兩個變數是固定的,env是指向jni環境的指針,thiz是這個函數所屬的java對象的引用,後面的參數就是自己實際定義的參數。註意,這些jni變數需要轉換成c類型才能進行後續的操作。
如果本地函數的聲明是這個樣子呢?public native String test(String str);其實這個函數對應的c函數是:
extern "C" JNIEXPORT jstring JNICALL //jstring表示返回值為jstring類型 Java_com_example_ndkstudy_MainActivity_test(JNIEnv *env, jobject thiz, jstring str) { // TODO: implement test() const char *p=env->GetStringUTFChars(str, nullptr);//將jstring類型轉換成c中的const char *類型 return env->NewStringUTF(p);//將const char *類型轉換成jstring類型 }//所以這個函數的功能就是返回傳進來的字元串
如果返回值是其他的類型,也和這個類似。比如,如果要返回一個int16_t類型,則函數的返回值類型設為jshort即可。