https://blog.csdn.net/bingjianit/article/details/75567410
前段時間由於做比賽的事,一直都沒時間寫博客,現在終於可以補上一篇了,一直想學習一點NDK開發的知識,但是遲遲沒有動手,正好有一個NDK相關的項目機會,便查閱了一些資料,遂將學習的一些心得方法記錄於此。
其實寫這篇博客還有一個目的,在我搜尋NDK相關學習資料的過程中,大部分都是基於eclipse開發的,所以有些過時,而現在Google推薦使用AndroidStudio+CMake的方式進行NDK開發,所以想更新一下有些知識,便於大家學習參考。
首先說說這次的開發工具及版本
AndroidStudio 2.3.3
NDK 15.1.4
CMake 3.6.4
Genymotion 模擬器
- 1
- 2
- 3
- 4
一、相關概念介紹
1 . 什麼是NDK
NDK是一個讓開發人員在android應用中嵌入使用本地代碼編寫的組件的工具集。 Android應用運行在Dalvik虛擬機中。NDK允許開發人員使用本地代碼語言(例如C和C++)實現應用的部分功能。
上面是比較官方的介紹,通俗點來講,就是幫助我們可以在Android應用中使用C/C++來完成特定功能的一套工具。
2 . NDK的應用場景
不是說什麼場景下我們都要使用NDK來開發Android的功能,由於NDK開發在一定程度上加大了項目的開發難度,我們應該綜合考慮各種因素和條件,在特定場景下選用NDK來開發Android的特定功能,下面就是一些NDK適用的場景。
1 . 重要核心代碼保護。由於java層代碼很容易反編譯,而C/C++代碼反彙編難度很大,所以對於重要的代碼,可以使用C/C++來編寫,Android去調用即可。
2 . Android中需要用到第三方的C/C++庫。由於很多優秀的第三方庫(比如FFmpeg)都是使用C/C++來編寫的,我們想要使用它們,就必須通過NDK的方式來操作。
3 . 便於代碼的移植。比如我們對於一些核心的公共組件(比如微信開源的的Mars),可能需要寫一套代碼在多個平臺上運行(比如在Android和iOS上共用一個庫),那麼就需要選用NDK的方式。
4 . 對於音視頻處理、圖像處理這種計算量比較大追求性能的場景,也需要使用到NDK。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
3 . 什麼是交叉編譯
交叉編譯通俗一點講,就是在一個平臺上生產在另一個平臺上可執行的代碼。比如我們在電腦上爲一些硬件開發驅動,最終編譯出的代碼需要在硬件上使用。還有我們在電腦上將C/C++代碼編譯成相應的庫,然後在ARM、x86、mips等平臺上使用。NDK中就我們提供了交叉編譯的工具,幫助我們可以將我們編寫的C/C++代碼生成各個平臺需要的庫。
4 . 什麼是jni
JNI的全稱是Java Native Interface,它允許Java語言可以按照一定的規則去調用其他語言,與其進行交互。
jni的實現流程如下:
編寫Java代碼(.java) —————> 編譯生成字節碼文件(.class) —————> 產生C頭文件(.h) —————> 編寫jni實現代碼(.c) —————> * 編譯成鏈接庫(.so)**
5 . 什麼是鏈接庫
鏈接庫可以簡單理解爲函數庫,就是我們的C/C++代碼編譯生成的產物,供我們的java進行調用,同時,它又分爲動態鏈接庫和靜態鏈接庫。
動態鏈接庫 : 在程序運行時才載入所需要的庫,所以控制比較靈活,整個可執行文件的體積較小。
靜態鏈接庫 : 在程序的鏈接階段,將其引用的代碼也一併打包在了最終的可執行文件中,這樣做的好處是可以不再依賴與環境,移植方便,但是這樣做會使可執行文件體積較大。在Android中的靜態鏈接庫是.a文件。
6 . 什麼是CMake
CMake是一款開源的跨平臺自動化構建系統,它通過CMakeLists.txt來聲明構建的行爲,控制整個編譯流程,我們在接下來的NDK開發中將會使用它配合Gradle來進行相關開發。
二、配置NDK開發環境
俗話說 工慾善其事必先利其器,接下來,我們先配置一下我們在開發NDK過程中要使用到的一些工具。
1 . 安裝NDK
打開AndroidStudio,在如圖所示的地方找到 SDK Tools, 勾選 NDK、LLDB、CMake,然後點擊 Apply ,等待其下載安裝完成,便配置好了基本的開發環境。
安裝的工具中NDK和CMake上面已經介紹過了,LLDB是一款在開發NDK過程中的調試器,這篇博客中將不會介紹。
- 1
做完了上面的步驟我們就可以開始我們的第一個NDK程序了。
三、創建第一個NDK程序
下面我將以圖示加序號的方式來說明新建步驟。
1 . 新建一個項目,填寫基本信息,記得勾選Include C++ support,便於AndroidStudio爲我們生成一些默認的配置。
2 . 接下來的幾個步驟就選擇默認設置
3 . 到最後一步如圖,C++ Standard 選擇 Toolchain Default,其它不變即可。
說明:
(a) C++ Standard是讓我們選擇C++標準,我們使用默認的CMake的設置
(b) Exceptions Support是添加C++中對於異常的處理,如果選中,Android Studio會
將 -fexceptions標誌添加到模塊級build.gradle文件的cppFlags中,Gradle會將其傳遞到CMake。
(c) Runtime Type Information Support是啓用支持RTTI,請選中此複選框。如果選中,Android Studio會將-frtti標誌添加到模塊級build.gradle文件的cppFlags中,Gradle會將其傳遞到 CMake。
新建好的項目如圖
下面我們看看這個默認的項目中AndroidStudio都爲我們做了哪些事 :
(1) 在app 模塊中新建了一個cpp文件夾用來放置我們的C/C++文件,此處默認的文件爲native-lib.cpp
native-lib.cpp文件內容:
#include <jni.h>
#include <string>
extern "C"
JNIEXPORT jstring JNICALL
Java_com_codekong_ndkdemo_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
上面的代碼中先是引入了固定的頭文件jni.h
,然後是引入了代碼中需要用到的頭文件,至於後面的返回字符串,我們在後面的時候將會講到,現在只需要知道它就是返回了Hello from C++
這個字符串即可。
上面的extern "C" 是告訴編譯器按照C語言的規則來編譯我們下面的代碼
(2) 在app 模塊下新建了一個CMakeLists.txt文件用於定義一些構建行爲
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.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/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} )
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
上面的完成的有註釋的內容,但其中最核心的也就幾句,下面分別做介紹:
cmake_minimum_required(VERSION 3.4.1)
用來設置在編譯本地庫時我們需要的最小的cmake版本,AndroidStudio自動生成,我們幾乎不需要自己管。
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).
src/main/cpp/native-lib.cpp )
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
add_library
用來設置編譯生成的本地庫的名字爲native-lib
,SHARED
表示編譯生成的是動態鏈接庫
(這個概念前面已經提到過了),src/main/cpp/native-lib.cpp
表示參與編譯的文件的路徑,這裏面可以寫多個文件的路徑。
find_library
是用來添加一些我們在編譯我們的本地庫的時候需要依賴的一些庫,由於cmake已經知道系統庫的路徑,所以我們這裏只是指定使用log
庫,然後給log
庫起別名爲log-lib
便於我們後面引用,此處的log
庫是我們後面調試時需要用來打log日誌的庫,是NDK爲我們提供的。
target_link_libraries
是爲了關聯我們自己的庫和一些第三方庫或者系統庫,這裏把我們把自己的庫native-lib
庫和log
庫關聯起來。
(3)在 app 模塊對應的build.gradle
文件中增加了一些配置,如下:
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "25.0.3"
defaultConfig {
applicationId "com.codekong.ndkdemo"
minSdkVersion 15
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.3.1'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
主要的變化就兩點:
(a) 在 android
的大括號內增加了 externalNativeBuild
標籤
externalNativeBuild {
cmake {
cppFlags ""
}
}
- 1
- 2
- 3
- 4
- 5
這裏的cppFlags
裏面的內容爲空,這裏其實就是配置了我們在新建項目的時候的第(3)步中講到的,如果我們勾選了異常支持和RTTI支持,這裏就會有相關的配置信息。
(b) 使用 externalNativeBuild
來指定 CMakeLists.txt
文件的路徑,由於build.gradle文件和CMakeLists.txt
文件在同一目錄下,所以此處就直接寫文件名啦。
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
- 1
- 2
- 3
- 4
- 5
(4) 最終在MainActivity.java
文件中我們看到了函數的調用過程如下:
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
static {
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.
*/
public native String stringFromJNI();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
我們看到其實這裏就主要做了三步操作:
(a)使用 native
關鍵字聲明瞭一個本地方法 stringFromJNI()
(b)使用loadLibrary()
方法載入我們編譯生成的動態鏈接庫,這裏要注意,雖然我們生成的動態鏈接庫名稱爲libnative-lib.so
,但是此處我們只需要寫 native-lib
,即就是我們在CMakeLists.txt
文件中指定的名稱,其中的lib前綴和.so後綴是系統爲我們添加的。
(c)我們在佈局文件中放了一個TextView
,然後將函數返回的字符串放到了TextView
中。
我們對比一下我們聲明的native方法和最終我們的ndk幫我們生成的c++代碼的函數名:
//我們聲明的native方法名
public native String stringFromJNI();
//ndk幫我們生成的c++方法名
JNIEXPORT jstring JNICALL
Java_com_codekong_ndkdemo_MainActivity_stringFromJNI(JNIEnv *env, jobject /* this */)
- 1
- 2
- 3
- 4
- 5
- 6
我們看到ndk生成的方法名是以 Java_包名類名方法名 的形式,其實這個方法名是javah
幫助我們生成的。
注:我們對於新創建的項目可以點擊菜單欄的Build
——> Make Project
來先編譯項目,然後在 <項目目錄>\app\build\intermediates\cmake\debug\obj\armeabi 下面就可以看到生成的動態鏈接庫。由於我們沒有指定我們需要生成什麼平臺的so庫,所以系統幫我們生成了各個平臺的庫,分別放在對應的文件夾下面。
好了,以上就是我們使用AndroidStudio創建的第一個項目的分析,瞭解了上面這些,我們就基本瞭解了NDK開發的的一般步驟。
四、NDK開發中常用的函數
上面我們只是看了AndroidStudio爲我們生成的代碼,還沒有自己動手寫一行代碼,下面我們就開始動手寫代碼啦。下面我們就自己新建一個項目,主要學習一下NDK裏面的字符串操作和數組的操作。
1 . 新建項目,這個過程,我們在上一步的 三、創建第一個NDK程序 中已經講到了,這裏不再贅述。
2 . 刪除項目爲我們自動生成的native-lib.cpp
文件,然後在cpp
目錄下新建一個hello-lib.c
的文件,這時候AndroidStudio就會提醒我們這個文件沒有在CMakeLists.txt
文件中進行配置,所以我們去改動一下該文件,改動如下:
cmake_minimum_required(VERSION 3.4.1)
add_library(hello-lib
SHARED
src/main/cpp/hello-lib.c )
find_library(log-lib
log )
target_link_libraries(hello-lib
${log-lib} )
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
這裏我們把我們新建的hello-lib.c
的路徑加入到了CMakeLists.txt
文件中,而且也將log庫與我們的庫關聯了起來,其他的具體信息前面已經講過了。
3 . 我們在MainActivity.java
文件對應的佈局文件中放入一個TextView
,並且在MainActivity.java
中獲取它。
package com.codekong.ndkdemo;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = (TextView) findViewById(R.id.sample_text);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
4 . 接着我們在MainActivity.java
文件中寫一個native函數sayHelloWorld()
,並將其返回的字符串設置給TextView
,然後使用loadLibrary
載入我們的自定義庫。
package com.codekong.ndkdemo;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("hello-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = (TextView) findViewById(R.id.sample_text);
//將返回值設置給TextView
tv.setText(sayHelloWorld());
}
//自定義的native函數
public native String sayHelloWorld();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
5 . 見證AndroidStudio強大的地方到了,我們在我們聲明的sayHelloWorld()
函數上按住Alt+Enter
,就會自動生成C++代碼,但是,這裏存在一個問題,初次生成,AndroidStudio會創建一個jni文件夾,然後在裏面創建hello-lib.c
文件,並且自動生成對應的C代碼,但是,由於我們在CMakeLists.txt
中指定的路徑爲src/main/cpp/hello-lib.c
,所以我們這裏直接將我們的src/main/jni/hello-lib.c
中的代碼拷貝到src/main/cpp/hello-lib.c
中,並將jni目錄刪除即可。hello-lib.c中的內容如下:
#include <jni.h>
JNIEXPORT jstring JNICALL
Java_com_codekong_ndkdemo_MainActivity_sayHelloWorld(JNIEnv *env, jobject instance) {
return (*env)->NewStringUTF(env, "Hello World");
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
上面的代碼中,我們拿到了jni環境指針,然後調用其NewStringUTF()方法,傳入env指針和我們需要的字符串,便可以了。
運行程序,便可以看到界面上顯示Hello World
。
下面我們開始看看java中的類型和native類型的對應關係:
可以看出上面的類型對應關係還是十分清楚的,其實我們在jni.h
文件中就可以看到上述的定義。
下面我們主要說說字符串的使用和數組的使用
(1)字符串的使用
其實上面新建的項目就已經演示了返回字符串的例子,使用(*env)->NewStringUTF(env, "Hello World");
即可返回字符串結果,下面在看看如何處理java傳入的字符串。通過jni將Java傳入的字符串寫入文件。
(a) 在Mainactivity中添加如下代碼
public native void writeFile(String filePath);
- 1
(b) 在hello-lib.c
中生成如下代碼
JNIEXPORT void JNICALL
Java_com_codekong_ndkdemo_MainActivity_writeFile(JNIEnv *env, jobject instance, jstring filePath_) {
const char *filePath = (*env)->GetStringUTFChars(env, filePath_, 0);
(*env)->ReleaseStringUTFChars(env, filePath_, filePath);
}
- 1
- 2
- 3
- 4
- 5
上面是AndroidStudio生成的代碼,可以看出它主要用到了 (*env)->GetStringUTFChars(env, filePath_, 0);
來將java傳入的字符串轉化爲C語言的char指針,最後又使用(*env)->ReleaseStringUTFChars(env, filePath_, filePath);
將我們的指針指向的空間釋放。
(c)我們可以在這個基礎上寫一個寫入文件的小例子,代碼如下:
JNIEXPORT void JNICALL
Java_com_codekong_ndkdemo_MainActivity_writeFile(JNIEnv *env, jobject instance, jstring filePath_) {
const char *filePath = (*env)->GetStringUTFChars(env, filePath_, 0);
FILE *file = fopen(filePath, "a+");
char data[] = "I am a boy";
int count = fwrite(data, strlen(data), 1, file);
if (file != NULL) {
fclose(file);
}
(*env)->ReleaseStringUTFChars(env, filePath_, filePath);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
以上代碼記得加頭文件
#include <jni.h>
#include <stdio.h>
#include <string.h>
- 1
- 2
- 3
(d)還要記得在AndroidMainfest.xml文件中添加文件讀寫權限,然後在MainActivity.java中調用native方法
static {
System.loadLibrary("hello-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String filePath = "/mnt/sdcard/boys.txt";
Toast.makeText(MainActivity.this, filePath, Toast.LENGTH_SHORT).show();
updateFile(filePath);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
注意:由於我這裏使用的是Genymotion模擬器,所以那樣寫文件路徑就表示文件管理器根目錄。
運行上面的程序,就可以在文件管理器根目錄下發現boys.txt
,並在其中發現我們寫入的字符串。
(2) 數組的使用
現在我們看看我們如何在jni中使用數組。
數組的操作主要有以下兩種方式(我們這裏仍然用我們剛纔的hello-lib.c
文件測試):
(a) 直接操作數組指針。
我們現在看看在MainActivity.java
和 hello-lib.c
文件中的代碼
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
int[] testData = new int[]{1, 2, 3, 4, 5};
for (int i = 0; i < testData.length; i++) {
Log.d(TAG, "testData: origin " + testData[i]);
}
//測試
operationArray(testData);
for (int i = 0; i < testData.length; i++) {
Log.d(TAG, "testData: after " + testData[i]);
}
//聲明方法
public native void operationArray(int[] args);
static {
//載入庫
System.loadLibrary("hello-lib");
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
上面的代碼寫完,我們仍然使用Alt+Enter
快捷鍵生成我們c語言的代碼,如下:
JNIEXPORT void JNICALL
Java_com_codekong_ndkdemo_MainActivity_operationArray(JNIEnv *env, jobject instance,
jintArray args_) {
//獲得數組指針
jint *args = (*env)->GetIntArrayElements(env, args_, NULL);
//獲得數組長度
jint len = (*env)->GetArrayLength(env, args_);
int i = 0;
for (; i < len; ++i) {
++args[i];
}
//釋放
(*env)->ReleaseIntArrayElements(env, args_, args, 0);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
最終結果: 數組中的每個元素都被加1
上面其實還是很好理解的,大家可以查看註釋。
(b) 將傳入的數組先拷貝一份,操作完以後再將數據拷貝回原數組
這次還是像上面一樣,只是我們在C++中換了一種操作數組的方式
//聲明我們的本地方法,其餘代碼與上面一致
public native void operationArray2(int[] args);
int[] testData2 = new int[]{1, 2, 3, 4, 5};
for (int i = 0; i < testData2.length; i++) {
Log.d(TAG, "testData2: origin " + testData2[i]);
}
operationArray2(testData2);
for (int i = 0; i < testData2.length; i++) {
Log.d(TAG, "testData2: afetr " + testData2[i]);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
JNIEXPORT void JNICALL
Java_com_codekong_ndkdemo_MainActivity_operationArray2(JNIEnv *env, jobject instance,
jintArray args_) {
//聲明一個native層的數組,用於拷貝原數組
jint nativeArray[5];
//將傳入的jintArray數組拷貝到nativeArray
(*env)->GetIntArrayRegion(env, args_, 0, 5, nativeArray);
int i = 0;
for (; i < 5; ++i) {
//給每個元素加5
nativeArray[i] += 5;
}
//將操作完成的結果拷貝回jintArray
(*env)->SetIntArrayRegion(env, args_, 0, 5, nativeArray);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
最終結果:數組中每個元素都加5
注意: 我們上面的兩種方式返回值都是void,也就是說我們對數組的改變都是最終改變了原來數組的值。
五、NDK自定義配置
下面我們說一下NDK裏面最常見的幾點配置方法,這裏也是記錄方便自己以後查閱
1 . 添加多個參與編譯的C/C++文件
首先,我們發現我們上面的例子都是涉及到一個C++文件,那麼我們實際的項目不可能只有一個C++文件,所以我們首先要改變CMakeLists.txt
文件,如下 :
add_library( HelloNDK
SHARED
src/main/cpp/HelloNDK.c
src/main/cpp/HelloJNI.c)
- 1
- 2
- 3
- 4
簡單吧,簡單明瞭,但是這裏要注意的是,你在寫路徑的時候一定要注意當前的CMakeLists.txt
在項目中的位置,上面的路徑是相對於CMakeLists.txt
寫的。
2 . 我們想編譯出多個so庫
大家會發現,我們上面這樣寫,由於只有一個CMakeLists.txt
文件,所以我們會把所有的C/C++文件編譯成一個so庫,這是很不合適的,這裏我們就試着學學怎麼編譯出多個so庫。
先放上我的項目文件夾結構圖:
然後看看我們每個CMakeLists.txt
文件是怎麼寫的:
one文件夾內的CMakeLists.txt
文件的內容:
ADD_LIBRARY(one-lib SHARED one-lib.c)
target_link_libraries(one-lib log)
- 1
- 2
- 3
two文件夾內的CMakeLists.txt
文件的內容:
ADD_LIBRARY(two-lib SHARED two-lib.c)
target_link_libraries(two-lib log)
- 1
- 2
- 3
app目錄下的CMakeLists.txt
文件的內容
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)
add_library( HelloNDK
SHARED
src/main/cpp/HelloNDK.c
src/main/cpp/HelloJNI.c)
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 )
target_link_libraries(HelloNDK log)
ADD_SUBDIRECTORY(src/main/cpp/one)
ADD_SUBDIRECTORY(src/main/cpp/two)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
通過以上的配置我們可以看出CMakeLists.txt
文件的配置是支持繼承的,所以我們在子配置文件中只是寫了不同的特殊配置項的配置,最後在最上層的文件中配置子配置文件的路徑即可,現在編譯項目,我們會在 <項目目錄>\app\build\intermediates\cmake\debug\obj\armeabi 下面就可以看到生成的動態鏈接庫。而且是三個動態鏈接庫
3 . 更改動態鏈接庫生成的目錄
我們是不是發現上面的so庫的路徑太深了,不好找,沒事,可以配置,我們只需要在頂層的CMakeLists.txt
文件中加入下面這句就可以了
#設置生成的so動態庫最後輸出的路徑
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI})
- 1
- 2
然後我們就可以在app/src/main下看到jniLibs
目錄,在其中看到我們的動態鏈接庫的文件夾和文件(這裏直接配置到了系統默認的路徑,如果配置到其他路徑需要在gradle文件中使用jinLibs.srcDirs = ['newDir']
進行指定)。
六、NDK錯誤調試
在開發的過程中,難免會遇到bug,那怎麼辦,打log啊,下面我們就談談打log和看log的姿勢。
1 . 在C/C++文件中打log
(1) 在C/C++文件中添加頭文件
#include <android/log.h>
- 1
上面是打印日誌的頭文件,必須添加
(2) 添加打印日誌的宏定義和TAG
//log定義
#define LOG "JNILOG" // 這個是自定義的LOG的TAG
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG,__VA_ARGS__) // 定義LOGD類型
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG,__VA_ARGS__) // 定義LOGI類型
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG,__VA_ARGS__) // 定義LOGW類型
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG,__VA_ARGS__) // 定義LOGE類型
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,LOG,__VA_ARGS__) // 定義LOGF類型
- 1
- 2
- 3
- 4
- 5
- 6
- 7
上面的日誌級別和Android中的log是對應的。
(3) 經過上面兩步,我們就可以打印日誌啦
int len = 5;
LOGE("我是log %d", len);
- 1
- 2
現在我們就可以在logcat中看到我們打印的日誌啦。
2 . 查看報錯信息
首先我們先手動寫一個錯誤,我們在上面的C文件中找一個函數,裏面寫入如下代碼:
int * p = NULL;
*p = 100;
- 1
- 2
上面是一個空指針異常,我們運行程序,發現崩潰了,然後查看控制檯,只有下面一行信息:
libc: Fatal signal 11 (SIGSEGV), code 1, fault addr 0x0 in tid 17481
- 1
完全看不懂上面的信息好吧,這個也太不明顯了,下面我們就學習一下如何將上面的信息變得清楚明瞭
我們需要用到是ndk-stack
工具,它在我們的ndk根目錄下,它可以幫助我們把上面的信息轉化爲更爲易懂更詳細的報錯信息,下面看看怎麼做:
(1) 打開AndroidStudio中的命令行,輸入adb logcat > log.txt
上面這句我們是使用adb命令捕獲log日誌並寫入log.txt文件,然後我們就可以在項目根目錄下看到log.txt文件
(2) 將log.txt打開看到報錯信息,如下:
F/libc (17481): Fatal signal 11 (SIGSEGV), code 1, fault addr 0x0 in tid 17481 (dekong.ndkdemo1)
I/DEBUG ( 67): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
I/DEBUG ( 67): Build fingerprint: 'generic/vbox86p/vbox86p:5.0/LRX21M/genymotion08251046:userdebug/test-keys'
I/DEBUG ( 67): Revision: '0'
I/DEBUG ( 67): ABI: 'x86'
I/DEBUG ( 67): pid: 17481, tid: 17481, name: dekong.ndkdemo1 >>> com.codekong.ndkdemo1 <<<
I/DEBUG ( 67): signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
I/DEBUG ( 67): eax 00000000 ebx f3494fcc ecx ffa881a0 edx 00000000
I/DEBUG ( 67): esi f434e2b0 edi 00000000
I/DEBUG ( 67): xcs 00000023 xds 0000002b xes 0000002b xfs 00000007 xss 0000002b
I/DEBUG ( 67): eip f3492a06 ebp ffa88318 esp ffa88280 flags 00210246
I/DEBUG ( 67):
I/DEBUG ( 67): backtrace:
I/DEBUG ( 67): #00 pc 00000a06 /data/app/com.codekong.ndkdemo1-2/lib/x86/libHelloNDK.so (Java_com_codekong_ndkdemo1_MainActivity_updateFile+150)
I/DEBUG ( 67): #01 pc 0026e27b /data/dalvik-cache/x86/data@[email protected]2@[email protected]
I/DEBUG ( 67): #02 pc 9770ee7d <unknown>
I/DEBUG ( 67): #03 pc a4016838 <unknown>
I/DEBUG ( 67):
I/DEBUG ( 67): Tombstone written to: /data/tombstones/tombstone_05
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
現在的報錯信息還是看不懂,所以我們需要使用ndk-stack
轉化一下:
(3) 繼續在AndroidStudio中的命令行中輸入如下命令(在這之前,我們必須要將ndk-stack的路徑添加到環境變量,以便於我們在命令行中直接使用它)
ndk-stack -sym app/build/intermediates/cmake/debug/obj/x86 -dump ./log.txt
- 1
上面的-sym
後面的參數爲你的對應平臺(我是Genymotion模擬器,x86平臺)的路徑,如果你按照上面的步驟改了路徑,那就需要寫改過的路徑,-dump
後面的參數就是我們上一步得出的log.txt文件,執行結果如下:
********** Crash dump: **********
Build fingerprint: 'generic/vbox86p/vbox86p:5.0/LRX21M/genymotion08251046:userdebug/test-keys'
pid: 17481, tid: 17481, name: dekong.ndkdemo1 >>> com.codekong.ndkdemo1 <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
Stack frame I/DEBUG ( 67): #00 pc 00000a06 /data/app/com.codekong.ndkdemo1-2/lib/x86/libHelloNDK.so (Java_com_codekon
g_ndkdemo1_MainActivity_updateFile+150): Routine Java_com_codekong_ndkdemo1_MainActivity_updateFile at F:\AndroidFirstCode\NDK
Demo1\app\src\main\cpp/HelloJNI.c:32
Stack frame I/DEBUG ( 67): #01 pc 0026e27b /data/dalvik-cache/x86/data@app@com.codekong.ndkdemo1-2@base.apk@classes.d
ex
Stack frame I/DEBUG ( 67): #02 pc 9770ee7d <unknown>: Unable to open symbol file app/build/intermediates/cmake/debug/
obj/x86/<unknown>. Error (22): Invalid argument
Stack frame I/DEBUG ( 67): #03 pc a4016838 <unknown>: Unable to open symbol file app/build/intermediates/cmake/debug/
obj/x86/<unknown>. Error (22): Invalid argument
Crash dump is completed
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
尤其是上面的一句:
g_ndkdemo1_MainActivity_updateFile+150): Routine Java_com_codekong_ndkdemo1_MainActivity_updateFile at F:\AndroidFirstCode\NDK
Demo1\app\src\main\cpp/HelloJNI.c:32
- 1
- 2
準確指出了發生錯誤的行數,便於我們定位錯誤。
好了,上面就是簡單介紹的調試技巧。
七、後記
終於,寫完了,這一次的內容有點多,但都是一些簡單的入門的知識,我也是剛接觸不久,希望通過總結加深理解,寫出來幫助有需要的人,真心希望可以幫助到他人,大神勿噴,錯誤之處,多多指點。