mirror of
https://github.com/android/ndk-samples
synced 2025-11-04 22:53:57 +08:00
Expand documentation for native-activity sample.
This expands the docs and comments in the native-activity sample to be something closer to a proper guide. There isn't a very clear flow to it since the documentation is necessarily spread out into multiple files, and sometimes the order of the code requires explaining things out of order, but it's better than nothing. If we wanted to turn this into an actual guide that could be written in a clear order without duplicating code snippets into the docs, which would surely be quickly out of date, we could use something like sphinx (possibly through readthedocs) to generate something closer to a literate code sample.
This commit is contained in:
@@ -1,15 +1,30 @@
|
||||
# Native Activity
|
||||
|
||||
> [!WARNING]
|
||||
> **Most apps should not use the app development model shown in this sample**.
|
||||
> Instead, use a Java or Kotlin `AppCompatActivity` and connect your native code
|
||||
> using JNI like the other samples in this repository. `NativeActivity` and
|
||||
> `GameActivity` attempt to translate the Android [activity lifecycle] into a
|
||||
> desktop style `main()` function with a polled event loop. That is not how
|
||||
> Android apps work, and while it may help you get your prototype running more
|
||||
> quickly, as your app matures you will likely end up retranslating the
|
||||
> `native_app_glue` model to again look like `Activity`.
|
||||
|
||||
This is an Android sample that uses [NativeActivity] with `native_app_glue`,
|
||||
which enables building NDK apps without having to write any Java code. In
|
||||
practice most apps, even games which are predominantly native code, will need to
|
||||
call some Java APIs or customize their app's activity further.
|
||||
call some Java APIs or customize their app's activity further. While it may save
|
||||
you a small amount of effort during prototyping, it may result in a difficult
|
||||
migration later. It's also worth noting that some of the code in this sample is
|
||||
spent undoing the work of `native_app_glue` to create a class very similar to
|
||||
`Activity`.
|
||||
|
||||
The more modern approach to this is to use [GameActivity], which has all the
|
||||
same benefits as `NativeActivity` and `native_app_glue`, while also making it
|
||||
easier to include Java code in your app without a rewrite later. It's also
|
||||
source compatible. This sample will likely migrate to `GameActivity` in the
|
||||
future.
|
||||
source compatible. However, it still has all the problems explained in the
|
||||
warning above, and in practice neither `NativeActivity` nor `GameActivity` is
|
||||
the recommended app development model.
|
||||
|
||||
The app here is intentionally quite simple, aiming to show the core event and
|
||||
draw loop necessary for an app using `native_app_glue` without any extra
|
||||
@@ -17,5 +32,38 @@ clutter. It uses `AChoreographer` to manage the update/render loop, and uses
|
||||
`ANativeWindow` and `AHardwareBuffer` to update the screen with a simple color
|
||||
clear.
|
||||
|
||||
[activity lifecycle]: https://developer.android.com/guide/components/activities/activity-lifecycle
|
||||
[GameActivity]: https://developer.android.com/games/agdk/game-activity
|
||||
[NativeActivity]: http://developer.android.com/reference/android/app/NativeActivity.html
|
||||
|
||||
## Walkthrough
|
||||
|
||||
The interesting sections of code in this sample are in three files:
|
||||
[AndroidManifest.xml], [CMakeLists.txt], and [main.cpp]. Each of those files has
|
||||
code comments explaining the portions relevant to using `NativeActivity`, but
|
||||
the high level details of the app are explained here.
|
||||
|
||||
This app uses `NativeActivity` rather than its own child class of `Activity` or
|
||||
`AppCompatActivity`. This is specified in the `<activity />` declaration in [the
|
||||
manifest].
|
||||
|
||||
Apps which use `NativeActivity` are typically written using `native_app_glue`,
|
||||
which adapts the Android activity lifecycle code to look more like a desktop
|
||||
program with a `main()` function and an event loop. This is set up in the app's
|
||||
[CMakeLists.txt file].
|
||||
|
||||
When using `native_app_glue` with a [version script], you must export
|
||||
`ANativeActivity_onCreate`. This sample does this in
|
||||
[libnative-activity.map.txt].
|
||||
|
||||
This is a fairly simple application, so all of the code is in a single file,
|
||||
[main.cpp]. The entry point for an app using `native_app_glue` is
|
||||
`android_main()`. That function is the best place to start reading in this file
|
||||
to learn how the sample works, then follow through to the definition of
|
||||
`engine_handle_cmd` and `Engine`.
|
||||
|
||||
[CMakeLists.txt file]: app/src/main/cpp/CMakeLists.txt
|
||||
[libnative-activity.map.txt]: app/src/main/cpp/libnative-activity.map.txt
|
||||
[main.cpp]: app/src/main/cpp/main.cpp
|
||||
[the manifest]: app/src/main/AndroidManifest.xml
|
||||
[version script]: https://developer.android.com/ndk/guides/symbol-visibility
|
||||
|
||||
@@ -1,38 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- BEGIN_INCLUDE(manifest) -->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0">
|
||||
android:versionName="1.0">
|
||||
|
||||
<!--
|
||||
This .apk has no Java/Kotlin code, so set hasCode to false.
|
||||
<!--
|
||||
This .apk has no Java/Kotlin code, so set hasCode to false.
|
||||
|
||||
If you copy from this sample and later add Java/Kotlin code, or add a
|
||||
dependency on a library that does (such as androidx), be sure to set
|
||||
`android:hasCode` to `true` (or just remove it, since that's the default).
|
||||
-->
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:fullBackupContent="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:hasCode="false">
|
||||
|
||||
<!-- Our activity is the built-in NativeActivity framework class.
|
||||
This will take care of integrating with our NDK code. -->
|
||||
<activity android:name="android.app.NativeActivity"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:exported="true">
|
||||
<!-- Tell NativeActivity the name of our .so -->
|
||||
<meta-data android:name="android.app.lib_name"
|
||||
android:value="native-activity" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
If you copy from this sample and later add Java/Kotlin code, or add a
|
||||
dependency on a library that does (such as androidx), be sure to set
|
||||
`android:hasCode` to `true` (or just remove it, since that's the default).
|
||||
-->
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:fullBackupContent="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:hasCode="false">
|
||||
|
||||
<!--
|
||||
This app uses android.app.NativeActivity rather than its own child class of
|
||||
Activity or AppCompatActivity. The advantage of this is that we do not have
|
||||
to write any Java code of our own. The Java portion of the app is provided
|
||||
by the OS.
|
||||
-->
|
||||
<activity
|
||||
android:name="android.app.NativeActivity"
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:exported="true">
|
||||
<!--
|
||||
This property tells NativeActivity which of our app's libraries
|
||||
provide the definition of ANativeActivity_onCreate, which is the
|
||||
entry point for apps using ANativeActivity.
|
||||
-->
|
||||
<meta-data
|
||||
android:name="android.app.lib_name"
|
||||
android:value="native-activity" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
<!-- END_INCLUDE(manifest) -->
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
#
|
||||
# Copyright (C) The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# Copyright (C) 2010 The Android Open Source Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
cmake_minimum_required(VERSION 4.1.0)
|
||||
project(NativeActivity LANGUAGES C CXX)
|
||||
|
||||
include(AppLibrary)
|
||||
include(AndroidNdkModules)
|
||||
|
||||
# This includes the AndroidNdkModules.cmake file, which is shipped with the
|
||||
# NDK's CMake distribution. Including this file defines
|
||||
# android_ndk_import_module_native_app_glue(), which defines the
|
||||
# native_app_glue target when called.
|
||||
include(AndroidNdkModules)
|
||||
android_ndk_import_module_native_app_glue()
|
||||
|
||||
add_app_library(native-activity SHARED main.cpp)
|
||||
|
||||
# Linking the native_app_glue target with our native-activity target (the
|
||||
# library which contains the main app code) includes native_app_glue in the app.
|
||||
target_link_libraries(native-activity
|
||||
android
|
||||
# We have to use $<LINK_LIBRARY:WHOLE_ARCHIVE,native_app_glue> rather than
|
||||
# the simpler native_app_glue spelling to instruct the linker that the
|
||||
# entire native_app_glue static library should be included in
|
||||
# libnative-activity.so, even if the linker does not find any calls in our
|
||||
# code to native_app_glue. This is because native_app_glue is a static
|
||||
# library rather than a shared library, and normally the linker will only
|
||||
# include code from static libraries when it has found a call to that code.
|
||||
# This is usually a good thing because it reduces the size of the app, but
|
||||
# in this case the calls to native_app_glue, specifically the
|
||||
# ANativeActivity_onCreate function, don't come from us, but instead come
|
||||
# from ANativeActivity, which the linker cannot detect.
|
||||
$<LINK_LIBRARY:WHOLE_ARCHIVE,native_app_glue>
|
||||
log
|
||||
)
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
LIBNATIVEACTIVITY {
|
||||
global:
|
||||
# When using NativeActivity and you don't need any of your own JNI
|
||||
# functions this is the only symbol that should be exported. If your app
|
||||
# needs additional JNI functions (and most apps will), then you'll need to
|
||||
# include JNI_OnLoad (or your individual Java_... functions if you're not
|
||||
# using RegisterNatives) here.
|
||||
ANativeActivity_onCreate;
|
||||
local:
|
||||
*;
|
||||
|
||||
@@ -1,19 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
// Copyright (C) 2010 The Android Open Source Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#include <android/choreographer.h>
|
||||
#include <android/hardware_buffer.h>
|
||||
@@ -71,13 +57,30 @@ enum class Color : uint32_t {
|
||||
};
|
||||
|
||||
/**
|
||||
* Shared state for our app.
|
||||
* The implementation for our app.
|
||||
*
|
||||
* This class implements the activity lifecycle behaviors akin to how Activity
|
||||
* would in a Java app. With native_app_glue, those lifecycle events are instead
|
||||
* communicated to this class from engine_handle_cmd, which is in turned called
|
||||
* by looper (see the description below in android_main).
|
||||
*
|
||||
* The comments here will briefly explain some aspects of the Android activity
|
||||
* lifecycle, but they cannot explain it fully. See
|
||||
* https://developer.android.com/guide/components/activities/activity-lifecycle
|
||||
* and the other docs in that section for more information.
|
||||
*/
|
||||
class Engine {
|
||||
public:
|
||||
explicit Engine(android_app* app) : app_(app) {}
|
||||
|
||||
void AttachWindow() {
|
||||
// This is called whenever a new native window is created for our app, so we
|
||||
// need to reinitialize the buffer format to the format our render loop
|
||||
// expects.
|
||||
//
|
||||
// Attaching the window will not cause the app to start running its update
|
||||
// and render loop. The app's update cycle is separately enabled by
|
||||
// Engine::Resume.
|
||||
if (ANativeWindow_setBuffersGeometry(
|
||||
app_->window, 0, 0, AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM) < 0) {
|
||||
LOGE("Unable to set window buffer geometry");
|
||||
@@ -89,10 +92,29 @@ class Engine {
|
||||
last_update_ = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
void DetachWindow() { window_initialized = false; }
|
||||
void DetachWindow() {
|
||||
// This is called whenever the native window for our app is destroyed. That
|
||||
// does not necessarily mean that the app is being killed, as it is also
|
||||
// done when the screen rotates.
|
||||
//
|
||||
// For a more typical app where the rendering is done with OpenGL or Vulkan,
|
||||
// this is where you'd perform any window cleanup needed by those
|
||||
// frameworks. For our app, it's sufficient to just set a flag to disable
|
||||
// our render loop.
|
||||
window_initialized = false;
|
||||
}
|
||||
|
||||
/// Resumes ticking the application.
|
||||
void Resume() {
|
||||
// This is called whenever the activity is resumed (brought into the
|
||||
// foreground). When that happens, we schedule our next update tick with
|
||||
// Choreographer. Choreographer is the Android system that paces app render
|
||||
// loops. If you instead render new frames in a loop without frame pacing,
|
||||
// you risk rendering more quickly than the display pipeline is able to
|
||||
// present new frames. This will increase the latency between frame
|
||||
// submission and presentation.
|
||||
// https://developer.android.com/ndk/reference/group/choreographer
|
||||
|
||||
// Checked to make sure we don't double schedule Choreographer.
|
||||
if (!running_) {
|
||||
running_ = true;
|
||||
@@ -104,7 +126,12 @@ class Engine {
|
||||
///
|
||||
/// When paused, sensor and input events will still be processed, but the
|
||||
/// update and render parts of the loop will not run.
|
||||
void Pause() { running_ = false; }
|
||||
void Pause() {
|
||||
// This is called whenever something interrupts the activity and moves it
|
||||
// into the background. In multiwindow mode the app might still be visible,
|
||||
// but it is no longer the focused app and should pause accordingly,
|
||||
running_ = false;
|
||||
}
|
||||
|
||||
private:
|
||||
android_app* app_;
|
||||
@@ -172,6 +199,7 @@ class Engine {
|
||||
return;
|
||||
}
|
||||
|
||||
// Lock the native window's buffer so we can write to it.
|
||||
ANativeWindow_Buffer buffer;
|
||||
if (ANativeWindow_lock(app_->window, &buffer, nullptr) < 0) {
|
||||
LOGE("Unable to lock window buffer");
|
||||
@@ -186,34 +214,50 @@ class Engine {
|
||||
return;
|
||||
}
|
||||
|
||||
// Write a solid color to the window buffer.
|
||||
for (auto y = 0; y < buffer.height; y++) {
|
||||
for (auto x = 0; x < buffer.width; x++) {
|
||||
// Note that we index the row by the buffers stride, not its width. The
|
||||
// buffer itself may be wider than the render area.
|
||||
size_t pixel_idx = y * buffer.stride + x;
|
||||
reinterpret_cast<uint32_t*>(buffer.bits)[pixel_idx] =
|
||||
static_cast<uint32_t>(color_);
|
||||
}
|
||||
}
|
||||
|
||||
// Now unlock the buffer, causing the display to update.
|
||||
ANativeWindow_unlockAndPost(app_->window);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Process the next main command.
|
||||
* The callback for native_app_glue's Activity lifecycle event queue.
|
||||
*/
|
||||
static void engine_handle_cmd(android_app* app, int32_t cmd) {
|
||||
auto* engine = (Engine*)app->userData;
|
||||
// There are a lot of lifecycle events that we're ignoring here. See
|
||||
// android_native_app_glue.h for the complete list that native_app_glue
|
||||
// handles (which may not be complete if Activity adds new lifecycle
|
||||
// methods!)
|
||||
//
|
||||
// Most applications will need to handle many more of than just this set.
|
||||
// We're getting away with ignoring most events because this app doesn't do
|
||||
// anything interesting.
|
||||
switch (cmd) {
|
||||
case APP_CMD_INIT_WINDOW:
|
||||
// https://developer.android.com/ndk/reference/struct/a-native-activity-callbacks#onnativewindowcreated
|
||||
engine->AttachWindow();
|
||||
break;
|
||||
case APP_CMD_TERM_WINDOW:
|
||||
// https://developer.android.com/ndk/reference/struct/a-native-activity-callbacks#onnativewindowdestroyed
|
||||
engine->DetachWindow();
|
||||
break;
|
||||
case APP_CMD_GAINED_FOCUS:
|
||||
// https://developer.android.com/ndk/reference/struct/a-native-activity-callbacks#onwindowfocuschanged
|
||||
engine->Resume();
|
||||
break;
|
||||
case APP_CMD_LOST_FOCUS:
|
||||
// https://developer.android.com/ndk/reference/struct/a-native-activity-callbacks#onwindowfocuschanged
|
||||
engine->Pause();
|
||||
break;
|
||||
default:
|
||||
@@ -222,19 +266,53 @@ static void engine_handle_cmd(android_app* app, int32_t cmd) {
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the main entry point of a native application that is using
|
||||
* android_native_app_glue. It runs in its own thread, with its own
|
||||
* event loop for receiving input events and doing other things.
|
||||
* `android_main()` is the entry point for an app using `native_app_glue`.
|
||||
*
|
||||
* This function is called from a separate thread spawned from
|
||||
* `ANativeActivity_onCreate`, which is the native equivalent of the
|
||||
* `onCreate` stage in the activity lifecycle:
|
||||
* https://developer.android.com/guide/components/activities/activity-lifecycle
|
||||
*
|
||||
* The `android_main()` implementation typically will perform application setup,
|
||||
* enter the main event loop, and shut down if necessary.
|
||||
*/
|
||||
void android_main(android_app* state) {
|
||||
Engine engine{state};
|
||||
|
||||
state->userData = &engine;
|
||||
// onAppCmd is called whenever native_app_glue receives one of the activity
|
||||
// lifecycle events from the framework:
|
||||
// https://developer.android.com/guide/components/activities/activity-lifecycle
|
||||
//
|
||||
// Typical native Android applications would implement the various
|
||||
// onPause(), onResume(), etc in JNI methods. native_app_glue handles that
|
||||
// for us and instead presents those method calls as if they were a pollable
|
||||
// event queue. Our engine_handle_cmd callback is the function that will
|
||||
// respond to new events in that queue.
|
||||
state->onAppCmd = engine_handle_cmd;
|
||||
|
||||
// The userData property will be passed to the callback we registered with
|
||||
// onAppCmd.
|
||||
state->userData = &engine;
|
||||
|
||||
// destroyRequested will be set when onDestroy() is called:
|
||||
// https://developer.android.com/guide/components/activities/activity-lifecycle#ondestroy
|
||||
while (!state->destroyRequested) {
|
||||
// Our input, sensor, and update/render logic is all driven by callbacks, so
|
||||
// we don't need to use the non-blocking poll.
|
||||
// native_app_glue communicates events to the app using Looper rather than
|
||||
// method calls like a Java activity would use. Looper is an Android API
|
||||
// similar to POSIX's select(2):
|
||||
// https://developer.android.com/ndk/reference/group/looper#alooper
|
||||
//
|
||||
// Whenever an activity lifecycle method is called on our ANativeActivity,
|
||||
// or an input event is received, native_app_glue will forward that to our
|
||||
// app as a looper event.
|
||||
//
|
||||
// Polling looper can be done in either a blocking or non-blocking manner.
|
||||
// If your app needs to wake periodically on this thread, pass a value for
|
||||
// the poll timeout. Most of the things you'd normally do during this loop
|
||||
// (respond to input or sensor updates, or even render the next frame of
|
||||
// your game) should be driven by callbacks registered with those
|
||||
// subsystems though rather than done here. Our main loop doesn't need to
|
||||
// do anything other than process looper events.
|
||||
android_poll_source* source = nullptr;
|
||||
auto result = ALooper_pollOnce(-1, nullptr, nullptr,
|
||||
reinterpret_cast<void**>(&source));
|
||||
@@ -246,4 +324,8 @@ void android_main(android_app* state) {
|
||||
source->process(state, source);
|
||||
}
|
||||
}
|
||||
|
||||
// Most cleanup code should actually run in response to activity lifecycle
|
||||
// events that are processed in engine_handle_cmd rather than after the main
|
||||
// loop exits, as would be more typical of main loops on desktop platforms.
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user