NDK 知識梳理(1) 使用 CMake 進行 NDK 開發之初體驗

澤毛發表於2017-12-13

一、前言

Eclipse的時代,我們進行NDK的開發一般需要通過手動執行NDK指令碼生成*.so檔案,再將.so檔案放到對應的目錄之後,之後再進行打包。

而如果使用的是Android Studio進行NDK開發,在2.2的版本以後,我們可以不需要手動地執行NDK指令碼來生成*.so檔案,而是將這一過程作為Gradle構建過程的依賴項,事先編寫好編譯的指令碼檔案,然後在build.gradle中指定編譯指令碼檔案的路徑就可以一次性完成生成原生庫並打包成APK的過程。

目前這種AS + GradleNDK開發方式又可以分為三種:ndk-buildCMakeExperimental Gradle

  • ndk-build:和上面談到的傳統方式相比,它們兩個的目錄結構相同,Gradle指令碼其實最終還是依賴於Android.mk檔案,對於使用傳統方式的專案來說,比較容易過度。
  • CMakeGradle指令碼依賴的是CMakeLists.txt檔案。
  • Experimental Gradle:需要引入實驗性的gradle外掛,全部的配置都可以通過build.gradle來完成,不再需要編寫Android.mk或者CMakeLists.txt,可能坑比較多,對於舊的專案來說過度困難。

目前,Android Studio已經將CMake作為預設的NDK實現方式,並且官網上對於NDK的介紹也是基於CMake,聲稱要永久支援。按照官方的教程使用下來,感覺這種方式有幾點好處:

  • 不需要再去通過javah根據java檔案生成標頭檔案,並根據標頭檔案生成的函式宣告編寫cpp檔案
  • 當在Java檔案中定義完native介面,可以在cpp檔案中自動生成對應的native函式,所需要做的只是補全函式體中的內容
  • 不需要手動執行ndk-build命令得到so,再將so拷貝到對應的目錄
  • 在編寫cpp檔案的過程中,可以有提示了
  • CMakeLists.txt要比Android.mk更加容易理解

下面,我們就來介紹一下如何使用CMake進行簡單的NDK開發,整個內容主要包括兩個方面:

  • 建立支援C/C++的全新專案
  • 在現有的專案中新增C/C++程式碼

二、建立支援C/C++的全新專案

2.1 安裝元件

在新建專案之前,我們需要通過SDK Manager安裝一些必要的元件:

  • NDK
  • CMake
  • LLDB

NDK 知識梳理(1)   使用 CMake 進行 NDK 開發之初體驗

2.2 建立工程

在安裝完必要的元件之後,我們建立一個全新的工程,這裡需要記得勾選include C++ Support選項:

NDK 知識梳理(1)   使用 CMake 進行 NDK 開發之初體驗
接下來一路Next,在最後一步我們會看見如下的幾個選項,它們的含義為:

  • C++ Standard:選擇C++的標準,Toolchain Default表示使用預設的CMake配置,這裡我們選擇預設。
  • Excptions Support:如果您希望啟用對C++異常處理的支援,請選中此核取方塊。如果啟用此核取方塊,Android Studio會將-fexceptions標誌新增到模組級 build.gradle檔案的cppFlags中,Gradle會將其傳遞到CMake
  • Runtime Type information Support:如果您希望支援RTTI,請選中此核取方塊。如果啟用此核取方塊,Android Studio會將-frtti標誌新增到模組級 build.gradle檔案的cppFlags中,Gradle會將其傳遞到CMake

NDK 知識梳理(1)   使用 CMake 進行 NDK 開發之初體驗
在新建工程完畢之後,我們得到的工程的結構如下圖所示:
NDK 知識梳理(1)   使用 CMake 進行 NDK 開發之初體驗
與傳統的工程相比,它有如下幾點區別:

(1) cpp 資料夾

用於存放C/C++的原始檔,在磁碟上對應於app/src/main/cpp資料夾,當新建工程時,它會生成一個native-lib.cpp的事例檔案,其內容如下:

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

extern "C"
JNIEXPORT jstring JNICALL
Java_com_demo_lizejun_cmakenewdemo_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
複製程式碼

(2) 增加 CMakeList.txt 指令碼

構建指令碼,在磁碟上對應於app/目錄下的txt檔案,其內容為如下圖所示,這裡面涉及到的CMake語法包括下面四種,關於CMake的語法,可以檢視 官方的 API 說明

  • cmake_minimum_required
  • add_library
  • find_library
  • target_link_libraries
# 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} )
複製程式碼

(3) build.gradle 指令碼

與傳統的專案相比,該模組所對應的build.gradle需要在裡面指定CMakeList.txt所在的路徑,也就是下面externalNativeBuild對應的選項。

apply plugin: 'com.android.application'

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.3"
    defaultConfig {
        applicationId "com.demo.lizejun.cmakenewdemo"
        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'
    testCompile 'junit:junit:4.12'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
}
複製程式碼

(4) 列印字串

MainActivity中,我們載入原生庫,並呼叫原生庫中的方法獲取了一個字串展示在介面上:

public class MainActivity extends AppCompatActivity {

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

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }
}
複製程式碼

(5) 執行結果

最後,我們執行一下這個工程,會得到下面的結果:

NDK 知識梳理(1)   使用 CMake 進行 NDK 開發之初體驗
通過APK Analyzer工具,我們可以看到在APK當中,增加了libnative-lib.so檔案:
NDK 知識梳理(1)   使用 CMake 進行 NDK 開發之初體驗

2.3 原理

下面,我們來解釋一下這一過程:

  • 首先,在構建時,通過build.gradlepath所指定的路徑,找到CMakeList.txt,解析其中的內容。
  • 按照指令碼中的命令,將src/main/cpp/native-lib.cpp編譯到共享的物件庫中,並將其命名為libnative-lib.so,隨後打包到APK中。
  • 當應用執行時,首先會執行MainActivitystatic程式碼塊的內容,使用System.loadLibrary()載入原生庫。
  • onCreate()函式中,呼叫原生庫的函式得到字串並展示。

2.4 小結

當通過CMake來對應用程式增加C/C++的支援時,對於應用程式的開發者,只需要關注以下三個方面:

  • C/C++原始檔
  • CMakeList.txt指令碼
  • 在模組級別的build.gradle中通過externalNativeBuild/cmake進行配置

三、在現有的專案中新增C/C++程式碼

下面,我們演示一下如何在現有的專案中新增對於C/C++程式碼的支援。

(1) 建立一個工程

和第二步不同,這次建立的時候,我們不勾選nclude C++ Support選項,那麼會得到下面這個普通的工程:

NDK 知識梳理(1)   使用 CMake 進行 NDK 開發之初體驗

(2) 定義介面

這一次,我們將它定義在一個單獨的檔案當中:

public class NativeCalculator {

    private static final String SELF_LIB_NAME = "calculator";

    static {
        System.loadLibrary(SELF_LIB_NAME);
    }

    public native int addition(int a, int b);

    public native int subtraction(int a, int b);
}
複製程式碼

這時候因為沒有找到對應的本地方法,因此會有如下的錯誤提示:

NDK 知識梳理(1)   使用 CMake 進行 NDK 開發之初體驗

(3) 定義 cpp 檔案

在模組根目錄下的src/main/新建一個資料夾cpp,在其中新增一個calculator.cpp檔案,這裡,我們先只引入標頭檔案:

#include <jni.h>
複製程式碼

(4) 定義 CMakeLists.txt

在模組根目錄下新建一個CMakeLists.txt檔案:

cmake_minimum_required(VERSION 3.4.1)

add_library(calculator SHARED src/main/cpp/calculator.cpp)
複製程式碼

(5) 在 build.gradle 中進行配置

之後,我們需要讓Gradle指令碼確定CMakeLists.txt所在的位置,我們可以在CMakeLists.txt上點選右鍵,之後選擇Link C++ Project with Gradle

NDK 知識梳理(1)   使用 CMake 進行 NDK 開發之初體驗
那麼在該模組下的build.gradle就會新增下面這句:
NDK 知識梳理(1)   使用 CMake 進行 NDK 開發之初體驗
在這步完成之後,我們選擇Build -> Clean Project

**(6) 實現 C++ **

在配置完上面的資訊之後,會發現一個神奇的地方,之前我們定義native介面的地方,多出了一個選項,它可以幫助我們直接在對應的C++檔案中生成函式的定義:

NDK 知識梳理(1)   使用 CMake 進行 NDK 開發之初體驗
點選Create function之後,我們的calculator.cpp檔案變成了下面這樣:

#include <jni.h>

extern "C"
JNIEXPORT jint JNICALL
Java_com_demo_lizejun_cmakeolddemo_NativeCalculator_addition(JNIEnv *env, jobject instance, jint a, jint b) {
    
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_demo_lizejun_cmakeolddemo_NativeCalculator_subtraction(JNIEnv *env, jobject instance,  jint a, jint b) {
    
}
複製程式碼

下面我們在函式體當中新增實現:

#include <jni.h>

extern "C"
JNIEXPORT jint JNICALL
Java_com_demo_lizejun_cmakeolddemo_NativeCalculator_addition(JNIEnv *env, jobject instance, jint a, jint b) {
    return a + b;
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_demo_lizejun_cmakeolddemo_NativeCalculator_subtraction(JNIEnv *env, jobject instance,  jint a, jint b) {
    return a - b;
}
複製程式碼

(9) 呼叫本地函式

public class MainActivity extends AppCompatActivity {

    private NativeCalculator mNativeCalculator;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mNativeCalculator = new NativeCalculator();
        Log.d("Calculator", "11 + 12 =" + (mNativeCalculator.addition(11,12)));
        Log.d("Calculator", "11 - 12 =" + (mNativeCalculator.subtraction(11,12)));
    }
}
複製程式碼

最終執行的結果為:

NDK 知識梳理(1)   使用 CMake 進行 NDK 開發之初體驗

四、小結

以上就是使用Android Studio 2.2以上版本,通過CMake來進行NDK開發的一個簡單例子,主要是學習一下開發的流程,下一篇文章,要學習一下CMakeLists.txt中的語法。

五、參考文獻

(1) 向您的專案新增 C 和 C++ 程式碼
(2) NDK筆記(二) - 在Android Studio中使用 ndk-build
(3) NDK開發 從入門到放棄(一:基本流程入門瞭解)
(4) NDK開發 從入門到放棄(七:Android Studio 2.2 CMAKE 高效 NDK 開發)
(5) The new NDK support in Android Studio
(6) Google NDK 官方文件
(7) Android Studio 2.2 對 CMake 和 ndk-build 的支援
(8) Android開發學習之路--NDK、JNI之初體驗
(9) NDK- JNI實戰教程(一) 在Android Studio執行第一個NDK程式
(10) cmake-commands
(11) CMake
(12) 開發自己的 NDK 程式
(13) NDK開發-Android Studio+gradle-experimental 開發 ndk
(14) Android NDK 開發入門指南

相關文章