NDK 知識梳理(2) - 使用 CMake 進(jìn)行 NDK 開發(fā)之如何編寫 CMakeLists.txt 腳本

一、前言

在前一篇文章 NDK 知識梳理(1) - 使用 CMake 進(jìn)行 NDK 開發(fā)之初體驗(yàn) 中,我們一起學(xué)習(xí)了如何在Android Studio中使用CMake來進(jìn)行NDK開發(fā),而編寫CMakeLists.txt構(gòu)建腳本是其中一個(gè)重要的環(huán)節(jié),今天我們就來一起學(xué)習(xí)CMakeLists.txt的一些應(yīng)用,介紹它在下面三種場景的用法:

  • 從原生代碼構(gòu)建一個(gè)原生庫
  • 添加Android NDKAPI
  • 引入第三方so

二、從原生代碼構(gòu)建一個(gè)原生庫

2.1 指定 CMake 最低版本

cmake_minimum_required用于指定CMake的最低版本信息,不加入會收到警告。

cmake_minimum_required(VERSION 3.4.1)

2.2 從原生代碼構(gòu)建一個(gè)原生庫

add_library()用于指示CMake從原生代碼構(gòu)建一個(gè)原生庫,通俗地說,就是從.cpp經(jīng)過編譯得到.so文件。正如我們在 NDK 知識梳理(1) - 使用 CMake 進(jìn)行 NDK 開發(fā)之初體驗(yàn) 中看到的那樣,我們通過add_libraryCMake根據(jù)native-lib.cpp源文件構(gòu)建一個(gè)名為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()括號中的內(nèi)容,可以分為三個(gè)部分:

(1) 指定原生庫的名字

add_library的第一個(gè)參數(shù),決定了最終生成的共享庫的名字,例如我們將共享庫的名字定義為native-lib,那么最終生成的so文件將在前面加上lib前綴,也就是libnative-lib.so,但是我們在代碼中加載該共享庫的時(shí)候,仍然應(yīng)當(dāng)使用native-lib,也就是像下面這樣:

static {
    System.loadLibrary(“native-lib”);
}

(2) 靜態(tài)庫 or 共享庫

通過第二個(gè)參數(shù),我們可以指定根據(jù)源文件編譯出來的是靜態(tài)庫還是共享庫,分別對應(yīng)STATIC/SHARED關(guān)鍵字,這里簡單提一下兩者的區(qū)別:

  • 靜態(tài)庫:以.a結(jié)尾。靜態(tài)庫在程序鏈接的時(shí)候使用,鏈接器會將程序中使用到函數(shù)的代碼從庫文件中拷貝到應(yīng)用程序中。一旦鏈接完成,在執(zhí)行程序的時(shí)候就不需要靜態(tài)庫了。
  • 共享庫:以.so結(jié)尾。在程序的鏈接時(shí)候并不像靜態(tài)庫那樣在拷貝使用函數(shù)的代碼,而只是作些標(biāo)記。然后在程序開始啟動運(yùn)行的時(shí)候,動態(tài)地加載所需模塊。

(3) 指定源文件

指定編譯的源文件,這里是一個(gè)和CMakeLists.txt相關(guān)的相對路徑,如果我們有多個(gè)源文件,那么就在后面添加文件的路徑即可。

2.3 關(guān)聯(lián)多個(gè)源文件的例子

下面,我們對 NDK 知識梳理(1) - 使用 CMake 進(jìn)行 NDK 開發(fā)之初體驗(yàn) 中的計(jì)算器的例子進(jìn)行優(yōu)化,把加法和減法的操作放在另一個(gè).cpp文件中實(shí)現(xiàn),以演示關(guān)聯(lián)多個(gè).cpp文件的例子,整個(gè)目錄的結(jié)構(gòu)變?yōu)椋?br>

  • 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系統(tǒng)當(dāng)中,預(yù)制了一些標(biāo)準(zhǔn)的NDK庫,這些庫函數(shù)的目的就是讓開發(fā)者能夠在原生方法中實(shí)現(xiàn)之前在Java層開發(fā)的一些功能,我們可以通過 NDK 庫 查找所需要的API

因?yàn)檫@些庫已經(jīng)預(yù)制在系統(tǒng)當(dāng)中了,所以如果我們要調(diào)用這些庫中的函數(shù),那么不需要將其打包到APK當(dāng)中,所需要做的就是向CMake提供希望使用的庫名稱,并將其關(guān)聯(lián)到自己的原生庫,最后在原生代碼中引入相應(yīng)的頭文件,調(diào)用方法就可以了。

下面,我們就介紹一個(gè)調(diào)用Android Native API的例子。我們給Calculator加上一個(gè)新的接口,而在其本地方法中調(diào)用Android NDK的方法來打印一串Java層傳過來的字符。

(1) 增加接口

我們在NativeCalculator.java中增加一個(gè)接口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 關(guān)聯(lián)

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:將一個(gè)變量和Android NDK的某個(gè)庫建立關(guān)聯(lián)關(guān)系。該函數(shù)的第二個(gè)參數(shù)為Android NDK中對應(yīng)的庫名稱,而調(diào)用該方法之后,它就被和第一個(gè)參數(shù)所指定的變量關(guān)聯(lián)在一起。
    在這種關(guān)聯(lián)建立以后,我們就可以使用這個(gè)變量在構(gòu)建腳本的其它部分引用該變量所關(guān)聯(lián)的NDK庫。
  • target_link_libraries:把NDK庫和我們自己的原生庫calculator進(jìn)行關(guān)聯(lián),這樣,我們就可以調(diào)用該NDK庫中的函數(shù)了。

(3) 在 Calculator.cpp 引入頭文件并調(diào)用打印 Log 的函數(shù)

#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>
  • 調(diào)用NDK庫中的方法
 __android_log_write(ANDROID_LOG_DEBUG, tag, log);

(4) 在 Java 中調(diào)用,觀察結(jié)果

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

}

最終的打印結(jié)果為:


四、引入第三方的.so

最后一部分,我們舉一個(gè)通過第三方.so庫來實(shí)現(xiàn)乘除法的例子,為了得到一個(gè).so庫,我們通過新建一個(gè)工程,然后將它編譯出的.apk文件解壓,取出其中的.so文件。

這里獲得第三方so的原理其實(shí)和我們之前一直談到的其實(shí)是一樣的,我們只是借助了Android Studio來模擬了這個(gè)流程。

3.1 獲得第三方 so 庫

新建工程的目錄結(jié)構(gòu)為:


  • 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節(jié)點(diǎn)下,增加構(gòu)建任務(wù):

    externalNativeBuild {
        cmake {
            path 'CMakeLists.txt'
        }
    }

這個(gè)工程編譯完畢之后,去app/build/outputs/apk/目錄下將編譯出來的APK文件解壓,得到libmultiplication_division.so庫,我們將它作為第三方的so庫導(dǎo)入到計(jì)算器的例子當(dāng)中。

3.2 引入第三方 so 庫

(1) 將 so 庫和頭文件拷貝到對應(yīng)的目錄

(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
    這里和之前在第二步中介紹的創(chuàng)建一個(gè)新的原生庫類似,區(qū)別在于最后一個(gè)參數(shù),我們通過IMPORTANT標(biāo)志告知CMake只希望將庫導(dǎo)入到項(xiàng)目中。

  • 指定目標(biāo)庫的路徑
    這里有幾點(diǎn)需要說明:

  • ${CMAKE_SOURCE_DIR}表示的是CMakeLists.txt所在的路徑,我們指定第三方so所在路徑時(shí),應(yīng)當(dāng)以這個(gè)常量為起點(diǎn)。

  • 按理來說,我們應(yīng)當(dāng)為每種ABI接口提供單獨(dú)的軟件包,那么,我們就可以在jinLibs下建立多個(gè)文件夾,每個(gè)文件夾對應(yīng)一種ABI接口類型,之后再通過${ANDROID_ABI}來泛化這一層目錄的結(jié)構(gòu),這樣將有助于充分利用特定的CPU架構(gòu)。

  • 將第三方的庫關(guān)聯(lián)到原生庫
    這里和將NDK庫關(guān)聯(lián)到原生庫的原理是一樣的。

(3) 聲明新的接口,并在 Calculator.cpp 引入第三方的頭文件,調(diào)用函數(shù)

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庫對應(yīng)的頭文件,再調(diào)用它提供的方法就可以了。

(4) 在 Java 中調(diào)用本地方法

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

}

運(yùn)行程序,最終打印的結(jié)果為:


四、小結(jié)

這一篇文章,我們簡要地總結(jié)了CMakeLists.txt在幾種場景下應(yīng)該如何編寫。在學(xué)習(xí)的過程中,感覺之前學(xué)的C/C++都忘光了,頭文件、靜態(tài)庫/動態(tài)庫、extern關(guān)鍵字,都不記得了,打算先好好復(fù)習(xí)一下相關(guān)的知識再繼續(xù)NDK的學(xué)習(xí)了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容