前言
文章所講述主要來源于一個視頻教學,算是一個筆記。不同的是,視頻在講述NDK開發時采用了傳統的開發模式和C語言,在這里將講述使用CMake來構建項目,以及使用C++實現本地方法,內容相對簡單。
參考的資料
- Android Studio對NDK開發的支持
- 視頻下載地址
- 谷歌官方文檔(已翻譯成中文)
- JNI/NDK開發指南
- 《Java核心技術 卷II》的第十二章
- 《Android開發藝術探索》的第十四章
概念
JNI
Java平臺有一個用于和本地C/C++代碼進行交互操作的API,稱為Java本地接口(JNI)??梢园阉斫獬蓞f議,JNI指導你如何實現Java與C/C++進行交互。
NDK
NDK是谷歌為了方便在Android應用中使用C/C++而發布的一款工具庫。Android Studio2.2或更高版本對NDK提供了支持,默認的編譯工具是CMake。
Java數據類型和C數據類型
Java編程語言 | C編程語言 | 字節 |
---|---|---|
boolean | jbolean | 1 |
byte | jbyte | 1 |
char | jchar | 2 |
short | jshort | 2 |
int | jint | 4 |
long | jlong | 8 |
float | jfloat | 4 |
double | jdouble | 8 |
boolean[] | jbooleanArray | / |
byte[] | jbyteArray | / |
char[] | jcharArray | / |
int[] | jintArray | / |
short[] | jshortArray | / |
long[] | jlongArray | / |
float[] | jfloatArray | / |
double[] | jdoubleArray | / |
Object[] | jobjectArray | / |
Class | jclass | / |
String | jstring | / |
環境搭建
我的環境:win10,Android Studio 3.2.0,ndk的版本r18b,CMake的版本3.6。
NDK配置
1、方法一,直接在其官網下載(NDK下載)
2、方法二,通過as來下載,打開settings界面,如下圖所示:
3、配置環境變量(方便使用ndk命令),復制ndk目錄下的build目錄路徑:我的電腦—>屬性(右鍵)—>高級系統設置—>環境變量—>Path(系統變量)—>新建,然后粘貼保存,如下如圖所示:
如果是win7、win8的記得用英文狀態下的
;
進行分隔。4、打開cmd命令行窗口,輸入
ndk-build
,然后回車,如果出現下圖所示表示配置成功:CMake配置
打開as的Settings窗口,如下圖所示:
CMake建議選擇3.6的,3.10不知道是什么原因,在定義好本地方法后Alt+Enter的方式幫你生成對應的C/C++代碼。而LLDB是C/C++的調試工具,選擇最新版本就好。
項目創建步驟
1、創建一個新的項目,且勾選上include C++ support
選項,如下圖所示:
2、步驟1后一直保持默認,一路
Next
下去,直到這一步為止,如下圖所示:接著Finish就好,上圖畫圈的內容可以參考這篇博客。
3、如果你是選擇從官網下載的NDK,那么項目會報一個錯誤——
NDK not configured.
,就是不知道NDK在那,如下圖所示:解決方法如圖所示:
然后等待項目構建完成。
4、項目構建完成后其運行的效果如下圖所示:
分析
下圖是項目的部分結構:
先來看
MainActivity
下的代碼,如下:
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
// 固定寫法,加載動態庫,要在調用本地方法之前調用,可以寫在其它類里
static {
// 庫的名稱是native-lib
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = (TextView) findViewById(R.id.sample_text);
tv.setText(stringFromJNI());
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
// 本地方法的聲明方式,該方法的主體實現在cpp目錄下的native-lib.cpp,
// 而且是用C/C++來實現的
// 該方法的目的是從C/C++哪里得到一個字符串
public native String stringFromJNI();
}
在上述代碼中的靜態代碼塊中,我們怎么知道要加載的動態庫的名稱是native-lib
呢,請看app
目錄下的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.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.
# 編譯后動態庫的名稱,可以改,但注意要和System.loadLibrary同步
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
# 編譯生成的動態庫所依賴的源文件,可以依賴多個源文件,也就是cpp目錄下的cpp或c文件
# 比如cpp目下還有一個test.cpp文件,則書寫如下,此處只是舉個例子
src/main/cpp/native-lib.cpp
# src/main/cpp/test.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} )
經過編譯后,會生成一個名稱為libnative-lib.so
的動態庫,在名稱前面的lib
是編譯后加上的,因為我們在CMakeLists.txt
的配置中庫的名稱是native-lib
,另外,在使用System.loadLibrary
加載庫時,庫的名稱寫的時native-lib
,而不是libnative-lib
。其編譯后的位置如下圖所示(官網上介紹是如果未指定目標 ABI,則 CMake 默認使用 armeabi-v7a,我也不知道為啥我的demo第一次編譯運行后是x86的,這個不影響后續的介紹):
最后看
cpp
目錄下native-lib.cpp
,也就是對本地方法stringFromJNI
的實現:
// jni.h這個頭文件必須要導進來
#include <jni.h>
#include <string>
// 你可以使用C++實現本地方法。然而,那樣你必須將實現本地方法的函數聲明為extern"C"
// (這可以阻止C++編譯器混編方法名)
// 方法命名的格式:
// JNIEXPORT 返回類型 JNICALL Java_+方法的全類名(.用_替代)
extern "C" JNIEXPORT jstring JNICALL
Java_com_jwstudio_ndkdemo_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
// 標準模板庫(STL)提供了一個std::string類
std::string hello = "Hello from C++";
// c_str(),一個將string轉換為 const* char的函數
return env->NewStringUTF(hello.c_str());
}
關于字符串(摘自《Java核心技術 卷II》),Java編程語言中的字符串是UTF-16編碼點的序列,而C的字符串則是以null結尾的字節序列,所以在這兩種語言中的的字符串是很不一樣的。Java本地接口有兩組操作字符串的函數,一組把Java字符串轉換成“改良的UTF-8”
字節序列,另一組將它們轉換成UTF-16
數值的數組,也就是說轉換成jchar
數組。
如果你的C代碼已經使用了Unicode,你可以使用第二組轉換函數。另一方面,如果你的字符串都僅限于使用ASCII字符,你可以使用“改良的UTF-8”轉換函數。
NewStringUTF
函數可以用來構造一個新的jstring
,而讀取現有jstring
對象的內容,需要使用GetStringUTFChars
函數。
接著是JNIEnv* env
,可以通過ctrl+鼠標左鍵
的方式,點擊JNIEnv*
,定位到其具體的實現,可以參考這篇文章??梢缘弥?,env
是一個二級指針,它是指向函數指針表的指針,封裝了JNI開發所需的函數,如NewStringUTF
。
在C中,必須在每個JNI調用前面加上(*env)->
,以便實際上解析對函數指針的引用。在C++中,JNIEnv
類的C++版本有一個內聯成員函數,它負責幫你查找函數指針,所以你可以這樣使用:jstr = env->NewStringUTF(greeting)
。
jobject
和this
引用等價。靜態方法得到的是類的引用,而非靜態方法得到的是對隱式的this
參數對象的引用。
那么,接下來的內容就是以jni為主了。
Java調C++
新建類JavaCallC,如下:
public class JavaCallC {
// 讓c代碼做加法運算,把結果返回
public native int add(int x, int y);
// 從Java傳入字符,c代碼進行拼接
public native String stringConcatenation(String s);
// 讓c代碼給每個元素都加上10
public native int[] increaseArrayElements(int[] intArry);
}
此時,as會提示這些方法有錯,那是因為在native-lib.cpp
并沒有創建與之對應的函數,通過alter+enter
可以快速創建。C++的實現如下:
// jni.h這個頭文件必須要導進來
#include <jni.h>
#include <string>
// 你可以使用C++實現本地方法。然而,那樣你必須將實現本地方法的函數聲明為extern"C"
// (這可以阻止C++編譯器混編方法名)
// 方法命名的格式:
// JNIEXPORT 返回類型 JNICALL Java_+方法的全類名(.用_替代)
extern "C" JNIEXPORT jstring JNICALL
Java_com_jwstudio_ndkdemo_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
// 標準模板庫(STL)提供了一個std::string類
std::string hello = "Hello from C++";
// c_str(),一個將string轉換為 const* char的函數
return env->NewStringUTF(hello.c_str());
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_add(JNIEnv *env, jobject instance, jint x, jint y) {
int result = x + y;
return result;
}
extern "C"
JNIEXPORT jintArray JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_increaseArrayElements(JNIEnv *env, jobject instance,
jintArray intArry_) {
jint *intArry = env->GetIntArrayElements(intArry_, NULL); // 訪問數組元素
jsize len = env->GetArrayLength(intArry_); // 數組長度
// 遍歷數組,給每個元素加10
for (int i = 0; i < len; ++i) {
*(intArry + i) += 10;
}
env->ReleaseIntArrayElements(intArry_, intArry, 0);
return intArry_; // 注意這里返回的是intArry_,而不是intArry,原因在于指針
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_stringConcatenation(JNIEnv *env, jobject instance, jstring s_) {
const char *s = env->GetStringUTFChars(s_, 0); // 得到java傳遞的string,固定寫法
// 使用C語言的方式來拼接字符串
char *fromC = ",I am C++.";
int len = strlen(s) + strlen(fromC);
char returnValue[len];
strcpy(returnValue, s); // 把s復制到returnValue
strcat(returnValue, fromC); // 把fromC所指向的字符串追加到returnValue所指向的字符串的結尾
// 虛擬機必須知道你何時使用完字符串,這樣它就能進行垃圾回收(垃圾回收器是在一個獨立線程中運行的,它能夠中
// 斷本地方法的執行),基于這個原因,你必須調用ReleaseStringUTFChars函數
env->ReleaseStringUTFChars(s_, s);
return env->NewStringUTF(returnValue); // 生成字符串并返回
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_greeting(JNIEnv *env, jclass type) { // 注意第二個參數
std::string returnValue = "Hello,I am static method.";
return env->NewStringUTF(returnValue.c_str());
}
C++調用Java
其大概流程如下:
1、在Java端定義一個native
方法并調用它
2、在C++端實現該native
方法,并實現C++調用Java的功能
編碼簽名
為了訪問實例域和調用用Java編程語言定義的方法,你必須學習將數據類型的名稱和方法名進行“混編”的規則(方法簽名描述了參數和該方法返回值的類型)。下面混編方案:
代表 | 類型 |
---|---|
B | byte |
C | char |
D | double |
F | float |
I | int |
J | long |
Lclassname | 類的類型 |
S | short |
V | void |
Z | boolean |
為了描述數組類型,要使用[
。例如,一個字符串數組如下:
[Ljava/lang/String;
一個float[][]
可以描述為:
[[F
注意:在L
表達式結尾處的分號是類型表達式的終止符,而不是參數之間的分隔符。另外,在這個編碼方案中,必須用/
代替.
分隔包和類名。
要建立一個方法的完整簽名,需要把括號內的參數類型都列出來,然后列出返回值類型。例如,一個接收兩個整形參數并返回一個整數的方法編碼為:
(II)I
查看方法簽名
方法一
先編譯項目,然后在下圖的所示的classes
的目錄右鍵,選擇Show in Explorer
:
注意是
classes
目錄下!打開cmd
命令行窗口,cd
到此目錄下,執行如下命令:
javap -s 全類名
// 如果要查看private的域
javap -s private 全類名
如下圖所示:
方法二
假設,桌面上有一個Test.java
,內容如下:
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Calendar;
public class Test
{
private int age;
public static void main(String[] args) {
Calendar calendar = Calendar.getInstance();
int currentYear = calendar.get(Calendar.YEAR);
int currentMonth = calendar.get(Calendar.MONTH);
int currentDay = calendar.get(Calendar.DAY_OF_MONTH);
System.out.println("currentDay:" + currentDay);
}
}
查看其方法簽名的大致步驟如下:
1、打開cmd
命令行窗口,cd
到Test.java
文件所在額目錄
2、編譯Test.java
——javac Test.java
3、執行命令——javap -s Test
效果如下圖所示:
方法三
通過生成的頭文件來查看。舉個例子,如下圖所示:
至于哪些報錯,只要不影響頭文件的生成就好。然后將生成的頭文件剪切粘貼到
cpp
目錄下,打開就可以查看方法簽名了。
注意:我們在前面的步驟中生成了頭文件且剪切粘貼到cpp目錄下(如果目錄下有其它的cpp或c文件,as自動生成的代碼時也不一定生成在你想要放的文件里),新建的native方法通過alt+enter的方式生成的對應函數不一定生成在你想要的文件里,比如native-lib.cpp(什么原因造成還不知道)。你可以手動將自動生成的代碼剪切粘貼到你想放的地方,可以先生成對應的函數再生成頭文件,或者在cpp目錄下新建一個includes目錄專門存放頭文件。所以,還是手動將它粘貼過來吧。
新建類CCallJava.java
,代碼如下:
public class CCallJava {
/**
* 當執行這個方法的時候,讓C代碼調用
* public int add(int x, int y)
*/
public native void callbackAdd();
/**
* 當執行這個方法的時候,讓C代碼調用
* public void helloFromJava()
*/
public native void callbackHelloFromJava();
/**
* 當執行這個方法的時候,讓C代碼調用void printString(String s)
*/
public native void callbackPrintString();
/**
* 當執行這個方法的時候,讓C代碼靜態方法 static void sayHello(String s)
*/
public native void callbackSayHello();
public int add(int x, int y) {
Log.e("TAG", "add() x=" + x + " y=" + y);
return x + y;
}
public void helloFromJava() {
Log.e("TAG", "helloFromJava()");
}
public void printString(String s) {
Log.e("TAG","C中輸入的:" + s);
}
public static void sayHello(String s){
Log.e("TAG", "我是java代碼中的JNI."
+ "java中的sayHello(String s)靜態方法,我被C調用了:"+ s);
}
}
native-lib.cpp
更改如下:
// jni.h這個頭文件必須要導進來
#include <jni.h>
#include <string>
// 你可以使用C++實現本地方法。然而,那樣你必須將實現本地方法的函數聲明為extern"C"
// (這可以阻止C++編譯器混編方法名)
// 方法命名的格式:
// JNIEXPORT 返回類型 JNICALL Java_+方法的全類名(.用_替代)
extern "C" JNIEXPORT jstring JNICALL
Java_com_jwstudio_ndkdemo_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
// 標準模板庫(STL)提供了一個std::string類
std::string hello = "Hello from C++";
// c_str(),一個將string轉換為 const* char的函數
return env->NewStringUTF(hello.c_str());
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_add(JNIEnv *env, jobject instance, jint x, jint y) {
int result = x + y;
return result;
}
extern "C"
JNIEXPORT jintArray JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_increaseArrayElements(JNIEnv *env, jobject instance,
jintArray intArry_) {
jint *intArry = env->GetIntArrayElements(intArry_, NULL); // 訪問數組元素
jsize len = env->GetArrayLength(intArry_); // 數組長度
// 遍歷數組,給每個元素加10
for (int i = 0; i < len; ++i) {
*(intArry + i) += 10;
}
env->ReleaseIntArrayElements(intArry_, intArry, 0);
return intArry_; // 注意這里返回的是intArry_,而不是intArry,原因在于指針
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_stringConcatenation(JNIEnv *env, jobject instance, jstring s_) {
const char *s = env->GetStringUTFChars(s_, 0); // 得到java傳遞的string,固定寫法
// 使用C語言的方式來拼接字符串
char *fromC = ",I am C++.";
int len = strlen(s) + strlen(fromC);
char returnValue[len];
strcpy(returnValue, s); // 把s復制到returnValue
strcat(returnValue, fromC); // 把fromC所指向的字符串追加到returnValue所指向的字符串的結尾
// 虛擬機必須知道你何時使用完字符串,這樣它就能進行垃圾回收(垃圾回收器是在一個獨立線程中運行的,它能夠中
// 斷本地方法的執行),基于這個原因,你必須調用ReleaseStringUTFChars函數
env->ReleaseStringUTFChars(s_, s);
return env->NewStringUTF(returnValue); // 生成字符串并返回
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_greeting(JNIEnv *env, jclass type) { // 注意第二個參數
std::string returnValue = "Hello,I am static method.";
return env->NewStringUTF(returnValue.c_str());
}
extern "C"
JNIEXPORT void JNICALL
Java_com_jwstudio_ndkdemo_CCallJava_callbackAdd(JNIEnv *env, jobject instance) {
// 得到字節碼,使用/代替.
jclass jclazz = env->FindClass("com/jwstudio/ndkdemo/CCallJava");
// 得到方法,最后一個參數的方法簽名
jmethodID jmethodIDs = env->GetMethodID(jclazz, "add", "(II)I");
// 實例化該類
jobject jobject1 = env->AllocObject(jclazz);
// 調用方法,方法的返回值是int
jint value = env->CallIntMethod(jobject1, jmethodIDs, 99, 1);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_jwstudio_ndkdemo_CCallJava_callbackHelloFromJava(JNIEnv *env, jobject instance) {
// 得到字節碼,使用/代替.
jclass jclazz = env->FindClass("com/jwstudio/ndkdemo/CCallJava");
// 得到方法,最后一個參數的方法簽名
jmethodID jmethodIDs = env->GetMethodID(jclazz, "helloFromJava", "()V");
// 實例化該類
jobject jobject1 = env->AllocObject(jclazz);
// 調用方法,方法的返回值是void
env->CallVoidMethod(jobject1, jmethodIDs);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_jwstudio_ndkdemo_CCallJava_callbackPrintString(JNIEnv *env, jobject instance) {
// 得到字節碼,使用/代替.
jclass jclazz = env->FindClass("com/jwstudio/ndkdemo/CCallJava");
// 得到方法,最后一個參數的方法簽名
jmethodID jmethodIDs = env->GetMethodID(jclazz, "printString", "(Ljava/lang/String;)V");
// 實例化該類
jobject jobject1 = env->AllocObject(jclazz);
// 調用方法
jstring jst = env->NewStringUTF("I am afu!!!(*env)->");
// 方法的返回值是void
env->CallVoidMethod(jobject1, jmethodIDs, jst);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_jwstudio_ndkdemo_CCallJava_callbackSayHello(JNIEnv *env, jobject instance) {
// 得到字節碼,使用/代替.
jclass jclazz = env->FindClass("com/jwstudio/ndkdemo/CCallJava");
// 得到方法,最后一個參數的方法簽名
jmethodID jmethodIDs = env->GetStaticMethodID(jclazz, "sayHello", "(Ljava/lang/String;)V");
// 實例化該類
jstring jst = env->NewStringUTF("I am 123456android");
// 注意這個調用的是static方法
env->CallStaticVoidMethod(jclazz, jmethodIDs, jst);
}
打印日志
在C++
里打印Log
日志。
在app
目錄下的build.gradle
文件添加如下代碼:
ndk {
ldLibs 'log'
}
具體位置:
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.jwstudio.ndkdemo"
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags ""
// 生成.so庫的目標平臺,如果未指定目標 ABI,則 CMake 默認使用 armeabi-v7a,其配置如下
// abiFilters 'armeabi', 'armeabi-v7a'
}
}
ndk {
ldLibs 'log'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
在類JavaCallC.java
中添加一個新的本地方法:
// 通過C++來打日志
public native void myLog();
在native-lib.cpp
中的頂部添加如下代碼:
#include <android/log.h>
// 定義Log的tag
#define LOG_TAG "System.out"
// 不同等級的Log
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
myLog()
方法在native-lib.cpp
的實現如下:
extern "C"
JNIEXPORT void JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_myLog(JNIEnv *env, jobject instance) {
// 打印不同等級的log
LOGD("result=%s", "result1");
LOGI("result=%s", "result2");
LOGE("result=%s", "result2");
}
總結
NDK開發的入門內容大致就這些,更詳細的說明還請查看推薦的參考資料。除了一些必要的配置外,其實大部分內容都是與JNI相關,另外,可能還需要補一補C/C++的知識。