AndroidStudio中的NDK開發(fā)初探

前段時(shí)間由于做比賽的事,一直都沒時(shí)間寫博客澎灸,現(xiàn)在終于可以補(bǔ)上一篇了,一直想學(xué)習(xí)一點(diǎn)NDK開發(fā)的知識(shí)遮晚,但是遲遲沒有動(dòng)手性昭,正好有一個(gè)NDK相關(guān)的項(xiàng)目機(jī)會(huì),便查閱了一些資料鹏漆,遂將學(xué)習(xí)的一些心得方法記錄于此。

其實(shí)寫這篇博客還有一個(gè)目的创泄,在我搜尋NDK相關(guān)學(xué)習(xí)資料的過(guò)程中艺玲,大部分都是基于eclipse開發(fā)的,所以有些過(guò)時(shí)鞠抑,而現(xiàn)在Google推薦使用AndroidStudio+CMake的方式進(jìn)行NDK開發(fā)饭聚,所以想更新一下有些知識(shí),便于大家學(xué)習(xí)參考搁拙。

首先說(shuō)說(shuō)這次的開發(fā)工具及版本

AndroidStudio 2.3.3
NDK 15.1.4
CMake 3.6.4
Genymotion  模擬器

一秒梳、相關(guān)概念介紹

1 . 什么是NDK

NDK是一個(gè)讓開發(fā)人員在android應(yīng)用中嵌入使用本地代碼編寫的組件的工具集法绵。 Android應(yīng)用運(yùn)行在Dalvik虛擬機(jī)中。NDK允許開發(fā)人員使用本地代碼語(yǔ)言(例如C和C++)實(shí)現(xiàn)應(yīng)用的部分功能酪碘。

上面是比較官方的介紹朋譬,通俗點(diǎn)來(lái)講,就是幫助我們可以在Android應(yīng)用中使用C/C++來(lái)完成特定功能的一套工具兴垦。

2 . NDK的應(yīng)用場(chǎng)景

不是說(shuō)什么場(chǎng)景下我們都要使用NDK來(lái)開發(fā)Android的功能徙赢,由于NDK開發(fā)在一定程度上加大了項(xiàng)目的開發(fā)難度,我們應(yīng)該綜合考慮各種因素和條件探越,在特定場(chǎng)景下選用NDK來(lái)開發(fā)Android的特定功能狡赐,下面就是一些NDK適用的場(chǎng)景。

1 . 重要核心代碼保護(hù)钦幔。由于java層代碼很容易反編譯枕屉,而C/C++代碼反匯編難度很大,所以對(duì)于重要的代碼鲤氢,可以使用C/C++來(lái)編寫搀擂,Android去調(diào)用即可。

2 . Android中需要用到第三方的C/C++庫(kù)铜异。由于很多優(yōu)秀的第三方庫(kù)(比如FFmpeg)都是使用C/C++來(lái)編寫的哥倔,我們想要使用它們,就必須通過(guò)NDK的方式來(lái)操作揍庄。

3 . 便于代碼的移植咆蒿。比如我們對(duì)于一些核心的公共組件(比如微信開源的的Mars),可能需要寫一套代碼在多個(gè)平臺(tái)上運(yùn)行(比如在Android和iOS上共用一個(gè)庫(kù))蚂子,那么就需要選用NDK的方式沃测。

4 . 對(duì)于音視頻處理、圖像處理這種計(jì)算量比較大追求性能的場(chǎng)景食茎,也需要使用到NDK蒂破。

3 . 什么是交叉編譯

交叉編譯通俗一點(diǎn)講,就是在一個(gè)平臺(tái)上生產(chǎn)在另一個(gè)平臺(tái)上可執(zhí)行的代碼别渔。比如我們?cè)陔娔X上為一些硬件開發(fā)驅(qū)動(dòng)附迷,最終編譯出的代碼需要在硬件上使用。還有我們?cè)陔娔X上將C/C++代碼編譯成相應(yīng)的庫(kù)哎媚,然后在ARM喇伯、x86、mips等平臺(tái)上使用拨与。NDK中就我們提供了交叉編譯的工具稻据,幫助我們可以將我們編寫的C/C++代碼生成各個(gè)平臺(tái)需要的庫(kù)。

4 . 什么是jni

JNI的全稱是Java Native Interface买喧,它允許Java語(yǔ)言可以按照一定的規(guī)則去調(diào)用其他語(yǔ)言捻悯,與其進(jìn)行交互匆赃。

jni的實(shí)現(xiàn)流程如下:

*編寫Java代碼(.java) —————> ** 編譯生成字節(jié)碼文件(.class) —————> ** 產(chǎn)生C頭文件(.h) —————> ** 編寫jni實(shí)現(xiàn)代碼(.c) —————> ** ** 編譯成鏈接庫(kù)(.so)

5 . 什么是鏈接庫(kù)

鏈接庫(kù)可以簡(jiǎn)單理解為函數(shù)庫(kù),就是我們的C/C++代碼編譯生成的產(chǎn)物今缚,供我們的java進(jìn)行調(diào)用算柳,同時(shí),它又分為動(dòng)態(tài)鏈接庫(kù)和靜態(tài)鏈接庫(kù)荚斯。

動(dòng)態(tài)鏈接庫(kù) : 在程序運(yùn)行時(shí)才載入所需要的庫(kù)埠居,所以控制比較靈活,整個(gè)可執(zhí)行文件的體積較小事期。

靜態(tài)鏈接庫(kù) : 在程序的鏈接階段滥壕,將其引用的代碼也一并打包在了最終的可執(zhí)行文件中,這樣做的好處是可以不再依賴與環(huán)境兽泣,移植方便绎橘,但是這樣做會(huì)使可執(zhí)行文件體積較大。在Android中的靜態(tài)鏈接庫(kù)是.a文件唠倦。

6 . 什么是CMake

CMake是一款開源的跨平臺(tái)自動(dòng)化構(gòu)建系統(tǒng)称鳞,它通過(guò)CMakeLists.txt來(lái)聲明構(gòu)建的行為,控制整個(gè)編譯流程稠鼻,我們?cè)诮酉聛?lái)的NDK開發(fā)中將會(huì)使用它配合Gradle來(lái)進(jìn)行相關(guān)開發(fā)冈止。

二、配置NDK開發(fā)環(huán)境

俗話說(shuō) 工欲善其事必先利其器候齿,接下來(lái)熙暴,我們先配置一下我們?cè)陂_發(fā)NDK過(guò)程中要使用到的一些工具。

1 . 安裝NDK

打開AndroidStudio慌盯,在如圖所示的地方找到 SDK Tools, 勾選 NDK周霉、LLDB、CMake,然后點(diǎn)擊 Apply 亚皂,等待其下載安裝完成俱箱,便配置好了基本的開發(fā)環(huán)境。

安裝NDK
安裝的工具中NDK和CMake上面已經(jīng)介紹過(guò)了灭必,LLDB是一款在開發(fā)NDK過(guò)程中的調(diào)試器狞谱,這篇博客中將不會(huì)介紹。

做完了上面的步驟我們就可以開始我們的第一個(gè)NDK程序了禁漓。

三跟衅、創(chuàng)建第一個(gè)NDK程序

下面我將以圖示加序號(hào)的方式來(lái)說(shuō)明新建步驟。

1 . 新建一個(gè)項(xiàng)目璃饱,填寫基本信息与斤,記得勾選Include C++ support,便于AndroidStudio為我們生成一些默認(rèn)的配置肪康。

新建項(xiàng)目1
新建項(xiàng)目2

2 . 接下來(lái)的幾個(gè)步驟就選擇默認(rèn)設(shè)置

3 . 到最后一步如圖荚恶,C++ Standard 選擇 Toolchain Default撩穿,其它不變即可。

新建項(xiàng)目3

說(shuō)明:

(a) C++ Standard是讓我們選擇C++標(biāo)準(zhǔn)谒撼,我們使用默認(rèn)的CMake的設(shè)置

(b) Exceptions Support是添加C++中對(duì)于異常的處理食寡,如果選中,Android Studio會(huì)
將 -fexceptions標(biāo)志添加到模塊級(jí)build.gradle文件的cppFlags中廓潜,Gradle會(huì)將其傳遞到CMake抵皱。

(c) Runtime Type Information Support是啟用支持RTTI,請(qǐng)選中此復(fù)選框辩蛋。如果選中呻畸,Android Studio會(huì)將-frtti標(biāo)志添加到模塊級(jí)build.gradle文件的cppFlags中,Gradle會(huì)將其傳遞到 CMake悼院。

新建好的項(xiàng)目如圖

新建好的項(xiàng)目

下面我們看看這個(gè)默認(rèn)的項(xiàng)目中AndroidStudio都為我們做了哪些事 :

(1) 在app 模塊中新建了一個(gè)cpp文件夾用來(lái)放置我們的C/C++文件伤为,此處默認(rèn)的文件為native-lib.cpp

native-lib.cpp文件內(nèi)容:

#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,然后是引入了代碼中需要用到的頭文件据途,至于后面的返回字符串绞愚,我們?cè)诤竺娴臅r(shí)候?qū)?huì)講到,現(xiàn)在只需要知道它就是返回了Hello from C++這個(gè)字符串即可颖医。

上面的extern "C" 是告訴編譯器按照C語(yǔ)言的規(guī)則來(lái)編譯我們下面的代碼

(2) 在app 模塊下新建了一個(gè)CMakeLists.txt文件用于定義一些構(gòu)建行為

CMakeLists.txt文件內(nèi)容 :

# 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} )

上面的完成的有注釋的內(nèi)容位衩,但其中最核心的也就幾句,下面分別做介紹:

cmake_minimum_required(VERSION 3.4.1) 用來(lái)設(shè)置在編譯本地庫(kù)時(shí)我們需要的最小的cmake版本熔萧,AndroidStudio自動(dòng)生成糖驴,我們幾乎不需要自己管。

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用來(lái)設(shè)置編譯生成的本地庫(kù)的名字為native-lib哪痰,SHARED表示編譯生成的是動(dòng)態(tài)鏈接庫(kù)(這個(gè)概念前面已經(jīng)提到過(guò)了)遂赠,src/main/cpp/native-lib.cpp表示參與編譯的文件的路徑,這里面可以寫多個(gè)文件的路徑晌杰。

find_library 是用來(lái)添加一些我們?cè)诰幾g我們的本地庫(kù)的時(shí)候需要依賴的一些庫(kù)跷睦,由于cmake已經(jīng)知道系統(tǒng)庫(kù)的路徑,所以我們這里只是指定使用log庫(kù)肋演,然后給log庫(kù)起別名為log-lib便于我們后面引用抑诸,此處的log庫(kù)是我們后面調(diào)試時(shí)需要用來(lái)打log日志的庫(kù),是NDK為我們提供的爹殊。

target_link_libraries 是為了關(guān)聯(lián)我們自己的庫(kù)和一些第三方庫(kù)或者系統(tǒng)庫(kù)蜕乡,這里把我們把自己的庫(kù)native-lib庫(kù)和log庫(kù)關(guān)聯(lián)起來(lái)。

(3)在 app 模塊對(duì)應(yīng)的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'
}

主要的變化就兩點(diǎn):
(a) 在 android 的大括號(hào)內(nèi)增加了 externalNativeBuild標(biāo)簽

externalNativeBuild {
    cmake {
        cppFlags ""
    }
}

這里的cppFlags里面的內(nèi)容為空层玲,這里其實(shí)就是配置了我們?cè)谛陆?xiàng)目的時(shí)候的第(3)步中講到的,如果我們勾選了異常支持和RTTI支持,這里就會(huì)有相關(guān)的配置信息辛块。

(b) 使用 externalNativeBuild 來(lái)指定 CMakeLists.txt文件的路徑畔派,由于build.gradle文件和CMakeLists.txt文件在同一目錄下,所以此處就直接寫文件名啦润绵。

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

(4) 最終在MainActivity.java 文件中我們看到了函數(shù)的調(diào)用過(guò)程如下:

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

我們看到其實(shí)這里就主要做了三步操作:
(a)使用 native 關(guān)鍵字聲明了一個(gè)本地方法 stringFromJNI()

(b)使用loadLibrary()方法載入我們編譯生成的動(dòng)態(tài)鏈接庫(kù),這里要注意线椰,雖然我們生成的動(dòng)態(tài)鏈接庫(kù)名稱為libnative-lib.so,但是此處我們只需要寫 native-lib,即就是我們?cè)?code>CMakeLists.txt文件中指定的名稱,其中的lib前綴和.so后綴是系統(tǒng)為我們添加的尘盼。

(c)我們?cè)诓季治募蟹帕艘粋€(gè)TextView,然后將函數(shù)返回的字符串放到了TextView中憨愉。

我們對(duì)比一下我們聲明的native方法和最終我們的ndk幫我們生成的c++代碼的函數(shù)名:

//我們聲明的native方法名
public native String stringFromJNI();

//ndk幫我們生成的c++方法名
JNIEXPORT jstring JNICALL
Java_com_codekong_ndkdemo_MainActivity_stringFromJNI(JNIEnv *env, jobject /* this */)

我們看到ndk生成的方法名是以 Java_包名類名方法名 的形式,其實(shí)這個(gè)方法名是javah幫助我們生成的卿捎。

注:我們對(duì)于新創(chuàng)建的項(xiàng)目可以點(diǎn)擊菜單欄的Build------> Make Project來(lái)先編譯項(xiàng)目配紫,然后在 <項(xiàng)目目錄>\app\build\intermediates\cmake\debug\obj\armeabi 下面就可以看到生成的動(dòng)態(tài)鏈接庫(kù)。由于我們沒有指定我們需要生成什么平臺(tái)的so庫(kù)午阵,所以系統(tǒng)幫我們生成了各個(gè)平臺(tái)的庫(kù)笨蚁,分別放在對(duì)應(yīng)的文件夾下面。

好了趟庄,以上就是我們使用AndroidStudio創(chuàng)建的第一個(gè)項(xiàng)目的分析括细,了解了上面這些,我們就基本了解了NDK開發(fā)的的一般步驟戚啥。

四奋单、NDK開發(fā)中常用的函數(shù)

上面我們只是看了AndroidStudio為我們生成的代碼,還沒有自己動(dòng)手寫一行代碼猫十,下面我們就開始動(dòng)手寫代碼啦览濒。下面我們就自己新建一個(gè)項(xiàng)目,主要學(xué)習(xí)一下NDK里面的字符串操作和數(shù)組的操作拖云。

1 . 新建項(xiàng)目贷笛,這個(gè)過(guò)程,我們?cè)谏弦徊降?三宙项、創(chuàng)建第一個(gè)NDK程序 中已經(jīng)講到了乏苦,這里不再贅述。

2 . 刪除項(xiàng)目為我們自動(dòng)生成的native-lib.cpp文件尤筐,然后在cpp目錄下新建一個(gè)hello-lib.c的文件汇荐,這時(shí)候AndroidStudio就會(huì)提醒我們這個(gè)文件沒有在CMakeLists.txt文件中進(jìn)行配置,所以我們?nèi)ジ膭?dòng)一下該文件盆繁,改動(dòng)如下:

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庫(kù)與我們的庫(kù)關(guān)聯(lián)了起來(lái)掀淘,其他的具體信息前面已經(jīng)講過(guò)了。

3 . 我們?cè)?code>MainActivity.java文件對(duì)應(yīng)的布局文件中放入一個(gè)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 . 接著我們?cè)?code>MainActivity.java文件中寫一個(gè)native函數(shù)sayHelloWorld(),并將其返回的字符串設(shè)置給TextView,然后使用loadLibrary載入我們的自定義庫(kù)革娄。

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);
        //將返回值設(shè)置給TextView
        tv.setText(sayHelloWorld());
    }

    //自定義的native函數(shù)
    public native String sayHelloWorld();
}

5 . 見證AndroidStudio強(qiáng)大的地方到了倾贰,我們?cè)谖覀兟暶鞯?code>sayHelloWorld()函數(shù)上按住Alt+Enter,就會(huì)自動(dòng)生成C++代碼拦惋,但是躁染,這里存在一個(gè)問題,初次生成架忌,AndroidStudio會(huì)創(chuàng)建一個(gè)jni文件夾,然后在里面創(chuàng)建hello-lib.c文件我衬,并且自動(dòng)生成對(duì)應(yīng)的C代碼叹放,但是,由于我們?cè)?code>CMakeLists.txt中指定的路徑為src/main/cpp/hello-lib.c,所以我們這里直接將我們的src/main/jni/hello-lib.c中的代碼拷貝到src/main/cpp/hello-lib.c中挠羔,并將jni目錄刪除即可井仰。hello-lib.c中的內(nèi)容如下:

#include <jni.h>

JNIEXPORT jstring JNICALL
Java_com_codekong_ndkdemo_MainActivity_sayHelloWorld(JNIEnv *env, jobject instance) {

    return (*env)->NewStringUTF(env, "Hello World");
}

上面的代碼中,我們拿到了jni環(huán)境指針破加,然后調(diào)用其NewStringUTF()方法俱恶,傳入env指針和我們需要的字符串,便可以了范舀。

運(yùn)行程序合是,便可以看到界面上顯示Hello World

下面我們開始看看java中的類型和native類型的對(duì)應(yīng)關(guān)系:


基本數(shù)據(jù)類型
引用類型
數(shù)組類型

可以看出上面的類型對(duì)應(yīng)關(guān)系還是十分清楚的锭环,其實(shí)我們?cè)?code>jni.h文件中就可以看到上述的定義聪全。

下面我們主要說(shuō)說(shuō)字符串的使用和數(shù)組的使用

(1)字符串的使用

其實(shí)上面新建的項(xiàng)目就已經(jīng)演示了返回字符串的例子,使用(*env)->NewStringUTF(env, "Hello World");即可返回字符串結(jié)果,下面在看看如何處理java傳入的字符串辅辩。通過(guò)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); 來(lái)將java傳入的字符串轉(zhuǎn)化為C語(yǔ)言的char指針玫锋,最后又使用(*env)->ReleaseStringUTFChars(env, filePath_, filePath);將我們的指針指向的空間釋放蛾茉。

(c)我們可以在這個(gè)基礎(chǔ)上寫一個(gè)寫入文件的小例子,代碼如下:

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文件中添加文件讀寫權(quán)限,然后在MainActivity.java中調(diào)用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模擬器撩鹿,所以那樣寫文件路徑就表示文件管理器根目錄谦炬。

運(yùn)行上面的程序,就可以在文件管理器根目錄下發(fā)現(xiàn)boys.txt节沦,并在其中發(fā)現(xiàn)我們寫入的字符串吧寺。

(2) 數(shù)組的使用

現(xiàn)在我們看看我們?nèi)绾卧趈ni中使用數(shù)組。

數(shù)組的操作主要有以下兩種方式(我們這里仍然用我們剛才的hello-lib.c文件測(cè)試):

(a) 直接操作數(shù)組指針散劫。

我們現(xiàn)在看看在MainActivity.javahello-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]);
        }
        //測(cè)試
        operationArray(testData);

        for (int i = 0; i < testData.length; i++) {
            Log.d(TAG, "testData: after " + testData[i]);
        }
        //聲明方法
        public native void operationArray(int[] args);
        static {
            //載入庫(kù)
            System.loadLibrary("hello-lib");
        }
}

上面的代碼寫完稚机,我們?nèi)匀皇褂?code>Alt+Enter快捷鍵生成我們c語(yǔ)言的代碼,如下:

JNIEXPORT void JNICALL
Java_com_codekong_ndkdemo_MainActivity_operationArray(JNIEnv *env, jobject instance,
                                                      jintArray args_) {
    //獲得數(shù)組指針
    jint *args = (*env)->GetIntArrayElements(env, args_, NULL);
    //獲得數(shù)組長(zhǎng)度
    jint len = (*env)->GetArrayLength(env, args_);
    int i = 0;
    for (; i < len; ++i) {
        ++args[i];
    }
    //釋放
    (*env)->ReleaseIntArrayElements(env, args_, args, 0);
}

最終結(jié)果: 數(shù)組中的每個(gè)元素都被加1

上面其實(shí)還是很好理解的获搏,大家可以查看注釋赖条。

(b) 將傳入的數(shù)組先拷貝一份失乾,操作完以后再將數(shù)據(jù)拷貝回原數(shù)組

這次還是像上面一樣,只是我們?cè)贑++中換了一種操作數(shù)組的方式

//聲明我們的本地方法,其余代碼與上面一致
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_) {
    //聲明一個(gè)native層的數(shù)組纬乍,用于拷貝原數(shù)組
    jint nativeArray[5];
    //將傳入的jintArray數(shù)組拷貝到nativeArray
    (*env)->GetIntArrayRegion(env, args_, 0, 5, nativeArray);
    int i = 0;
    for (; i < 5; ++i) {
        //給每個(gè)元素加5
        nativeArray[i] += 5;
    }

    //將操作完成的結(jié)果拷貝回jintArray
    (*env)->SetIntArrayRegion(env, args_, 0, 5, nativeArray);
}

最終結(jié)果:數(shù)組中每個(gè)元素都加5

注意: 我們上面的兩種方式返回值都是void碱茁,也就是說(shuō)我們對(duì)數(shù)組的改變都是最終改變了原來(lái)數(shù)組的值。

五仿贬、NDK自定義配置

下面我們說(shuō)一下NDK里面最常見的幾點(diǎn)配置方法纽竣,這里也是記錄方便自己以后查閱

1 . 添加多個(gè)參與編譯的C/C++文件

首先,我們發(fā)現(xiàn)我們上面的例子都是涉及到一個(gè)C++文件茧泪,那么我們實(shí)際的項(xiàng)目不可能只有一個(gè)C++文件蜓氨,所以我們首先要改變CMakeLists.txt文件,如下 :

add_library( HelloNDK
             SHARED
             src/main/cpp/HelloNDK.c
             src/main/cpp/HelloJNI.c)

簡(jiǎn)單吧队伟,簡(jiǎn)單明了穴吹,但是這里要注意的是简烘,你在寫路徑的時(shí)候一定要注意當(dāng)前的CMakeLists.txt在項(xiàng)目中的位置缎浇,上面的路徑是相對(duì)于CMakeLists.txt 寫的。

2 . 我們想編譯出多個(gè)so庫(kù)

大家會(huì)發(fā)現(xiàn)秕磷,我們上面這樣寫锈颗,由于只有一個(gè)CMakeLists.txt文件顷霹,所以我們會(huì)把所有的C/C++文件編譯成一個(gè)so庫(kù),這是很不合適的击吱,這里我們就試著學(xué)學(xué)怎么編譯出多個(gè)so庫(kù)泼返。

先放上我的項(xiàng)目文件夾結(jié)構(gòu)圖:

文件夾結(jié)構(gòu)

然后看看我們每個(gè)CMakeLists.txt文件是怎么寫的:

one文件夾內(nèi)的CMakeLists.txt文件的內(nèi)容:

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

target_link_libraries(one-lib log)

two文件夾內(nèi)的CMakeLists.txt文件的內(nèi)容:

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

target_link_libraries(two-lib log)

app目錄下的CMakeLists.txt文件的內(nèi)容

# 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)

通過(guò)以上的配置我們可以看出CMakeLists.txt 文件的配置是支持繼承的,所以我們?cè)谧优渲梦募兄皇菍懥瞬煌奶厥馀渲庙?xiàng)的配置,最后在最上層的文件中配置子配置文件的路徑即可姨拥,現(xiàn)在編譯項(xiàng)目绅喉,我們會(huì)在 <項(xiàng)目目錄>\app\build\intermediates\cmake\debug\obj\armeabi 下面就可以看到生成的動(dòng)態(tài)鏈接庫(kù)。而且是三個(gè)動(dòng)態(tài)鏈接庫(kù)

3 . 更改動(dòng)態(tài)鏈接庫(kù)生成的目錄

我們是不是發(fā)現(xiàn)上面的so庫(kù)的路徑太深了叫乌,不好找柴罐,沒事,可以配置憨奸,我們只需要在頂層的CMakeLists.txt文件中加入下面這句就可以了

#設(shè)置生成的so動(dòng)態(tài)庫(kù)最后輸出的路徑
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI})

然后我們就可以在app/src/main下看到jniLibs目錄革屠,在其中看到我們的動(dòng)態(tài)鏈接庫(kù)的文件夾和文件(這里直接配置到了系統(tǒng)默認(rèn)的路徑,如果配置到其他路徑需要在gradle文件中使用jinLibs.srcDirs = ['newDir']進(jìn)行指定)排宰。

六似芝、NDK錯(cuò)誤調(diào)試

在開發(fā)的過(guò)程中,難免會(huì)遇到bug板甘,那怎么辦党瓮,打log啊,下面我們就談?wù)劥騦og和看log的姿勢(shì)盐类。

1 . 在C/C++文件中打log

(1) 在C/C++文件中添加頭文件

#include <android/log.h>

上面是打印日志的頭文件寞奸,必須添加

(2) 添加打印日志的宏定義和TAG

//log定義
#define  LOG    "JNILOG" // 這個(gè)是自定義的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類型

上面的日志級(jí)別和Android中的log是對(duì)應(yīng)的呛谜。

(3) 經(jīng)過(guò)上面兩步,我們就可以打印日志啦

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

現(xiàn)在我們就可以在logcat中看到我們打印的日志啦枪萄。

2 . 查看報(bào)錯(cuò)信息

首先我們先手動(dòng)寫一個(gè)錯(cuò)誤隐岛,我們?cè)谏厦娴腃文件中找一個(gè)函數(shù),里面寫入如下代碼:

int * p = NULL;
*p = 100;

上面是一個(gè)空指針異常瓷翻,我們運(yùn)行程序聚凹,發(fā)現(xiàn)崩潰了,然后查看控制臺(tái)齐帚,只有下面一行信息:

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

完全看不懂上面的信息好吧妒牙,這個(gè)也太不明顯了,下面我們就學(xué)習(xí)一下如何將上面的信息變得清楚明了

我們需要用到是ndk-stack工具童谒,它在我們的ndk根目錄下,它可以幫助我們把上面的信息轉(zhuǎn)化為更為易懂更詳細(xì)的報(bào)錯(cuò)信息沪羔,下面看看怎么做:

(1) 打開AndroidStudio中的命令行,輸入adb logcat > log.txt

上面這句我們是使用adb命令捕獲log日志并寫入log.txt文件,然后我們就可以在項(xiàng)目根目錄下看到log.txt文件

(2) 將log.txt打開看到報(bào)錯(cuò)信息饥伊,如下:

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@app@com.codekong.ndkdemo1-2@base.apk@classes.dex

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

現(xiàn)在的報(bào)錯(cuò)信息還是看不懂,所以我們需要使用ndk-stack轉(zhuǎn)化一下:

(3) 繼續(xù)在AndroidStudio中的命令行中輸入如下命令(在這之前蔫饰,我們必須要將ndk-stack的路徑添加到環(huán)境變量琅豆,以便于我們?cè)诿钚兄兄苯邮褂盟?

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

上面的-sym后面的參數(shù)為你的對(duì)應(yīng)平臺(tái)(我是Genymotion模擬器,x86平臺(tái))的路徑篓吁,如果你按照上面的步驟改了路徑茫因,那就需要寫改過(guò)的路徑,-dump后面的參數(shù)就是我們上一步得出的log.txt文件杖剪,執(zhí)行結(jié)果如下:

********** 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

準(zhǔn)確指出了發(fā)生錯(cuò)誤的行數(shù)冻押,便于我們定位錯(cuò)誤。

好了盛嘿,上面就是簡(jiǎn)單介紹的調(diào)試技巧洛巢。

七、后記

終于次兆,寫完了稿茉,這一次的內(nèi)容有點(diǎn)多,但都是一些簡(jiǎn)單的入門的知識(shí)芥炭,我也是剛接觸不久漓库,希望通過(guò)總結(jié)加深理解,寫出來(lái)幫助有需要的人园蝠,真心希望可以幫助到他人渺蒿,大神勿噴,錯(cuò)誤之處彪薛,多多指點(diǎn)蘸嘶。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末良瞧,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子训唱,更是在濱河造成了極大的恐慌褥蚯,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件况增,死亡現(xiàn)場(chǎng)離奇詭異赞庶,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)澳骤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門歧强,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人为肮,你說(shuō)我怎么就攤上這事摊册。” “怎么了颊艳?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵茅特,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我棋枕,道長(zhǎng)白修,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任重斑,我火速辦了婚禮兵睛,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘窥浪。我一直安慰自己祖很,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布漾脂。 她就那樣靜靜地躺著突琳,像睡著了一般。 火紅的嫁衣襯著肌膚如雪符相。 梳的紋絲不亂的頭發(fā)上拆融,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音啊终,去河邊找鬼镜豹。 笑死,一個(gè)胖子當(dāng)著我的面吹牛蓝牲,可吹牛的內(nèi)容都是我干的趟脂。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼例衍,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼昔期!你這毒婦竟也來(lái)了已卸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤硼一,失蹤者是張志新(化名)和其女友劉穎累澡,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體般贼,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡愧哟,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了哼蛆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蕊梧。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖腮介,靈堂內(nèi)的尸體忽然破棺而出肥矢,到底是詐尸還是另有隱情,我是刑警寧澤叠洗,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布甘改,位于F島的核電站,受9級(jí)特大地震影響惕味,放射性物質(zhì)發(fā)生泄漏楼誓。R本人自食惡果不足惜玉锌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一名挥、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧主守,春花似錦禀倔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至涎才,卻和暖如春鞋既,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背耍铜。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工邑闺, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人棕兼。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓陡舅,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親伴挚。 傳聞我的和親對(duì)象是個(gè)殘疾皇子靶衍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容