Topic 1: Android Operating Systems Architecture
📖 8 min read · 🎯 Beginner · 🧭 Prerequisites: None
Why this matters
Before you write a single line of Android code, let me show you what's actually running underneath it. Most beginners jump straight into building screens — and that's fine — but the moment something breaks or behaves strangely, you'll wish you knew what was holding everything up. Android isn't just an app launcher. It's a carefully stacked system, from the Linux Kernel at the very bottom all the way to the apps you use every day. Understanding these layers — including where the Native Development Kit fits in — will make you a sharper developer from day one.
What You'll Learn
- The five-layer architecture of the Android operating system and what each layer is responsible for
- When and why to use the Android Native Development Kit (NDK) over pure Java/Kotlin
- How to set up NDK, write C++ native functions, and link them to your Java/Kotlin code via JNI
- How to manage memory in native code and debug native functions inside Android Studio
- How to apply native code to a real use case: pixel-level image processing with
android/bitmap.h
The Analogy
Think of Android's architecture like a modern skyscraper. The Linux Kernel is the bedrock and building codes — invisible to tenants, but everything depends on it for power, plumbing, and security. The Hardware Abstraction Layer is the building's standardized electrical sockets: no matter which brand of appliance (hardware chip) you plug in, the interface is always the same. The Android Runtime is the elevator system — it gets your bytecode from the loading dock up to the floor where it runs. The Application Framework is the front desk and concierge: it hands your app a keycard for the UI, lifecycle events, and resources. Finally, the Applications are the tenants — the Phone app, Contacts, and every app a user downloads from the Play Store, all living on the top floors, blissfully unaware of the plumbing below.
Chapter 1: The Five Layers of Android OS Architecture
Android's architecture is deliberately modular and flexible, which is why it can run on phones, tablets, TVs, watches, and cars. Every device shares the same layered stack.
Layer 1 — Linux Kernel
The foundation of Android. It handles:
- Security — process isolation, permissions enforcement
- Memory management — allocation and reclamation across all processes
- Process management — scheduling, forking, killing processes
- Network stack — TCP/IP, Wi-Fi, Bluetooth at the OS level
- Device drivers — display, camera, audio, USB, and more
Layer 2 — Hardware Abstraction Layer (HAL)
The HAL standardizes the interface between Android's upper layers and the hardware beneath. Because different manufacturers ship different chips, cameras, and radios, the HAL gives the Android framework a single, consistent surface to talk to — no matter what's underneath.
Layer 3 — Android Runtime (ART) and Core Libraries
ART executes the bytecode generated from Java and Kotlin source files. It replaced the older Dalvik runtime and introduced ahead-of-time (AOT) compilation for faster app start times and smoother performance. The Core Libraries provide essential APIs — collections, I/O, concurrency — that every app relies on.
Layer 4 — Application Framework
This is the layer most Android developers interact with daily. It exposes higher-level services and APIs for:
- UI management — Views, ViewGroups, RecyclerView
- Resource management — strings, drawables, layouts
- Lifecycle management — Activity, Fragment, Service lifecycles
- Content Providers, Broadcast Receivers, Notifications, and more
Layer 5 — Applications
The topmost layer — what users see and touch. This includes system apps (Phone, Contacts, Settings) bundled by the manufacturer, and third-party apps installed from the Google Play Store or sideloaded.
graph TD
A["Applications\n(Phone, Contacts, Your App)"]
B["Application Framework\n(Activity Manager, Window Manager, Content Providers)"]
C["Android Runtime (ART) + Core Libraries"]
D["Hardware Abstraction Layer (HAL)"]
E["Linux Kernel\n(Security · Memory · Process · Network · Drivers)"]
A --> B
B --> C
C --> D
D --> E
Chapter 2: Why Go Native — The Android NDK
Most Android apps are written entirely in Java or Kotlin, and that's perfectly fine. But there are scenarios where dropping into C or C++ using the Android Native Development Kit (NDK) gives you capabilities you simply can't get from the managed runtime alone.
Reasons to Use the NDK
- Performance — Native code can be hand-tuned for performance-critical work: graphics processing pipelines, real-time audio processing, physics simulations, and complex mathematical computations. The compiler can emit SIMD instructions and other hardware-specific optimizations that ART cannot.
- Existing Libraries — If your team already has battle-tested C or C++ libraries (image codecs, signal processing, game engines), the NDK lets you reuse them directly instead of rewriting them in Kotlin.
- Hardware Access — Native code provides lower-level access to hardware features — useful when you need to squeeze every cycle out of the device.
The NDK is a power tool, not a default. Use it when profiling reveals a genuine bottleneck, not as a first instinct.
Chapter 3: Setting Up the NDK
Step 1 — Install NDK via Android Studio
Open Android Studio and navigate to:
File > Project Structure > SDK Location
From there, download the NDK version that matches your project requirements. Android Studio manages NDK versions alongside the SDK.
Step 2 — Configure Gradle
Update your module-level build.gradle to tell the build system where to find your CMake configuration and which C++ standard to use:
android {
defaultConfig {
externalNativeBuild {
cmake {
cppFlags "-std=c++11"
}
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
}
}
}
Chapter 4: Creating a Native Library
Let's build a simple native library that exposes an add function to Java.
Step 1 — Write the C++ Source File
Place your native source files under src/main/cpp/. The function name follows the JNI naming convention: Java_<package>_<class>_<method>.
// src/main/cpp/native-lib.cpp
#include <jni.h>
extern "C" JNIEXPORT jint JNICALL
Java_com_example_myapp_MainActivity_add(JNIEnv* env, jobject /* this */, jint a, jint b) {
return a + b;
}
Step 2 — Configure CMake
Create a CMakeLists.txt file that tells CMake how to compile and link your library:
# src/main/cpp/CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
add_library(
native-lib # Name of the output library
SHARED # Shared library (.so)
native-lib.cpp
)
find_library(
log-lib
log # Links to Android's log library
)
target_link_libraries(
native-lib
${log-lib}
)
Step 3 — Load the Library in Java
Declare the native method in your Java class and load the .so library in a static block:
// MainActivity.java
package com.example.myapp;
import android.os.Bundle;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
int result = add(3, 5);
TextView textView = findViewById(R.id.sample_text);
textView.setText("Result of addition: " + result);
}
public native int add(int a, int b);
}
Chapter 5: Android Native Development Concepts
JNI — Java Native Interface
JNI is the bridge between the JVM world (Java/Kotlin) and the native world (C/C++/Assembly). It defines the conventions for:
- Naming — the
Java_<package>_<class>_<method>convention that lets the runtime locate your function - Type mapping —
jint,jstring,jobject,jbyteArrayetc. are JNI's representations of Java types in C - Environment pointer — every JNI function receives a
JNIEnv*that provides access to JVM operations like creating objects, throwing exceptions, and accessing fields
JNI is essential for any NDK integration. Without it, native code has no way to communicate with the Android runtime.
Memory Management in Native Code
This is the biggest operational difference between managed Java/Kotlin and native C/C++:
| Concern | Java / Kotlin | C / C++ (Native) |
|---|---|---|
| Allocation | new Object() — GC tracked | malloc() / new — manual |
| Deallocation | Garbage collector handles it | free() / delete — your responsibility |
| Memory leaks | Rare (GC collects unreachable objects) | Common if you forget to free |
| Dangling pointers | Not possible | Possible — causes crashes |
Careful native memory management is critical. Failing to call free or delete for every allocation will cause memory leaks that grow over time. Freeing the same pointer twice causes undefined behavior.
Debugging Native Code
Android Studio supports full native debugging. You can:
- Set breakpoints in C++ files the same way you do in Java
- Inspect variables — local C++ variables appear in the Variables panel
- Step through native code line by line with the debugger
- View the native call stack alongside the Java stack
To get debugging symbols, ensure your ndk-build or CMake configuration includes them. For CMake, build in Debug mode or add:
set(CMAKE_BUILD_TYPE Debug)
Chapter 6: Example — Native Image Processing
Let's apply what we know to a real-world task: pixel-level image inversion using android/bitmap.h.
Step 1 — Native Image Processing Function
// src/main/cpp/native-lib.cpp
#include <jni.h>
#include <android/bitmap.h>
extern "C" JNIEXPORT void JNICALL
Java_com_example_myapp_MainActivity_processImage(JNIEnv* env, jobject /* this */, jobject bitmap) {
AndroidBitmapInfo info;
void* pixels;
if (AndroidBitmap_getInfo(env, bitmap, &info) < 0) return;
if (AndroidBitmap_lockPixels(env, bitmap, &pixels) < 0) return;
// Simple inversion filter: inverts each R, G, B channel per pixel
for (int y = 0; y < info.height; ++y) {
uint32_t* line = (uint32_t*)pixels;
for (int x = 0; x < info.width; ++x) {
uint32_t pixel = line[x];
uint32_t r = 255 - ((pixel >> 16) & 0xFF);
uint32_t g = 255 - ((pixel >> 8) & 0xFF);
uint32_t b = 255 - (pixel & 0xFF);
line[x] = (0xFF << 24) | (r << 16) | (g << 8) | b;
}
pixels = (char*)pixels + info.stride;
}
AndroidBitmap_unlockPixels(env, bitmap);
}
Key NDK APIs used here:
AndroidBitmap_getInfo— reads width, height, stride, and pixel format from the JavaBitmapobjectAndroidBitmap_lockPixels— pins the pixel buffer in memory so native code can safely read/write itAndroidBitmap_unlockPixels— releases the lock; always call this or the bitmap becomes permanently inaccessible
Step 2 — Java Side: Load and Display the Processed Bitmap
// MainActivity.java
package com.example.myapp;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.widget.ImageView;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.sample_image);
processImage(bitmap);
ImageView imageView = findViewById(R.id.image_view);
imageView.setImageBitmap(bitmap);
}
public native void processImage(Bitmap bitmap);
}
The bitmap is decoded from resources, passed to the native function which mutates it in place via locked pixels, then handed to the ImageView — no copy needed.
🧪 Try It Yourself
Task: Wire up the add native function end-to-end and confirm it works on a real device or emulator.
- Create a new Android project in Android Studio (Empty Activity, Java or Kotlin).
- Install the NDK via File > Project Structure > SDK Location.
- Add
src/main/cpp/native-lib.cppwith theaddfunction shown in Chapter 4. - Add
src/main/cpp/CMakeLists.txtwith the configuration from Chapter 4. - Update your module-level
build.gradlewith theexternalNativeBuildblock. - In
MainActivity, add thestatic { System.loadLibrary("native-lib"); }block and declarepublic native int add(int a, int b);. - Call
add(7, 8)inonCreateand display the result in aTextView.
Success criterion: You should see Result of addition: 15 rendered in the TextView on screen. If you see a crash with UnsatisfiedLinkError, double-check that the library name in loadLibrary matches the add_library name in CMakeLists.txt.
Starter snippet for CMakeLists.txt:
cmake_minimum_required(VERSION 3.4.1)
add_library(native-lib SHARED native-lib.cpp)
find_library(log-lib log)
target_link_libraries(native-lib ${log-lib})
🔍 Checkpoint Quiz
Q1. Which layer of the Android architecture is responsible for memory management, process scheduling, and hardware drivers at the OS level?
A) Application Framework B) Hardware Abstraction Layer C) Linux Kernel D) Android Runtime (ART)
Q2. Given this JNI function signature:
extern "C" JNIEXPORT jint JNICALL
Java_com_example_myapp_MainActivity_multiply(JNIEnv* env, jobject obj, jint a, jint b) {
return a * b;
}
Which of the following Java declarations correctly pairs with it?
A) public static int multiply(int a, int b);
B) public native int multiply(int a, int b);
C) public native void multiply(int a, int b);
D) private int multiply(int a, int b) { return 0; }
Q3. A developer writes this native code:
char* buffer = (char*)malloc(1024);
// ... uses buffer ...
// forgot to call free(buffer)
What problem will this eventually cause in a long-running Android app?
A) A compile-time error — Kotlin will refuse to link the library
B) A memory leak, because the 1024 bytes are never returned to the OS
C) An automatic garbage collection pass that reclaims the buffer
D) A NullPointerException on the Java side
Q4. You are building a real-time audio equalizer for an Android app. The DSP math is running too slowly in Kotlin. How would you use the NDK to fix this?
(Open-ended — write 2–3 sentences describing your approach.)
A1. C) Linux Kernel. The Linux Kernel is the lowest layer and owns security, memory, process management, and device drivers. ART handles bytecode execution; HAL abstracts hardware; the Application Framework provides developer APIs.
A2. B) public native int multiply(int a, int b); The native keyword tells the JVM this method is implemented in a shared library loaded at runtime. The return type must match jint → int, and the method must be declared native — not static (unless the JNI signature matches) and not given a Java body.
A3. B) A memory leak. Unlike Java's garbage collector, C's malloc requires a matching free. Without it the 1024 bytes remain allocated for the lifetime of the process, and repeated calls will grow the app's memory footprint until the OS kills it for exceeding its budget.
A4. A strong answer covers three steps: (1) profile first to confirm the DSP loop is the bottleneck; (2) move the equalizer math into a C++ function in src/main/cpp/ exposed via a JNI method that accepts an audio sample buffer (e.g., jfloatArray); (3) configure CMake to compile the library with -O2 or -O3 optimization flags so the compiler can auto-vectorize the inner DSP loop using NEON SIMD instructions available on ARM Android devices.
🪞 Recap
- Android's architecture has five layers: Linux Kernel, HAL, ART + Core Libraries, Application Framework, and Applications — each with a distinct responsibility.
- The NDK lets you write performance-critical code in C or C++ and call it from Java/Kotlin, making it ideal for graphics, audio, and compute-heavy tasks.
- JNI is the bridge between the JVM and native code; function names must follow the
Java_<package>_<class>_<method>convention exactly. - Unlike Java, native C/C++ code requires manual memory management — every
malloc/newmust have a matchingfree/delete. - Android Studio supports native debugging with breakpoints, variable inspection, and step-through in C++ files when debugging symbols are included in the build.
📚 Further Reading
- Android NDK Guides — official comprehensive reference for NDK setup, CMake, and JNI
- JNI Tips — Android Developers — performance and correctness guidelines straight from the Android team
- Android Bitmap NDK API — full reference for
AndroidBitmap_getInfo,lockPixels, andunlockPixels - CMake Documentation — deep reference for writing and maintaining
CMakeLists.txtfiles - ⬅️ Previous: None
- ➡️ Next: Reasons for Mobile-First Applications / Mobile Operating Systems