FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Static binding names never match at runtime since 9.0.0 — every `extend()` falls back to runtime dex generation · Issue #2024 · NativeScript/android · GitHub

Static binding names never match at runtime since 9.0.0 — every extend() falls back to runtime dex generation #2024

Description

Since 9.0.0, the class name the static binding generator emits and the class name the runtime asks for are different, so DexFactory.resolveClass never finds a pre-generated binding and generates a proxy dex at runtime for every anonymous extend() in the app — including the ~60 that @nativescript/core itself performs (gesture listeners, click listeners, adapters, fragments, …).

It is invisible on healthy devices (it "only" costs startup time), but it turns a whole class of storage/IO/concurrency failures in DexFactory into hard failures of the app. It also means the pre-generated bindings that ship in every APK/AAB are dead weight.

Two independent defects stack up. The first one alone is enough to break the match.


Defect 1 — the SBG appends an MD5 the runtime never computes

#1836 (feat: ES modules (ESM) support, released in 9.0.0) changed the naming in Generator.java:

-        // name of the class: last portion of the full file name + line + column + variable name
-        String[] lastFilePathPart = dataRow.getFile().split("_");
-        name += lastFilePathPart[lastFilePathPart.length - 1] + "_" + dataRow.getLine() + "_" + dataRow.getColumn() + "_" + dataRow.getNewClassName();
+        // Generate a unique identifier that prevents naming collisions
+        // especially with .mjs files and complex structures
+        String fileIdentifier = generateUniqueFileIdentifier(dataRow.getFile());
+        name += fileIdentifier + "_" + dataRow.getLine() + "_" + dataRow.getColumn() + "_" + dataRow.getNewClassName();

generateUniqueFileIdentifier returns <last up-to-3 "_"-segments>_<first 3 bytes of md5(filePath) as hex>.

The runtime side of the same name — MetadataNode::GetExtendLocation in MetadataNode.cpp — still does what the old SBG line did:

Util::SplitString(fullPathToFile, "_", pathParts);
std::string lastPathPart = pathParts.back();
fullPathToFile = lastPathPart;

There is no MD5 anywhere in the runtime. #1836 did not touch MetadataNode.cpp (see the PR's file list), and git log on test-app/runtime/src/main/cpp/MetadataNode.cpp shows no commit between 2025-02-20 and 2026-08-03. So from 9.0.0 on, the SBG emits …_vendor_7c3613_… while the runtime asks for …_vendor_….

Defect 2 — the runtime strips 3 characters assuming .js, but bundles are .mjs

Same function, 9.0.4 / 9.0.5:

int strToTakeLen = (srcFileName.length() - startIndex - 3); // 3 refers to .js at the end of file name
fullPathToFile = srcFileName.substr(startIndex, strToTakeLen);

std::replace(fullPathToFile.begin(), fullPathToFile.end(), '/', '_');
std::replace(fullPathToFile.begin(), fullPathToFile.end(), '.', '_');
…
std::vector<std::string> pathParts;
Util::SplitString(fullPathToFile, "_", pathParts);
std::string lastPathPart = pathParts.back();

With file:///data/data/<pkg>/files/app/vendor.mjs, cutting 3 chars leaves app/vendor. → app_vendor_ → SplitString yields ["app", "vendor", ""] → back() is the empty string. So the file component is not merely different, it is empty.

This half is already fixed on main (commit bce66986c7, #1965) — .mjs is stripped and the last non-empty part is taken. But that fix does not close the gap: I checked v9.1.0-rc.0, where the runtime now yields vendor and Generator.java still yields vendor_7c3613. Defect 1 is untouched on main.


Reproduction / evidence

NativeScript 9 app (@nativescript/core 9.0.20, @nativescript/android 9.0.4), release AAB.

The SBG emits, and the AAB ships:

$ strings base/dex/*.dex | grep TapAndDoubleTapGestureListenerImpl
Lcom/tns/gen/android/view/GestureDetector_SimpleOnGestureListener_vendor_7c3613_2_1631471_TapAndDoubleTapGestureListenerImpl;

sbg-bindings.txt row (file component is vendor, line 2, column 1631471):

android.view.GestureDetector.SimpleOnGestureListener*vendor*2*1631471*TapAndDoubleTapGestureListenerImpl*onSingleTapUp,onDown,onLongPress,_handleSingleTap,_handleDoubleTap,DoubleTapTimeout***

md5("vendor") = 7c3613dba5171cb6027c67835dd3b9d4 → 7c3613, confirming where the suffix comes from.

The runtime asks for the same line/column with an empty file component, fails, and generates the dex at runtime — here is a user-facing crash from a device where that generation failed:

Calling js method onCreateView failed
Error: java.lang.ClassNotFoundException: Didn't find class
"android.view.GestureDetector_SimpleOnGestureListener__2_1631471_TapAndDoubleTapGestureListenerImpl"
on path: DexPathList[[zip file "/data/user/0/<pkg>/code_cache/secondary-dexes/android.view.GestureDetector_SimpleOnGestureListener__2_1631471_TapAndDoubleTapGestureListenerImpl.jar"]]
  at com.tns.DexFactory.resolveClass(DexFactory.java:166)
  at com.tns.ClassResolver.resolveClass(ClassResolver.java:25)
  at com.tns.Runtime.resolveClass(Runtime.java:882)
  …
Suppressed: java.io.IOException: Failed to open dex files from …/secondary-dexes/….jar
  because: Unable to open '…' : No such file or directory

Note _SimpleOnGestureListener__2_1631471_ (empty) vs the shipped _SimpleOnGestureListener_vendor_7c3613_2_1631471_. Same base class, same line, same column — only the file identifier differs, so the pre-generated class can never be loaded.

In this app 62 of the 96 generated com.tns.gen.** classes carry a file/line/column component, i.e. 62 proxies are eligible to be regenerated at runtime on every fresh install, on every device.

Confirmed live on 9.0.5 / Android 16

I upgraded the same app to @nativescript/android 9.0.5 (TNS.Runtime: NativeScript Runtime Version 9.0.5, commit 6bc84d4) and listed the proxy cache after a normal session:

$ adb shell run-as <pkg> ls code_cache/secondary-dexes
android.net.ConnectivityManager_NetworkCallback__69772_58_NetworkCallbackImpl.dex
android.view.GestureDetector_SimpleOnGestureListener__93550_32_TapAndDoubleTapGestureListenerImpl.dex
android.view.View_AccessibilityDelegate__67855_32_TNSAccessibilityDelegateImpl.dex
androidx.activity.OnBackPressedCallback__85603_69_.dex
androidx.recyclerview.widget.RecyclerView_OnScrollListener__59807_28_ListViewNativeScrollListenerImpl.dex
java.lang.Object__59398_28_ListViewItemClickListenerImpl.dex
java.lang.Object__79094_32_MenuItemClickListenerImpl.dex
java.lang.Object__85639_32_TouchListenerImpl.dex
…

Every entry has the empty file component (NetworkCallback__69772_58_), and every one of these has a pre-compiled counterpart sitting unused in the APK. Opening two screens and tapping one list item generated twenty of them.

Quick verification, without an app

For any NativeScript 9 Android project, after a build:

# what the build produced
ls platforms/android/app/src/main/java/com/tns/gen/android/view/
#   GestureDetector_SimpleOnGestureListener_vendor_7c3613_2_1631471_TapAndDoubleTapGestureListenerImpl.java

# what the SBG was told the file was called
grep GestureDetector platforms/android/build-tools/sbg-bindings.txt
#   android.view.GestureDetector.SimpleOnGestureListener*vendor*2*1631471*…

# where the suffix comes from
printf 'vendor' | md5   # 7c3613dba517…

and then enable DexFactory's logger — every one of those classes logs
Pre-generated class not found: … followed by generating proxy in place.


Why this matters beyond startup cost

The runtime dex path is where the recently reported failures live, and this bug routes 100% of traffic through it:

  • #2019 documents unsynchronized state in exactly this code — a truncated jar being persisted read-only and reused on later launches, EACCES from the check-then-act jarFile.exists() / setReadOnly() pair, and static scratch buffers in Dump that can bake wrong method descriptors into a dex. The issue opens by saying #2016 "makes runtime proxy dex generation a routine path … instead of a rare one" — in practice it has been the only path since 9.0.0.
  • #1962 / #1968: release builds crashed on the first runtime-generated proxy. With working pre-generated bindings that would have been rare; as it stands it is the first gesture on the first screen.
  • #2016 is written on the assumption that pre-compiled classes are the normal case ("A precompiled class is still the sturdier answer where a build step can provide one"). That assumption does not currently hold for any app.

Suggested fix

Make the two sides compute the same string, and add a regression test that asserts it.

The lowest-risk direction is to revert the SBG to the last-segment rule (drop generateUniqueFileIdentifier) and keep the main runtime fix for .mjs + last-non-empty part, since that is the shape both sides already agreed on before #1836. If the collision concern behind #1836 is real, the identifier has to be derived from information the runtime also has — the stack frame gives it only the script URL, so any hash must be computed from the normalized script path on both sides, not from the SBG's dataRow.getFile().

Either way it would help to fail loudly rather than silently: DexFactory.resolveClass currently swallows the pre-generated lookup miss and logs only when the (disabled by default) logger is on. A one-line warning on the miss would have surfaced this within a day of 9.0.0.

Environment

  • @nativescript/android 9.0.4 and 9.0.5 (verified in source for both; v9.1.0-rc.0 still mismatched)
  • @nativescript/core 9.0.20, @nativescript/webpack 5.0.35
  • Android app, minSdk 21 / target 36; reproduced on a signed release build and on a debug build, Android 16 emulator and a user's physical device

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions


    Back | FazBrowse Home | New Git URL