Full Android 13 running inside an iOS app. VirtualAndroid is an Android emulator for iPhone and iPad: a complete AOSP Android 13 (API 33, arm64) system with its own Linux kernel, emulated with QEMU inside a normal iOS app, with GPU-accelerated graphics and the phone's sensors, camera, location, audio and network bridged into Android.
Research project. It boots to the Android home screen and runs third-party apps on a real iPhone, but it is not a finished product. It cannot be distributed through the App Store; you build and sideload it yourself.
- The real thing, unmodified. The official AOSP emulator image
(
system-images;android-33;default;arm64-v8a) is used as is: kernel, system and vendor are not patched. - Whole-system emulation in-process. QEMU 10 (UTM fork) is built as a
framework and runs on a thread of the app, with the Android emulator's
ranchuboard (goldfish pipe, sync, battery, address space; virtio disks, input, sound, network). - GPU rendering. Guest OpenGL ES goes through the goldfish pipe to the
gfxstream host renderer, which draws with ANGLE on Metal into IOSurfaces
shown by a
CAMetalLayer(zero-copy, GLES 3.0). - Fast without JIT. Stock iOS apps cannot generate code. On a device without JIT the engine runs UTM's TCTI threaded interpreter plus our AOT translation cache: about 260,000 of the hottest guest code blocks, translated offline to C, compiled into the app and matched at runtime by content hash (2.3-3.4x faster than plain TCTI).
- Optional JIT via StikDebug (on by default). Opened through StikDebug, the app switches to QEMU's real JIT, including on iOS 26/27 devices with TXM. Apps start about twice as fast as with TCTI + AOT.
- Seconds to resume. Leaving the app suspends the VM into a snapshot (guest RAM lives in a file, so only device and renderer state is written); reopening it restores the running Android in seconds instead of booting.
- Android hardware bridged to iOS: multi-touch and keys, accelerometer, gyroscope, magnetometer and barometer (Core Motion), GPS (Core Location), camera preview and photos (AVFoundation), speaker and microphone, clipboard, battery, Wi-Fi with internet access (virtual access point plus NAT), and a virtual Bluetooth controller. adb over vsock for development.
Measured on an iPhone 17 Pro Max (A19 Pro, iOS 27.2) with a 2 GB, 4-vCPU guest:
| JIT (StikDebug) | TCTI + AOT cache | |
|---|---|---|
Cold boot to sys.boot_completed |
~85 s ¹ | ~433 s |
| Resume from snapshot | seconds ¹ | ~4 s |
| Settings / Clock / Calendar / Dialer cold start | 0.9-1.6 s | 1.6-2.5 s |
| Xiaohongshu cold start | 13.6 s | 26.7 s |
| CoolApk cold start | 16.8 s | 35.4 s |
| Settings scrolling, janky frames | 1.9 % | 3.5 % |
¹ After the debugger has prepared the JIT memory. With StikDebug that step is not timed yet; the lldb stand-in used for these runs took about 3.5 minutes for the 512 MB code buffer (one page per round trip; StikDebug batches 128).
Details and methodology: docs/JIT.md, docs/AOT.md.
Works: boot, launcher, system apps, installing and running third-party apps, touch, keyboard, rotation, sensors, GPS, camera, audio in and out, clipboard, Wi-Fi internet, snapshots. Known limits: the guest has 2 GB of RAM (big apps lean on the 1 GB zram swap); no Google Play services (AOSP image); Bluetooth has no real radio; no vibration, light or proximity sensor; without JIT, apps whose code is not in the AOT cache are slow.
┌──────────── iOS app (SwiftUI) ─────────────────────────────────────────┐
│ VMController ── Metal display (CAMetalLayer) ── touch/keys → virtio-input │
│ Host bridges: CoreMotion, CoreLocation, AVFoundation, audio, UIDevice │
│ battery, UIPasteboard │
├──────────── va_host.h (C interface between engine and platform) ───────┤
│ QEMU 10 (UTM fork) as a framework, running on its own thread │
│ machine "ranchu" = virt + goldfish {pipe, sync, battery, events}, │
│ goldfish address space │
│ virtio: blk ×5, net, rng, console, multitouch, keyboard, sound, vsock │
│ pipe services: qemud (boot properties, sensors, GPS, camera, …), │
│ opengles, clipboard, adb │
│ CPU: TCG JIT (simulator, StikDebug) or TCTI + AOT cache (device) │
├──────────── va_gfx.h ──────────────────────────────────────────────────┤
│ libva_gfx: gfxstream host renderer → ANGLE (Metal) / MoltenVK → IOSurface │
└──────────────────────────────────────────────────────────────────────────┘
Guest: the official AOSP Android 13 emulator image, unmodified
| Path | Contents |
|---|---|
app/ |
The iOS app (SwiftUI, Metal display, host bridges, VM lifecycle, snapshots) |
host/include/va_host.h |
Engine ↔ platform interface (callbacks for sensors, camera, audio, …) |
gfx/ |
libva_gfx: gfxstream + ANGLE + MoltenVK build, renderer glue, tests |
engine/scripts/ios/ |
Cross-compiling QEMU and its dependencies for iOS, AOT cache build |
tools/aot/ |
AOT translation cache: profiler plugin, TCG-to-C translator, runtime, tests |
tools/jit/ |
JIT test helpers (lldb stand-in for StikDebug, benchmarks) |
tools/ |
Image and initrd preparation, macOS run script, snapshot tool |
third_party/ref-emu/ |
Reference sources from the Android emulator (kept under their own licenses) |
docs/ |
Design notes and measurements |
The QEMU fork (the ranchu board, goldfish devices, pipe services, AOT
runtime, iOS JIT support) lives in its own repository,
VirtualAndroid-qemu, and is
checked out at engine/qemu.
You need an Apple Silicon Mac with Xcode 26.1 or later, and an iPhone or iPad
on iOS 17 or later with enough memory: the app needs about 4 GB (2 GB guest,
code buffer, renderer). It has only been tested on an iPhone 17 Pro Max
(12 GB); devices with less than 8 GB of RAM will probably not fit it. A
paid Apple Developer account is required for the
increased-memory-limit and extended-virtual-addressing entitlements.
brew install meson ninja pkg-config bison glib e2fsprogs xcodegen
# Sources (the engine and some build inputs are separate checkouts)
git clone https://github.com/spianmo/VirtualAndroid-iOS.git VirtualAndroid && cd VirtualAndroid
git clone https://github.com/spianmo/VirtualAndroid-qemu.git engine/qemu # the QEMU fork
engine/scripts/ios/fetch_deps.sh # UTM recipe + dependency tarballs
gfx/scripts/fetch-sources.sh # gfxstream, aemu, ANGLE (pinned)
# Engine: dependencies, then the JIT and the no-JIT (TCTI) builds of QEMU
S=engine/scripts/ios
$S/build_deps.sh -p ios
$S/build_qemu.sh -p ios -b tcg -s engine/qemu
$S/build_qemu.sh -p ios -b tcti -s engine/qemu
$S/make_xcframeworks.sh --device-backend tcg+tcti
# Renderer
for P in ios-sim ios; do
gfx/scripts/build-aemu.sh $P && gfx/scripts/build-gfxstream.sh $P &&
gfx/scripts/build-angle.sh $P && gfx/libva_gfx/build.sh $P &&
gfx/scripts/package-apple.sh $P
done
gfx/scripts/package-apple.sh xcframework
# Android image: the AOSP emulator image from the Android SDK
sdkmanager "system-images;android-33;default;arm64-v8a"
mkdir -p images/android-33
cp -R "$ANDROID_HOME/system-images/android-33/default/arm64-v8a" images/android-33/
app/scripts/make_userdata_qcow2.sh --pack # -> app/build/ImagePack
# App: set DEVELOPMENT_TEAM in app/project.yml first
cd app && xcodegen generate && cd ..
xcodebuild -project app/VirtualAndroid.xcodeproj -scheme VirtualAndroid \
-sdk iphoneos -destination 'generic/platform=iOS' \
-derivedDataPath app/build/dd-device -allowProvisioningUpdates build
xcrun devicectl device install app --device <device> \
app/build/dd-device/Build/Products/Debug-iphoneos/VirtualAndroid.appThen copy the app/build/ImagePack folder into the app's Documents as
ImagePack (Finder file sharing or the Files app, or "Import image pack
folder…" in the app). The first launch installs it and boots Android.
The AOT cache is optional and built from profiled runs of your exact engine
build; without it the TCTI engine runs uncached. See
docs/AOT.md and engine/scripts/ios/build_aot.sh. For the iOS
simulator, build the ios_simulator platform and the ios-sim renderer
instead; the simulator always has JIT. More detail in
engine/scripts/ios/README.md,
gfx/README.md and app/README.md.
- Install StikDebug and LocalDevVPN, create a pairing file (StikDebug can pair on the device itself on iOS 27), turn on LocalDevVPN and let StikDebug mount the developer disk image.
- Either open VirtualAndroid from the home screen: it asks StikDebug to
attach (confirm "Enable JIT?") and boots with JIT once StikDebug switches
back. Or, in StikDebug, assign the
universal.jsscript to VirtualAndroid and launch it from there. - The start screen shows which engine this launch uses. Without a debugger the app falls back to TCTI + AOT; both use the same snapshot.
On iOS 26+ devices with TXM the debugger has to prepare the JIT memory while QEMU allocates it; the engine speaks StikDebug's protocol for that and then detaches the debugger. See docs/JIT.md.
- docs/PLAN.md: goals, constraints, architecture, milestones (Chinese)
- docs/JIT.md: JIT on the iPhone with StikDebug
- docs/AOT.md: the AOT translation cache
- docs/SNAPSHOT.md: fast resume (Chinese)
- app/README.md: the app, launch arguments, image packs
Built on QEMU, UTM (its QEMU fork, iOS build recipe and TCTI interpreter), the Android Emulator (ranchu board and goldfish devices, used as reference), gfxstream and aemu, ANGLE, MoltenVK and StikDebug.
Apache License 2.0, see LICENSE. NOTICE lists the
exceptions: a few files here are GPL-2.0-or-later as stated in their headers,
and the reference sources in third_party/ref-emu keep their own licenses.
The engine, VirtualAndroid-qemu, is a QEMU fork and therefore GPL-2.0. An app build embeds it as a separate framework (the same arrangement as UTM); distributing such a build means following the GPL for the engine. Build dependencies are downloaded at build time under their own licenses. The Android system image is not included; it comes from the Android SDK under Google's terms.
Android is a trademark of Google LLC. This project is not affiliated with Google or Apple.
