Notes on Android Architecture
Big picture
Android:
- is a platform for running code;
- is build on the Linux kernel, but Android is no Linux, concretely:
- No native windowing system;
- No glibc support;
- Does not include the full set of standard Linux utilities (that you can find on distro like Debian, RedHat etc.)
So why the Linux kernel if Android is not Linux system ?
- It provides great memory and process management
- it provides basic plumbing and services to support the higher level platform and applications.
- It also provides a great permissions-based security model
- Proven driver model that let provide a great abstraction layer between the hardware devices, peripherals such as LCD touch screen, audio and the software that uses it
- Support shared libraries and real pluggable module architecture that allow us to plug things very easily
- The best of all - It’s already open source
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:
- Alarm driver that provides timers that can wake the device up from sleep and based on monotonic time base
- Ashmem - Android SHared MEMory driver that allows applications to share memory and manages this at a kernel level
- Binder - Open Binder based IPC driver that facilitates inter process communication
- Power Manager
- Low Memory Killer
- Kernel Debugger
- Logger
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
- Defines the interface that Android requires hardware “drivers” to implement
- Separates the Android platform logic from the hardware interface
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 ?
- Not all components have a standardized interface and the kernel today. Like vibrator driver or LED driver, these aren’t always exposed as standard kernel drivers
- Kernel drivers are GPL - if you put anything in the Linux kernel it pretty much needs to be shared as open source software and
- Android has specific requirements for hardware drivers
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 ?
- Licence: we want to keep GPL out of user-space (Requires all code working on GPL must be also GPL). Bionic is under BSD Licence (allows people to keep things closed source).
- Size: Will load in each process, so it needs to be small. Bionic is (200k - half of standard glibc) and fast code paths.
- Fast limited CPU power means we need to be very fast and small. Bionic uses custom pthread implementation which uses 4 byte mutexes instead of standard 12. That makes threading very fast.
- It doesn’t contains certain POSIX features
- Doesn’t support C++ exceptions because these are just too heavy for embedded devices.
- Not compatible with Gnu Libc (glibc)
- All native code must be compiled against bionic
WebKit
Browser engine based on open source WebKit browser (the same used in Apple’s Safari)
- Renders pages in full (desktop) view.
- Full CSS, Javascript, DOM, AJAX support so it allows you to build rich web applications.
- Support for single-column and adaptive view rendering.
Media Framework
- Based on PacketVideo OpenCORE platform (codecs mpeg-4 h.264 mpeg-4)
- Supports standard video, audio, still-frame formats
- Support for hardware / software codec plug-ins
SQLite
- Light-weight transactional and relational data store
- Back end for most platform data storage behind the content provider (Contacts, SMS, MMS)
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.
- Can combine 2D and 3D surfaces onto a single output source. It makes it easier for all applications not to worry about what they are writing to or how they’re being rendered, you just send it down to the surface flinger and it will compose them together, and get them out the frame buffer. Surfaces passed as buffers via Binder IPC calls Can use OpenGL ES and 2D hardware accelerator for its compositions. You can plug in hardware acceleration if you have a 3D chip on the device through the OpenGL ES defined by the Khronos group
- Double buffering using page-flip. Basically paints to the Y plane on the frame buffer
Audio Flinger
- Manages all audio output devices, so it takes audio output streams from different apps running in different processes that are outputting simple tone audio or maybe game audio, or media player audio such as mp3 and it routes all of these to the various output devices whether that’s an earpiece , speakerphone or a bluetooth headset. It can sit on top of many of the standard audio implementations or drivers on Linux such as ALSA advanced linux sound architecture or OSS. This is not replacing Linux audio implementation - this sits on top of these and provides the Android specific features to the platform.
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.
Dalvik is a virtual machine to
- run on a slow CPU;
- with relatively little RAM;
- on an OS without swap space;
- while powered by a battery.
It does not run Java bytecode, dalvik runs an optimized byte called dex (dalvik executable)
- Supports multiple virtual machine processes per device
- Highly CPU-optimized bytecode interpreter
- Uses runtime memory very efficiently
Memory Efficiency
Multiple independent mutally-suspicious processes Separate address spaces, separate memory
Kinds Of Memory
clean vs. dirty
- clean: mmap()ed and unwritten. It’s simply any memory that OS Kernel can drop from main memory and not have any impact on the overall semantics of the system. The reason it can drop is either because it is just a page full of zeros and the kernel knows that it can recreate a page of zeros on demand, and similarly if it’s backed by a file you can map in those pages from the file again as you need on demand. It’s ideal because kernel can just eject those pages from RAM
- dirty: malloc()ed
shared vs. private
- Shared: used by many processes. It’s better because any cost of that memory that is going to have to stay in main memory until it get’s freed that cost is at least amortized across multiple processes.
- private : used by only one process
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:
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 ?
- There is a lot of native code to do this heavy lifting code so JIT doesn’t matter
- JNI is avaiable.
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
- Byte-swapping and padding (unnecessary on ARM)
- Static linking - when a device arrives on a device it will have symbolic references to methods and fields but afterwards it might just be a simple integer V table offset so that when invoking a method instead of having to do a string based lookup it can just simply index into V table
- “Inlining” special native methods
- pruning empty methods
- Adding auxiliary data
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.
- Activity Manager - manages the application lifecycle for all activities tells them when to start up when to shut down or not shut down but basically be persisted to memory so we can reclaim space. It maintains a back stack so you can navigate through applications the same way you do on browser.
- Package Manager - is used by the activity manager to load information about all the apks files (Android Package Files) on the device, and these are just archves that include activites, services, fragments, intent receivers and thing like that. The package manager will tell the rest of the system what applications are loaded on the device or what packages and what the capabilities of those packages are.
- Window Manager - sits on top of Surface Flinger and basically handles all of the various windows draw by different application, manages the Z order of those so that you know which one should be show in front of the others. Passes tverything down to the surface flinger to render on the deivce.
- Resource Manager - handles all non code resources in an application or service so these are thing like externalize text string or image files or audio files etc.
- Content Provider - components that allows you to share data between applications. For example content provider provides access to contacts database on the device and you can share that between different applications so they can all have access to that data. Many of these are backed by SQLite DB but it may be backed by something else.
- Content Provider - components that allows you to share data between applications. For example content provider provides access to contacts database on the device and you can share that between different applications so they can all have access to that data. Many of these are backed by SQLite DB but it may be backed by something else.
- Hardware Services - typically accessed through local Manager object
- Telephony Service - handles all communication with the baseband radio through the radio interface layer
- Location Service - handles all access to the GPS device
- Bluetooth Service
- WiFi Service
- USB Service
- Sensor Service
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:
- Databases, preferences, file space, etc.;
- Linux process.
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
- an encapulation of partucal operation;
- run in process of .APK which installed them;
- optionally associated with a window (UI);
- An Execution context - context in a mechanissm you use to access other resources (get services, bind services, access resources etc.).
Task
- More of a notion that a concrete API entity, it’s essentialy a run time record of a sequence of activites
- Collection of related activities
- Capable of spanning multiple precesses Associated with thier own UI history stack
Process
- Android Process = Linux Process
- By default 1 process per APK
- By default 1 thread per profess
- Most components interleave events into the main thread
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
- Init process because it has to run as root, it’s a way the linux works
- Zygote process because when it execute arbitrary component he needs to do chuser to transform itself into appropriate UserID, you can only do that as root
- Runtime process - components that are talking to device drivers, writing directly to frame buffer (surface flinger) or audio hardware(audio flinger), they need to run as root.
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
- Services if you want to expose functionality.
- Content Providers. If you want to expose data.
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”.
- Each process has by default one Thread.
- Most components share the single thread (sometime Services and Content Providers don’t).
- Each Java thread has a program counter & stack (unique).
- The heap and static areas are shared across threads (common)
- Each thread has a looper to handle a message queue.
- Events from all components (View UI Events, IntentReceiver firing etc.) are interleaved into Looper.
- Loopers assume that they are associated with single process so they are not intended to be thread safe.
- Loopers can not accommodate multi-thread access. They are designed to play nicely with MessageHandlers. And MessageHandlers play nicely with Loopers.
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
- UI Events
- Key presses,
- Touch Screen events,
- Screen motion events,
- anything that system decides it need to be alert you out.
- System events
- Broadcasting intent to you,
- And simmilar things.