Android之NDK開發入門

注意:本文操作環境爲mac,Android Studio版本3.5

前言

NDK全稱Native Development Kit,是Android的一個工具開發包,能夠快速開發C,C++的動態庫,並自動將so和應用打包成APK。而NDK的使用場景就是通過NDK在Android中使用JNI,那麼JNI又是啥呢?JNI全稱是Java Native Interface,即Java的本地接口,JNI可以使得Java與C,C++語言進行交互。這麼一來,通過NDK和JNI,就可以很方便的在Android的開發環境中使用c,c++的開源庫。

一、安裝和配置NDK

1.安裝NDK

可通過Android Studio下載和官網下載,下面爲Android Studio下載

  1. 打開Android Studio,點擊Android Studio->Preferences,搜索SDK,然後在SDK Tools中勾選LLDB,NDK,Cmke進行下載。其中LLDB是調試本地代碼的工具,可調試C++代碼在這裏插入圖片描述
  2. 打開File->Project Structure,然後在SDK Location中配置NDK路徑,點擊右下角就會出現我們剛剛下載的NDK路徑,點擊Default NDK路徑即可,如果是在官網中下載的,可以根據自己下載的路徑進行配置
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-vGku4a3U-1589177307895)(/Users/jaceyuan/md/博客/Android:NDK開發/2.png)]2.配置NDK環境變量

2.配置NDK環境

  1. 啓動終端,進入當前用戶的home目錄

    cd ~(注意中間的空格)
    
  2. 創建.bash_profile(假如之前已經創建好了,執行這個命令行不會對原本的文件內容造成影響)

    終端輸入:touch .bash_profile
    
  3. 查看、編輯.bash_profile

    如果忘記了NDK的目錄,可以通過Android Studio的File->Project Structure中的NDK location查看

    export NDK_ROOT=/Users/{你的用戶名}/Library/Android/sdk/ndk-bundle
    export PATH=$PATH:$NDK_ROOT
    
  4. 保存然後關閉.bash_profile文件

  5. 更新剛配置的環境變量

    終端輸入: source .bash_profile
    
  6. 重新打開終端,檢查是否配置成功(如果不成功,記得先關閉當前終端然後打開)

    終端輸入: ndk-build
    

    如果出現下列結果的即爲成功

    Android NDK: Could not find application project directory !
    Android NDK: Please define the NDK_PROJECT_PATH variable to point to it.
    

二、CMake的方式編譯生成so庫

1. Android Studio自動生成的示例

1.1 新建Native C++工程

在Android Studio新建一個Native C++的工程,然後填寫項目名,並選擇Toolchain Default使用默認的C++標準。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Eyt77dA2-1589177307901)(/Users/jaceyuan/md/博客/Android:NDK開發/3.png)]

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-15bP6O2A-1589177307903)(/Users/jaceyuan/md/博客/Android:NDK開發/4.png)]

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-u7w2KQw5-1589177307904)(/Users/jaceyuan/md/博客/Android:NDK開發/5.png)]

1.2 分析AS創建和添加的文件

當點擊Finish後,Android Studio會自動添加NDK開發相關的文件。cpp是AS幫我們自動生成的,裏面有兩個文件:

  • CMakeLists.text:構建腳本
  • Native-lib.cpp:示例C++源文件

另外還對MainActivity,build.gradle進行了一些改動。下面將分析這四個重要的文件

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-elYNgbq4-1589177307905)(/Users/jaceyuan/md/博客/Android:NDK開發/6.png)]

  1. CMakeLists.txt

    cmake_minimum_required(VERSION 3.4.1)
    # 這裏會把 native-lib.cpp轉換成共享庫,並命名爲 native-lib
    add_library( # 庫的名字
            native-lib
    
            # 設置成共享庫
            SHARED
    
            # 庫的源文件(由於native-lib.app和CMakeLists.txt同處於一個包,因此可以直接寫文件名
            # 不然的話需要寫成src/main/cpp/native-lib.cpp)
            native-lib.cpp)
    
    # 如果需要使用第三方庫,可以使用 find-library來找到
    find_library( # so庫的變量路徑名字,在關聯的時候使用
            log-lib
    
            # 你需要關聯的so名字
            log)
    
    # 通過link將源文件的庫和第三方庫添加進來
    target_link_libraries( 
            # 源文件庫的名字
            native-lib
    
            # 添加第三方庫的變量名
            ${log-lib})
    

    需要注意的是第三庫是路徑變量名,因此需要用${}方式引用

  2. Native-lib.cpp

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-khciEW8X-1589177307906)(/Users/jaceyuan/md/博客/Android:NDK開發/7.png)]

    一看就是C++的代碼,這個方法的作用其實就是返回一個字符串“Hello from C++”。需要重點注意的是這個方法的命名格式,包名,類名,方法名其實就是在Java代碼中定義這個native方法stringFromJNI所在的包名和類名。

  3. MainActivity

    public class MainActivity extends AppCompatActivity {
    
        //加載so庫
        static {
            System.loadLibrary("native-lib");
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            
            TextView tv = findViewById(R.id.sample_text);
            //直接調用native方法
            tv.setText(stringFromJNI());
        }
    
        //native方法
        public native String stringFromJNI();
    }
    

    在這裏我們驗證了native-lib.cpp裏面方法的命名格式,在MainActivity確實有一個stringFromJNI的方法。在這裏我們首先需要加載so庫,so庫的名稱就是我們在CmakeList.txt定義的庫的名字。然後通過定義的native方法就可以調用C++層的Java_com_example_ndkdemo_MainActivity_stringFromJNI方法。

  4. build.gradle

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ibQ34NaX-1589177307906)(/Users/jaceyuan/md/博客/Android:NDK開發/8.png)]

    看到這,你可以先運行下,看看AS自動生成的JNI例子是否運行成功了。想必當你看到成功運行後是不是已經熱血沸騰,迫不及待的想自己嘗試下。並且一個so庫中不可能只有一個方法,因此接下來就讓我們照貓畫虎的添加自己編寫的c++文件。

2. 自己編寫的so庫

  1. 創建Java對應的加載類。在這裏我們不準備在MainActivity中加載.so庫,而是新建了一個JNI工具類來完成加載.so庫和聲明native方法的任務。然後將MainActivity中的native方法複製過來,並且新建了一個helloFromJNI的方法。另外爲了在新項目中使用該so庫,我們將so庫的名字更改爲hello,下面也會在CMakeList.txt中更改so庫的名稱。(可以發現下面的native方法是紅色的,這是因爲我們還沒有在C++層中實現這兩個方法)

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-6HCgDEtR-1589177307907)(/Users/jaceyuan/md/博客/Android:NDK開發/9.png)]

  2. 添加需要的C/C++文件。我們直接在cpp中新建一個就行,cpp->右鍵->new->c/c++ source File。然後就可以命名一個c/c++文件了,並且勾選create an associated header,表示在創建才C/C++文件的同時會創建對應的頭文件。

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-CBQA13iV-1589177307907)(/Users/jaceyuan/md/博客/Android:NDK開發/10.png)]

    (1) 編寫頭文件hello.h,你可以將頭文件看成Java的接口,在這裏我們需要聲明方法。

    #ifndef NDKDEMO_HELLO_H
    #define NDKDEMO_HELLO_H
    
    //聲明接口
    extern const char* helloWorld();
    #endif //NDKDEMO_HELLO_H
    

    (2) 然後在hello.cpp中實現這個頭文件,可以發現在這裏我們只是簡單的返回了一個hello world的字符串

    #include "hello.h"
    extern const char* helloWorld(){
        return "Hello World";
    }
    
  3. 在native-lib中引入hello.h頭文件。這個操作跟Java中的導包有點類似,並且我們新建了一個在前面JniUtil中聲明的native方法,注意的是由於我們將加載so庫和聲明native方法都放到了JniUtil中,因此我們需要更改之前stringFromJNI的包名和方法名。

    #include <jni.h>
    #include <string>
    #include "hello.h"
    
    extern "C" JNIEXPORT jstring JNICALL
    Java_com_example_ndkdemo_util_JniUtil_stringFromJNI(
            JNIEnv *env,
            jobject /* this */) {
        std::string hello = "Hello from C++";
        return env->NewStringUTF(hello.c_str());
    }
    
    
    extern "C" JNIEXPORT jstring JNICALL
    Java_com_example_ndkdemo_util_JniUtil_helloFromJNI(
            JNIEnv *env,
            jobject /* this */) {
        std::string helloStr = helloWorld();
        return env->NewStringUTF(helloStr.c_str());
    }
    
  4. CMakeList.txt中加入hello.cpp的路徑添加

    這裏需要注意的是,如果是多次使用add_library,則會生成多個so庫。在這裏我們只是將多個本地文件編譯到一個so庫中,因此只需要在原本的add_library中添加hello的相對路徑。並且爲了方便在新項目中使用該so庫,在這裏我將之前native-lib的名字改成了hello。因此生成so庫的時候也會生成libhello.so文件(生成so庫的時候會自動加上lib的前綴)

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-T28Ek8Pz-1589177307908)(/Users/jaceyuan/md/博客/Android:NDK開發/11.png)]

  5. 在MainActivity中使用調用JniUtil中的native方法

    public class MainActivity extends AppCompatActivity {
    
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            TextView tv = findViewById(R.id.sample_text);
            //直接調用native方法
            tv.setText(JniUtil.helloFromJNI());
        }
    }
    
  6. 運行項目

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-XAPnknng-1589177307909)(/Users/jaceyuan/md/博客/Android:NDK開發/12.png)]

  7. 查看so庫。在app->intermediates->cmake中就會生成對應類型的so庫,因爲生成so庫的時候會自動加上lib的前綴。

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Ni0VTIMh-1589177307909)(/Users/jaceyuan/md/博客/Android:NDK開發/13.png)]

三、使用CMake引入第三方so庫

通常情況下,引入第三方.so庫會有兩種場景:

  • JNI規範的so。比如返回的是JNI直接支持的類型,比如說上述NdkDemo中的native-lib.cpp中的兩個方法。
  • 只提供.so庫和頭文件。第三方共享.so庫一般情況下只提供.so文件和頭文件,就是沒有將C++文件直接暴露給JAVA層,也沒有編寫JNI方法的C++文件,比如上述的hello.cpp,這個C++文件中的方法並不是JNI直接支持的類型。

在實際開發中,更常見的是第二種場景。兩種場景的引入方法不同,第一種可以直接引入第三方so庫,而第二種需要引入自己的so庫,然後將自己的so庫與第三方so庫和頭文件進行相關聯。接下來我們就來分析這兩種引入方式。

1. 引入JNI規範的so

  1. 新建一個普通的Android項目。引入JNI規範的.so庫並不需要Native C++類型的項目。

  2. 在main中新建一個jniLibs。我們在app->src->main中新建立一個jniLibs,然後將上面生成的libhello.so文件拷貝過來,這裏我們直接將上面cmake->debug->obj中的四個文件夾都拷貝過來

    在這裏插入圖片描述

  3. 新建一個JniUtil類。注意包名和類名都要跟引入so庫中的暴露的JNI方法中的一致,接着就是加載hello這個so庫,然後聲明native方法,這個native方法就是hello.so庫中暴露的JNI方法。其實你會發現這個JniUtil中的代碼跟上述的NdkDemo中的是一樣的。

    在這裏插入圖片描述

  4. 在MainActivity中使用JniUtil中的native方法

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            TextView tv = findViewById(R.id.tv);
            //調用native方法
            tv.setText(JniUtil.helloFromJNI() + " & "+JniUtil.stringFromJNI());
        }
    }
    
  5. 運行項目

    在這裏插入圖片描述

可以發現引入JNI規範的.so庫是很簡單的,因爲我們知道hello.so庫中接口方法的命名方式,不用CMake,不用編寫C++文件,直接在JniUtil中聲明native方法,然後運行即可。

2. 引入第三方so庫和頭文件

這裏我們使用場景就是,在Java層中要調用hello.so庫中hello.cpp中的helloWorld方法

  1. 新建一個Native C++工程

    因爲引入這種類型的so庫,我們需要創建自己的so文件,然後在自己的so文件裏再調用第三方so,最後在Java層中調用自己的so,因此需要進行NDK開發。而上面我們已經分析了新建Native C++工程AS幫我們建立和修改的文件,因此如果你想在原有的項目中進行NDK開發的話,其實就是自己手動增加和修改這些文件即可。

  2. 新增文件夾,用來存放要導入的第三方so庫以及頭文件。主要是在cpp文件中新建include文件和在main中新建jniLibs,然後將第三方的頭文件放在include中,第三方so庫放入jniLibs中。

    在這裏插入圖片描述

  3. 配置CMakeLists.txt。我們需要關聯第三方頭文件到native-lib,並配置好第三方so庫以及頭文件導入的路徑。這裏需要注意的是set_target_properties這裏配置的so庫目錄,你可以利用message打印,so庫的路徑是否正確,CMAKE_SOURCE_DIR代表着CMakeLists.txt的路徑,由於我的CMakeLists.txt在cpp中,因此需要加上/…進行回退到上一級的main目錄,然後配置libhello.so的相對路徑。

    cmake_minimum_required(VERSION 3.4.1)
    
    # 利用這個打印路徑
    message("******************************************************************")
    message("CMAKE_SOURCE_DIR=${CMAKE_SOURCE_DIR}")
    message("******************************************************************")
    # 這裏會把 native-lib.cpp轉換成共享庫,並命名爲 native-lib
    add_library( # 庫的名字
            native-lib
    
            # 設置成共享庫
            SHARED
    
            # 庫的源文件(由於native-lib.app和CMakeLists.txt同處於一個包,因此可以直接寫文件名
            # 不然的話需要寫成src/main/cpp/native-lib.cpp)
    
            native-lib.cpp)
    
    # 如果需要使用第三方庫,可以使用 find-library來找到
    find_library( # so庫的變量路徑名字,在關聯的時候使用
            log-lib
    
            # 你需要關聯的so名字
            log)
    
    #將native-lib關聯到第三方庫頭文件
    #由於我的inclue目錄與CMakeList都在cpp目錄,因此可以直接寫include,否則需要寫相對目錄
    include_directories(include)
    #導入第三庫,不同到第三方庫需要分開導入,因爲有4個so庫需要導入,因此需要4add_library(hello SHARED IMPORTED)
    #設置導入第三庫名稱,目標位置
    set_target_properties(hello
            PROPERTIES IMPORTED_LOCATION
            ${CMAKE_SOURCE_DIR}/../jnilibs/${ANDROID_ABI}/libhello.so)
    
    # 通過link將源文件的庫和第三方庫添加進來
    target_link_libraries(
            # 源文件庫的名字
            native-lib
            #第三方庫的名稱
            hello
    
            # 添加第三方庫的變量名
            ${log-lib})
    
  4. 新建JniUtil用於加載so庫和聲明native方法。在引入JNI規範的so庫時,我們特別強調了該類要與hello.so庫中的JniUtil包名,類名要一致。而在這裏並不需要,因爲在這裏我們並不是引入hello.so庫,而是引入自己的so庫(native-lib),我們只是爲了方便管理,然後取JniUtil。

    在這裏插入圖片描述

  5. 在native-lib.cpp中引入第三方頭文件(hello.h)。在這裏我們引入了hello.h的頭文件,然後實現了對外的JNI方法,在該方法中我們引用了第三方庫中的hello.cpp中的helloWorld方法,而這也就是我們引入第三方so庫和頭文件的最終目的。

    注意:JNI方法的包名,類名,方法名與上面的JniUtil一致

    #include <jni.h>
    #include <string>
    #include "hello.h"
    
    
    extern "C" JNIEXPORT jstring JNICALL
    Java_com_example_jnidemo_util_JniUtil_helloFromJNI(
            JNIEnv *env,
            jobject /* this */) {
        std::string helloStr = helloWorld();
        return env->NewStringUTF(helloStr.c_str());
    }
    
    
  6. 在MainActivity中引用JniUtil中的native方法

    在這裏插入圖片描述

  7. 運行項目。你就能發現神奇的HelloWorld

四、踩坑

  1. 在引入JNI規範的.so庫時一定要記得包名,類名要和引入的so庫中的一致,不然運行時會報No implementation found for java.lang.String com.example…之類的錯誤,然後閃退

  2. 在引入第三so庫的時候,如果你將so庫放在src/main/jniLibs時,可以不在項目的build.gradle中配置so庫路徑,因爲AS默認加載so庫的路徑就是src/main/jniLibs。但是如果放在其他地方的時候,或者不取名jniLibs時,比如我們放在了src/main/jniLib,這時候就得在build.gradle中配置,如下
    在這裏插入圖片描述

  3. 引入第二種so庫和頭文件中配置CMakeList.txt的時候,我們會通過set_target_properties來設置目標so庫的路徑,網上大部分的教程配置的路徑都是:${CMAKE_SOURCE_DIR}/jnilibs/${ANDROID_ABI}/so庫完整名字.so,但其實是要看具體情況的,如果你編譯或運行的時候出現了類似下面這種的錯誤,那麼大概率是由於so庫的路徑配置錯誤導致的。

     'F:/Android/JNIDemo/app/src/main/cpp/jnilibs/armeabi-v7a/libhello.so', needed by 'F:/Android/JNIDemo/app/build/intermediates/cmake/debug/obj/armeabi-v7a/libnative-lib.so', missing and no known rule to make it
    

    這時候我們可以利用message來打印${CMAKE_SOURCE_DIR}/jnilibs/${ANDROID_ABI}/so庫完整名字.so這個路徑,然後對比一下你引入第三方so庫的位置,就可以進行判斷是否路徑配置錯誤。

    在這裏插入圖片描述

    添加打印信息後我們進行編譯,如果編譯錯誤的話,應該能夠在build中看到打印的message信息,如果看不到的話,可以查看app->.cxx->cmake->debug->隨便一個機型->build_output.txt中的打印信息,然後對比你引入第三方庫的位置。因爲我的jnilibs是放在main層,所以這個路徑明顯是錯誤的,因此需要在${CMAKE_SOURCE_DIR}加上/…進行回退一個目錄,即最終的目錄應該爲${CMAKE_SOURCE_DIR}/../jnilibs/${ANDROID_ABI}/libhello.so

    在這裏插入圖片描述
    在這裏插入圖片描述

    如果你確定你的路徑配置沒有錯誤,那麼你也可以看看報錯的so庫位置,也有可能是因爲你運行的環境缺少了相關的機型,比如在虛擬機中運行需要x86的環境,而你引入so庫的時候沒有將x86的so庫導進來,或者是說你的手機運行需要arm64-v8a或armeabi-v7a的環境,但是你沒有引入對應的環境,也有可能missing and no known rule to make it的錯誤,所以最好是將所有機型的so庫文件拷貝過來。

總結

自己是NDK開發和C++的小白,所以整個過程下來感覺收穫很多。從安裝到使用NDK開發,一路上下來也踩了不少的坑,所以想記錄這整個過程,如果有錯誤的還請大家多多包涵,同時也歡迎大家指出錯誤。

參考博客:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章