[译] 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前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索