[譯] C++ 和 Android 本地 Activity 初探

簡介

我會帶你完成一個簡單的 Android 本地 Activity。我將介紹一下基本的設置,並盡力將進一步學習所需的工具提供給你。前端

雖然個人重點是遊戲編程,但我不會告訴你如何寫一個 OpenGL 應用或者如何構建一款本身的遊戲引擎。這些東西得寫整本書來討論。android

爲何用 C++

在 Android 上,系統及其所支持的基礎設施旨在支持那些用 Java 或 Kotlin 寫的程序。用這些語言編寫的程序得益於深度嵌入系統底層架構的工具。Android 系統不少核心的特性,好比 UI 界面和 Intent 處理,只經過 Java 接口公開。ios

使用 C++ 並不會比 Kotlin 或 Java 這類語言對 Android 來講更「本地化」。與直覺相反,你經過某種方式編寫了一個只有 Android 部分特性可用的程序。對於大多數程序,Koltin 這類語言會更合適。git

然而此規則有一些意外狀況。對我來講最接近的就是遊戲開發。因爲遊戲通常會使用自定義的渲染邏輯(一般使用 OpenGL 或 Vulkan 編寫),因此預計遊戲看起來會與標準的 Android 程序不一樣。當你還考慮到 C 和 C++ 幾乎在全部平臺上都通用,以及相關的支持遊戲開發的 C 庫時,使用本地開發可能更合理。github

若是你想從頭開始或者在現有遊戲的基礎上開發一款遊戲,Android 本地開發包(NDK)已備好待用。實際上,即將展現給你的本地 activity 提供了一鍵式操做,你能夠在其中設置 OpenGL 畫布並開始收集用戶的輸入。你可能會發現,儘管 C 有學習成本,但使用 C++ 解決一些常見代碼難題,好比從遊戲數據中構建頂點屬性數組,會比用高級語言更容易。編程

我不打算講的內容

我不會告訴你如何初始化 VulkanOpenGL 的上下文。儘管我會給一些提示讓你學習的輕鬆一點,但仍是建議你閱讀 Google 提供的示例。你也能夠選擇使用相似 SDL 或者 Google 的 FPLBase 這樣的庫。後端

設置你的 IDE

首先須要確保你已經安裝了本地開發所需的內容。爲此,咱們須要用到 Android NDK。啓動 Android Studio:數組

在 「Configure」 下面選擇 「SDK Manager」:緩存

從這裏安裝 LLDB(本地調試器)、CMake(構建系統)和 NDK 自己:bash

建立工程

到此你已經設置好了全部內容,咱們將建一個工程。咱們想建立一個沒有 Activity 的空工程:

NativeActivity 自 Android Gingerbread 開始就有了,若是你剛開始學習,建議選擇當前可用的最高目標版本。

如今咱們須要建一個 CmakeLists.txt 文件來告訴 Android 如何構建咱們的 C++ 工程。在工程視圖下右擊 app 建立一個新文件:

命名爲 CMakeLists.txt:

建立一個簡單的 CMake 文件:

cmake_minimum_required(VERSION 3.6.0)

add_library(helloworld-c
    SHARED
    
    src/main/cpp/helloworld-c.cpp)
複製代碼

咱們聲明瞭在 Android Studio 中使用最新版本的 CMake(3.6.0),將構建一個名爲 hellworld-c 的共享庫。我還添加了一個必需要建立的源文件。

爲何是共享庫而不是可執行文件呢?Android 使用一個名爲 Zygote 的進程來加速在 Android Runtime 內部啓動的應用或服務的過程。這對 Android 內全部面向用戶的進程都適用,所以你的代碼首次運行的地方是在一個虛擬機內。而後代碼必須加載一個含有你的邏輯的共享庫文件,若是你使用了本地 Activity,該共享庫將爲你處理。與之相反,當構建一個可執行文件時,咱們但願操做系統直接加載你的程序並運行一個名爲 「main」 的 C 方法。在 Android 裏也有可能,可是我還沒找到這方面的任何實踐用途。

如今建立 C++ 文件:

將其放入咱們在 make 文件內指定的目錄下:

再加入少許內容以告訴咱們是否構建成功:

//
// Created by Patrick Martin on 1/30/19.
//

#include <jni.h>
複製代碼

最後讓咱們把這個 C++ 工程連接到咱們的應用上:

若是一切順利,工程會更新成功:

而後你能夠不出錯地執行一次構建操做:

至於在你的構建腳本中發生了什麼變化,若是你打開 app 下的 build.gradle 文件,你會看到 externalNativeBuild

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.pux0r3.helloworldc"
        minSdkVersion 28
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    externalNativeBuild {
        cmake {
        path file('CMakeLists.txt')
        }
    }
}
複製代碼

建立一個本地 Activity

一個 Activity 是 Android 用來顯示你的應用的用戶界面的基本窗口。一般你會用 Java 或 Kotlin 編寫一個繼承自 Activity 的類,可是 Google 建立了一個等價的用 C 寫的本地 Activity。

設置你的構建文件

建立一個本地 Activity 最好的方式是包含 native_app_glue。不少示例程序將其從 SDK 拷貝至他們的工程中。這沒什麼錯,可是我我的更願意將其作爲個人遊戲能夠依賴的庫。我把它作成靜態庫,因此不須要動態庫調用的額外開銷:

cmake_minimum_required(VERSION 3.6.0)

add_library(native_app_glue STATIC
    ${ANDROID_NDK}/sources/android/native_app_glue/android_native_app_glue.c)
target_include_directories(native_app_glue PUBLIC
    ${ANDROID_NDK}/sources/android/native_app_glue)

find_library(log-lib
    log)

set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -u ANativeActivity_onCreate")
add_library(helloworld-c SHARED
    src/main/cpp/helloworld-c.cpp)

target_link_libraries(helloworld-c
    android
    native_app_glue
    ${log-lib})
複製代碼

這裏有很多事情要作,咱們繼續。首先用 add_library 建了一個名爲 native_app_glue 的庫並把它標記爲一個 STATIC 的庫。而後在 NDK 的安裝路徑下查找自動生成的環境變量 ${ANDROID_NDK} 從而來尋找一些文件。如此,我找到了 native_app_glue 的實現:android_native_app_glue.c

將代碼與目標關聯後,我想說一下目標是在哪裏找到它的頭文件的。我使用 target_include_directories 將包含它的全部頭文件的文件夾包含進來並將設置爲 PUBLIC。其餘選項還有 INTERNALPRIVATE 但目前還用不到。有些教程可能會用 include_directories 代替 target_include_directories。這是一種較早的作法。最近的 target_include_directories 可讓你的目錄關聯到目標,這有助於下降較大工程的複雜性。

如今,我想在在 Android 的 Logcat 中打印一些內容。只使用與普通 C 或 C++ 應用中那樣的標準的輸出(如:std::coutprintf)是無效的。使用 find_library 去定位 log,咱們緩存了 Android 的日誌庫以便稍後使用。

最後咱們經過 target_link_libraries 告訴 CMake,helloworld-c 要依賴 native_app_glue、native_app_glue 和被命名爲 log-lib 的庫。如此能夠在咱們的 C++ 工程中引用本地應用的邏輯。在 add_library 以前的 set 也確保 helloworld-c 不會實現名爲 ANativeActivity_onCreate 的方法,該方法由 android_native_app_glue 提供。

寫一個簡單的本地 Activity

如今一切就緒,構建咱們的 app 吧!

//
// Created by Patrick Martin on 1/30/19.
//

#include <android_native_app_glue.h>
#include <jni.j>

extern "C" {
void handle_cmd(android_app *pApp, int32_t cmd) {
}
    
void android_main(struct android_app *pApp) {
    pApp->onAppCmd = handle_cmd;
    
    int events;
    android_poll_source *pSource;
    do {
        if (ALooper_pollAll(0, nullptr, &events, (void **) &pSource) >= 0) {
            if (pSource) {
                pSource->process(pApp, pSource);
            }
        }
    } while (!pApp->destroyRequested);
}
}
複製代碼

這裏發生了什麼?

首先,經過 extern "C"{},咱們告訴連接器把花括號中的內容當成 C 看待。這裏你仍然能夠寫 C++ 代碼,但這些方法在咱們程序其他部分看起來都像是 C 方法。

我寫了一個小的佔位方法 handle_cmd。未來其能夠做爲咱們的消息循環。任何的觸摸事件、窗口事件都會通過這裏。

這段代碼最主要的是 android_main。當你的應用啓動的時候這個方法會被 android_native_app_glue 調用。咱們首先將 pApp->onAppCmd 指向咱們的消息循環以便讓系統消息有一個可去的地方。

接着咱們用 ALooper_pollAll 處理全部已排隊的系統事件,第一個參數是超時參數。若是上述方法返回的值大於或等於 0,咱們須要藉助 pSource 來處理事件,不然,咱們將繼續直到應用程序關閉。

如今依然不能運行這個 Activity,卻能夠隨意構建以確保一切正常。

在 ApplicationManifest 中添加必需的信息

如今咱們須要在 AndroidManifest.xml 填入內容來告訴系統如何運行你的應用。該文件位於 app>manifests>AndroidManfiest.xml:

首先咱們告訴系統是哪一個本地 Activity(名爲 「android.app.NativeActivity」) 並在屏幕方向變化或者鍵盤狀態變化的時候不銷燬這個 Activity:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.pux0r3.helloworldc">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name="android.app.NativeActivity"
            android:configChanges="orientation|keyboardHidden"
            android:label="@string/app_name"></activity>
    </application>
</manifest>
複製代碼

而後咱們告訴該本地 Activity 去哪裏找咱們想運行的代碼。若是你忘了名字的話,去檢查你的 CMakeLists.txt 文件吧!

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.pux0r3.helloworldc">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name="android.app.NativeActivity"
            android:configChanges="orientation|keyboardHidden"
            android:label="@string/app_name">
            <meta-data
                android:name="android.app.lib_name"
                android:value="helloworld-c" />
        </activity>
    </application>
</manifest>
複製代碼

咱們還告訴 Android 操做系統這是啓動 Activity 也是主 Activity:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.pux0r3.helloworldc">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name="android.app.NativeActivity"
            android:configChanges="orientation|keyboardHidden"
            android:label="@string/app_name">
            <meta-data
                android:name="android.app.lib_name"
                android:value="helloworld-c" />

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>
複製代碼

若是一切順利,你能夠點擊調試並會看到一個空白窗口!

準備 OpenGL

在谷歌的示例庫中已有優秀的 OpenGL 示例程序了:

我會給你一些有用的提示。首先,爲了使用 OpenGL,在你的 CMakeLists.txt 文件中添加如下內容:

這裏你能夠對不一樣的 Android 架構平臺作不少處理,但對最近版本的 Android 來講,添加 EGL 和 GLESv3 到你的目標是一個不錯的操做。

接下來,我建立了一個名爲 Renderer 的類來處理渲染邏輯。若是你建了一個類,它用構造器來初始渲染器、用析構器來銷燬它、用 render() 方法來渲染,那麼我建議你的 app 看起來應該像這樣:

extern "C" {
void handle_cmd(android_app *pApp, int32_t cmd) {
    switch (cmd) {
        case APP_CMD_INIT_WINDOW:
            pApp->userData = new Renderer(pApp);
            break;

        case APP_CMD_TERM_WINDOW:
            if (pApp->userData) {
                auto *pRenderer = reinterpret_cast<Renderer *>(pApp->userData);
                pApp->userData = nullptr;
                delete pRenderer;
            }
    }
}

void android_main(struct android_app *pApp) {
    pApp->onAppCmd = handle_cmd;
    pApp->userData;

    int events;
    android_poll_source *pSource;
    do {
        if (ALooper_pollAll(0, nullptr, &events, (void **) &pSource) >= 0) {
            if (pSource) {
                pSource->process(pApp, pSource);
            }
        }

        if (pApp->userData) {
            auto *pRenderer = reinterpret_cast<Renderer *>(pApp->userData);
            pRenderer->render();
        }
    } while (!pApp->destroyRequested);
}
}
複製代碼

因此,我所作的第一件事就是在 android_app 使用名爲 userData 的字段。你能夠在這裏存儲任何你想存儲的東西,每個 android_app 實例均可以獲取它。我把它加入到個人渲染器中。

接着,只有在窗口初始化後才能獲得一個渲染器而且必須在窗口銷燬的時候釋放它。我使用前面提到過的 handle_cmd 方法來執行此操做。

最後,若是有了一個渲染器(即:窗口已建立),我從 android_app 中獲取並使其執行渲染操做。不然只是繼續處理這個循環。

總結

如今你能夠像在其餘平臺同樣使用 OpenGL ES 3 了。若是你須要更多資源或教程的話,下面是一些有用的連接:

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索