English | 简体中文
Run Android APKs in the browser with plain JavaScript. apk.js parses classes.dex, AndroidManifest.xml and resources.arsc straight from the APK, executes the application logic with a DEX bytecode interpreter, and paints the UI through a Canvas-based subset of the Android Framework. No device, no emulator, no native libraries.
一切开始,尽在 apk.js — Everything starts with apk.js.
- Pure frontend. Unpacking, parsing, execution and rendering all happen inside the browser. There is no backend.
- DEX interpreter. Covers the bulk of the Dalvik instruction set, with exception handling,
switch, arrays, fields and method dispatch. - Resource parsing. Binary
AndroidManifest.xml(AXML) and theresources.arscresource table. - Framework subset.
Activitylifecycle,Viewtree with measure and layout,LinearLayout/FrameLayout/RelativeLayout/ScrollView,TextView/Button/EditText/ImageView/ProgressBar,ActionBar/Toolbar,Toast,Context/Intent/Bundle, and more. - Drawables and themes.
shape/vector/ripple/selectorrendering,RippleDrawablecontent unwrapping,GradientDrawable,backgroundTint,@android:color/*resolution, andContext.setThemeinheritance. - Soft keyboard input. Tapping an
EditTextfocuses it and raises the system keyboard through a hidden-input bridge. SupportsInputMethodManager, a blinking caret, andgetSelectionStart/setSelection. - System and reflection.
android.os.Build/VERSION,Class.forName/getMethod/Method.invoke(including primitive argument unboxing), and multi-Activitynavigation throughstartActivitywithIntentextras. - Runnable sample. The repository ships
samples/hello-dex.apk. Open the page and it runs: counters, input echo, dynamic view insertion, progress bars, shape-backed buttons,Buildinfo, reflection calls, and a jump to a secondActivity.
# Install dependencies
npm install
# Bundle into dist/
npm run build
# Serve the static site
npm run serveOpen http://localhost:8080, then click Choose APK or drop an APK onto the page.
The build output dist/apk.min.js and dist/android-framework.min.js is committed, so you can also copy index.html, dist/, vendor/ and samples/ onto any static host and skip the build step.
npm run build runs webpack and produces two ES modules:
| Artifact | Contents |
|---|---|
dist/android-framework.min.js |
Framework only: DEX parsing, runtime, resource parsing, Canvas Android Framework subset. No DOM or page logic. |
dist/apk.min.js |
Caller layer: device info, render target, file loading, status and log callbacks. Depends on the file above. |
index.html loads dist/apk.min.js through <script type="module">, which pulls in android-framework.min.js. JSZip is provided as a global from vendor/jszip.min.js.
apk.min.js exports createSimulator and ApkSimulator. The caller supplies the device info and the render target:
<script src="vendor/jszip.min.js"></script>
<script type="module">
import { createSimulator } from './dist/apk.min.js';
const sim = createSimulator({
// Render target: a CSS selector, a DOM element, or an existing canvas
element: '#phone-screen',
// Device info
device: {
width: 360,
height: 640,
dpr: window.devicePixelRatio || 1,
style: 'material',
maxApkSize: 50 * 1024 * 1024,
},
onLog: (entry) => console.log(`[${entry.level}] ${entry.tag}: ${entry.message}`),
onFinished: () => console.log('Activity finished'),
onError: (error) => console.error(error),
});
// Load from a File object (for example from <input type="file">)
await sim.loadFromFile(file);
// URL, ArrayBuffer and an unpacked JSZip instance also work
// await sim.loadFromUrl('samples/hello-dex.apk');
// await sim.loadFromArrayBuffer(buffer);
// await sim.loadFromZip(zip);
sim.setDevice({ width: 280, height: 480 }); // Resize while running
sim.destroy(); // Stop and remove the canvas
</script>| Option | Description |
|---|---|
element |
Render target. Accepts a CSS selector string or a DOM element; a canvas is created inside it. |
canvas |
Use an existing <canvas>. Takes precedence over element. |
device.width / device.height |
Logical resolution. Defaults to 360×640. |
device.dpr |
Device pixel ratio. Defaults to window.devicePixelRatio capped at 2. The canvas backing size is logical size × dpr. |
device.style |
Theme style. Defaults to material. |
device.maxApkSize |
APK size limit. Defaults to 50MB. |
onLog / onFinished / onError |
Log, finish and error callbacks. |
| Method | Description |
|---|---|
loadFromFile(file) |
Load from a browser File. |
loadFromUrl(url, options) |
Download with fetch, then load. |
loadFromArrayBuffer(buffer) |
Load from binary contents. |
loadFromZip(zip) |
Load from a JSZip instance. |
setDevice(device) |
Update device info and apply the new size immediately. |
stop() / destroy() |
Stop running / stop and clean up the canvas. |
vm |
The current JavaVM instance (read-only). |
During development, npm run build:dev produces an unminified bundle with source maps.
.
├── apk.js # Public entry source (webpack entry)
├── index.html # Demo page
├── webpack.config.js # Build configuration
├── dist/ # Build output (committed)
│ ├── apk.min.js
│ └── android-framework.min.js
├── samples/
│ ├── hello-dex.apk # Runnable sample APK
│ └── apk-src/ # Sample APK source
├── src/
│ ├── core/binary.js # Binary reading helpers
│ ├── dex/ # DEX parsing and instruction definitions
│ ├── res/ # AXML / ARSC parsing
│ ├── runtime/ # VM, interpreter, class registry, value model
│ └── framework/ # Canvas Android Framework subset
├── tests/ # Regression tests
│ └── debug/ # Ad-hoc debugging scripts kept for reference
├── image-debug/ # node-canvas renders of real APKs
├── tools/ # Sample APK build script and render helper
├── docs/ # VitePress documentation site
└── vendor/jszip.min.js # JSZip (provided globally)
| Directory | Responsibility |
|---|---|
core/ |
Endian-safe binary reading. |
dex/ |
DEX header, string pool, types, methods, fields, class definitions and bytecode. |
res/ |
Binary parsing of AndroidManifest.xml and resources.arsc. |
runtime/ |
JavaVM, DexInterpreter, ClassRegistry, the JavaObject / JavaArray value model. |
framework/ |
Engine orchestration, View hierarchy, layout, widgets, resources, windows, Context / Intent, and native implementations of Java core classes. |
tools/render-apk.mjs runs an APK through node-canvas and writes a PNG to image-debug/:
npm run render -- /path/to/app.apk my-app
# writes image-debug/my-app.pngSet RENDER_WIDTH, RENDER_HEIGHT or RENDER_DENSITY to change the output size.
- Java/Kotlin APKs only. APKs containing native libraries under
lib/cannot run. - APK size limit is 50MB.
- The framework is a small subset of Android and keeps growing. Unimplemented framework classes or methods surface as log errors.
startActivityonly resolves activities inside the same app. Launching another app throwsActivityNotFoundException.- The demo renders at 360×640.
# Full regression: load the sample APK and simulate taps
npm test
# Verify the APK parse result
npm run check
# All of the above
npm run test:allnpm test parses samples/hello-dex.apk, builds the view tree, then simulates a counter tap, focusing the editor and echoing keyboard input, dynamic view insertion, progress advancement, a shape-backed button tap, reading Build info, a reflection call, and a startActivity jump to a second Activity with argument checks.
The scripts in tests/debug/ are older exploratory probes from development. They are not part of the regression suite.
tools/build-sample-apk.sh shows how to compile samples/apk-src into a runnable APK. It needs the Android SDK and build-tools installed locally.
- Some large third-party apps still fail during
onStartbecause the framework coverage is incomplete. - Debug switches (
DBG_INV,DBG_CAST,CAS_DBG,FV_DBG) are replaced withfalseat build time. tests/debug/contains roughly 400 ad-hoc scripts from development. They are kept for reference and will be pruned.
The VitePress site lives in docs/:
npm run docs:dev # Local dev server
npm run docs:build # Static build
npm run docs:preview # Preview the build