通過CMake在AndroidStudio項目中引入JNI編程

早期的Android工程(Eclipse時代)以及Gradle2.2之前,Android只能通過ndk-build命令結合.mk文件來編譯本地庫。

從Gradle2.2開始,我們可以使用CMake方式來引入JNI編程,下面就開始介紹。

下載NDK和構建工具

一共有以下三種組件需要下載:

  • NDK(The Android Native Development Kit):讓你能在Android上面使用C和C++代碼的工具集。
  • CMake: 外部構建工具。
  • LLDB: Android Studio上面調試本地代碼的工具。

我們通過SDK Manager來安裝上述組件:

  1. 打開一個項目,從菜單欄中選擇Tools > Android > SDK Manager
  2. Android SDK目錄下,選擇需要下載的組件執行下載和安裝

    這裏寫圖片描述

創建新項目

創建新項目很簡單。

勾選include C++ Support,意思就是支持C++代碼,如圖:

這裏寫圖片描述

剩下的步驟都和默認的一樣,創建完成之後發現,默認給出一個“Hello from C++”的JNI例子,main目錄下出現了一個和java並列的文件夾cpp

這裏寫圖片描述

分別看一下MainActivitynative-lib.cpp的內容,

MainActivity

package com.tsnt.jni.androidjnidemo;

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

    // 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");
    }
}

native-lib.cpp

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

extern "C"
jstring
Java_com_tsnt_jni_androidjnidemo_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

很顯然在MainActivity中調用了native方法–stringFromJNI()

如果沒配置NDK目錄,需要配置:

這裏寫圖片描述

然後把項目跑起來,成功運行:

這裏寫圖片描述

在已有項目中引入JNI

相比較於創建新項目,我們要做的就是去創建缺少的文件。

創建cpp文件夾,和cpp文件

app\src\main目錄下創建和java文件夾並列的cpp文件夾,然後選擇C/C++ Source File

這裏寫圖片描述

這裏寫圖片描述

至於如何使用JNI編程,請參考Android JNI編程入門

創建CMake構建腳本

在app目錄下創建一個名爲CMakeLists的txt文件

這裏寫圖片描述

新建項目時默認的CMakeLists是這樣的:

# Sets the minimum version of CMake required to build the native
# library. You should either keep the default value or only pass a
# value of 3.4.0 or lower.

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 it 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).
             # Associated headers in the same location as their source
             # file are automatically included.
             src/main/cpp/native-lib.cpp )

# Searches for a specified prebuilt library and stores the path as a
# variable. Because system libraries are included 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 the
# 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} )

分爲如下四部分:

1.最低版本聲明

cmake_minimum_required(VERSION 3.4.1)

這個很簡單,意思就是CMake最小版本使用的是3.4.1

2.增加本地庫

add_library( native-lib  SHARED  src/main/cpp/native-lib.cpp )

括號裏有三個參數:

1.第一個參數native-lib表示庫名

在項目中,如果需要使用這個so文件,引用的名稱就是這個。值得注意的是,實際上生成的so文件名稱是libnative-lib。當run項目或者build項目時,在Module級別的build\intermediates\transforms\mergeJniLibs\debug\folders\2000\1f\main目錄下會生成相應的so庫文件,像這樣:

這裏寫圖片描述

2.第二個參數SHARED表示庫的類型

  • STATIC:靜態庫,是目標文件的歸檔文件,在鏈接其它目標的時候使用。
  • SHARED:動態庫,會被動態鏈接,在運行時被加載。
  • MODULE:模塊庫,是不會被鏈接到其它目標中的插件,但是可能會在運行時使用dlopen-系列的函數動態鏈接。
    具體可以去查看C++靜態庫與動態庫的相關知識。

3.第三個參數src/main/cpp/native-lib.cpp表示庫的源文件

怎麼增加其它的預構建的庫呢?比如import-lib是一個外部庫。

其實和添加本地庫(native library)類似。由於預編譯庫是已經構建好的,你想就要使用 IMPORTED標誌去告訴CMake,你只需要將其導入到你的項目中即可:

add_library( imported-lib SHARED IMPORTED )

一些庫會根據不同的 CPU 使用不同的包,或者是 Application Binary Interfaces(ABI),並且將他們歸類到不同的目錄中。這樣做的好處是,可以充分發揮特定的 CPU 架構。你可以使用 ANDROID_ABI 路徑變量,將多個 ABI 版本的庫添加到你的 CMake 構建腳本中。這個變量使用了一些 NDK 默認支持的 ABI,以及一些需要手動配置到 Gradle 的 ABI。然後你需要使用 set_target_properties() 命令去指定庫的路徑,就像下面的代碼那樣:

add_library(...)
set_target_properties( # Specifies the target library.
                       imported-lib

                       # Specifies the parameter you want to define.
                       PROPERTIES IMPORTED_LOCATION

                       # Provides the path to the library you want to import.
                       imported-lib/src/${ANDROID_ABI}/libimported-lib.so )

編譯NDK自帶的庫

NDK同樣也包含一些只包含源碼的library,這些就需要你去編譯,然後鏈接到你的本地庫(native library)。你可以在CMake構建腳本中使用add_library()命令將源碼編譯進本地庫。這時就需要提供你的本地NDK安裝路徑,通常將該路徑保存在ANDROID_NDK變量中,這樣Android Studio可以自動爲你識別。

下面的命令告訴CMake去構建android_native_app_glue.c,將其導入靜態庫中,然後將其鏈接至native-lib:

add_library( app-glue
             STATIC
             ${ANDROID_NDK}/sources/android/native_app_glue/android_native_app_glue.c )

# You need to link static libraries against your shared native library.
target_link_libraries( native-lib app-glue ${log-lib} )

3.定位預構建庫

find_library(log-lib  log )

find_library()命令添加到你的CMake構建腳本中,這樣就可以定位NDK library的位置,並將其位置存儲在一個變量之中。你可以在構建腳本的其他地方使用這個變量,來代指 NDK library。以上的示例代碼將Android-specific log support library的位置存儲到變量log-lib 中:

find_library( # Defines the name of the path variable that stores the
              # location of the NDK library.
              log-lib

              # Specifies the name of the NDK library that
              # CMake needs to locate.
              log )

4.將預構建庫與你本地庫相關聯

target_link_libraries( native-lib  ${log-lib} )

關聯本地庫與Gradle

使用圖形化界面

你可以使用圖形化界面來將Gradle與外部的CMake或者ndk-build項目關聯起來:

  1. 打開 IDE 左邊的 Project 面板,選擇Android視圖。

  2. 右鍵點擊你想鏈接本地庫的module,比如app module,然後從菜單中選擇Link C++ Project with Gradle。你應該能看見一個和下圖很像的對話框。

  3. 在下拉菜單中,選擇CMake或者ndk-build。
    a. 如果你選擇CMake需要在Project Path中指定CMakeLists.txt腳本文件的路徑
    b. 如果你選擇ndk-build需要在Project Path中指定Android.mk腳本文件的路徑
    這裏寫圖片描述

  4. 點擊ok,就完成了。

手動配置

module層級的build.gradle文件中添加externalNativeBuild {}代碼塊,並且在該代碼塊中配置cmake {}ndkBuild {},如下:

android {
    ...
    defaultConfig {...}
    buildTypes {...}

    // Encapsulates your external native build configurations.
    externalNativeBuild {

        // Encapsulates your CMake build configurations.
        cmake {

            // Provides a relative path to your CMake build script.
            path "CMakeLists.txt"
        }
    }
}

可選配置

你可以在你的 module 層級的build.gradle文件中的defaultConfig {}代碼塊中,添加 externalNativeBuild {}代碼塊,爲 CMake 或 ndk-build 配置一些額外參數。當然,你也可以在你的構建配置中的其他每一個生產渠道重寫這些屬性。

比如,如果你的 CMake 或者 ndk-build 項目中定義了多個本地庫,你想在某個生產渠道使用這些本地庫中的幾個,你就可以使用 targets 屬性來構建和打包。下面的代碼展示了一些你可能會用到的屬性:

android {
  ...
  defaultConfig {
    ...
    // This block is different from the one you use to link Gradle
    // to your CMake or ndk-build script.
    externalNativeBuild {

      // For ndk-build, instead use ndkBuild {}
      cmake {

        // Passes optional arguments to CMake.
        arguments "-DCMAKE_VERBOSE_MAKEFILE=TRUE"

        // Sets optional flags for the C compiler.
        cFlags "-D_EXAMPLE_C_FLAG1", "-D_EXAMPLE_C_FLAG2"

        // Sets a flag to enable format macro constants for the C++ compiler.
        cppFlags "-D__STDC_FORMAT_MACROS"
      }
    }
  }

  buildTypes {...}

  productFlavors {
    ...
    demo {
      ...
      externalNativeBuild {
        cmake {
          ...
          // Specifies which native libraries to build and package for this
          // product flavor. If you don't configure this property, Gradle
          // builds and packages all shared object libraries that you define
          // in your CMake or ndk-build project.
          targets "native-lib-demo"
        }
      }
    }

    paid {
      ...
      externalNativeBuild {
        cmake {
          ...
          targets "native-lib-paid"
        }
      }
    }
  }

  // You use this block to link Gradle to your CMake or ndk-build script.
  externalNativeBuild {
    cmake {...}
    // or ndkBuild {...}
  }
}

指定ABI(Application Binary Interfaces)

一般情況下,Gradle會將你的本地庫構建成 .so 文件,然後將其打包到你的 APK 中。如果你想 Gradle 構建並打包某個特定的 ABI 。你可以在你的 module 層級的 build.gradle 文件中使用 ndk.abiFilters 標籤來指定他們:

android {
  ...
  defaultConfig {
    ...
    externalNativeBuild {
      cmake {...}
      // or ndkBuild {...}
    }

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

大多數情況,你只需要像上面的代碼那樣,在 ndk {} 代碼塊中指定 abiFilters 即可。如果你想控制 Gradle 構建、依賴你希望的東西,你就需要在 defaultConfig.externalNativeBuild.cmake {} 代碼塊或 defaultConfig.externalNativeBuild.ndkBuild {} 代碼塊中,配置其他的 abiFilters 標籤。Gradle 會構建這些 ABI 配置,但是隻會將 defaultConfig.ndk {} 代碼塊中指定的東西打包到 APK 中。


demo地址AndroidJNIDemo

參考:

  1. Android NDK探究奧祕一:Android Studio創建第一個JNI項目
  2. 在 Android Studio 2.2 中愉快地使用 C/C++
  3. Android Studio 2.2 更方便地創建JNI項目-CMake
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章