Notes on Android Architecture

Big picture

Android:

So why the Linux kernel if Android is not Linux system ?

Android Kernel

Android Kernel is a variant of the Linux OS kernel optimized for power conservation & local IPC. It’s written in C (Display Driver, Camera Driver, Bluetooth Driver, Shared Memory Driver, Binder IPC Driver, USB Driver, KeyPad Driver, WiFi Driver, Audio Drivers, Power Management)

Some of the Linux Kernel enhancements:

Inter-process communication

Applications and services may run in separate processes so we start all of the applications and separate processes on Android and they must communicate between each other and share data. Typically IPC Inter Process Communication can introduce significant process overhead and security holes and we need a solution in architecture that was going to be lightweight and powerful enough to be able to facilitate this design of different applications and services running in different processes.

Android provides a bunch of inter-process communication (IPC) mechanisms. And they are able to do various things, they mediate interaction between apps and system services, they’re used to be able to communicate between an app on the phone or some piece of system running in a cloud service somewhere. There are 3 types of communication mechanisms with Android Linux,

TCP/IP

used primary for off device network communication, and its quite overkill to use those web-agnostic protocols with build-in optimization mechanisms, on the reliable and deterministic environment which is single device. You don’t need checksums, because if the data is corruped, then you have bigger problems :D

Unix domain sockets

Unix domain socket is used for communication between processes on the device for things like sending, events, UI commands.

Binder

Binder is a replacement for Unix domain socket is the kernel level module, which is optimized for IPC on one device. It also provides message oriented communication, it sends chunks rather than byte streams, and thats significant because if you send data through byte stream, then the sender and receiver has and extra work of lineraization (serialization) and delinearization (deserialization) the data from native format into the byte stream format

Binder is high-performance because it uses shared memory so data between applications is not actually copied and marshaled through Java serialization or something like that from one process to the next it’s actually shared through shared memory.

The binder driver manages a per process thread pool for processing requests, so that services which are registered as binder services or IPC servers don’t need to worry about all the different threads to start up and manage to receive incoming requests, binder driver handles that for you. It has reference counting mapping of object references so objects passed between two application and service are are tracked and can be cleaned up afterwards when neither processes using them anymore and it supports synchronous calls between processes.

Android uses Binder Driver not directly, but through AIDL which is Java-interfaces-like language. That is used to define protocol—something like protobuf for gRPC. AIDL is used for remote method invocation, which allows to invoke method calls from different process in object oriented manner through proxy and stub. Additionally it allows you to send messages back and forth in a strongly typed way, where you don’t need to think about details on how to move from native Java Code to something that can go across the device, and then be handled in a different process. Binder is written in C/C++ and runs on OS kernel in other to provide hight performance. And then the lower lever binder collaborate with higer level binder framework that is written in Java that does the serialization (marschaling) and deserialization (demarchaling) to go from a method call to a message and then back from a message into a method call. Those layers are important to simplyfy object-oriented IPC .

Power Management

Android took more aggressive approach to power management, it does not replace Linux power management - It’s build on top of standard Linux Power Management.-More aggressive Power Management policy - that guarantee that no application or service specifically requests at the CPU or LCD be kept on, it will be shut down so your mobile device will last longer. Components can make the requests to keep the power on through the concept on wakelocks. We have partial wakelocks which say - keep the CPU on but I don’t really care about the display (maybe I’m playing mp3). I don’t want the CPU to shut down. Full wake lock for lock on CPU and Screen (videos or navigation)When we release the wakelock and there is no other locks the CPU or/and Screen will be shut down (for our application), but there’ll be still modem processor there that will receive calls and be able to wake up.

Hardware abstraction layer

The main goals is to help separate concerns and the Android System Architecture. This is a common techuniqe used in many large-scale software development projects in order to be able to isolate key challenges and complexities to certain layers so layers atop can be simplified, when we talked about earlier in our lesson when we discussed the Android application of the layers pattern. In particular the Android HAL helps to decouple the Android platform software from the hardware, because hardware changes and envolves. But we want the software be stable. So we need to add something like stable “interface” on top of the hardware so the software can depend on, without having to depend on the implementation. Another goal, is to decouple Android framework layer from the OS layer. This layer can be programmed via method calls on Java objects, rather than C system function calls.

Provide a better abstraction layer between the hardware and the upper layer of the Android platform. These are native libraries, they contain abstractions for things like Graphics, Audio, Camera, Bluetooth, GPS, Radio, WiFi

The goal is to make porting easier, we’are trying to define a clear set of API’s in the same way we have for application developers through the SDK we want to define those for anyone that will be porting Android once it’s open-source onto real hardware so that they don’t have to dig through 10k lines of source code to figure out how it’s working. They just implement these drivers for specific interfaces.

Why do we need user-space HAL ?

Libraries

Libraries such as: Surface Manager, Media Framework, SQLite, WebKit, Libc, OpenGL, AudioManager, SSL, FreeType

Bionic

Is a custom libc or C runtime implementation, optimized for embedded use Why Bionic ?

WebKit

Browser engine based on open source WebKit browser (the same used in Apple’s Safari)

Media Framework
SQLite

Native Servers

Processes which are running and doing some heavy lifting to control input/output devices on the platform.

Surfaceflinger

System-wide composer for Android. It takes surfaces drawn by different applications running in different processes and composes them all onto the single frame buffer to output to the device display.

Audio Flinger

Android Runtime

Dalvik Virtual Machine (Optimized Java Virtual Machine)

All the applications and services that you run will be running inside of a virtual environment powered by the dalvik virtual machine. It provides application portability meaning that an application written in the SDK will run on different devices that may have different low-level hardware implementations, are compiled with different tool chains. They will all run on the same virtual machine environment. Dalvik uses register machine model, while JVM uses stack machine model. dx program then transforms Java bytecode in class files into .dex-formatted bytecodes.

dex-compilation

Dalvik is a virtual machine to

It does not run Java bytecode, dalvik runs an optimized byte called dex (dalvik executable)

Memory Efficiency

Multiple independent mutally-suspicious processes Separate address spaces, separate memory

Kinds Of Memory

clean vs. dirty
shared vs. private
Private Shared
Clean Common uncompressed dex files (libraries) are get mapped to memory. Application-speific dex files are get mapped to memory
Dirty Application “live” dex structures. Any associated data when you start VM, load a class when you refer to methods. Appliaction heap. When you construct objects, they have to go to memory and that becomes dirty memory. Library “live” dex structures. Shared copy-on-write heap (mostly not written)

Shared clean is ideal memory, and private dirty we want to minimize. The solution is Zygote process, it comes into existence fairly early on during the boot of an android system and its job is to load up those classes that we belive will be used across many applications. It creates a heap, sits for a socket and waits for commands to start up new application. When it happen—it does normal unix fork, then, the child process becomes that target application. The result is this: dex-compilation

Zygote has made the heap of objects, live dex structures, and then each application, when starts up instead of loading it’s own things it just shares it witl zygote and also with any other app on the system. If you have embedded mark bits, you are garbage collecting, the cache lines for that object will already be warm by the time you need to scan the object

CPU Efficiency

Android does not use JIT Compiler, why ?

System do a lot of work upfront to avoid doing work at runtime so one of the major things we do is verification and optimization of of dex files.

Verification

what it means is that as a type-safe reference-safe runtime we want to ensure that code that we’re running doesn’t volate the constraints of the system - doesn’t violate type-safety, doesn’t violate reference safety. For Android this is more about minimizing the impact of bugs in application as opposed to being a security consideration in and of itself. This is because for Android the platform security is really being guarded by the process boundaries as opposed to anything that we’re doing within a single process, although this is at least a little bit of a concern for our more sensitive system processes where we really want to guard aginst bugs because those bugs could turn into security violations.

Optimization

First time that dex file lands on a device we do that verification work, we also augment that file, if we have to we will do

So that when it comes time to run we can run that much faster. Most radical thing we do in dex files is to have entirely new bytecode instruction set Dex bytecode is defined in terms of an infinite register machine with no intra-frame stack so there’s a normal machine stack in terms of one method calling another, but within a method it’s all just registers. We chose this because it let us have very efficient interpreter because each instruction that we interpret is semantically more dense.

Android Runtime ART

Dalvik has been replaced with an improved “Android Runtime” (ART). ART uses AOT (Ahead Of Time) compiler which takes the dex files and converts them into native code when app is downloaded and installed on a machine (that’s why your device can lag when the downloading of an app finishes). What comes out is something that can be run in a more efficient way by native execution on the process, and instruction set, than interpreting. Add better GC. There is also JIT that further optimize ART’s AOT compiled code at runtime

Application Framework

Contains all classes and core system services that will be used to build applications.

Manages application lifecycle, manage packages and loading resources. They are working behind the scenes you typically interact with them directly when building an application.

Runtime Walkthrough

Published as a separate post.

Applications

Android APK is not a application it’s is collection of components. Components share set of resources:

Every Android Component has a managed lifecycle. Each APK is assosiated with a process, so you have a process in which all your components will execute.

Activities

Task

Process

Any time user or system component request component belong to your apk to get executed 1) binding to a service, 2) binding to Content provider; 3) firing an intent receiver; 4) start a activity. System will start your process for a given user ID if it’s not running. In general, a process run until they are killed by the system, even when your components are shuted down. It’s done for efficiency.

Sandboxing apps with Linux’s notion of user

It’s actually installed under a unique generated user ID for that apk, we do this for security reasons. Linux and UNIX system in general are really good at user level sandboxing, meaning that if you have a file that’s own by user A, Unix is very good at keeping user B from getting access to user A file or processes or other resources, and so from that perspective of operating system, the notion of multiple users on the phone doesn’t make a lot of sense, so we have a situation where we are not using these user ID for anything else so we might as well use them for application sandboxing, and this is the fundament of Android Security model.

There are going to be only three processes running as a root on the production system

Process + user ID = security Each application has access to only its own data. If you want to expose data or resources to other processes, you do that through

By separating this processes we basically build IPC bridge that connects two processes together and lets them share data and issue commands to each other—good achieving security.

Binder

Serialization is slow, memory transfers is slow. CPU is not the bottleneck, the memory & bandwidh is.

Binder bridges kernel and process space. Binder is a kernel module that plugs into the kernel, provides various memory sharing resources, and has as its unit of currency the parcel.

Parcel

Parcel is just a more or less kind of like C struct or protocol of definition for a sequence of data, and the binder knows how to shuffle parcels around, either store them or save them in memory or share them between processes.

Parcelable

In the framework level there is a class Parcelable which is interface that classes can implement to signify that they know how to serialize themselves to a parcel, in other words, a class that is parclable know how to write itself into a parcel so that the binder can share it between processes.

Bundle

The most common implementation of parcelable that developers interact is Bundle. This is a method that you use in intent receivers, in activities. In activities you see a bundle onSaveInstanceState. You can also implement your own implementation of parcelable. The reason you would do that is because you’re building a service or something you want to share data between or you want to expose functionallity to an activity. All IPC in system goes through the binder. Binder can handle this both from the Java Framework at the dalvik level but it can also handle it from native code, so binde is a general-purpose IPC mechanism. And it’s used anytime you need share data across processes.

Parcelable is a class which can marshal its state to something Binder can handle – “Parcel” Standard Java serialization has semantics Parcelable doesn’t don’t need. Supporting full serialization would mean wasting CPU. Java serialization in very general terms you can describe as standardized mechanisms for writing a state of java class to a format that is easily streamed. Meaning that it’s generally intended to be used, for things like writing over HTTP connection and so on. What Android needs is serialization or marshalling format that can be used to share data via block memory transfers. It’s actually kind of a very different thing. Java serialization format doesn’t lend itself well to use with binder, and also the serialization standard defines semantic around what it means to be serialized, and there is method that serialize object are allowed to implement to do a transform and so one. Because we can’t support all this semantics, or at least we didn’t want to try this in first version, we haven’t supported mechanisms for passing data through the binder. Bundle is type-safe map.

Why not HashMap ?

We don’t use hash-maps as onSaveInstanceState, because they are limited to either single type data (you are using generics) or you use them as typeless buckets to store object. The problem with both of those it that it allows developers to put data into the bundle that the binder can’t actually serialize so instead of using hashmap we use a binder which is our own class which has a whole bunch of really obnoxious methods like getString(), putString(), getDouble(), putDouble(). We do that because it’s used to be type-safe so that we can guarantee that the only thing that developers can put into bundle is something Binder can serialize. Bundles are typesafe containers of primitives.

Concurrency

Hollywood principle — “Don’t call us, we’ll call you”.

Package (.apk) we install comes with a process, that process by default has one thread. Within that thread we have a looper, and the looper is essentially the sole owner of that MessageQueue. Looper actually invokes methods like (onIntentReceived, onStart, onCreate, and other lifecycle methods), so various components that you interact with, get invoked by the looper in response to messages in MessageQueue, that come from things like

threads model