Mobile Development 14 min read

Organizing Native Layer Code and Implementing Dynamic Loading/Unloading of .so Libraries in Android

The article outlines a systematic method for structuring native‑layer code and using dlopen, dlsym, and dlclose to dynamically load and unload .so libraries on Android, enabling selective loading, reduced memory usage, hot‑fix updates, and solutions to STL version, permission, and C++ name‑mangling challenges.

Tencent Music Tech Team
Tencent Music Tech Team
Tencent Music Tech Team
Organizing Native Layer Code and Implementing Dynamic Loading/Unloading of .so Libraries in Android

This article presents a systematic approach to organizing native‑layer code hierarchy and demonstrates how to achieve dynamic loading and unloading of shared libraries (.so) on Android. It also discusses the challenges encountered during implementation and provides practical solutions.

1. Why Dynamically Load .so Libraries in the Native Layer

As Android apps grow, performance‑critical features are often implemented in C/C++ and accessed from Java via JNI. To keep the native codebase manageable, developers split functionality into separate .so modules, using a dedicated JNI wrapper .so that Java loads. However, this static linking approach has drawbacks:

Hot‑fix scenarios require the ability to replace faulty .so files at runtime, which is not possible when the functional .so is only indirectly referenced through the JNI wrapper.

Even if a function is never called, the JNI wrapper forces all dependent .so files to be loaded, increasing resident memory.

To address these issues, the functional .so libraries should be loaded and unloaded directly from the native layer, allowing selective loading, reduced memory footprint, and easier hot‑fix replacement.

2. Implementation of Dynamic Loading in the Native Layer

In C/C++ the standard POSIX functions dlopen() , dlsym() and dlclose() (declared in <dlfcn.h> ) are used:

dlopen() opens a shared library and returns a handle.

dlsym() retrieves the address of a symbol (function or variable) from the handle.

dlclose() decrements the library’s reference count and unloads it when the count reaches zero.

The typical loading flow is:

Call dlopen() with the library path and a loading mode ( RTLD_NOW or RTLD_LAZY ). RTLD_LAZY is preferred for on‑demand loading.

Use dlsym() to obtain function pointers for the required symbols.

Invoke the retrieved functions as needed.

When finished, call dlclose() to unload the library.

If an error occurs, call dlerror() to obtain the diagnostic message.

3. Java Layer Invocation of Native Dynamic Loading

The native code is reorganized to include a common data‑definition .so that provides base classes and interfaces. The JNI wrapper .so loads functional .so files at runtime via dlopen() . The Java side supplies the library path to native code through a JNI call.

The loading process consists of:

Java provides the .so path; native code receives it via a JNI callback.

The JNI wrapper calls dlopen() on the functional .so, then uses dlsym() to obtain constructor and destructor functions (exported with extern "C" ), creates an instance, and stores the handle and destructor in a map keyed by the interface object.

When the library needs to be released, the map is consulted, the destructor is called, and dlclose() unloads the library.

4. Problems to Solve When Dynamically Loading .so Libraries

Inconsistent STL versions across native modules cause type mismatches (e.g., std::string ). The solution is to enforce a unified C++ runtime by setting APP_STL (e.g., stlport_static or stlport_shared ) in a common Application.mk .

Loading .so files from the SD card triggers Permission denied (UnsatisfiedLinkError) because the SD card is mounted without executable permissions. The fix is to copy the library to an internal directory (e.g., /data/data/your.app/lib ) before loading.

The POSIX dynamic‑loading functions are C‑oriented and do not handle C++ name mangling. To expose C++ classes, export C‑style factory functions with extern "C" that construct and destroy objects. The native side then uses these functions via dlsym() to manage class instances.

// Declare two interface function pointer types for creating and destroying the base class instance
    typedef BaseClass* (*createClassFcn)();
    typedef void (*destroyClassFcn)(BaseClass*);

    // Exported C functions in the dynamically loaded .so
    extern "C" SubClass* create_SubClass() {
        return new SubClass;
    }

    // Exported C function to destroy the object
    extern "C" void destroy_SubClass(SubClass* p) {
        delete p;
    }

    // Load the library and obtain function pointers
    void* libHandler = dlopen(libNameStr, RTLD_LAZY);
    if (!libHandler) {
        return ERROR;
    }
    createClassFcn p_create_fcn = (createClassFcn)dlsym(libHandler, "create_SubClass");
    const char* err = dlerror();
    if (err) {
        return ERROR;
    }
    destroyClassFcn p_destroy_fcn = (destroyClassFcn)dlsym(libHandler, "destroy_SubClass");
    err = dlerror();
    if (err) {
        return ERROR;
    }
    // Use the function pointers
    BaseClass* instance = p_create_fcn();
    p_destroy_fcn(instance);

5. Summary

After adopting dynamic .so loading, performance remains comparable to static linking while achieving higher modularity. Functional libraries become independent of the JNI wrapper, enabling hot‑fix replacement and optional unloading, which reduces the native layer’s resident memory.

NativeAndroidCdynamic loadingJNIdlopenShared Library
Tencent Music Tech Team
Written by

Tencent Music Tech Team

Public account of Tencent Music's development team, focusing on technology sharing and communication.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.