This is a source-reading primer, not a Java programming course. Its purpose is to make later AOSP excerpts legible: method calls, interfaces, exceptions, managed runtime, JNI, generated IPC, and explicit data encoding. Android source examples below are pinned where indicated; small teaching programs are illustrations, not copied AOSP source.
Before you start#
Read Programs, Memory & System Calls, Soong & Your First Change (or at least its C/C++ primer), and Find Your Way Around AOSP. You need to recognize a function/method, a process, and source-file paths. No Java or Android app experience is required. The exercise does not require installing a JDK: source-reading is the required path; compilation is optional if a JDK is already available.
Learning goals#
After this chapter, you can read a small Java call chain, explain class/object/interface/method/exception, describe source-to-DEX-to-ART at a high level, identify where JNI crosses between managed and native code, and understand why AIDL-generated Binder code serializes supported values instead of transmitting arbitrary object memory.
Read the Java shape#
Java source is text that describes declarations and statements. A class groups state and behavior; an object is one runtime instance of a class; a method is a named operation declared in a class. A method call such as counter.add(2) selects a method on an object. A Java interface names operations that an implementing class promises to provide. It is a contract, not necessarily a network or process boundary. implements therefore does not mean “uses Binder.”
// Teaching illustration; not AOSP source.
interface Counter {
int add(int amount);
}
final class SimpleCounter implements Counter {
private int value;
@Override
public int add(int amount) {
if (amount < 0) {
throw new IllegalArgumentException("amount must be non-negative");
}
value += amount;
return value;
}
}
public final class CounterDemo {
public static void main(String[] args) {
Counter counter = new SimpleCounter();
System.out.println(counter.add(2)); // 2
}
}
Read top to bottom. Counter declares a method signature; SimpleCounter implements it. private hides a field from ordinary callers; public allows access subject to platform/API rules. @Override asks the compiler to verify that this method implements an inherited declaration. main is an entry method in this standalone illustration. In Android apps, framework entry points and lifecycle components usually take control instead of your app calling main directly.
An exception is a control-flow path representing a failure or rejected operation. The throw statement transfers control to a matching catch block or propagates the exception to the caller. For source reading, ask: what input can trigger it, does this method catch it, and what does the caller do? Do not assume that every Binder or JNI failure has the same exception type.
The call stack is the current chain of method calls for one thread. If A() calls B() which calls C(), then C is the current frame; when it returns, execution resumes after the call in B. A stack trace is a snapshot of such frames, often with file and line information. Separate threads have separate call stacks, while objects may be shared between threads. That is why seeing an object reference in two methods does not prove they execute on the same thread.
Source, compilation, DEX, and ART
- Java / Kotlin source
Classes, methods, interfaces, and application logic.
- Build pipeline → DEX in APK
Compilation commonly passes through class files and conversion to Android bytecode.
- ART inside a process
Loads and executes managed code, with class loading, garbage collection, and compilation support.
Java/Kotlin source is compiled as part of an Android build into intermediate class files and then DEX bytecode packaged in an APK (details depend on the build pipeline). DEX is Android's register-based bytecode format; it is not Java source and is not native machine code. At runtime, ART loads DEX and executes managed code. Depending on Android version, device policy, and app usage, ART may interpret code, compile code just-in-time, or use ahead-of-time compilation artifacts. ART includes garbage collection, class loading, and runtime support. ART is not the Java source compiler, and a DEX method does not map one-to-one to one native instruction.
For system boot, Zygote starts an ART runtime, preloads selected classes/resources, and forks processes (pinned ZygoteInit.java). That later chapter explains the process story; this primer only separates source language, bytecode, and runtime. App developers may also bundle native libraries compiled for an ABI (such as arm64-v8a); their machine instructions execute in the same app process under that process's identity. Native does not mean privileged.
JNI: a language boundary, not a privilege boundary
- JNI: managed ↔ native
- Typically within one app process. Convert representations and respect reference/thread lifetimes. Not an automatic privilege change.
- Remote Binder: client ↔ server
- Across processes. Generated proxy/stub encode agreed values; the server independently authorizes the caller.
JNI (Java Native Interface) is a defined interface for Java/ART code to call native code and for native code to interact with managed objects. A Java method can be declared native; a native implementation is registered or found through JNI conventions. The boundary requires conversion and lifetime rules (JNI tips): Java strings/arrays are managed objects, native code receives handles/pointers through JNI APIs, and references have scoped/global lifetime rules. Native code must release resources it acquires and must not keep an invalid local reference beyond its scope. Errors may be represented by a pending Java exception, a return status, or both, depending on the API.
Illustrative shape (not AOSP source and not a complete buildable JNI sample):
package example;
final class Checksum {
static { System.loadLibrary("checksum"); }
static native int sumBytes(byte[] input);
}
// Illustrative C shape only: JNI headers, registration, bounds, and build rules omitted.
JNIEXPORT jint JNICALL
Java_example_Checksum_sumBytes(JNIEnv *env, jclass type, jbyteArray input) {
// Must acquire array elements via JNI, check for failure/exception,
// calculate, then release with the correct mode before returning.
return 0;
}
The signature indicates a static Java method (jclass receiver) taking a byte array and returning an integer. It is not safe to treat jbyteArray as a C pointer to bytes. Real code uses JNI functions such as GetByteArrayElements or region APIs and follows their release and exception semantics. JNIEnv* is thread-specific; native-created threads must attach to the VM before using JNI and detach when appropriate (JNI tips). Consult the exact function's documentation and neighboring source; do not copy this skeleton into a product.
JNI introduces memory-safety and lifetime risks, not a sandbox escape. Native code in an ordinary app still runs as that app's UID and under its SELinux/app restrictions; the Application Sandbox applies to native code as it does to interpreted code. In AOSP, search native declarations and JNI_OnLoad/registration in the same component; Android often registers methods explicitly (RegisterNatives) rather than using a name-derived symbol (JNI tips). The Java declaration and native implementation may live in different projects, so verify the build target and registration call before concluding a method is missing.
AIDL, generated bindings, and serialization
An AIDL file declares a typed IPC interface. For example:
// Teaching illustration; not a checked-in AOSP declaration.
interface ICounter {
int add(int amount);
}
The AIDL compiler generates backend-specific bindings (the Java backend writes the descriptor with writeInterfaceToken(DESCRIPTOR); see pinned generate_java_binder.cpp). A Java client can call a proxy method; generated code encodes the interface descriptor and method arguments into a Parcel, calls IBinder.transact with a transaction identifier, and decodes the reply. Server-side generated dispatch reads the matching fields and invokes the implementation. The generated files are generally build outputs, not necessarily checked into the source tree. Never present hand-written generated-looking code as an exact AOSP excerpt. To inspect the real path, find the .aidl, its Android.bp/module, the AIDL compiler/backend version, and the source implementation; if generated output is unavailable, label any pseudo-code as an algorithm sketch.
Serialization means converting values into an agreed representation and reconstructing them on the other side. A Parcel is not a raw memory dump and cannot magically transfer an arbitrary Java object graph. Both ends must agree on field order and type. For the example, conceptually the proxy writes an integer amount; the stub reads an integer in the same position. If one side writes a string and the other reads an integer, decoding is invalid. Parcelable/AIDL types define their own encoding; Binder object references and file descriptors have special kernel-mediated handling. Large payloads may be passed through shared-memory or file-descriptor mechanisms instead of copying the entire payload through a Binder Parcel; the choice is API- and use-case-specific. For example, Android documents shared Bitmap transfer this way. Shared Bitmap guidance
The AIDL interface token (descriptor) helps a receiver check that a transaction targets the expected interface (generated stubs call enforceInterface on it). It is not a password, caller identity, or authorization grant. Service code must enforce permissions and policy independently. Binder calls can be synchronous or oneway; thread dispatch, reentrancy, and process death matter. Those topics are traced in Binder: app to system service.
Practice#
Prerequisites: Any text editor and the source-reading prerequisites above. The required practice is paper/source inspection and works without Java installed. Optional compile/run requires an already installed JDK (java -version, javac -version); do not install software just for this optional check.
- In the illustration, circle the interface, implementation class, object creation, method call, and exception path. Write the output if
add(2)is called once and then called with-1inside atry/catchthat prints the exception message. - Trace
java source → compiler/build → DEX → ART. Put native code/JNI on a separate branch and mark where values cross representation/lifetime boundaries. - Find a JNI bridge in a pinned AOSP source checkout using
rg -n 'native |JNI_OnLoad|RegisterNatives' frameworks system. Search results are candidates, not proof. Record Java declaration, registration/implementation path, and build module. If you have no checkout, inspect the Android JNI tips and state that you did not inspect local source.
Optional host compile, from a disposable directory (commands run in the workstation shell): save the first Java block as CounterDemo.java, then run javac CounterDemo.java and java CounterDemo. Expected output: 2. A compiler error such as javac: command not found means the optional JDK is unavailable; stop there. If you deliberately change 2 to -1 without adding a catch, expect an IllegalArgumentException stack trace. That is a teaching program on the host, not Android execution and not an AOSP runtime test.
Knowledge check#
- Does a Java interface necessarily create a process boundary?
- What is DEX, and what role does ART play?
- Why is a JNI array argument not safely treated as a C pointer?
- What does the AIDL interface token check, and what does it not authorize?
Answers#
- No. It is a language-level contract; implementation may be in the same object/process or used through IPC.
- DEX is Android bytecode. ART loads and executes it, with runtime services and version/device-dependent interpretation/compilation.
- JNI arrays are managed objects with access, pin/copy, exception, and release rules; native code must use JNI APIs and correct lifetime handling.
- It identifies/validates the expected interface descriptor for dispatch. It does not prove the caller's identity or permission to perform an operation.
Checkpoint#
Submit an annotated call chain for the example, a source-to-runtime diagram, and a five-line explanation of JNI ownership/error handling. Pass if you distinguish language interfaces from IPC, DEX from native code, and descriptor validation from authorization. If not, revisit the corresponding section and annotate each boundary with “same process?” and “who owns this data?”
Sources#
- ART runtime overview — DEX execution and runtime compilation (AOT).
- Dalvik bytecode — DEX is register-based.
- JIT compiler — ART just-in-time compilation.
- Application Sandbox — native code is sandboxed like interpreted code; apps get distinct UIDs.
- JNI tips — current official guidance for JNI references, arrays, threads, and native method registration; the link redirects from the older path.
- AIDL overview, AIDL backends, and Binder overview — interface/compiler and IPC context; large-payload examples depend on the specific API.
- AOSP 14
ZygoteInit.java— runtime process creation context. - Pinned AOSP 14
generate_java_binder.cpp— generated Java stubs/proxies:writeInterfaceToken(DESCRIPTOR)andenforceInterface. - The code snippets in this chapter are teaching illustrations, not source excerpts.