超詳細的Android NDK開發環境搭建

前言

技術的更新換代真的很快,以前做NDK開發用的是ndk-build,最近要用到ndk,查了一下資料,幾年前已經改用CMake了,其實之前有學習過這個,但是時間一長,又給忘了,所以,好記性不如爛筆頭,這次得做個筆記了。

NDK開發環境搭建

  1. 創建一個新項目,起名爲“NdkDemo”

  2. 點擊AndroidStudio右上角的SDK Manager圖標,安裝NDK和CMake,如下
    在這裏插入圖片描述在這裏插入圖片描述

  3. 打開項目結構(Ctrl + Shift + Alt + S),並設置NDK位置,如下:
    在這裏插入圖片描述

  4. 創建jni目錄:右擊app > New > Folder > JNI Folder,此時會在app/src/main目錄下生成jni目錄,最新官方文檔教程中使用的是cpp目錄,所以也可以手動創建名爲cpp的目錄,創建jni也可以,在Android視圖下,jni目錄會顯示成cpp,如下:
    在這裏插入圖片描述
    對於強迫症,還是想知道爲什麼會這樣?

    在AndroidStudio3.5版本以及更新的版本中,對於native有了新的項目結構。
    舊版本native源文件存放於app/src/main/jni/目錄,CMakeLists.txt存放於app/目錄
    新版本native源文件和CMakeLists.txt都存放於app/src/main/cpp/目錄

    所以,我們儘量使用新版本的方式吧!

  5. 創建C++文件:右擊cpp > New > C/C++ Source File,在彈出的對話框中輸入文件名爲demo(可以隨意取名),Type選擇".cpp",這樣就創建出了一個demo.cpp的文件,用於寫C++代碼。

  6. 創建CMakeLists.txt文件:右擊cpp > New > File,在彈出來的輸入框中輸入:CMakeLists.txt (注:必須用這個名字),並輸入如下代碼:

    # 設置構建native library所需的CMake最低版本。
    cmake_minimum_required(VERSION 3.4.1)
    
    #創建一個庫(多次調用add_library即可創建多個庫)
    add_library( # 設置庫的名稱
                 demo-lib
    
                 # 將庫設置爲共享庫(即so文件)
                 SHARED
    
                 # 指定源文件的相對路徑
                 demo.cpp )
    

    CMakeLists.txt就是一個配置文件,指定了要編譯的源文件,而且配置了要生成一個名爲demo-lib的庫,它的文件名爲libdemo-lib.so。

  7. AndroidStudio是中構建apk文件是使用gradle來完成的,所以我們還要告訴gradle在構建的時候要編譯so文件並打包到apk中,只需要把CMakeLists.txt配置文件告訴gradle即可,方法如下:
    右擊app > Link C++ Project with Gradle,在彈出來的對話框中指定CMakeLists.txt文件的位置,如下:
    在這裏插入圖片描述
    此時會在app目錄下的build.gradle文件中生成如下內容:

    android {
        externalNativeBuild {
            cmake {
                path file('src/main/jni/CMakeLists.txt')
            }
        }
    }
    

    如果這個時候在Build窗口中報出一個構建失敗的錯誤,如下:
    在這裏插入圖片描述
    大概意思是說執行native build失敗了,沒關係,因爲我們的demo.cpp文件中什麼也沒寫。

  8. 在MainActivity中聲明一個native方法,如下:

    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
        }
     
        external fun getString(): String
    }
    

    注:這裏使用的是kotlin語言,方法使用external修飾,如果是java則用native修飾。

  9. 在demo.cpp文件中實現對應的jni函數(getString方法),如下:

    #include <jni.h>
    #include <string>
    
    extern "C"
    JNIEXPORT jstring JNICALL
    Java_com_even_app_ndkdemo_MainActivity_getString(JNIEnv *env, jobject thiz) {
        std::string hello = "Hello from C++";
        return env->NewStringUTF(hello.c_str());
    }
    

    此函數的功能爲返回一個字符串:“Hello from C++”。此時如果發現代碼有紅色報錯,沒關係,點擊右上角的同步Gradle按鈕即可消除,如下:
    在這裏插入圖片描述

  10. 加載動態連接庫(即so文件),並調用native方法,如下:

    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            Toast.makeText(this, getString(), Toast.LENGTH_SHORT).show()
        }
    
        external fun getString(): String
    
        companion object {
            init {
                System.loadLibrary("demo-lib")
            }
        }
    }
    
  11. 運行查看效果,如下:
    在這裏插入圖片描述
    OK,大告成功,如上圖所示,拿到了從C++函數中返回的字符串:“Hello from C++”。

其他細節

1、自動生成native方法

完成上面的例子後,如果還想再增加第二個native方法時,可以使用自動生成功能,如在MainActivity中添加一個add方法,如下:

external fun add(x: Int, y: Int): Int

此時會報錯,把光標定位到add上,按Alt + Enter,選擇創建jni函數即可,如下:
在這裏插入圖片描述
此時就會自動在demo.cpp中增加對應的jni函數,如下:

extern "C"
JNIEXPORT jint JNICALL
Java_com_even_app_ndkdemo_MainActivity_add(JNIEnv *env, jobject thiz, jint x, jint y) {
    // TODO: implement add()
}

在創建第一個jni函數的時候無法使用此方式來自動生成,這應該算是AndroidStudio的一個Bug吧,只有手動寫了一個jni函數之後,第二個纔會出現創建jni函數的命令。

2、使用創建嚮導生成帶ndk的項目

File > New > New Project > Native C++,此時可能會報一個錯,如下:
在這裏插入圖片描述
錯誤說的是NDK沒有配置,這應該也算AndroidStudio的一個Bug吧,我已經安裝有NDK,有好幾個版本(包含最新版本),AndroidStudio應該自動給我選一個,用最新的也行。上面描述說的意思是首選的NDK版本是21.0.6113669,我並沒有安裝這個版本,所以它提示我們安裝,其實沒有必要安裝這個版本的,按Ctrl + Shift + Alt + S打開項目結構,在彈出來的對話框中設置一個已經安裝的NDK位置即可。然後就可以直接運行查看效果了。

如果是要做一個新項目,這樣的方式創建NDK項目非常方便。

使用嚮導創建的NDK項目還會在app下的build.gradle中多一個cppFlag的配置,如下:

android {

    defaultConfig {
        externalNativeBuild {
            cmake {
                cppFlags ""
            }
        }
    }
	
}

這個應該是用於指定C++的版本,cppFlags爲空,這是因爲我在創建這個項目的嚮導中選擇的C++版本時選擇了默認,如下:
在這裏插入圖片描述
點擊下拉箭頭,可以看到我們當前設置的NDK的版本所支持的C++版本,不同的NDK版本可能會支持不同的C++版本,截圖如下:
在這裏插入圖片描述
假如我們選擇C++17,則cppFlag屬性如下:

android {

    defaultConfig {
        externalNativeBuild {
            cmake {
                cppFlags "-std=c++17"
            }
        }
    }
	
}

3、查看生成的so文件

直接在AndroidStudio中運行項目時,AndroidStudio會根據手機的CPU類型來生成對應的CPU架構的so文件。點擊Build > Analyze APK,在彈出來的對話框中選擇app\build\outputs\apk\debug\app-debug.apk,這樣就能看到apk文件的內部結構了,如下:
在這裏插入圖片描述
因爲我的手機CPU是arm64-v8a結構的,所以直接運行時它只生成和手機對應的so文件,如果想要生成所有的,可以點擊Build > Build Bundle(s) / APK(s) > Build APK(s),這樣的話也會生成debug.apk,但是這個apk並不針對哪一個手機,所以會生成所有的so,如下:
在這裏插入圖片描述
這裏說的所有的so是指特定NDK版本支持的so CPU類型,從NDK 17開始,不再支持armeabi,所以上圖中沒有看到armeabi的so。爲什麼不支持armeabi了呢?因爲現在的手機基本上都支持armeabi-v7a了。

如果只想配置生成特定的so類型,可以在app下的build.gradle中設置如下:

android {
    
    defaultConfig {
        ndk {
            // Specifies the ABI configurations of your native
            // libraries Gradle should build and package with your APK.
            abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
        }
    }

}

4、相關連接

NDK入門:https://developer.android.google.cn/ndk/guides
代碼實驗室:https://codelabs.developers.google.com/codelabs/android-studio-cmake/#0
Github上提供的NDK Demo:https://github.com/android/ndk-samples
關於CMake:https://developer.android.com/ndk/guides/cmake.html
配置CMake:https://developer.android.google.cn/studio/projects/configure-cmake#add-ndk-api
添加native代碼:https://developer.android.google.cn/studio/projects/add-native-code
JNI相關:https://developer.android.google.cn/training/articles/perf-jni
JNI接口規範:https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html

5、關於生成so文件到jniLibs目錄

以前我們都是把so文件放到jniLibs目錄中來使用,現在好了,打包時直接編譯源碼爲so並打包到apk。人有時候就是想使用原來的方式,網上找了一下,在CMakeLists.txt中加入下面這句代碼:

# 設置so文件輸出到jniLibs目錄中
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})

在AndroidStudio中直接運行應用,確實會在jniLibs目錄中生成對應的so文件了,但是報一個錯誤,如下:

More than one file was found with OS independent path ‘lib/armeabi-v7a/libdemo-lib.so’. If you are using jniLibs and CMake IMPORTED targets, see https://developer.android.com/studio/preview/features#automatic_packaging_of_prebuilt_dependencies_used_by_cmake

提示中有一個鏈接,但是點擊這個連接之後找不到相關的內容,應該是Google重新編輯網頁,刪除掉了,這個鏈接是跳轉到AndroidStudio4.1的新特性的,在這裏面會有“Automatic packaging of prebuilt dependencies used by CMake”的相關內容,但是已經被編輯掉了,在Google中搜索這段英文時能找到鏈接,但是要點快照,出來的網頁就會有需要的內容,這裏把原文搬過來,並配上翻譯如下:
Automatic packaging of prebuilt dependencies used by CMake
自動打包CMake使用的預構建依賴項
Prior versions of the Android Gradle Plugin required that you explicitly package any prebuilt libraries used by your CMake external native build by using jniLibs:
早期版本的Android Gradle插件要求您使用以下命令顯式打包CMake外部本機內部版本使用的所有預構建庫 jniLibs:

sourceSets {
    main {
        // The libs directory contains prebuilt libraries that are used by the
        // app's library defined in CMakeLists.txt via an IMPORTED target.
        // libs目錄包含預構建的庫,這些庫由CMakeLists.txt中通過導入目標定義的應用程序庫使用。
        jniLibs.srcDirs = ['libs']
    }
}

With Android Gradle Plugin 4.0, the above configuration is no longer necessary and will result in a build failure:
使用Android Gradle Plugin 4.0時,不再需要上述配置,並且會導致構建失敗:

What went wrong:
Execution failed for task ‘:app:mergeDebugNativeLibs’.
A failure occurred while executing com.android.build.gradle.internal.tasks.Workers$ActionFacade
More than one file was found with OS independent path ‘lib/x86/libprebuilt.so’

External native build now automatically packages those libraries, so explicitly packaging the library with jniLibs results in a duplicate. To avoid the build error, simple remove the jniLibs configuration from your build.gradle file.
現在,外部本機構建會自動打包這些庫,因此將jniLibs結果與庫明確打包在一起。爲避免生成錯誤,只需jniLibs從build.gradle文件中刪除配置即可。

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