Developing mobile applications for Android is traditionally associated with Java or Kotlin, but what to do if you need maximum performance, working with low-level APIs or legacy code migration? This is where language C - the basis Android NDK (Native Development Kit) comes to the rescue. Despite the more complex development process compared to Android SDK, using C/C++ opens access to direct memory management, optimized calculations and cross-platform compatibility.

In this guide we will look at the entire creation cycle native Android application on C โ€”from setting up the environment to publishing to Google Play. You will learn how to integrate JNI (Java Native Interface) for communication between Javacode and native libraries, optimize performance and avoid common compilation errors. We will pay special attention compatibility with the latest versions of Android (14+)where the requirements for security and library architecture have changed.

If you are already familiar with the basics of programming on C and have a basic understanding about Android Studio, this material will help you move to the next level. For beginners, we have prepared checklists and explanations of key terms to facilitate immersion in the topic.

1. Why C for Android: Pros and Cons

Before diving into the technical details, it is important to understand when it is justified to use C instead of standard tools Android SDK. Main advantages:

  • โšก Performance: Native code runs 2โ€“10 times faster than Java/Kotlin, which is critical for games, video processing or scientific computing.
  • ๐Ÿ”„ Transferring legacy code: If you already have libraries on C/C++ (for example, for working with equipment), they can be integrated into an Android application without a complete rewrite.
  • ๐Ÿ› ๏ธ Low-level access: Working with OpenGL ES, Vulkan, hardware sensors or custom firmware requires native libraries.
  • ๐ŸŒ Cross-platform: The same Ccode can be used in projects for iOS, Linux or embedded systems.

However, this approach also has serious limitations:

  • ๐Ÿข Development complexity: Debugging native code requires knowledge GDB/LLDB, and integration with Java via JNI adds overhead.
  • ๐Ÿ”’ Security: Manual memory management is fraught with leaks (memory leaks) and vulnerabilities, especially in multi-threaded scenarios.
  • ๐Ÿ“ฆ Increasing APK size: Native libraries (.so-files) add 5-50 MB to the final package.
  • ๐Ÿ“ฑ Limited access to the API: Not all functions Android are available via NDK (for example, working with UI requires wrappers via Java).
โš ๏ธ Attention: Starting with Android 14, Google tightens the requirements for native libraries Applications using outdated versions NDK (below r25), may not pass the test Google Play Console. Always check the current requirements in official documentation.

For most tasks (for example, simple utilities or business applications) Kotlin remains the optimal choice. But if your project requires maximum performance or interaction with hardware, data-i="97">will become a powerful tool. C will become a powerful tool.

2. Setting up the development environment

To start writing on C for Android, you will need:

  1. Android Studio (version Giraffe 2022.3.1 or later).
  2. NDK (Native Development Kit) - a set of tools for compiling native code. Included in Android Studio, but requires separate installation via SDK Manager.
  3. CMake or ndk-build โ€” build systems for native libraries.
  4. LLDB โ€” debugger for native code (built into Android Studio).

Step-by-step guide:

  1. Install Android Studio s official website. During installation, check the boxes Android Native Development and C++ Support.
  2. Open SDK Manager (Tools โ†’ SDK Manager) and install:
    • ๐Ÿ“Œ NDK (Side by side) โ€” the latest stable version (at the time of writing - r26).
    • ๐Ÿ“Œ CMake (version 3.22.1 or higher).
    • ๐Ÿ“Œ LLDB and Ninja (to speed up the build).
  • Create a new project in Android Studioby selecting a template Native C++ (it will automatically generate templates for JNI).
  • The latest version of Android Studio is installed|NDK p26+ is available in the SDK Manager|CMake is enabled in the project settings|A test project has been created with the Native C++ template

    -->

    After configuration, check that NDK is correctly connected. line: build.gradle (Module: app) and make sure that in the block android there is a line:

    android {
    

    ...

    externalNativeBuild {

    cmake {

    path "src/main/cpp/CMakeLists.txt"

    version "3.22.1"

    }

    }

    }

    โš ๏ธ Attention: If you use Windows, the path to NDK should not contain spaces or Cyrillic characters. Otherwise, CMake may produce errors during assembly. It is recommended to install Android Studio in. root directory of the disk (for example, C:\Android\).

    3. Project structure and first native code

    The project with native code on C has the following structure:

    MyApp/
    

    โ”œโ”€โ”€ app/

    โ”‚ โ”œโ”€โ”€ src/

    โ”‚ โ”‚ โ”œโ”€โ”€ main/

    โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ java/ # Java/Kotlin code

    โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ cpp/ # Native C/C++ code

    โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ native-lib.cpp

    โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ CMakeLists.txt

    โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ res/ # Resources

    โ”‚ โ”‚ โ””โ”€โ”€ AndroidManifest.xml

    โ”‚ โ””โ”€โ”€ build.gradle

    Main files:

    • ๐Ÿ“„ native-lib.cpp โ€”the native code is written here for C/C++.
    • ๐Ÿ“„ CMakeLists.txt โ€”the build configuration for CMake.
    • ๐Ÿ“„ MainActivity.java (or .kt)โ€”the entry point into the application where the native code is called via JNI.

    Minimal code example for C (native-lib.cpp):

    #include <jni.h>
    

    #include <string>

    // Function that will be called from Java

    extern "C" JNIEXPORT jstring JNICALL

    Java_com_example_myapp_MainActivity_stringFromJNI(

    JNIEnv* env,

    jobject / this /) {

    std::string hello = "Hello from C++ (compiled with NDK " + std::string(ANDROID_NDK_VERSION) + ")";

    return env->NewStringUTF(hello.c_str());

    }

    Pay attention to:

    • ๐Ÿ”น JNIEXPORT and JNICALL โ€”macros for exporting a function.
    • ๐Ÿ”น JNIEnv* โ€” pointer to the environment JNIthrough which we interact with Java.
    • ๐Ÿ”น The function name is formed according to the template: Java_{package}_{class}_{method}.

    In file MainActivity.java add a declaration of a native method:

    public class MainActivity extends AppCompatActivity {
    

    // Declaration of a native method

    public native String stringFromJNI();

    // Loading a native library

    static {

    System.loadLibrary("myapp");

    }

    @Override

    protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main);

    // Calling a native method

    TextView tv = findViewById(R.id.sample_text);

    tv.setText(stringFromJNI());

    }

    }

    javac -h ./jni com/example/myapp/MainActivity.java

    This will save time when working with large projects.-->

    4. CMake configuration and project build

    File CMakeLists.txt defines how the native code will be built. Minimum configuration:

    cmake_minimum_required(VERSION 3.22.1)
    
    

    project("myapp")

    Create a native library

    add_library(

    myapp

    SHARED

    native-lib.cpp

    )

    Indicate that the library is intended for Android

    find_library(

    log-lib

    log

    )

    Include a log library (for android.util.Log)

    target_link_libraries(

    myapp

    ${log-lib}

    )

    Key parameters:

    Parameter Description Example value
    SHARED Library type (dynamic) SHARED (for .so)
    STATIC Library type (static) STATIC (for .a)
    find_library Search for system libraries (for example, liblog.so for logs) find_library(log-lib log)
    target_link_libraries Connecting dependencies target_link_libraries(myapp ${log-lib})
    ANDROID_NDK Environment variable with path to NDK $ANDROID_NDK_HOME

    After settings CMakeLists.txt synchronize the project with Gradle (button Sync Project with Gradle Files to Android Studio). If everything is configured correctly, when building the project (Build โ†’ Make Project) in the app/.externalNativeBuild/cmake/ compiled libraries will appear libmyapp.so for different architectures (arm64-v8a, armeabi-v7a, x86_64).

    โš ๏ธ Attention: By default CMake compiles libraries for all supported architectures, which increases the size APK. To optimize the size, edit build.gradle, leaving only the necessary ABIs:
    android {
    

    defaultConfig {

    ndk {

    abiFilters 'arm64-v8a', 'armeabi-v7a'

    }

    }

    }

    Only arm64-v8a (modern devices)|arm64-v8a + armeabi-v7a (maximum compatibility)|I add x86_64 for emulators|All available ABIs (optimization is not important size)-->

    5. Working with JNI: transferring data between Java and C

    JNI (Java Native Interface) is a bridge between Java/Kotlin and native code. It allows you to:

    • ๐Ÿ”„ Call functions C from Java.
    • ๐Ÿ“ค Pass primitive types (int, float), strings and objects.
    • ๐Ÿ”ง Manage memory and exceptions.

    Basic data types JNI:

    Java type JNI type Description
    int jint 32-bit integer
    String jstring String in UTF-16 encoding
    Object jobject Any Java object
    int[] jintArray Array of integers
    boolean jboolean Boolean value (0 or 1)

    Example of passing an array from Java to C and back:

    Java (MainActivity.java):

    public native int[] processArray(int[] input);
    
    

    // Call from onCreate()

    int[] data = {1, 2, 3, 4, 5};

    int[] result = processArray(data);

    C (native-lib.cpp):

    extern "C" JNIEXPORT jintArray JNICALL
    

    Java_com_example_myapp_MainActivity_processArray(JNIEnv* env, jobject obj, jintArray array) {

    // Get the length of the array

    jsize length = env->GetArrayLength(array);

    // Get a pointer to the elements

    jint* body = env->GetIntArrayElements(array, nullptr);

    // We process the data (for example, multiply by 2)

    for (int i = 0; i < length; i++) {

    body[i] *= 2;

    }

    // Create a new array for the result

    jintArray result = env->NewIntArray(length);

    env->SetIntArrayRegion(result, 0, length, body);

    // Release resources

    env->ReleaseIntArrayElements(array, body, 0);

    return result;

    }

    What will happen if you do not call ReleaseIntArrayElements?

    If you do not release the array using ReleaseIntArrayElements, this will lead to a memory leak. Moreover, JNI can lock the array on the heap Javawhich will cause errors the next time it is accessed from Javacode. Always check the return value of Get*functions on nullptr!

    Important rules for working with JNI:

    • ๐Ÿ”น All objects created in C (for example, through NewStringUTF) must be deleted manually (for example, DeleteLocalRef), otherwise a memory leak will occur.
    • ๐Ÿ”น Avoid frequent transitions between Java and C โ€”this slows down the work. Group data into large blocks.
    • ๐Ÿ”น For multi-threaded applications, use AttachCurrentThreadto bind a thread to JVM.

    6. Debugging native code

    Debugging Ccode on Android is more difficult than Java, but Android Studio provides the necessary tools:

    1. Setting breakpoints: Open the file native-lib.cpp and put a breakpoint on the desired line.
    2. Connecting a debugger:
      • ๐Ÿ“ฑ Run the application on the device in debugging mode (Run โ†’ Debug 'app').
      • ๐Ÿ› ๏ธ In the log, select the process of your application and connect via Attach Debugger to Android Process.
  • View variables: The window Debug will display variable values C.
  • If the debugger does not connect:

    • ๐Ÿ”น Make sure that debugging is enabled on the device USB (Settings โ†’ For developers โ†’ USB debugging).
    • ๐Ÿ”น Check that the build.gradle flag is set debuggable true.
    • ๐Ÿ”น For x86emulators, you may need to install Intel HAXM.

    To output logs from native code, use __android_log_print:

    #include <android/log.h>
    
    

    #define LOG_TAG "MyAppNative"

    #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)

    void nativeFunction() {

    LOGD("This message is from native code!");

    }

    You can view logs via Logcat in Android Studio (filter by tag MyAppNative).

    โš ๏ธ Attention: Logs from native code are not displayed in Logcatif the application is built in Releasemode. For debugging, always use Debug-assembly.
    ๐Ÿ’ก

    To speed up debugging of native code, use LLDB-commands directly in the console Android StudioFor example, print *env will show. structure JNIEnv, and bt is the call stack.

    7. Optimization and common errors

    Native code requires special attention to optimization and security. Let's look at the key points:

    Performance optimization

    • ๐Ÿš€ Use -O3 or -Ofast: Add a compiler flag for maximum optimization: CMakeLists.txt add a compiler flag for maximum optimization:
      set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3")
    • ๐Ÿ”„ Minimize transitions through JNI: Combine small calls into one large one.
    • ๐Ÿง  Use SIMD instructions: For mathematical calculations, use NEON (for ARM) or SSE (for x86).

    Typical errors and how to avoid them

    Error Cause Solution
    unsatisfied link error Native function not found Check the function name in JNI (must match javah)
    Crash when working with strings Not freed jstring Use ReleaseStringUTFChars after GetStringUTFChars
    SIGSEGV (segmentation fault) Accessing invalid memory Check pointers and array boundaries
    Leaks memory Unreleased jobject Use DeleteLocalRef for temporary objects

    For performance analysis, use Android Profiler (View โ†’ Tool Windows โ†’ ProfilerThe tab CPU will show the time. spent on native code, and Memory - leaks.

    adb shell valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes /path/to/your/app

    This requires root access, but gives a detailed report.-->

    8. Build and publish the application

    When the application is ready, all that remains is to assemble it in APK or AAB (Android App Bundle) and publish to Google Play.

    Build steps:

    1. Check build.gradle:
      • ๐Ÿ”น minSdkVersion not below 21 (for support NDK).
      • ๐Ÿ”น targetSdkVersion not below 33 (requirements Google Play for 2026).
  • Collect Releaseversion:
    ./gradlew assembleRelease

    File APK will in app/build/outputs/apk/release/.

  • For AAB (recommended for Google Play):
    ./gradlew bundleRelease

    The file will appear in app/build/outputs/bundle/release/.

  • Publish on Google Play

    • ๐Ÿ“Œ Upload AAB in Google Play Console.
    • ๐Ÿ“Œ Make sure that all required permissions are specified in the manifest (for example, android.permission.INTERNET).
    • ๐Ÿ“Œ For applications with native code Google Play requires filling out the section 64-bit support (support arm64-v8a or x86_64).
    โš ๏ธ Attention: Starting from August 2026, Google Play blocks the downloading of new applications compiled with outdated versions NDK (below r25Update NDK and rebuild the project before publishing.
    ๐Ÿ’ก

    Use Android App Bundle (AAB) instead of APK โ€”this reduces the size of the file downloaded by the user due to the dynamic delivery of native libraries for a specific device architecture.

    FAQ: Frequently asked questions about C development for Android

    Is it possible to write the entire application in C, without Java/Kotlin?

    Technically yes, but it is extremely inconvenient. Android requires Java/Kotlin to work with UI, manifest and majority API. Native code is usually used for individual modules (for example, the game engine or data processing algorithms), and the main logic remains on Java/Kotlin.

    How to reduce the size of an APK with native libraries?

    Several ways:

    • ๐Ÿ”น Leave only the ones you need ABI (for example, arm64-v8a).
    • ๐Ÿ”น Use strip to remove debugging symbols:
      strip --strip-unneeded libmyapp.so
    • ๐Ÿ”น Divide the code into dynamically loaded libraries (.so), which are loaded on demand.

    How to debug native code on a real device?

    Connect device with debugging enabled, then: USB with debugging enabled, then:

    1. Run the application in debugging mode.
    2. In Android Studio select Run โ†’ Attach to Process and specify your application.
    3. Set breakpoints in .cppfiles.

    For advanced debugging, use gdbserver via adb:

    adb forward tcp:5039 tcp:5039
    

    gdbclient :5039

    What alternatives to JNI exist?

    If JNI seems too complex, consider:

    • ๐Ÿ”น Kotlin/Native - experimental support for multi-platform code.
    • ๐Ÿ”น Rust + android-ndk-rs - a secure alternative C with minimal overhead.
    • ๐Ÿ”น Go Mobile - for cross-platform applications.

    However, these solutions are less mature than NDK + JNI, and may have limitations.

    How to protect native code from reverse engineering?

    Native libraries (.so) are easier to decompile than Java-code. Basic protection measures:

    • ๐Ÿ”น Obfuscation: Use llvm-obfuscator or ollvm