NDK 知識梳理(2) 使用 CMake 進行 NDK 開發之如何編寫 CMakeLists txt 指令碼

澤毛發表於2017-12-21

一、前言

在前一篇文章 NDK 知識梳理(1) - 使用 CMake 進行 NDK 開發之初體驗 中,我們一起學習瞭如何在Android Studio中使用CMake來進行NDK開發,而編寫CMakeLists.txt構建指令碼是其中一個重要的環節,今天我們就來一起學習CMakeLists.txt的一些應用,介紹它在下面三種場景的用法:

  • 從原生程式碼構建一個原生庫
  • 新增Android NDKAPI
  • 引入第三方so

二、從原生程式碼構建一個原生庫

2.1 指定 CMake 最低版本

cmake_minimum_required用於指定CMake的最低版本資訊,不加入會收到警告。

cmake_minimum_required(VERSION 3.4.1)
複製程式碼

2.2 從原生程式碼構建一個原生庫

add_library()用於指示CMake從原生程式碼構建一個原生庫,通俗地說,就是從.cpp經過編譯得到.so檔案。正如我們在 NDK 知識梳理(1) - 使用 CMake 進行 NDK 開發之初體驗 中看到的那樣,我們通過add_libraryCMake根據native-lib.cpp原始檔構建一個名為native-lib的共享庫:

# Specifies a library name, specifies whether the library is STATIC or
# SHARED, and provides relative paths to the source code. You can
# define multiple libraries by adding multiple add.library() commands,
# and CMake builds them for you. When you build your app, Gradle
# automatically packages shared libraries with your APK.

add_library( # Specifies 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 )
複製程式碼

對於add_library()括號中的內容,可以分為三個部分:

(1) 指定原生庫的名字

add_library的第一個引數,決定了最終生成的共享庫的名字,例如我們將共享庫的名字定義為native-lib,那麼最終生成的so檔案將在前面加上lib字首,也就是libnative-lib.so,但是我們在程式碼中載入該共享庫的時候,仍然應當使用native-lib,也就是像下面這樣:

static {
    System.loadLibrary(“native-lib”);
}
複製程式碼

(2) 靜態庫 or 共享庫

通過第二個引數,我們可以指定根據原始檔編譯出來的是靜態庫還是共享庫,分別對應STATIC/SHARED關鍵字,這裡簡單提一下兩者的區別:

  • 靜態庫:以.a結尾。靜態庫在程式連結的時候使用,連結器會將程式中使用到函式的程式碼從庫檔案中拷貝到應用程式中。一旦連結完成,在執行程式的時候就不需要靜態庫了。
  • 共享庫:以.so結尾。在程式的連結時候並不像靜態庫那樣在拷貝使用函式的程式碼,而只是作些標記。然後在程式開始啟動執行的時候,動態地載入所需模組。

(3) 指定原始檔

指定編譯的原始檔,這裡是一個和CMakeLists.txt相關的相對路徑,如果我們有多個原始檔,那麼就在後面新增檔案的路徑即可。

2.3 關聯多個原始檔的例子

下面,我們對 NDK 知識梳理(1) - 使用 CMake 進行 NDK 開發之初體驗 中的計算器的例子進行優化,把加法和減法的操作放在另一個.cpp檔案中實現,以演示關聯多個.cpp檔案的例子,整個目錄的結構變為:

NDK 知識梳理(2)   使用 CMake 進行 NDK 開發之如何編寫 CMakeLists txt 指令碼

  • addition_subtraction.cpp
int addition(int a, int b) {
    return a + b;
}

int subtraction(int a, int b) {
    return a - b;
}


複製程式碼
  • addition_subtraction.h
#ifndef CMAKEOLDDEMO_ADDITION_SUBTRACTION_H
#define CMAKEOLDDEMO_ADDITION_SUBTRACTION_H

//加法
int addition(int a, int b);

//減法
int subtraction(int a, int b);

#endif
複製程式碼
  • calculator.cpp
#include <jni.h>
#include "../include/addition_subtraction.h"

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


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

那麼我們需要將add_library改寫為:

cmake_minimum_required(VERSION 3.4.1)

add_library(calculator SHARED src/main/cpp/calculator/calculator.cpp src/main/cpp/calculator/addition_subtraction.cpp)

include_directories(src/main/cpp/include/)
複製程式碼

三、新增 NDK API

Android系統當中,預製了一些標準的NDK庫,這些庫函式的目的就是讓開發者能夠在原生方法中實現之前在Java層開發的一些功能,我們可以通過 NDK 庫 查詢所需要的API

NDK 知識梳理(2)   使用 CMake 進行 NDK 開發之如何編寫 CMakeLists txt 指令碼

因為這些庫已經預製在系統當中了,所以如果我們要呼叫這些庫中的函式,那麼不需要將其打包到APK當中,所需要做的就是向CMake提供希望使用的庫名稱,並將其關聯到自己的原生庫,最後在原生程式碼中引入相應的標頭檔案,呼叫方法就可以了。

下面,我們就介紹一個呼叫Android Native API的例子。我們給Calculator加上一個新的介面,而在其本地方法中呼叫Android NDK的方法來列印一串Java層傳過來的字元。

(1) 增加介面

我們在NativeCalculator.java中增加一個介面logByNative

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);
    
    public native void logByNative(String tag, String log);

}
複製程式碼

(2) 在 CMakeLists.txt 引入 Android NDK 的 log 庫並把它和 calculator 關聯

cmake_minimum_required(VERSION 3.4.1)

add_library(calculator SHARED src/main/cpp/calculator/calculator.cpp src/main/cpp/calculator/addition_subtraction.cpp)

include_directories(src/main/cpp/include/)

find_library(log-lib log)

target_link_libraries(calculator ${log-lib})
複製程式碼

對比於之前,我們增加了下面這兩句:

find_library(log-lib log)

target_link_libraries(calculator ${log-lib})
複製程式碼

它們的作用分別是:

  • find_library:將一個變數和Android NDK的某個庫建立關聯關係。該函式的第二個引數為Android NDK中對應的庫名稱,而呼叫該方法之後,它就被和第一個引數所指定的變數關聯在一起。 在這種關聯建立以後,我們就可以使用這個變數在構建指令碼的其它部分引用該變數所關聯的NDK庫。
  • target_link_libraries:把NDK庫和我們自己的原生庫calculator進行關聯,這樣,我們就可以呼叫該NDK庫中的函式了。

(3) 在 Calculator.cpp 引入標頭檔案並呼叫列印 Log 的函式

#include <jni.h>
#include "../include/addition_subtraction.h"
#include <android/log.h>

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


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

extern "C"
JNIEXPORT void JNICALL
Java_com_demo_lizejun_cmakeolddemo_NativeCalculator_logByNative(JNIEnv *env, jobject instance, jstring tag_, jstring log_) {
    const char *tag = env->GetStringUTFChars(tag_, 0);
    const char *log = env->GetStringUTFChars(log_, 0);
    __android_log_write(ANDROID_LOG_DEBUG, tag, log);
    env->ReleaseStringUTFChars(tag_, tag);
    env->ReleaseStringUTFChars(log_, log);
}
複製程式碼

這裡需要做的就是兩步:

  • 引入標頭檔案
#include <android/log.h>
複製程式碼
  • 呼叫NDK庫中的方法
 __android_log_write(ANDROID_LOG_DEBUG, tag, log);

複製程式碼

(4) 在 Java 中呼叫,觀察結果

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)));
        mNativeCalculator.logByNative("Calculator", "Log By Native");
    }

}
複製程式碼

最終的列印結果為:

NDK 知識梳理(2)   使用 CMake 進行 NDK 開發之如何編寫 CMakeLists txt 指令碼

四、引入第三方的.so

最後一部分,我們舉一個通過第三方.so庫來實現乘除法的例子,為了得到一個.so庫,我們通過新建一個工程,然後將它編譯出的.apk檔案解壓,取出其中的.so檔案。

這裡獲得第三方so的原理其實和我們之前一直談到的其實是一樣的,我們只是藉助了Android Studio來模擬了這個流程。

3.1 獲得第三方 so 庫

新建工程的目錄結構為:

NDK 知識梳理(2)   使用 CMake 進行 NDK 開發之如何編寫 CMakeLists txt 指令碼

  • multiplication_division.h
#ifndef SOMAKER_MULTIPLICATION_DIVISION_H
#define SOMAKER_MULTIPLICATION_DIVISION_H

int multiplication(int a, int b);

int division(int a, int b);

#endif
複製程式碼
  • multiplication_division.cpp
int multiplication(int a, int b) {
    return a * b;
}

int division(int a, int b) {
    return a / b;
}


複製程式碼
  • CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)

add_library(multiplication_division SHARED src/main/cpp/multiplication_division.cpp)

include_directories(src/main/cpp/include/)

複製程式碼

build.gradleandroid節點下,增加構建任務:

    externalNativeBuild {
        cmake {
            path 'CMakeLists.txt'
        }
    }
複製程式碼

這個工程編譯完畢之後,去app/build/outputs/apk/目錄下將編譯出來的APK檔案解壓,得到libmultiplication_division.so庫,我們將它作為第三方的so庫匯入到計算器的例子當中。

NDK 知識梳理(2)   使用 CMake 進行 NDK 開發之如何編寫 CMakeLists txt 指令碼

3.2 引入第三方 so 庫

(1) 將 so 庫和標頭檔案拷貝到對應的目錄

NDK 知識梳理(2)   使用 CMake 進行 NDK 開發之如何編寫 CMakeLists txt 指令碼

(2) 修改 CMakeLists.txt 檔案

cmake_minimum_required(VERSION 3.4.1)

add_library(calculator SHARED src/main/cpp/calculator/calculator.cpp src/main/cpp/calculator/addition_subtraction.cpp)

include_directories(src/main/cpp/include/)

add_library(multiplication_division SHARED IMPORTED)

set_target_properties(multiplication_division PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libmultiplication_division.so )

find_library(log-lib log)

target_link_libraries(calculator multiplication_division ${log-lib})
複製程式碼

這裡相比於之前,修改了以下三句:

add_library(multiplication_division SHARED IMPORTED)

set_target_properties(multiplication_division PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libmultiplication_division.so )

target_link_libraries(calculator multiplication_division ${log-lib})
複製程式碼

這三句話的作用分別為:

  • 新增第三方so庫 這裡和之前在第二步中介紹的建立一個新的原生庫類似,區別在於最後一個引數,我們通過IMPORTANT標誌告知CMake只希望將庫匯入到專案中。

  • 指定目標庫的路徑 這裡有幾點需要說明:

  • ${CMAKE_SOURCE_DIR}表示的是CMakeLists.txt所在的路徑,我們指定第三方so所在路徑時,應當以這個常量為起點。

  • 按理來說,我們應當為每種ABI介面提供單獨的軟體包,那麼,我們就可以在jinLibs下建立多個資料夾,每個資料夾對應一種ABI介面型別,之後再通過${ANDROID_ABI}來泛化這一層目錄的結構,這樣將有助於充分利用特定的CPU架構。

  • 將第三方的庫關聯到原生庫 這裡和將NDK庫關聯到原生庫的原理是一樣的。

(3) 宣告新的介面,並在 Calculator.cpp 引入第三方的標頭檔案,呼叫函式

package com.demo.lizejun.cmakeolddemo;

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

    public native void logByNative(String tag, String log);

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

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

}
複製程式碼
#include <jni.h>
#include "../include/addition_subtraction.h"
#include <android/log.h>
#include "../include/multiplication_division.h"

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


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

extern "C"
JNIEXPORT void JNICALL
Java_com_demo_lizejun_cmakeolddemo_NativeCalculator_logByNative(JNIEnv *env, jobject instance, jstring tag_, jstring log_) {
    const char *tag = env->GetStringUTFChars(tag_, 0);
    const char *log = env->GetStringUTFChars(log_, 0);
    __android_log_write(ANDROID_LOG_DEBUG, tag, log);
    env->ReleaseStringUTFChars(tag_, tag);
    env->ReleaseStringUTFChars(log_, log);
}

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

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

之前的步驟完成之後就很簡單了,我們只需要引入該so庫對應的標頭檔案,再呼叫它提供的方法就可以了。

(4) 在 Java 中呼叫本地方法

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)));
        mNativeCalculator.logByNative("Calculator", "Log By Native");
        Log.d("Calculator", "11 * 12 = " + (mNativeCalculator.multiplication(11,12)));
        Log.d("Calculator", "11 / 12 = " + (mNativeCalculator.division(11,12)));
    }

}
複製程式碼

執行程式,最終列印的結果為:

NDK 知識梳理(2)   使用 CMake 進行 NDK 開發之如何編寫 CMakeLists txt 指令碼

四、小結

這一篇文章,我們簡要地總結了CMakeLists.txt在幾種場景下應該如何編寫。在學習的過程中,感覺之前學的C/C++都忘光了,標頭檔案、靜態庫/動態庫、extern關鍵字,都不記得了,打算先好好複習一下相關的知識再繼續NDK的學習了。

相關文章