NativeActivity is added to Android from API 9, and used for games and apps that write almost of all logic with native code. The NativeActivity is used to pass basic Android app’s lifecycle to native code, and help them to manage its logic with Android app lifecycle aware. There are also some glue code from NDK for NativeActivity and real native logic to pass Android app’s lifecycle. This article will show the pipeline of passing Android app’s lifecycle from NativeActivity to native code.

Code base

AOSP android-12.0.0_r21

App sample

This article uses official NativeActivity sample for analyzing. If you are not familiar with NativeActivity, you can clone this project and run it with emulator to experience NativeActivity.

What does NativeActivity in pure Java world do?

frameworks/base/core/java/android/app/NativeActivity.java

Receive and pass lifecycle to native

The NativeActivity is an implementation of Activity, and used to receive Android app’s lifecycle:

public class NativeActivity extends Activity implements SurfaceHolder.Callback2,
        InputQueue.Callback, OnGlobalLayoutListener {
    //...
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        byte[] state = onSaveInstanceStateNative(mNativeHandle);
        if (state != null) {
            outState.putByteArray(KEY_NATIVE_SAVED_STATE, state);
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
        onStartNative(mNativeHandle);
    }
    //...
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        if (!mDestroyed) {
            onConfigurationChangedNative(mNativeHandle);
        }
    }

    @Override
    public void onLowMemory() {
        super.onLowMemory();
        if (!mDestroyed) {
            onLowMemoryNative(mNativeHandle);
        }
    }
    //...
}

The NativeActivity uses native methods, such as onLowMemoryNative, onConfigurationChangeNative etc, to pass lifecycle state to native. Those native methods are bound to JNI methods, and we will take look at those JNI methods at later part.

Load app’s native code

App may implements almost logic at native, so NativeActivity defines a contract for native library, and will load assigned native library at NativeActivity#onCreate:

// ...
/**
 * Optional meta-that can be in the manifest for this component, specifying
 * the name of the native shared library to load.  If not specified,
 * "main" is used.
 */
public static final String META_DATA_LIB_NAME = "android.app.lib_name";
    
/**
 * Optional meta-that can be in the manifest for this component, specifying
 * the name of the main entry point for this native activity in the
 * {@link #META_DATA_LIB_NAME} native code.  If not specified,
 * "ANativeActivity_onCreate" is used.
 */
public static final String META_DATA_FUNC_NAME = "android.app.func_name";
    
private static final String KEY_NATIVE_SAVED_STATE = "android:native_state";
// ...
@Override
protected void onCreate(Bundle savedInstanceState) {
    String libname = "main";
    String funcname = "ANativeActivity_onCreate";
    ActivityInfo ai;
    
    try {
        ai = getPackageManager().getActivityInfo(
                getIntent().getComponent(), PackageManager.GET_META_DATA);
        if (ai.metaData != null) {
            String ln = ai.metaData.getString(META_DATA_LIB_NAME);
            if (ln != null) libname = ln;
            ln = ai.metaData.getString(META_DATA_FUNC_NAME);
            if (ln != null) funcname = ln;
        }
    } catch (PackageManager.NameNotFoundException e) {
        throw new RuntimeException("Error getting activity info", e);
    }

    BaseDexClassLoader classLoader = (BaseDexClassLoader) getClassLoader();
    String path = classLoader.findLibrary(libname);

    if (path == null) {
        throw new IllegalArgumentException("Unable to find native library " + libname +
                                        " using classloader: " + classLoader.toString());
    }

    byte[] nativeSavedState = savedInstanceState != null
        ? savedInstanceState.getByteArray(KEY_NATIVE_SAVED_STATE) : null;

    mNativeHandle = loadNativeCode(path, funcname, Looper.myQueue(),
        getAbsolutePath(getFilesDir()), getAbsolutePath(getObbDir()),
        getAbsolutePath(getExternalFilesDir(null)),
        Build.VERSION.SDK_INT, getAssets(), nativeSavedState,
        classLoader, classLoader.getLdLibraryPath());

    if (mNativeHandle == 0) {
        throw new UnsatisfiedLinkError(
                "Unable to load native library \"" + path + "\": " + getDlError());
    }
    // ...
}

The NativeActivity#onCreate will read android.app.lib_name and android.app.func_name as so file name and native created method name from app’s meta data defined in AndroidManifest.xml. There is an example from official NativeActivity sample:

<!-- Tell NativeActivity the name of our .so -->
<meta-data android:name="android.app.lib_name"
        android:value="native-activity" />

After reading so file name and native created method name(maybe not existed), NativeActivity#onCreate uses BaseDexClassLoader#findLibrary to search so file’s full path. The so file will be found at extracted apk directory, if extractNativeLibs is enabled(default value is true); otherwise it will be found at apk file. We don’t discuss the occasion that system apps use so file added at /system/lib*, vendor/lib* or other supported so search paths.

If so file path is found, the NativeActivity#onCreate will call native method loadNativeCode to load so file to load native code.

Meets NativeActivity’s JNI

frameworks/base/core/jni/android_app_NativeActivity.cpp

Bind methods between Java and native

In previous part, we have saw NativeActivity in pure Java world logic to load native library and pass lifecycle with native methods, such as loadNativeCode. Those methods are defined in NativeActivity’s JNI part:

static const JNINativeMethod g_methods[] = {
    { "loadNativeCode",
        "(Ljava/lang/String;Ljava/lang/String;Landroid/os/MessageQueue;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILandroid/content/res/AssetManager;[BLjava/lang/ClassLoader;Ljava/lang/String;)J",
        (void*)loadNativeCode_native },
    { "getDlError", "()Ljava/lang/String;", (void*) getDlError_native },
    { "unloadNativeCode", "(J)V", (void*)unloadNativeCode_native },
    { "onStartNative", "(J)V", (void*)onStart_native },
    { "onResumeNative", "(J)V", (void*)onResume_native },
    // ...
};

static const char* const kNativeActivityPathName = "android/app/NativeActivity";

int register_android_app_NativeActivity(JNIEnv* env)
{
    //ALOGD("register_android_app_NativeActivity");
    jclass clazz = FindClassOrDie(env, kNativeActivityPathName);

    gNativeActivityClassInfo.finish = GetMethodIDOrDie(env, clazz, "finish", "()V");
    gNativeActivityClassInfo.setWindowFlags = GetMethodIDOrDie(env, clazz, "setWindowFlags",
                                                               "(II)V");
    gNativeActivityClassInfo.setWindowFormat = GetMethodIDOrDie(env, clazz, "setWindowFormat",
                                                                "(I)V");
    gNativeActivityClassInfo.showIme = GetMethodIDOrDie(env, clazz, "showIme", "(I)V");
    gNativeActivityClassInfo.hideIme = GetMethodIDOrDie(env, clazz, "hideIme", "(I)V");

    return RegisterMethodsOrDie(env, kNativeActivityPathName, g_methods, NELEM(g_methods));
}

The g_methods defines the bound relationship between native methods in NativeActivity.java and android_app_NativeActivity.cpp. For example, the method loadNativeCode_native in android_app_NativeActivity.cpp is the real implementation of loadNativeCode in NativeActivity.java.

Load native library code

The loadNativeCode_native uses OpenNativeLibrary in art/libnativeloader/native_loader.h to load native library found by BaseDexClassLoader in NativeActivity.java:

ScopedUtfChars pathStr(env, path);
std::unique_ptr<NativeCode> code;
bool needs_native_bridge = false;

char* nativeloader_error_msg = nullptr;
void* handle = OpenNativeLibrary(env,
                                sdkVersion,
                                pathStr.c_str(),
                                classLoader,
                                nullptr,
                                libraryPath,
                                &needs_native_bridge,
                                &nativeloader_error_msg);

Maybe you can notify the variable needs_native_bridge in loadNativeCode_native. needs_native_bridge is used for native bridge to determine whether need to load bridge libraries for ABI compatibility, for example loading x86_64 arch libraries for ARM arch libraries on Android-x86 platform.

Establish MessageQueue and Looper

Another work of loadNativeCode_native is to establish message queue with looper mechanism:

code->messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueue);
if (code->messageQueue == NULL) {
    g_error_msg = "Unable to retrieve native MessageQueue";
    ALOGW("%s", g_error_msg.c_str());
    return 0;
}

int msgpipe[2];
if (pipe(msgpipe)) {
    g_error_msg = android::base::StringPrintf("could not create pipe: %s", strerror(errno));
    ALOGW("%s", g_error_msg.c_str());
    return 0;
}
code->mainWorkRead = msgpipe[0];
code->mainWorkWrite = msgpipe[1];
int result = fcntl(code->mainWorkRead, F_SETFL, O_NONBLOCK);
SLOGW_IF(result != 0, "Could not make main work read pipe "
        "non-blocking: %s", strerror(errno));
result = fcntl(code->mainWorkWrite, F_SETFL, O_NONBLOCK);
SLOGW_IF(result != 0, "Could not make main work write pipe "
        "non-blocking: %s", strerror(errno));
code->messageQueue->getLooper()->addFd(
        code->mainWorkRead, 0, ALOOPER_EVENT_INPUT, mainWorkCallback, code.get());

It creates a pair pipe, one for writing, and one for reading. code->mainWorkWrite is used by native methods need passing values, such as android_NativeActivity_setWindowFlags in android_app_NativeActivity.cpp:

void android_NativeActivity_setWindowFlags(
        ANativeActivity* activity, int32_t values, int32_t mask) {
    NativeCode* code = static_cast<NativeCode*>(activity);
    write_work(code->mainWorkWrite, CMD_SET_WINDOW_FLAGS, values, mask);
}

and code->mainWorkRead is used for looper of message queue to receive pipe data from code->mainWorkWrite and trigger a callback method calling of looper. The real callback is ANativeActivityCallbacks defined in frameworks/native/include/android/native_activity.h. And loadNativeCode_native calls code->createActivityFunc(code.get(), rawSavedState, rawSavedSize) to call defined ANativeActivity initialized methods or default ANativeActivity_onCreate to initialize ANativeActivityCallbacks. We have saw logic to parse android.app.func_name at NativeActivity.java’s onCreate method, and its default value is ANativeActivity_onCreate.

After searching and analyzing, android_NativeActivity_setWindowFlags looks like called by native test code finally(for example external/deqp/framework/platform/android/tcuAndroidTestActivity.cpp). In another word, code->mainWorkWrite is used for native code.

Initialize JNI environment

The third thing that loadNativeCode_native does is to initialize JNI env for native code:

code->env = env;
code->clazz = env->NewGlobalRef(clazz);

const char* dirStr = env->GetStringUTFChars(internalDataDir, NULL);
code->internalDataPathObj = dirStr;
code->internalDataPath = code->internalDataPathObj.string();
env->ReleaseStringUTFChars(internalDataDir, dirStr);

if (externalDataDir != NULL) {
    dirStr = env->GetStringUTFChars(externalDataDir, NULL);
    code->externalDataPathObj = dirStr;
    env->ReleaseStringUTFChars(externalDataDir, dirStr);
}
code->externalDataPath = code->externalDataPathObj.string();

code->sdkVersion = sdkVersion;

code->javaAssetManager = env->NewGlobalRef(jAssetMgr);
code->assetManager = NdkAssetManagerForJavaObject(env, jAssetMgr);

It initializes AssetManager for native code too. And AssetManager will provide the ability to load Android resources for native code.

Pass app lifecycle

In NativeActivity.java, it calls onPauseNative at onPause method to pass pause state to native. And onPause_native in android_app_NativeActivity.cpp is the real implementation of onPauseNative:

static void
onPause_native(JNIEnv* env, jobject clazz, jlong handle)
{
    if (kLogTrace) {
        ALOGD("onPause_native");
    }
    if (handle != 0) {
        NativeCode* code = (NativeCode*)handle;
        if (code->callbacks.onPause != NULL) {
            code->callbacks.onPause(code);
        }
    }
}

onPause_native calls onPause of ANativeActivityCallbacks to pass pause state to app’s native code. We know ANativeActivity_onCreate in app native code will initialize those callbacks, but where is ANativeActivity_onCreate implementation?

NDK’s glue code

prebuilts/ndk/current/sources/android/native_app_glue/android_native_app_glue.c

At android_native_app_glue.c, we can see the implementation of ANativeActivity_onCreate and the initialization of callbacks, including onPause:

//...
static void onPause(ANativeActivity* activity) {
    LOGV("Pause: %p", activity);
    android_app_set_activity_state(ToApp(activity), APP_CMD_PAUSE);
}
//...
static void android_app_set_activity_state(struct android_app* android_app, int8_t cmd) {
    pthread_mutex_lock(&android_app->mutex);
    android_app_write_cmd(android_app, cmd);
    while (android_app->activityState != cmd) {
        pthread_cond_wait(&android_app->cond, &android_app->mutex);
    }
    pthread_mutex_unlock(&android_app->mutex);
}
//...
JNIEXPORT
void ANativeActivity_onCreate(ANativeActivity* activity, void* savedState, size_t savedStateSize) {
    LOGV("Creating: %p", activity);

    activity->callbacks->onConfigurationChanged = onConfigurationChanged;
    activity->callbacks->onContentRectChanged = onContentRectChanged;
    activity->callbacks->onDestroy = onDestroy;
    activity->callbacks->onInputQueueCreated = onInputQueueCreated;
    activity->callbacks->onInputQueueDestroyed = onInputQueueDestroyed;
    activity->callbacks->onLowMemory = onLowMemory;
    activity->callbacks->onNativeWindowCreated = onNativeWindowCreated;
    activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed;
    activity->callbacks->onNativeWindowRedrawNeeded = onNativeWindowRedrawNeeded;
    activity->callbacks->onNativeWindowResized = onNativeWindowResized;
    activity->callbacks->onPause = onPause;
    activity->callbacks->onResume = onResume;
    activity->callbacks->onSaveInstanceState = onSaveInstanceState;
    activity->callbacks->onStart = onStart;
    activity->callbacks->onStop = onStop;
    activity->callbacks->onWindowFocusChanged = onWindowFocusChanged;

    activity->instance = android_app_create(activity, savedState, savedStateSize);
}

When onPause in android_native_app_glue.c is called, it will send APP_CMD_PAUSE command to app’s native code with method named android_app_set_activity_state and android_app_write_cmd. Actually, android_app_write_cmd use pair pipe to pass data too, and we can see the initialization at android_app_create in android_native_app_glue.c. We know android_app_create is called at the end of ANativeActivity_onCreate, and it will create the instance of android_app defined in prebuilts/ndk/current/sources/android/native_app_glue/android_native_app_glue.h, and return it to activity->instance.

One thing about android_app_create should be pointed out specifically: this method creates custom thread for android_app:

pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&android_app->thread, &attr, android_app_entry, android_app);

// Wait for thread to start.
pthread_mutex_lock(&android_app->mutex);
while (!android_app->running) {
   pthread_cond_wait(&android_app->cond, &android_app->mutex);
}
pthread_mutex_unlock(&android_app->mutex);

So app’s native code will run in separate thread, and it is the reason that android_app needs pair pipe for communication.

Go to app finally

https://github.com/android/ndk-samples/tree/master/native-activity

android_native_app_glue.c is provided by NDK, and app can use it directly like native-activity’s app/src/main/cpp/CMakeLists.txt:

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

And android_app_create in android_native_app_glue.c calls pthread_create to run android_thread_entry method in single thread:

pthread_create(&android_app->thread, &attr, android_app_entry, android_app);

And android_app_entry will call android_main methods in native library to run app’s native code:

android_main(android_app);

If app’s native code implements android_main method, and it will be used as native code entry. We can see the implementation of native-activity at app/src/main/cpp/main.cpp:

/**
 * 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.
 */
void android_main(struct android_app* state) {
    struct engine engine{};

    memset(&engine, 0, sizeof(engine));
    state->userData = &engine;
    state->onAppCmd = engine_handle_cmd;
    state->onInputEvent = engine_handle_input;
    engine.app = state;
    //...
}

native-activity uses its engine_handle_cmd method as callback for app command, including app lifecycle. The onAppCmd is bound to looper at android_app_entry in android_native_app_glue.c:

ALooper* looper = ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS);
ALooper_addFd(looper, android_app->msgread, LOOPER_ID_MAIN, ALOOPER_EVENT_INPUT, NULL,
        &android_app->cmdPollSource);
android_app->looper = looper;

When there are events coming to looper, android_main in app/src/main/cpp/main.cpp will call process method to trigger onAppCmd calling(see process_cmd in android_native_app_glue.c):

while ((ident=ALooper_pollAll(engine.animating ? 0 : -1, nullptr, &events,
                                (void**)&source)) >= 0) {

    // Process this event.
    if (source != nullptr) {
        source->process(state, source);
    }
    //...
}

The android_main initializes app’s native method as callback to process app commands. It also call ALooper_pollAll to wait looper events from NDK’s app glue and call source->process to leverage app glue’s process to call onAppCmd implementation, app’s engine_handle_cmd to process app lifecycle and other commands. The source is android_poll_source instance, and android_poll_source is defined in NDK’s app_native_app_glue.h, so app’s native library works under NDK’s app glue.

App’s engine_handle_cmd is responsible for passing commands. Unfortunately, native-activity doesn’t process APP_CMD_PAUSE, we can take look at another command:

case APP_CMD_INIT_WINDOW:
    // The window is being shown, get it ready.
    if (engine->app->window != nullptr) {
        engine_init_display(engine);
        engine_draw_frame(engine);
    }
    break;

When APP_CMD_INIT_WINDOW coming, it will call engine_init_display to initialize OpenGL ES environment, and call engine_draw_frame after it to draw contents. Other app commands use similar process and logic to process.

Share Surface between Java and native code

App’s native code uses OpenGL ES and EGL to draw contents on Android’s surface:

surface = eglCreateWindowSurface(display, config, engine->app->window, nullptr);

And it uses engine->app->window, aka android_app->window(see android_native_app_glue.h), as Android’s surface for eglCreateWindowSurface. So where does android_app->window comes from?

android_app->window is initialized with android_app->pendingWindow by android_app_pre_exec_cmd in android_native_app_glue.c. And android_app->pendingWindow is initialized by android_app_set_window in android_native_app_glue.c. The android_app_set_window is called by onNativeWindowCreated of android_native_app_glue.c.

If we come back to NativeActivity.java, we know NativeActivity.java uses getWindow().takeSurface(this) to take ownership of window surface that NativeActivity attached to, and call native callbacks when surface lifecycle changed, including surface created:

public void surfaceCreated(SurfaceHolder holder) {
    if (!mDestroyed) {
        mCurSurfaceHolder = holder;
        onSurfaceCreatedNative(mNativeHandle, holder.getSurface());
    }
}

The implementation of onSurfaceCreatedNative in android_app_NativeActivity.cpp will convert surface passed from Java to ANativeWindow:

//...
void setSurface(jobject _surface) {
    if (_surface != NULL) {
        nativeWindow = android_view_Surface_getNativeWindow(env, _surface);
    } else {
        nativeWindow = NULL;
    }
}
//...
static void
onSurfaceCreated_native(JNIEnv* env, jobject clazz, jlong handle, jobject surface)
{
    if (kLogTrace) {
        ALOGD("onSurfaceCreated_native");
    }
    if (handle != 0) {
        NativeCode* code = (NativeCode*)handle;
        code->setSurface(surface);
        if (code->nativeWindow != NULL && code->callbacks.onNativeWindowCreated != NULL) {
            code->callbacks.onNativeWindowCreated(code,
                    code->nativeWindow.get());
        }
    }
}
//...

onSurfaceCreated_native calls onNativeWindowCreated finally to pass ANativeWindow instance, converted from surface passed from Java part, to android_app_set_window. With previous analyzing, the window surface that NativeActivity.java bound to, will passed to android_app->window, and used for egl and OpenGL ES drawing. There is an official article called EGLSurfaces and OpenGL ES that describes the relationship between egl, OpenGL ES and ANativeWindow, and you can read it you have interested in it.

Custom NativeActivity.java

Actually, we can implement NativeActivity.java and add our custom logic to it. But we should take care about android:hasCode in AndroidManifest.xml’s <application> tag. If hasCode is false, frameworks will not load any custom Java code, and our customized NativeActivity.java will not be used. We must set it to true if we have customized NativeActivity.java or other Java code.

Summary

I only analyze app lifecycle process and surface sharing between Java and native code briefly at this article, and I don’t analyze more detailed content, because skeleton of NativeActivity is enough for me. NativeActivity provides a mechanism to pass Android’s specific lifecycle and input mechanism to app’s native code with NDK’s app glue. NativeActivity make native library Android platform aware and provide the ability to get platform related surface for egl and OpenGL ES drawing for app’s native library. It’s very useful and important for cross-platform GUI apps to add minimum platform related codes to let itself run Android platform too. If you have similar need, NativeActivity can be a candidate solution for you.