1. 程式人生 > >AndroidStudio中的NDK開發初探

AndroidStudio中的NDK開發初探

前段時間由於做專案緊,一直都沒時間寫部落格,現在終於可以補上一篇了,一直想學習一點NDK開發的知識,但是遲遲沒有動手,正好有一個NDK相關的專案機會,便查閱了一些資料,遂將學習的一些心得方法記錄於此。
其實寫這篇部落格還有一個目的,在我搜尋NDK相關學習資料的過程中,大部分都是基於eclipse開發的,所以有些過時,而現在Google推薦使用AndroidStudio+CMake的方式進行NDK開發,所以想更新一下有些知識,便於大家學習參考。

首先說說這次的開發工具及版本

AndroidStudio 2.3.3
NDK 15.1.4
CMake 3.6.4
Genymotion  模擬器

一、相關概念介紹

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。

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

安裝的工具中NDK和CMake上面已經介紹過了,LLDB是一款在開發NDK過程中的偵錯程式,這篇部落格中將不會介紹。

做完了上面的步驟我們就可以開始我們的第一個NDK程式了。

三、建立第一個NDK程式

下面我將以圖示加序號的方式來說明新建步驟。
1 . 新建一個專案,填寫基本資訊,記得勾選Include C++ support,便於AndroidStudio為我們生成一些預設的配置。

新建專案1

新建專案2
2 . 接下來的幾個步驟就選擇預設設定

3 . 到最後一步如圖,C++ Standard 選擇 Toolchain Default,其它不變即可。

新建專案3

說明:

(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());
}

上面的程式碼中先是引入了固定的標頭檔案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} )

上面的完成的有註釋的內容,但其中最核心的也就幾句,下面分別做介紹:

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 )

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'
}

主要的變化就兩點:
(a) 在 android 的大括號內增加了 externalNativeBuild標籤

externalNativeBuild {
    cmake {
        cppFlags ""
    }
}

這裡的cppFlags裡面的內容為空,這裡其實就是配置了我們在新建專案的時候的第(3)步中講到的,如果我們勾選了異常支援和RTTI支援,這裡就會有相關的配置資訊。

(b) 使用 externalNativeBuild 來指定 CMakeLists.txt檔案的路徑,由於build.gradle檔案和CMakeLists.txt檔案在同一目錄下,所以此處就直接寫檔名啦。

externalNativeBuild {
    cmake {
        path "CMakeLists.txt"
    }
}

(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();
}

我們看到其實這裡就主要做了三步操作:
(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 */)

我們看到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} )

這裡我們把我們新建的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);
    }
}

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();
}

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");
}

上面的程式碼中,我們拿到了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);

(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);
}

上面是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);
}

以上程式碼記得加標頭檔案

#include <jni.h>
#include <stdio.h>
#include <string.h>

(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);

注意:由於我這裡使用的是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");
        }
}

上面的程式碼寫完,我們仍然使用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

上面其實還是很好理解的,大家可以檢視註釋。

(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]);
}
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);

最終結果:陣列中每個元素都加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)

簡單吧,簡單明瞭,但是這裡要注意的是,你在寫路徑的時候一定要注意當前的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)

two資料夾內的CMakeLists.txt檔案的內容:

ADD_LIBRARY(two-lib SHARED two-lib.c)

target_link_libraries(two-lib log)

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)

通過以上的配置我們可以看出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})

然後我們就可以在app/src/main下看到jniLibs目錄,在其中看到我們的動態連結庫的資料夾和檔案(這裡直接配置到了系統預設的路徑,如果配置到其他路徑需要在gradle檔案中使用jinLibs.srcDirs = [‘newDir’]進行指定)。

六、NDK錯誤除錯

在開發的過程中,難免會遇到bug,那怎麼辦,打log啊,下面我們就談談打log和看log的姿勢。

1. 在C/C++檔案中打log

(1) 在C/C++檔案中新增標頭檔案

#include <android/log.h>

上面是列印日誌的標頭檔案,必須新增

(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型別

上面的日誌級別和Android中的log是對應的。

(3) 經過上面兩步,我們就可以列印日誌啦

int len = 5;
LOGE("我是log %d", len);

現在我們就可以在logcat中看到我們列印的日誌啦。

2 . 檢視報錯資訊

首先我們先手動寫一個錯誤,我們在上面的C檔案中找一個函式,裡面寫入如下程式碼:

int * p = NULL;
*p = 100;

上面是一個空指標異常,我們執行程式,發現崩潰了,然後檢視控制檯,只有下面一行資訊:

libc: Fatal signal 11 (SIGSEGV), code 1, fault addr 0x0 in tid 17481

完全看不懂上面的資訊好吧,這個也太不明顯了,下面我們就學習一下如何將上面的資訊變得清楚明瞭

我們需要用到是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/[email protected]@com.codekong.ndkdemo1-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

現在的報錯資訊還是看不懂,所以我們需要使用ndk-stack轉化一下:

(3) 繼續在AndroidStudio中的命令列中輸入如下命令(在這之前,我們必須要將ndk-stack的路徑新增到環境變數,以便於我們在命令列中直接使用它)

ndk-stack -sym app/build/intermediates/cmake/debug/obj/x86 -dump ./log.txt

上面的-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

尤其是上面的一句:

g_ndkdemo1_MainActivity_updateFile+150): Routine Java_com_codekong_ndkdemo1_MainActivity_updateFile at F:\AndroidFirstCode\NDK
Demo1\app\src\main\cpp/HelloJNI.c:32

準確指出了發生錯誤的行數,便於我們定位錯誤。

好了,上面就是簡單介紹的除錯技巧。

七、後記

終於寫完了,這一次的內容有點多,但都是一些簡單的入門的知識,我也是剛接觸不久,希望通過總結加深理解,寫出來幫助有需要的人,真心希望可以幫助到他人,大神勿噴,錯誤之處,多多指點。還有就是最近專案在做跑步功能,類似悅跑圈,功能開發完後會再分享一篇博文,敬請關注!

相關推薦

AndroidStudioNDK開發初探

前段時間由於做專案緊,一直都沒時間寫部落格,現在終於可以補上一篇了,一直想學習一點NDK開發的知識,但是遲遲沒有動手,正好有一個NDK相關的專案機會,便查閱了一些資料,遂將學習的一些心得方法記錄於此。 其實寫這篇部落格還有一個目的,在我搜尋NDK相關學習資料的

AndroidStudio2.2 Preview3NDK開發之CMake和傳統 JNI在目錄結構和配置檔案上的區別

 自從AndroidStudio更新到2.2,就有了CMake和傳統JNI兩種開發NDK的方法,主要就是在目錄結構和build.gradle上的區別,下面我們將分別介紹目錄區別和build.gradle種配置的區別(提示:在第一次用CMake時,最好在新建專案時勾選Include C++

Android studioNDK開發(一)——使用CMake構建NDKDemo

一、前言 NDK可以支援使用C/C++來編寫Android程式,不但可以接入C/C++中優秀的庫資源,而且處理效率高,在音視訊方面有著廣泛的應用。本篇主要以CMake的方式來構建NDK,主要以最常規的方式建立一個NDKImportDemo,並對自動建立官方例子進行分析構建的過程以及CMakeLi

Android studioNDK開發(一):CMakeLists.txt編寫入門

自定義變數 主要有隱式定義和顯式定義兩種。  隱式定義的一個例子是PROJECT指令,它會隱式的定義< projectname >_BINARY_DIR和< projectname >_SOURCE_DIR兩個變數;顯式定義使用SE

eclipseNDK開發配置

下載的android-ndk32-r10b-windows-x86_64   NDK不能放在有空格的目錄如:D:\Program Files,否則會報錯eclipse 生成.h標頭檔案:進入src目錄(

AndroidNDK開發基礎

簡介 Android NDK 是在SDK前面又加上了“原生”二字,即Native Development Kit,因此又被Google稱為“NDK”。 眾所周知,Android程式執行在Dalvik虛擬機器中,NDK允許使用者使用類似C / C++之類的原生

NDK學習( 二),在NDK開發引入第三方庫(AndroidStudio Cmake)

在NDK中可能需要複用之前已經編譯好的so檔案,所以本章的目標是給一個之前編譯好的so檔案外加一個需要呼叫介面的標頭檔案,在現在專案中複用。 在本次實踐過程中,已經有編譯完成的各種cpu架構的libstringutil-lib.so以及標頭檔案,Stringutil.h

AndroidStudio配置NDK開發環境和編譯Fresco

本文記錄在 AndroidStudio 中配置 NDK 開發環境並編譯 Fresco 原始碼。 有兩種方法可以檢視 Fresco 的原始碼,第一種是比較常見的,在AndroidStudio 中通過 Gradle 的 compile 匯入 Fresco,然後就能

CMake語法簡介(androidstudio利用CMake開發NDK

1,設定目標路徑(就是Cmakelist.txt所在的路徑) set(src_files ${CMAKE_SOURCE_DIR}/../../../../distribution) 2,指定標頭檔案 include_directories( ${

android NDK開發,用Cygwin調試本地代碼時報錯“Another debug session running,Use --force to kill it”原因及解決的方法

能夠 att cati kill 時報 andro 使用 deb gdb調試 在使用ndk-gdb調試的時候。運行$NDK/ndk-gdb --verbose報錯“Another debug session running,Use --force to kil

android -------- 解決NDK開發的 Method 'NewStringUTF' could not be resolved

-- bsp 編譯 use string not 解析 wstring 無法 創建NDK項目時, .cpp文件中出現錯誤, Method ‘NewStringUTF‘ could not be resolved 如圖: 網上看了很多解決方式 項目右鍵->

在Android Studio進行NDK開發的一般流程

1 在類中宣告native方法 2 在 app/src/main 下建立 jni 目錄 3 在 app/src/main/java 下執行命令 javah -jni -d ../jni com.path2class.ClassName 4 在 app/src/main/jni

Android Studio 2.2 利用CAMKE進行OpenCV的NDK開發

我在http://www.cnblogs.com/fx-blog/p/8206737.html一文中提到了如何在Android Studio中Java層匯入OpenCV(包含opencv_contrib部分),但是這僅僅是Java層的匯入,隨著學習的深入,我們可以漸漸的發

AndroidStudio NDK之使用OpenCV——第一節】使用AndroidStudio搭建OpenCV的NDK開發環境

一、OpenCV介紹   OpenCV是一個基於開源的跨平臺計算機視覺庫,實現了許多影象處理和計算機視覺方面的通用演算法,是計算機視覺領域最有力的研究工具之一。    OpenCV應用領域:人機互動 物體識別 影象分割 人臉識別 動作識別 運動跟蹤 機器人 運

AndroidStudio的內網開發離線配置

美中不足的就是貼出的java拷貝程式碼有問題,待我慢慢道來。 需求:接入第三方的SDK,使用他們的部分功能 步驟_0:首先在外網開發環境裡安裝了 AndroidStudio,配置了 AndroidSDK。作為一個小白對於AndroidStudio一無所知,花時間看了從

安卓開發——AndroidStudio獲取聯網許可權

開啟:專案名->manifests->AndroidManifest.xml 新增:<uses-permission android:name="android.permission

AndroidStudio gradle:3.2.0配置NDK開發環境

AndroidStudio gradle:3.2.0配置NDK開發環境 最近這段時間在學習Android開發人臉識別,踩了很多坑,也學到挺多東西的,第一篇筆記,記錄一下NDK環境搭建。 首先開啟SDK 然後下載這三個東西,是搭建環境需要的包正常情況下這樣就可以了,我們開啟File

Elipse Android NDK 開發配置 Paths and Symbols的Includes修改

Eclipse開發Android NDK,有時候換了新版ndk,導致路徑變化,但是已有專案中 Paths and Symbols中的Includes配置中並不能生效,而且在配置中只能新增,不能編輯和刪除adt外掛新增的路徑.包括把專案匯出,再匯入,這個路徑配置就丟了.這點

NDK開發_AndroidStuido建立和編譯ndk流程

一、根據native類生成 native類的標頭檔案    1> 如下圖所示,com.lhs.serial.ObdNative 是自己建的native類;    2> cmd 切到 工程的java目錄下, 輸入 javah -d ../

android ndk開發char和unsigned char問題

官方說明 看看cflags中 fsigned-char的說明: -fsigned-char — Allows the type char in the native libraries to be signed, like signed char. Each kind