From 92818f0323571244d7644abc08f17aef2eae10fc Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 11 May 2026 10:31:56 -0300 Subject: [PATCH] fix(DexFactory): add support for injecting DEX into parent class loader --- .../src/main/java/com/tns/DexFactory.java | 62 +++++++++++++++++-- .../src/main/java/com/tns/Runtime.java | 3 +- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/test-app/runtime/src/main/java/com/tns/DexFactory.java b/test-app/runtime/src/main/java/com/tns/DexFactory.java index 3622803fc..1e7b2e8a1 100644 --- a/test-app/runtime/src/main/java/com/tns/DexFactory.java +++ b/test-app/runtime/src/main/java/com/tns/DexFactory.java @@ -18,11 +18,14 @@ import java.io.InputStreamReader; import java.io.InvalidClassException; import java.io.OutputStreamWriter; +import java.lang.reflect.Array; +import java.lang.reflect.Field; import java.util.HashMap; import java.util.HashSet; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; +import dalvik.system.BaseDexClassLoader; import dalvik.system.DexClassLoader; public class DexFactory { @@ -34,15 +37,21 @@ public class DexFactory { private final String dexThumb; private final ClassLoader classLoader; private final ClassStorageService classStorageService; + private final boolean injectIntoParentClassLoader; private ProxyGenerator proxyGenerator; private HashMap> injectedDexClasses = new HashMap>(); DexFactory(Logger logger, ClassLoader classLoader, File dexBaseDir, String dexThumb, ClassStorageService classStorageService) { + this(logger, classLoader, dexBaseDir, dexThumb, classStorageService, false); + } + + DexFactory(Logger logger, ClassLoader classLoader, File dexBaseDir, String dexThumb, ClassStorageService classStorageService, boolean injectIntoParentClassLoader) { this.logger = logger; this.classLoader = classLoader; this.dexDir = dexBaseDir; this.dexThumb = dexThumb; + this.injectIntoParentClassLoader = injectIntoParentClassLoader; this.odexDir = new File(this.dexDir, "odex"); this.proxyGenerator = new ProxyGenerator(this.dexDir.getAbsolutePath()); @@ -158,12 +167,14 @@ public Class resolveClass(String baseClassName, String name, String className jarFile.setReadOnly(); Class result; - DexClassLoader dexClassLoader = new DexClassLoader(jarFilePath, this.odexDir.getAbsolutePath(), null, classLoader); + String classNameToLoad = isInterface ? fullClassName : desiredDexClassName; - if (isInterface) { - result = dexClassLoader.loadClass(fullClassName); + if (injectIntoParentClassLoader && classLoader instanceof BaseDexClassLoader) { + injectDexIntoClassLoader((BaseDexClassLoader) classLoader, jarFilePath); + result = classLoader.loadClass(classNameToLoad); } else { - result = dexClassLoader.loadClass(desiredDexClassName); + DexClassLoader dexClassLoader = new DexClassLoader(jarFilePath, this.odexDir.getAbsolutePath(), null, classLoader); + result = dexClassLoader.loadClass(classNameToLoad); } classStorageService.storeClass(result.getName(), result); @@ -371,4 +382,47 @@ private String getCachedProxyThumb(File proxyDir) { return null; } + + /** + * Injects a DEX jar into the app's PathClassLoader so that classes in it are + * findable by Class.forName(). This is needed because Android framework components + * (e.g. FragmentFactory) use Class.forName() to instantiate classes by name, but + * NativeScript's dynamically-generated classes normally live in isolated DexClassLoaders + * that Class.forName() doesn't search. + */ + private void injectDexIntoClassLoader(BaseDexClassLoader targetClassLoader, String jarFilePath) { + try { + // Create a temporary DexClassLoader to produce the optimized dex + DexClassLoader tempLoader = new DexClassLoader(jarFilePath, this.odexDir.getAbsolutePath(), null, targetClassLoader); + + // Get pathList from both classloaders + Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList"); + pathListField.setAccessible(true); + + Object targetPathList = pathListField.get(targetClassLoader); + Object sourcePathList = pathListField.get(tempLoader); + + // Get dexElements from both pathLists + Field dexElementsField = targetPathList.getClass().getDeclaredField("dexElements"); + dexElementsField.setAccessible(true); + + Object targetElements = dexElementsField.get(targetPathList); + Object sourceElements = dexElementsField.get(sourcePathList); + + int targetLen = Array.getLength(targetElements); + int sourceLen = Array.getLength(sourceElements); + + // Create merged array: target + source + Object merged = Array.newInstance(targetElements.getClass().getComponentType(), targetLen + sourceLen); + System.arraycopy(targetElements, 0, merged, 0, targetLen); + System.arraycopy(sourceElements, 0, merged, targetLen, sourceLen); + + dexElementsField.set(targetPathList, merged); + } catch (Exception e) { + if (logger.isEnabled()) { + logger.write("Failed to inject dex into parent classloader: " + e.getMessage()); + } + // Non-fatal: class will still be loadable via the ClassStorageService fallback + } + } } diff --git a/test-app/runtime/src/main/java/com/tns/Runtime.java b/test-app/runtime/src/main/java/com/tns/Runtime.java index a76903653..b610ddb0c 100644 --- a/test-app/runtime/src/main/java/com/tns/Runtime.java +++ b/test-app/runtime/src/main/java/com/tns/Runtime.java @@ -765,7 +765,8 @@ private void init(Logger logger, String appName, String nativeLibDir, File rootD try { this.logger = logger; - this.dexFactory = new DexFactory(logger, classLoader, dexDir, dexThumb, classStorageService); + boolean isMainThread = this.workerId == 0; + this.dexFactory = new DexFactory(logger, classLoader, dexDir, dexThumb, classStorageService, isMainThread); if (logger.isEnabled()) { logger.write("Initializing NativeScript JAVA");