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

1、前言

Eclipse的時代,咱們進行NDK的開發通常須要經過手動執行NDK腳本生成*.so文件,再將.so文件放到對應的目錄以後,以後再進行打包。html

而若是使用的是Android Studio進行NDK開發,在2.2的版本之後,咱們能夠不須要手動地運行NDK腳原本生成*.so文件,而是將這一過程做爲Gradle構建過程的依賴項,事先編寫好編譯的腳本文件,而後在build.gradle中指定編譯腳本文件的路徑就能夠一次性完成生成原生庫並打包成APK的過程。java

目前這種AS + GradleNDK開發方式又能夠分爲三種:ndk-buildCMakeExperimental Gradleandroid

  • ndk-build:和上面談到的傳統方式相比,它們兩個的目錄結構相同,Gradle腳本其實最終仍是依賴於Android.mk文件,對於使用傳統方式的項目來講,比較容易過分。
  • CMakeGradle腳本依賴的是CMakeLists.txt文件。
  • Experimental Gradle:須要引入實驗性的gradle插件,所有的配置均可以經過build.gradle來完成,再也不須要編寫Android.mk或者CMakeLists.txt,可能坑比較多,對於舊的項目來講過分困難。

目前,Android Studio已經將CMake做爲默認的NDK實現方式,而且官網上對於NDK的介紹也是基於CMake,聲稱要永久支持。按照官方的教程使用下來,感受這種方式有幾點好處:android-studio

  • 不須要再去經過javah根據java文件生成頭文件,並根據頭文件生成的函數聲明編寫cpp文件
  • 當在Java文件中定義完native接口,能夠在cpp文件中自動生成對應的native函數,所須要作的只是補全函數體中的內容
  • 不須要手動執行ndk-build命令獲得so,再將so拷貝到對應的目錄
  • 在編寫cpp文件的過程當中,能夠有提示了
  • CMakeLists.txt要比Android.mk更加容易理解

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

  • 建立支持C/C++的全新項目
  • 在現有的項目中添加C/C++代碼

2、建立支持C/C++的全新項目

2.1 安裝組件

在新建項目以前,咱們須要經過SDK Manager安裝一些必要的組件:app

  • NDK
  • CMake
  • LLDB

2.2 建立工程

在安裝完必要的組件以後,咱們建立一個全新的工程,這裏須要記得勾選include C++ Support選項: ide

接下來一路 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

在新建工程完畢以後,咱們獲得的工程的結構以下圖所示:
與傳統的工程相比,它有以下幾點區別:

(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) 運行結果

最後,咱們運行一下這個工程,會獲得下面的結果:

經過 APK Analyzer工具,咱們能夠看到在 APK當中,增長了 libnative-lib.so文件:

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進行配置

3、在現有的項目中添加C/C++代碼

下面,咱們演示一下如何在現有的項目中添加對於C/C++代碼的支持。

(1) 建立一個工程

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

(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);
}
複製代碼

這時候由於沒有找到對應的本地方法,所以會有以下的錯誤提示:

(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

那麼在該模塊下的 build.gradle就會新增下面這句:
在這步完成以後,咱們選擇 Build -> Clean Project

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

在配置完上面的信息以後,會發現一個神奇的地方,以前咱們定義native接口的地方,多出了一個選項,它能夠幫助咱們直接在對應的C++文件中生成函數的定義:

點擊 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)));
    }
}
複製代碼

最終運行的結果爲:

4、小結

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

5、參考文獻

(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 開發入門指南

相關文章
相關標籤/搜索