> ## Content Index
> Fetch the complete content index at: https://alohabrowser.ghost.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# How we speed up the Chromium engine startup: a resource-remapping story
- URL: https://alohabrowser.ghost.io/posts-how-we-speed-up-the-chromium-engine-startup-a-resource-remapping-story/
- Published: 2026-08-14T12:20:11.000Z
- Updated: 2026-08-15T18:30:27.000Z
- Description: Remapping 2,450 Chromium resource IDs at startup cost our Android engine about 300 ms. Here's how we moved that work to build time, reducing it to 12 ms.
- Author: Alex Piven
- Tags: Aloha Open Source, Open Source, Open source browser, Aloha Browser for Android

**Our own engine — with blackjack and…**

At Aloha, we created a browser designed for private, secure browsing. It includes an ad blocker and powerful features for viewing and downloading media. To ensure reliability and ease of use, these functions are built directly into the engine, either fully or partially.

For desktop and Android, we chose Chromium as the most convenient and popular engine available. This article is about the Android branch, where we work with the `android_webview` source code. The source code of our engine lives [**on GitHub**](https://github.com/AlohaBrowser/aloha-core)**.** Nobody wants to rebuild Chromium for every minor change. Our main goal was to quickly add new features and iterate fast. Since building Chromium takes a lot of time, we aimed to make the development process faster and more convenient. So we decided to take the `android_webview` source, customize it, and build it for ourselves as an AAR engine called `aloha-core`. The engine is built once, and on top of it the browser developers write features in Kotlin — far faster and more comfortably.

> If you want the broader history first, read [Aloha Core: I love and hate you](https://alohabrowser.com/posts-aloha-core-i-love-and-hate-you/). The [Aloha Core repository](https://github.com/AlohaBrowser/aloha-core) is public, and includes the build guide used by the project.

##  Building the engine

We build the APK as `android_webview` and then repackage it into an AAR. An AAR is just a zip with a structure AGP (Android Gradle Plugin) understands: `AndroidManifest.xml`, `libs/*.jar`, `jni/<abi>/*.so`, `res/`, `assets/`.In short:

- all `*.javac.jar` files from `obj/` for a single architecture are collected recursively into `libs/`;
- the APK of each architecture is unpacked, and from it we extract:
  - `lib/<abi>/*.so` → `jni/<abi>/` (native libraries, per-arch);
  - the V8 snapshot `*.bin` → `assets/` with an architecture suffix;
  - all `*.resources.zip` are unpacked into a single `res/`;
- the AAR's Android manifest.

After that, everything is published to a Maven repository.

![Chromium build being packaged as the Aloha Core AAR](https://storage.ghost.io/c/9b/e8/9be874bd-0377-40be-b16f-9c460166ff13/content/images/2026/08/Aloha-Chromium-1200x630-1.png)

A customized Chromium WebView build is converted into an AAR.

## **The integration issue**

On the browser side (from here on — the host app), this AAR is loaded, and all processes are initialized and started. And this is where it gets interesting.

Because `aloha-core` is built simply as `android_webview.apk`, after `aapt2` (***Android*** Asset Packaging Tool) the resources will have IDs in this format:

![Android resource ID split into package, type, and entry bytes](https://storage.ghost.io/c/9b/e8/9be874bd-0377-40be-b16f-9c460166ff13/content/images/2026/08/Aloha-Chromium-1200x630-2.png)

**The package byte is the part that can change when the host application links the engine's resources.*

The host app runs its own `aapt2` to build its APK. Its resource merger throws everything into one pile: the app's own resources + appcompat + material + ... + our `res/` from the AAR. Then `link` numbers it all together - sequentially, in merge order. `min_screen_width_bucket`, which got `0x7f0c0008` in the Chromium build, ends up as, say, `0x7f0c0038` in the host's table: the type is still the same - `0c` (integer)-, but the entry and the ordering within the type changed, because there are now thousands of foreign records next to it.

However, the bytecode remains unaware of these changes and still references resources using the outdated identifiers. To resolve this, we needed a method to substitute the class's fields with the correct values during runtime.

## The simple solution

The first solution was simple and stable: at runtime, right after startup, fetch the new IDs via `getIdentifier`. It looked like this:

java  
`javaClass<?> rClass = Class.forName("org.chromium.ui.R$integer").getSuperclass();for (Field f : rClass.getDeclaredFields()) {  
 f.setAccessible(true);int id = resources.getIdentifier(f.getName(), "integer", "com.alohamobile.browser");  
 f.setInt(null, id); // *0x7f0c0008 → 0x7f0c0038*}`

Logically flawless: ask the system, "What is the ID of the resource with this name right now?" and enter the answer into the field. The main issue is the cost associated with each question.

 One `getIdentifier` call is a lookup of a resource by string. There's no reverse name→ID index in `.arsc`, so under the hood it's a linear scan over the type's records with string-key comparisons (a miss is the worst case - you have to scan the whole type to the end). On a device that's \~120 µs per call. For 2450 fields: 2450 × \~120 µs ≈ **294 ms** of pure `getIdentifier` \- and all of it on the main thread, before WebView is first used (noticeably more on low-end devices).

And then, at last, there was time and energy to work on speeding up app startup - and the method that performs the resource remap was the first candidate.

## Attempt #1 — shared library (package id 0x00)

The first idea was straightforward. It all starts with building `resources.arsc` \- we do it with the `--shared-lib` flag.A reminder: a value inside `.arsc` is always a `Res_value` structure, not a bare number. It records "what type I am" and "my data". A reference to another resource is a special case: type `TYPE_REFERENCE`, with the target ID in the data. The file is split into chunks. The chunks are nested; the hierarchy looks like this (simplified but essentially accurate):

![Diagram of a binary Android resource table and the package ID used by a shared library](https://storage.ghost.io/c/9b/e8/9be874bd-0377-40be-b16f-9c460166ff13/content/images/2026/08/Aloha-Chromium-1200x630-3.png)

**A shared resource table leaves its package base open for runtime assignment.*

`ResTable_package` is the "resource book of one package". It has a `.id` field - that top byte, the "package base". This is exactly what `--shared-lib` zeroes out. In a normal APK it's `0x7f`; in a shared-lib it's `0x00`.An ID is laid out as `0xPPTTEEEE`, which is why lookup is O(1):

- `PP` (package) → pick the right `ResTable_package` (by matching `.id`);
- `TT` (type) → pick the right `ResTable_type` (of the right type and the current configuration);
- `EEEE` (entry) → this is a direct index into the value array inside `ResTable_type`.

All we need is to correctly control `PP`.

### What --shared-lib gives you

Here's what `--shared-lib` gave us:**`ResTable_package.id = 0x00`.** In a normal APK it's `0x7f`. `0x00` is an agreed-upon marker: "base not assigned, it'll be assigned at load time". By itself it "does" nothing - it's a signal for the loader.**All internal references are written with a top byte of `0x00`.** This is needed because a table can contain values that reference another resource within the same table. That's a `Res_value` of type `TYPE_REFERENCE`, and its data holds the full target ID - all four bytes `0xPPTTEEEE`. In a normal `0x7f`\-APK the linker would write `0x7f`, since the target lives in the same `0x7f` package. And that's exactly why such an APK is non-relocatable: its internal references are nailed down to the `0x7f` base; load it into a process where it would get a different base, and the references would point into a foreign package.`--shared-lib` writes `0x00` into these references instead of `0x7f`. The meaning of a `0x00` byte in a reference is: "the target is inside me, in this same library, and whatever my base turns out to be is unknown at build time — substitute it at read time". So the references become position-independent too. That's why in the file they stay `0x00` forever, and `0x02` only appears at resolve time.

**A `RES_TABLE_LIBRARY` chunk is added** — the "dynamic library export table":

`ResTable_lib_header { ResChunk_header; u32 count; } // *number of entries*  
ResTable_lib_entry[count] {  
 u32 packageId; // *build-time id: 0x00 for us*  
 u16 packageName[128]; // *UTF-16, "com.android.webview"*  
}`  
The meaning: "my name is `com.android.webview`, and my id at build time was `0x00`"

![Flow diagram of the first shared-library initialization attempt](https://storage.ghost.io/c/9b/e8/9be874bd-0377-40be-b16f-9c460166ff13/content/images/2026/08/Aloha-Chromium-1200x630-4.png)

**Attempt one extracted the package, added it to one AssetManager, read the assigned package ID, and rewrote the global R fields.*

## Runtime: four steps

Next we put `bromium_webview_apk.ap_` (\~1.3 MB) into our AAR under `assets/` with the name `bromium-resources.apk`. During the build the host neither unpacks nor touches it - it just carries it as-is in `assets/`. All work with the table happens at runtime.

**Step 1 - `extract()`.** The APK sits inside `assets/`, but the resource loader can only work with a file on disk. So we copy `bromium-resources.apk` from `assets/` to `filesDir`, cached by `lastUpdateTime` (the second launch skips the copy).

**Step 2 - `addAssetPath(path)`** via reflection adds our table to the `AssetManager`. At exactly that moment, inside native code, `BuildDynamicRefTable` fires: the loader sees our package (`id == 0x00`, flagged `IsDynamic()` because of `RES_TABLE_LIBRARY`) and assigns it the next free runtime byte, starting from `0x02`. Our dynamic package is the only one → it gets `0x02`. Right there a `DynamicRefTable` is set up with the entry "build-time `0x00` → runtime `0x02`".

**Step 3 - `getAssignedPackageIdentifiers()`.** We ask the `AssetManager`: which byte did you hand us? It answers `0x2 -> com.android.webview`.

**Step 4 - `onResourcesLoaded(0x02)`.** We rewrite all the fields of the `R` class. Recall the arithmetic laid out earlier: the fields are baked with `0x7f`, the transform is `(0x02 ^ 0x7f) << 24 = 0x7d000000`, and we apply XOR to every field - `0x7f0c0008 ^ 0x7d000000 = 0x020c0008`. Compiled arithmetic, no strings and no reflection - the whole step is \~2 ms.

Note the asymmetry - it's what decides everything in a moment. The `R` fields are one per process: they're rewritten exactly once (guarded by the `sResourcesDidLoad` flag) and to a single base. The `DynamicRefTable`, however, is separate in each `AssetManager`. The first is global, the second is per-namespace. In attempt #1, these two drift apart, and that's exactly where it breaks.

After launch, a check, and everything works. Or so it seemed to us.

## Attempt #1: what went wrong

After a quick manual check, everything functioned correctly and was handed off to the testing team. During testing, a crash occurred when a long-press was made on the text. The issue was caused by the context menu needing to be drawn. The stack trace appears as follows:

`longtap text selection  
 → SelectionPopupControllerImpl.onCreateActionMode (build the selection menu)  
 → DeviceFormFactor.isWindowOnTablet(windowAndroid (phone or tablet? menu layout depends on it)  
 → detectScreenWidthBucket(windowAndroid.getContext()) ← here the WINDOW CONTEXT is taken  
 → activityResources.getInteger(0x020c0008) (read min_screen_width_bucket)  
 → crash: Resources$NotFoundException: Resource ID #0x020c0008`  
Chromium wants to figure out whether it's looking at a tablet or a phone, to pick the menu layout. For that, it reads the `min_screen_width_bucket` resource. And it reads it through the window context -`windowAndroid.getContext()` \- which is an Activity context, not the Application one. Almost all the logic went through Application, and here, out of nowhere - A.

### The Activity context

But why isn't the ID found through an Activity context? Here's the thing. The `AssetManager` is not one per process: the Application context has its own, and every Activity has its own separate one. And our `addAssetPath` (step 2) added the `0x02` table to exactly ONE `AssetManager` \- the one belonging to the Application context, through whose `Resources` we made the call. Resolution always starts by picking the package by the top byte. The byte `0x02` is correct, but in this namespace there is no package it routes to. The miss is not on the entry - it's on the package itself. And here's the key subtlety: the ID in the crash, `0x020c0008`, is already correct - `onResourcesLoaded` did its job, the top byte `0x02` is in place. It's not the constant that went stale, it's the absence of the table in this `AssetManager`. This is the inversion of the original bug: before, the constant was wrong and the table was in place; now the constant is right and the table isn't in the required namespace. Exactly the asymmetry from step 4: we rewrote `R` globally, but `addAssetPath`'d the table into only one of many `AssetManager`s.We could manually `addAssetPath` the table into the `AssetManager` of every context we can reach. But the set of contexts is unenumerable: `createConfigurationContext`, an external display / DeX, split-screen - you can't cover them all.

## The second defect: the base isn't guaranteed

Worse, even having reached every context, we'd run into a second defect: the `0x02` base isn't guaranteed. Each `AssetManager` assigns it independently, in the order dynamic packages load within that particular manager. Where a vendor RRO overlay landed earlier (overlays live in the same `0x02–0x7e` range), our package would get `0x03` — while the `R` fields are rewritten globally to a single base. The same "global `R` vs. per-namespace base" drift, just from the other side. There's no way to patch this hole, so we started looking for another way.

![Escher-style staircase graphic labeled with the shared-library and getIdentifier loop](https://storage.ghost.io/c/9b/e8/9be874bd-0377-40be-b16f-9c460166ff13/content/images/2026/08/Aloha-Chromium-1200x630-5.png)

**The first attempt led back to the original problem.*

## Attempt #2 - merge into the host + a link-time index (the winner)

Since the only way to be guaranteed present in every `AssetManager` is to be part of the host's `0x7f`, let's build on that. We hand the host the raw `res/` (not the `.arsc`, but the source XML), and its `aapt2` merges our resources into its own `0x7f` package. From that point on, Chromium's resources are ordinary host-app resources, indistinguishable from its own. Since we're now part of `0x7f`, whenever the system creates any `AssetManager` (Application, each Activity, `createConfigurationContext`, a future one - any at all) from the `ApplicationInfo` paths, it pulls in `0x7f` in its entirety, and us inside it. Both defects of attempt #1 vanish by construction: there are no namespaces left to patch, and there's simply no dynamic base that could drift to `0x03`. 

But we've come right back to what we were running away from. The host's `aapt2`, merging our `res/` into `0x7f`, hands out IDs anew - to everyone together. Our `dimen/foo`, which got `0x7f07001A` in the chromium build, becomes, say, `0x7f070123` in the host's table - because there are now thousands of foreign records next to it (the app itself + appcompat + material…), and the numbering is different. Yet chromium's bytecode still executes `getstatic `[R$dimen.foo](http://R$dimen.foo), where the field holds the chromium-baked `0x7f07001A`.We already rejected the naive approach: `getIdentifier `on every field is a string lookup, 2450 fields ≈ 294 ms. We need another way.

## Piggybacking on the host's aapt2

Here's what we know: the host's `aapt2` already turns every name into a final ID. It does this for any `@`\-reference in any layout - that's its regular job, a single pass at build time, with indexes and no runtime cost. So let's slip it a file that references all our resources and pick up the finished numbers at runtime. We're not speeding up the name lookup - we're moving it to the host's build stage, where it already happens for free.

### Step 1\. 

At build time, we generate an index and place a file `res/xml/bromium_res_index.xml` into the shipped `res/`: 

xml

`<index><i n="integer/min_screen_width_bucket" r="@integer/min_screen_width_bucket"/><i n="attr/select_dialog_multichoice" r="?attr/select_dialog_multichoice"/>  
 ... <!– 1216 entries --></index>`

- `n` \- the key: the `R` field name in `type/name` form (`integer/min_screen_width_bucket`). This is what we'll look up later when rewriting. (For styleables, the `R` field is called `Foo_Bar` while the resource is [Foo.Bar](http://Foo.Bar); that's accounted for.)
- `r` \- a reference to the real resource by its real name. Usually an `@`\-reference (`@integer/...`), but sometimes `?attr/...` \- we'll come back to this difference in step 3\. This is the bait for the host's linker.

### Step 2 (the host's `aapt2`) - the linker stamps the final IDs into the index. 

The host builds the app, its resource merger merges our `res/` with its own, and `aapt2 link` compiles everything, including our `bromium_res_index.xml`, into binary AXML. And here's the interesting part. When `aapt2` compiles the XML, the attribute `r="@integer/min_screen_width_bucket"` stops being a string. In binary AXML an attribute value is stored as `Res_value{size, dataType, data}`. And `@integer/...` compiles into `dataType = TYPE_REFERENCE`, with the final merged ID of that resource placed into `data`. The passes that are literally named this way in the `aapt2` sources - `ReferenceLinker` / `XmlReferenceLinker` \- resolve every symbolic `type/name` reference against the symbol table of the merged resources and stamp in the number. Exactly like a static linker: symbol → address. So after the host's build, inside the compiled `bromium_res_index.xml`, each entry holds: `n = "integer/min_screen_width_bucket"` (the string key) and `r = Res_value{TYPE_REFERENCE, data = 0x7f070123}` (the correct host ID).

![Diagram showing AAPT2 merging resources and stamping final IDs into a compiled XML index](https://storage.ghost.io/c/9b/e8/9be874bd-0377-40be-b16f-9c460166ff13/content/images/2026/08/Aloha-Chromium-1200x630-6.png)

**The host link step resolves each symbolic resource reference and writes the final ID into the compiled XML index.*

### Step 3 (engine initialization) - `AlohaCoreResources.init()` rewrites the `R` fields.

1. One! `getIdentifier("bromium_res_index", "xml", pkg)` \- the single name lookup in the whole remapping. The same expensive one that used to be called 2450 times.
2. `res.getXml(id)` → `XmlResourceParser` on top of the native `ResXMLParser`. Critically: the AXML blob is `mmap`'d from the APK and traversed in place - this is not text parsing, but a walk over a ready binary structure with interned strings.
3. For each entry, `getAttributeResourceValue(null, "r", 0)` \- takes `Res_value.data` of the `r` attribute straight from the `mmap`'d blob; a `TYPE_REFERENCE` is returned as a number. Zero lookups into the resource table - we don't resolve the resource, we read the ID already stamped into the attribute. We put it into a `HashMap<"type/name", id>`. A special case is the `?attr/...` entries: they compile not into `TYPE_REFERENCE` but into `TYPE_ATTRIBUTE`, and `getAttributeResourceValue` returns a default for them. For those we use `getAttributeValue`, get a string of the form `"?<decimal id>"`, and parse it. A minor fork that doesn't change the essence.
4. Reflection - we rewrite the `R` fields. For each type: `Class.forName("org.chromium.ui.R$string").getSuperclass()` yields `gen.base_module.R$string` (per-library R classes are empty subclasses of a common base where the fields are declared once), then `field.setInt(null, id)`.

The runtime total: one `getIdentifier` \+ a linear scan over the `mmap`'d blob + reflection (which was in the original too). \~12 ms.

![Mouse at a computer after resolving the resource-ID problem](https://storage.ghost.io/c/9b/e8/9be874bd-0377-40be-b16f-9c460166ff13/content/images/2026/08/Aloha-Chromium-1200x630-7.png)

**After moving the work to link time.*

## The bottom line

| Approach                       | remap at startup |
| ------------------------------ | ---------------- |
| Original (getIdentifier ×2450) | 300–770 ms       |
| merge + index (compiled XML)   | **\~12 ms**      |

Attempt #1 optimized relocation but lost on visibility. Attempt #2 gave up relocation in favor of merging into `0x7f` (visibility becomes free and absolute), and it pays down the cost of the merge — the renumbering — by offloading name resolution onto the host's linker and picking up the finished result almost for nothing.

### Related reading

- [Aloha Browser Takes a Bold Step Towards Open Source: A Commitment to Privacy and Collaboration](https://alohabrowser.com/posts-aloha-browser-takes-a-bold-step-towards-open-source/)
- [Aloha Core. I Love and Hate You](https://alohabrowser.com/posts-aloha-core-i-love-and-hate-you/)