Title: DexFactory parent class loader injection (#1951) crashes release builds with "Attempt to register dex file ... with multiple class loaders" · Issue #1962 · NativeScript/android · GitHub
Open Graph Title: DexFactory parent class loader injection (#1951) crashes release builds with "Attempt to register dex file ... with multiple class loaders" · Issue #1962 · NativeScript/android
X Title: DexFactory parent class loader injection (#1951) crashes release builds with "Attempt to register dex file ... with multiple class loaders" · Issue #1962 · NativeScript/android
Description: Summary On release (non debuggable) builds, the first time the runtime generates a proxy class on the main thread, the app crashes with: java.lang.InternalError: Attempt to register dex file /data/user/0/
Open Graph Description: Summary On release (non debuggable) builds, the first time the runtime generates a proxy class on the main thread, the app crashes with: java.lang.InternalError: Attempt to register dex file /data/...
X Description: Summary On release (non debuggable) builds, the first time the runtime generates a proxy class on the main thread, the app crashes with: java.lang.InternalError: Attempt to register dex file /data/...
Opengraph URL: https://github.com/NativeScript/android/issues/1962
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"DexFactory parent class loader injection (#1951) crashes release builds with \"Attempt to register dex file ... with multiple class loaders\"","articleBody":"## Summary\n\nOn release (non debuggable) builds, the first time the runtime generates a proxy class on the main thread, the app crashes with:\n\n```\njava.lang.InternalError: Attempt to register dex file\n /data/user/0/\u003capp\u003e/code_cache/secondary-dexes/\u003cproxy\u003e.jar with multiple class loaders\n```\n\nThis was introduced by [#1951](https://github.com/NativeScript/android/pull/1951) (commit [`c9d41e62`](https://github.com/NativeScript/android/commit/c9d41e620b3413a67c1bd766e3f68e534a082814), currently on `main`). Debug builds are not affected, so it passes normal development and only appears once an app ships a release build. The runtime reports version 9.0.4 (the commit sits a few commits past the 9.0.4 tag).\n\n## What triggers it\n\nAny JavaScript that implements or extends a native type whose proxy is produced at runtime, on the main thread (worker id 0). For example:\n\n```js\n// Main thread. JS implements a native interface, so the runtime must\n// generate a Java proxy class for it and load it through DexFactory.\nconst runnable = new java.lang.Runnable({\n run() {\n console.log(\"invoked from Java\");\n }\n});\n\nnew android.os.Handler(android.os.Looper.getMainLooper()).post(runnable);\n```\n\nStatically bound proxies are not affected, because `resolveClass` finds the pre-generated class and returns before reaching the new code path. If the static binding generator covers the example above, force the runtime path (for example by building the implementation dynamically) so that `DexFactory.resolveClass` actually performs generation. You can confirm the path is taken by logging inside `injectDexIntoClassLoader`.\n\n## Background: the class loader hierarchy\n\nAndroid resolves classes through a chain of class loaders. Each one delegates to its parent before trying itself:\n\n```\nBootClassLoader framework classes (android.*, java.*)\n ^ parent\nPathClassLoader the app's own classes, from the APK dex files\n ^ parent\nDexClassLoader a dex or jar loaded on demand from an arbitrary path\n```\n\n`PathClassLoader` is the app's primary loader, created by the system when the process starts. `DexClassLoader` is created by code at runtime to load a dex or jar from a chosen path. NativeScript uses it to load proxy classes it generates at runtime, with the `PathClassLoader` as the parent so the proxy can resolve the app and framework types it references.\n\nTwo runtime rules are relevant:\n\n1. A class's identity is the pair (name, defining class loader). The same bytecode loaded by two loaders produces two distinct types.\n2. A given dex file may be registered with only one class loader. ART enforces this and throws `InternalError: Attempt to register dex file ... with multiple class loaders` when a dex is claimed by a second loader.\n\n## Root cause\n\nBefore #1951, `resolveClass` loaded each runtime generated proxy through [its own `DexClassLoader`](https://github.com/NativeScript/android/blob/c9d41e620b3413a67c1bd766e3f68e534a082814/test-app/runtime/src/main/java/com/tns/DexFactory.java#L176): one dex, one owner.\n\n#1951 added `injectIntoParentClassLoader`, enabled for the main thread runtime in [`Runtime.init`](https://github.com/NativeScript/android/blob/c9d41e620b3413a67c1bd766e3f68e534a082814/test-app/runtime/src/main/java/com/tns/Runtime.java#L768-L769). When enabled, [`resolveClass`](https://github.com/NativeScript/android/blob/c9d41e620b3413a67c1bd766e3f68e534a082814/test-app/runtime/src/main/java/com/tns/DexFactory.java#L172-L178) calls [`injectDexIntoClassLoader`](https://github.com/NativeScript/android/blob/c9d41e620b3413a67c1bd766e3f68e534a082814/test-app/runtime/src/main/java/com/tns/DexFactory.java#L393-L427), which:\n\n1. creates a [temporary `DexClassLoader`](https://github.com/NativeScript/android/blob/c9d41e620b3413a67c1bd766e3f68e534a082814/test-app/runtime/src/main/java/com/tns/DexFactory.java#L396) over the proxy jar, then\n2. [splices the dex element](https://github.com/NativeScript/android/blob/c9d41e620b3413a67c1bd766e3f68e534a082814/test-app/runtime/src/main/java/com/tns/DexFactory.java#L420) into the app's `PathClassLoader` and [loads the proxy through the `PathClassLoader`](https://github.com/NativeScript/android/blob/c9d41e620b3413a67c1bd766e3f68e534a082814/test-app/runtime/src/main/java/com/tns/DexFactory.java#L174).\n\nBoth the temporary `DexClassLoader` and the app `PathClassLoader` now reference the same `DexFile` object (through the spliced element). Registering one `DexFile` against a second class loader is what ART rejects: the check is keyed on `DexFile` identity and is unconditional. `ClassLinker::RegisterDexFile` throws `ThrowDexFileAlreadyRegisteredError` with no `android:debuggable` guard. Whether a second registration actually happens depends on the build type, which is why only release crashes (see below).\n\n## Why debug builds are not affected\n\nThe single-owner rule is enforced identically in both modes; the throw is not gated on `android:debuggable`. Both loaders reference one `DexFile` (the spliced element), so the only build-dependent question is whether the temporary loader also registers it:\n\n- **Release:** the temporary loader ends up registering the proxy `DexFile` (a non-debuggable runtime verifies, and may AOT-compile, the dex in-process). The later load through the app `PathClassLoader` then registers the same `DexFile` against a second loader, and ART throws.\n- **Debug:** a debuggable runtime is interpret-only and does not use AOT code ([`class_linker.cc`](https://android.googlesource.com/platform/art/+/refs/heads/main/runtime/class_linker.cc): \"For debuggable runtimes we don't use AOT code\"), and the temporary loader never claims the dex, so it is registered exactly once through the app loader and nothing throws.\n\nThe exact ART step that registers the dex against the temporary loader in release is not instrumented here; what is certain is that two loaders share one `DexFile` and that `ClassLinker::RegisterDexFile` is identity-keyed and unconditional, so the second registration throws. The release-only crash is consistent with the interpret-only vs verify/AOT split. (The `odexDir` passed as the `DexClassLoader` `optimizedDirectory` has [no effect since API 26](https://developer.android.com/reference/dalvik/system/DexClassLoader), so it is not the relevant factor.)\n\n## Expected behavior\n\nRuntime generated proxies load without crashing in release, as they did before #1951.\n\n## Suggested direction\n\nThe goal of #1951 is to make generated proxies discoverable through the app's `PathClassLoader` (for example for `Class.forName`). The current implementation opens the dex with a separate loader first, which is what creates the second owner. Two options:\n\n1. Register the dex with the `PathClassLoader` as its only owner, for example by building the dex element through the `PathClassLoader`'s own `DexPathList` instead of through a temporary `DexClassLoader`. The dex is then registered exactly once.\n2. Make the injection opt in and keep the previous isolated `DexClassLoader` path as the default, so apps that do not need `Class.forName` discovery are unaffected.\n\n## Environment\n\n- NativeScript Android runtime: `main` at commit `c9d41e62` (reports version 9.0.4)\n- Introduced by: #1951\n- Build type: release (`android:debuggable=false`). Debug builds do not reproduce.\n","author":{"url":"https://github.com/adrian-niculescu","@type":"Person","name":"adrian-niculescu"},"datePublished":"2026-06-05T10:16:00.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":0},"url":"https://github.com/1962/android/issues/1962"}
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:45a6af6a-285a-1c85-b70c-74d1d9d8f1c6 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | 93F0:119F46:122A407:1A1B5BC:6A6340B8 |
| html-safe-nonce | e4614a0f54cc36f77b69180b4145406aa78500e5e37b99c860d61f58aa69beae |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiI5M0YwOjExOUY0NjoxMjJBNDA3OjFBMUI1QkM6NkE2MzQwQjgiLCJ2aXNpdG9yX2lkIjoiNjQxNjUyMDY4MjAzMDcxMDk2OCIsInJlZ2lvbl9lZGdlIjoiaWFkIiwicmVnaW9uX3JlbmRlciI6ImlhZCJ9 |
| visitor-hmac | 37e8ed9127655346b1a2dfa72e0110bb38fb97d055e0392e8df51f884f13d403 |
| hovercard-subject-tag | issue:4596124463 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/NativeScript/android/1962/issue_layout |
| twitter:image | https://opengraph.githubassets.com/cbab86a9d0292957ad5f52b29316f770ab901319503eb3586ceca56b92dcbe63/NativeScript/android/issues/1962 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/cbab86a9d0292957ad5f52b29316f770ab901319503eb3586ceca56b92dcbe63/NativeScript/android/issues/1962 |
| og:image:alt | Summary On release (non debuggable) builds, the first time the runtime generates a proxy class on the main thread, the app crashes with: java.lang.InternalError: Attempt to register dex file /data/... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | adrian-niculescu |
| hostname | github.com |
| expected-hostname | github.com |
| None | 59e55daad7174ca59d63c6974d58276ccb5477442e550bebb3c035e1bef11c94 |
| turbo-cache-control | no-preview |
| go-import | github.com/NativeScript/android git https://github.com/NativeScript/android.git |
| octolytics-dimension-user_id | 7392261 |
| octolytics-dimension-user_login | NativeScript |
| octolytics-dimension-repository_id | 29137950 |
| octolytics-dimension-repository_nwo | NativeScript/android |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 29137950 |
| octolytics-dimension-repository_network_root_nwo | NativeScript/android |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 990295d92a4cc7b63fbbd83a046217cd7d77d49c |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width